From bbe3917c8fa03cd020e0cff13f2c07407651c6ba Mon Sep 17 00:00:00 2001 From: aniampio Date: Thu, 8 Aug 2024 12:22:38 +0100 Subject: [PATCH 001/207] WithdrawalRequest: make sure h is non-identity --- .../src/scheme/withdrawal.rs | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 082f969508..51bb9fa41c 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -115,7 +115,39 @@ fn compute_private_attribute_commitments( (openings, commitments) } +/// Generates a non-identity hash of joined commitment. +/// +/// This function attempts to create a valid joined commitment and hash by +/// repeatedly generating a random `joined_commitment_opening` and computing +/// the corresponding `joined_commitment` and `joined_commitment_hash`. +/// It continues this process until the `joined_commitment_hash` is not the +/// identity element. +fn generate_non_identity_h( + params: &GroupParameters, + sk_user: &SecretKeyUser, + v: Scalar, + expiration_date: Scalar, + t_type: Scalar, +) -> (G1Projective, G1Projective, Scalar) { + let gamma = params.gammas(); + loop { + let joined_commitment_opening = params.random_scalar(); + + // Compute joined commitment for all attributes (public and private) + let joined_commitment = + params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v; + + // Compute commitment hash h + let joined_commitment_hash = + hash_g1((joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes()); + + // Check if the joined_commitment_hash is not the identity element + if !bool::from(joined_commitment_hash.is_identity()) { + return (joined_commitment, joined_commitment_hash, joined_commitment_opening); + } + } +} /// Generates a withdrawal request for the given user to request a zk-nym credential wallet. /// /// # Arguments @@ -148,19 +180,13 @@ pub fn withdrawal_request( let params = ecash_group_parameters(); // Generate random and unique wallet secret let v = params.random_scalar(); - let joined_commitment_opening = params.random_scalar(); - - let gamma = params.gammas(); let expiration_date = date_scalar(expiration_date); let t_type = type_scalar(t_type); - // Compute joined commitment for all attributes (public and private) - let joined_commitment = - params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v; + // Generate a non-identity commitment hash + let (joined_commitment, joined_commitment_hash, joined_commitment_opening) = + generate_non_identity_h(¶ms, sk_user, v, expiration_date, t_type); - // Compute commitment hash h - let joined_commitment_hash = - hash_g1((joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes()); // Compute Pedersen commitments for private attributes (wallet secret and user's secret) let private_attributes = vec![sk_user.sk, v]; @@ -519,3 +545,28 @@ pub fn verify_partial_blind_signature( .is_identity() .into() } + +#[cfg(test)] +mod tests { + use crate::ecash_group_parameters; + use bls12_381::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar}; + use crate::scheme::keygen::SecretKeyUser; + use super::generate_non_identity_h; + + #[test] + fn test_generate_non_identity_h() { + let params = ecash_group_parameters(); + // Create dummy values for testing + let sk_user = SecretKeyUser { sk: params.random_scalar() }; + let v = params.random_scalar(); + let expiration_date = params.random_scalar(); + let t_type = params.random_scalar(); + + // Generate the commitment and hash + let (_, joined_commitment_hash, _) = + generate_non_identity_h(¶ms, &sk_user, v, expiration_date, t_type); + + // Ensure that the joined_commitment_hash is not the identity element + assert!(!bool::from(joined_commitment_hash.is_identity()), "Joined commitment hash should not be the identity element"); + } +} \ No newline at end of file From 7722a7080c918a0097b37bd79715e24ba057f397 Mon Sep 17 00:00:00 2001 From: aniampio Date: Thu, 8 Aug 2024 12:36:48 +0100 Subject: [PATCH 002/207] RequestVerify: check if h is non-identity --- common/nym_offline_compact_ecash/src/scheme/withdrawal.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 51bb9fa41c..b744450628 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -255,6 +255,10 @@ pub fn request_verify( let expiration_date = date_scalar(expiration_date); let t_type = type_scalar(t_type); + if bool::from(req.joined_commitment_hash.is_identity()) { + return Err(CompactEcashError::WithdrawalRequestVerification); + } + let expected_commitment_hash = hash_g1( (req.joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(), ); @@ -549,7 +553,6 @@ pub fn verify_partial_blind_signature( #[cfg(test)] mod tests { use crate::ecash_group_parameters; - use bls12_381::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar}; use crate::scheme::keygen::SecretKeyUser; use super::generate_non_identity_h; From 6ea746e7f3e48cdedc62a67e144368b67058f26d Mon Sep 17 00:00:00 2001 From: aniampio Date: Thu, 8 Aug 2024 16:47:17 +0100 Subject: [PATCH 003/207] Add test for check if h is identity in the verify_partial_blind_signature --- common/nym_offline_compact_ecash/src/error.rs | 3 ++ .../src/scheme/withdrawal.rs | 42 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/common/nym_offline_compact_ecash/src/error.rs b/common/nym_offline_compact_ecash/src/error.rs index 0d49581ec6..a1afef7b62 100644 --- a/common/nym_offline_compact_ecash/src/error.rs +++ b/common/nym_offline_compact_ecash/src/error.rs @@ -40,6 +40,9 @@ pub enum CompactEcashError { #[error("issuance verification failed")] IssuanceVerification, + #[error("verification of partial blind signature failed")] + PartialBlindSignatureVerification, + #[error("trying to spend more than what's available. Spending : {spending}, available : {remaining}")] SpendExceedsAllowance { spending: u64, remaining: u64 }, diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index b744450628..3a0b64077e 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -493,7 +493,12 @@ pub fn verify_partial_blind_signature( if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { return false; } - + // Note: This check is useful if someone uses the code of those functions + // to verify Pointcheval-Sanders signatures in a context different for their use + // in zk-nyms + if bool::from(blind_sig.h.is_identity()){ + 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.c.to_affine().neg(); @@ -553,8 +558,10 @@ pub fn verify_partial_blind_signature( #[cfg(test)] mod tests { use crate::ecash_group_parameters; - use crate::scheme::keygen::SecretKeyUser; - use super::generate_non_identity_h; + use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; + use super::{generate_non_identity_h, verify_partial_blind_signature}; + use crate::common_types::{BlindedSignature}; + use bls12_381::{G1Projective}; #[test] fn test_generate_non_identity_h() { @@ -572,4 +579,33 @@ mod tests { // Ensure that the joined_commitment_hash is not the identity element assert!(!bool::from(joined_commitment_hash.is_identity()), "Joined commitment hash should not be the identity element"); } + + #[test] + fn test_verify_partial_blind_signature_blind_sig_identity() { + let params = ecash_group_parameters(); + let private_attribute_commitments = vec![params.gen1() * params.random_scalar()]; + let public_attributes = vec![]; + // Create a blinded signature with h being the identity element + let blind_sig = BlindedSignature { + h: G1Projective::identity(), + c: params.gen1() * params.random_scalar(), + }; + // Create a mock partial verification key + let partial_verification_key = VerificationKeyAuth { + alpha: params.gen2() * params.random_scalar(), + beta_g1: vec![params.gen1() * params.random_scalar()], + beta_g2: vec![params.gen2() * params.random_scalar()], + }; + + // Test with identity h, expecting false + assert!( + !verify_partial_blind_signature( + &private_attribute_commitments, + &public_attributes, + &blind_sig, + &partial_verification_key + ), + "Expected verification to return false for identity h in blind signature" + ); + } } \ No newline at end of file From 07de1868ff87a28cd4908cfaea8383f59fad5268 Mon Sep 17 00:00:00 2001 From: aniampio Date: Thu, 8 Aug 2024 17:54:18 +0100 Subject: [PATCH 004/207] Coconut: add check if h is not identity in the prepare_blind_sign function --- common/nymcoconut/src/scheme/issuance.rs | 45 +++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/common/nymcoconut/src/scheme/issuance.rs b/common/nymcoconut/src/scheme/issuance.rs index 3779e377d6..973b9ebe6e 100644 --- a/common/nymcoconut/src/scheme/issuance.rs +++ b/common/nymcoconut/src/scheme/issuance.rs @@ -252,11 +252,24 @@ pub fn prepare_blind_sign( }); } - let (commitment_opening, commitment) = - compute_attributes_commitment(params, private_attributes, public_attributes, hs); + let mut commitment_hash; + let mut commitment; + let mut commitment_opening; - // Compute the challenge as the commitment hash - let commitment_hash = compute_hash(commitment, public_attributes); + loop { + // Compute the attributes commitment + let (c_opening, c) = compute_attributes_commitment(params, private_attributes, public_attributes, hs); + commitment_opening = c_opening; + commitment = c; + + // Compute the commitment hash + commitment_hash = compute_hash(commitment, public_attributes); + + // Check if the commitment hash is not the identity point + if !bool::from(commitment_hash.is_identity()) { + break; + } + } let (pedersen_commitments_openings, pedersen_commitments) = compute_pedersen_commitments_for_private_attributes( @@ -499,6 +512,28 @@ mod tests { ); } + #[test] + fn test_prepare_blind_sign_non_identity_commitment_hash() { + let params = Parameters::new(1).unwrap(); + random_scalars_refs!(private_attributes, params, 1); + random_scalars_refs!(public_attributes, params, 0); + + // Call the function to prepare the blind sign + let result = prepare_blind_sign(¶ms, &private_attributes, &public_attributes); + + // Ensure the result is Ok + assert!(result.is_ok(), "prepare_blind_sign should succeed"); + + let (_, blind_sign_request) = result.unwrap(); + + // Ensure the commitment_hash is not the identity point + assert!( + !bool::from(blind_sign_request.commitment_hash.is_identity()), + "commitment_hash should not be the identity point" + ); + } + + #[test] fn successful_verify_partial_blind_signature() { let params = Parameters::new(4).unwrap(); @@ -574,4 +609,6 @@ mod tests { validator2_keypair.verification_key() ),); } + + } From 57436249489d825da2d97b8563cf1d5bac99a257 Mon Sep 17 00:00:00 2001 From: aniampio Date: Fri, 9 Aug 2024 00:11:03 +0100 Subject: [PATCH 005/207] Add check for identity point in all required Coconut functions --- common/nymcoconut/src/scheme/aggregation.rs | 5 +++ common/nymcoconut/src/scheme/issuance.rs | 36 +++++++++++++++++++++ common/nymcoconut/src/scheme/mod.rs | 5 +++ 3 files changed, 46 insertions(+) diff --git a/common/nymcoconut/src/scheme/aggregation.rs b/common/nymcoconut/src/scheme/aggregation.rs index c97fd4b8ad..5338be2dfa 100644 --- a/common/nymcoconut/src/scheme/aggregation.rs +++ b/common/nymcoconut/src/scheme/aggregation.rs @@ -115,6 +115,11 @@ pub fn aggregate_signatures_and_verify( .map(|(&attr, beta_i)| beta_i * attr) .sum::(); + if bool::from(signature.0.is_identity()){ + return Err(CoconutError::Aggregation( + "Verification of the aggregated signature failed - h is an identity point".to_string(), + )); + } if !check_bilinear_pairing( &signature.0.to_affine(), &G2Prepared::from((alpha + tmp).to_affine()), diff --git a/common/nymcoconut/src/scheme/issuance.rs b/common/nymcoconut/src/scheme/issuance.rs index 973b9ebe6e..f882962e8c 100644 --- a/common/nymcoconut/src/scheme/issuance.rs +++ b/common/nymcoconut/src/scheme/issuance.rs @@ -317,6 +317,11 @@ pub fn blind_sign( // Verify the commitment hash let h = compute_hash(blind_sign_request.commitment, public_attributes); + if bool::from(blind_sign_request.commitment_hash.is_identity()){ + return Err(CoconutError::Issuance( + "Commitment hash should not be an identity point".to_string(), + )); + } if !(h == blind_sign_request.commitment_hash) { return Err(CoconutError::Issuance( "Failed to verify the commitment hash".to_string(), @@ -385,6 +390,9 @@ pub fn verify_partial_blind_signature( if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { return false; } + if bool::from(blind_sig.0.is_identity()){ + 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 @@ -534,6 +542,34 @@ mod tests { } + #[test] + fn test_blind_sign_with_identity_commitment_hash() { + let params = Parameters::new(1).unwrap(); + random_scalars_refs!(private_attributes, params, 1); + random_scalars_refs!(public_attributes, params, 0); + + // Call the function to prepare the blind sign + let (_commitments_openings, blind_sign_request) = prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); + let blind_sign_request = BlindSignRequest { + commitment_hash: G1Projective::identity(), + ..blind_sign_request // This copies the other fields from the existing instance + }; + + let signing_secret_key = SecretKey{ + x: params.random_scalar(), + ys: vec![params.random_scalar()], + }; + + // Call blind_sign and ensure it returns an error due to identity commitment hash + let result = blind_sign(¶ms, &signing_secret_key, &blind_sign_request, &public_attributes); + + // The result should be an error + assert!( + result.is_err(), + "blind_sign should return an error when commitment_hash is the identity point" + ); + } + #[test] fn successful_verify_partial_blind_signature() { let params = Parameters::new(4).unwrap(); diff --git a/common/nymcoconut/src/scheme/mod.rs b/common/nymcoconut/src/scheme/mod.rs index 691a8dc064..ce33dbb694 100644 --- a/common/nymcoconut/src/scheme/mod.rs +++ b/common/nymcoconut/src/scheme/mod.rs @@ -103,6 +103,11 @@ impl Signature { commitment_hash: &G1Projective, ) -> Result<()> { // Verify the commitment hash + if bool::from(self.0.is_identity()){ + return Err(CoconutError::Verification( + "Commitment hash should not be an identity point".to_string(), + )); + } if !(commitment_hash == &self.0) { return Err(CoconutError::Verification( "Verification of commitment hash from signature failed".to_string(), From eff725e8ecc6e16f5bbda7175a77db1fe83bf8e1 Mon Sep 17 00:00:00 2001 From: aniampio Date: Fri, 9 Aug 2024 00:11:27 +0100 Subject: [PATCH 006/207] Run cargo fmt --- common/nymcoconut/src/scheme/aggregation.rs | 2 +- common/nymcoconut/src/scheme/issuance.rs | 22 ++++++++++++--------- common/nymcoconut/src/scheme/mod.rs | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/common/nymcoconut/src/scheme/aggregation.rs b/common/nymcoconut/src/scheme/aggregation.rs index 5338be2dfa..68dbb037ef 100644 --- a/common/nymcoconut/src/scheme/aggregation.rs +++ b/common/nymcoconut/src/scheme/aggregation.rs @@ -115,7 +115,7 @@ pub fn aggregate_signatures_and_verify( .map(|(&attr, beta_i)| beta_i * attr) .sum::(); - if bool::from(signature.0.is_identity()){ + if bool::from(signature.0.is_identity()) { return Err(CoconutError::Aggregation( "Verification of the aggregated signature failed - h is an identity point".to_string(), )); diff --git a/common/nymcoconut/src/scheme/issuance.rs b/common/nymcoconut/src/scheme/issuance.rs index f882962e8c..fb3a09a7dd 100644 --- a/common/nymcoconut/src/scheme/issuance.rs +++ b/common/nymcoconut/src/scheme/issuance.rs @@ -258,7 +258,8 @@ pub fn prepare_blind_sign( loop { // Compute the attributes commitment - let (c_opening, c) = compute_attributes_commitment(params, private_attributes, public_attributes, hs); + let (c_opening, c) = + compute_attributes_commitment(params, private_attributes, public_attributes, hs); commitment_opening = c_opening; commitment = c; @@ -317,7 +318,7 @@ pub fn blind_sign( // Verify the commitment hash let h = compute_hash(blind_sign_request.commitment, public_attributes); - if bool::from(blind_sign_request.commitment_hash.is_identity()){ + if bool::from(blind_sign_request.commitment_hash.is_identity()) { return Err(CoconutError::Issuance( "Commitment hash should not be an identity point".to_string(), )); @@ -390,7 +391,7 @@ pub fn verify_partial_blind_signature( if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { return false; } - if bool::from(blind_sig.0.is_identity()){ + if bool::from(blind_sig.0.is_identity()) { return false; } @@ -541,7 +542,6 @@ mod tests { ); } - #[test] fn test_blind_sign_with_identity_commitment_hash() { let params = Parameters::new(1).unwrap(); @@ -549,19 +549,25 @@ mod tests { random_scalars_refs!(public_attributes, params, 0); // Call the function to prepare the blind sign - let (_commitments_openings, blind_sign_request) = prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); + let (_commitments_openings, blind_sign_request) = + prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); let blind_sign_request = BlindSignRequest { commitment_hash: G1Projective::identity(), ..blind_sign_request // This copies the other fields from the existing instance }; - let signing_secret_key = SecretKey{ + let signing_secret_key = SecretKey { x: params.random_scalar(), ys: vec![params.random_scalar()], }; // Call blind_sign and ensure it returns an error due to identity commitment hash - let result = blind_sign(¶ms, &signing_secret_key, &blind_sign_request, &public_attributes); + let result = blind_sign( + ¶ms, + &signing_secret_key, + &blind_sign_request, + &public_attributes, + ); // The result should be an error assert!( @@ -645,6 +651,4 @@ mod tests { validator2_keypair.verification_key() ),); } - - } diff --git a/common/nymcoconut/src/scheme/mod.rs b/common/nymcoconut/src/scheme/mod.rs index ce33dbb694..6ac5feb6a1 100644 --- a/common/nymcoconut/src/scheme/mod.rs +++ b/common/nymcoconut/src/scheme/mod.rs @@ -103,7 +103,7 @@ impl Signature { commitment_hash: &G1Projective, ) -> Result<()> { // Verify the commitment hash - if bool::from(self.0.is_identity()){ + if bool::from(self.0.is_identity()) { return Err(CoconutError::Verification( "Commitment hash should not be an identity point".to_string(), )); From 29bfd544f17821f537fe0703861dafb2b087b332 Mon Sep 17 00:00:00 2001 From: aniampio Date: Fri, 9 Aug 2024 00:29:29 +0100 Subject: [PATCH 007/207] Fix checks for non identity h in offline ecash --- common/nym_offline_compact_ecash/src/scheme/aggregation.rs | 3 +++ common/nym_offline_compact_ecash/src/scheme/withdrawal.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs index 4d7c116121..e237cee2ad 100644 --- a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs +++ b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs @@ -115,6 +115,9 @@ pub fn aggregate_signatures( .map(|(attr, beta_i)| beta_i * attr) .sum::(); + if bool::from(signature.h.is_identity()){ + return Err(CompactEcashError::AggregationVerification); + } if !check_bilinear_pairing( &signature.h.to_affine(), &G2Prepared::from((verification_key.alpha + tmp).to_affine()), diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 3a0b64077e..97cbb5ec88 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -416,6 +416,9 @@ pub fn issue_verify( if req_info.joined_commitment_hash != blind_signature.h { return Err(CompactEcashError::IssuanceVerification); } + if bool::from(blind_signature.h.is_identity()){ + return Err(CompactEcashError::IssuanceVerification); + } // Unblind the blinded signature on the partial signature let blinding_removers = vk_auth From 47a3c53cfb82ad91d092fde005a710842f356ce0 Mon Sep 17 00:00:00 2001 From: aniampio Date: Mon, 12 Aug 2024 21:43:32 +0100 Subject: [PATCH 008/207] Run fmt in ecash files --- .../src/scheme/aggregation.rs | 2 +- .../src/scheme/withdrawal.rs | 33 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs index e237cee2ad..0a8388196d 100644 --- a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs +++ b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs @@ -115,7 +115,7 @@ pub fn aggregate_signatures( .map(|(attr, beta_i)| beta_i * attr) .sum::(); - if bool::from(signature.h.is_identity()){ + if bool::from(signature.h.is_identity()) { return Err(CompactEcashError::AggregationVerification); } if !check_bilinear_pairing( diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 97cbb5ec88..8cc6f0e95e 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -139,12 +139,17 @@ fn generate_non_identity_h( params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v; // Compute commitment hash h - let joined_commitment_hash = - hash_g1((joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes()); + let joined_commitment_hash = hash_g1( + (joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(), + ); // Check if the joined_commitment_hash is not the identity element if !bool::from(joined_commitment_hash.is_identity()) { - return (joined_commitment, joined_commitment_hash, joined_commitment_opening); + return ( + joined_commitment, + joined_commitment_hash, + joined_commitment_opening, + ); } } } @@ -187,7 +192,6 @@ pub fn withdrawal_request( let (joined_commitment, joined_commitment_hash, joined_commitment_opening) = generate_non_identity_h(¶ms, sk_user, v, expiration_date, t_type); - // Compute Pedersen commitments for private attributes (wallet secret and user's secret) let private_attributes = vec![sk_user.sk, v]; let (private_attributes_openings, private_attributes_commitments) = @@ -416,7 +420,7 @@ pub fn issue_verify( if req_info.joined_commitment_hash != blind_signature.h { return Err(CompactEcashError::IssuanceVerification); } - if bool::from(blind_signature.h.is_identity()){ + if bool::from(blind_signature.h.is_identity()) { return Err(CompactEcashError::IssuanceVerification); } @@ -499,7 +503,7 @@ pub fn verify_partial_blind_signature( // Note: This check is useful if someone uses the code of those functions // to verify Pointcheval-Sanders signatures in a context different for their use // in zk-nyms - if bool::from(blind_sig.h.is_identity()){ + if bool::from(blind_sig.h.is_identity()) { return false; } // TODO: we're losing some memory here due to extra allocation, @@ -560,17 +564,19 @@ pub fn verify_partial_blind_signature( #[cfg(test)] mod tests { + use super::{generate_non_identity_h, verify_partial_blind_signature}; + use crate::common_types::BlindedSignature; use crate::ecash_group_parameters; use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; - use super::{generate_non_identity_h, verify_partial_blind_signature}; - use crate::common_types::{BlindedSignature}; - use bls12_381::{G1Projective}; + use bls12_381::G1Projective; #[test] fn test_generate_non_identity_h() { let params = ecash_group_parameters(); // Create dummy values for testing - let sk_user = SecretKeyUser { sk: params.random_scalar() }; + let sk_user = SecretKeyUser { + sk: params.random_scalar(), + }; let v = params.random_scalar(); let expiration_date = params.random_scalar(); let t_type = params.random_scalar(); @@ -580,7 +586,10 @@ mod tests { generate_non_identity_h(¶ms, &sk_user, v, expiration_date, t_type); // Ensure that the joined_commitment_hash is not the identity element - assert!(!bool::from(joined_commitment_hash.is_identity()), "Joined commitment hash should not be the identity element"); + assert!( + !bool::from(joined_commitment_hash.is_identity()), + "Joined commitment hash should not be the identity element" + ); } #[test] @@ -611,4 +620,4 @@ mod tests { "Expected verification to return false for identity h in blind signature" ); } -} \ No newline at end of file +} From 96ead0b19e3f9230b8ae73fc58efeccc7b915564 Mon Sep 17 00:00:00 2001 From: aniampio Date: Mon, 12 Aug 2024 21:51:16 +0100 Subject: [PATCH 009/207] Fix function input --- common/nym_offline_compact_ecash/src/scheme/withdrawal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 8cc6f0e95e..18666a35d6 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -190,7 +190,7 @@ pub fn withdrawal_request( // Generate a non-identity commitment hash let (joined_commitment, joined_commitment_hash, joined_commitment_opening) = - generate_non_identity_h(¶ms, sk_user, v, expiration_date, t_type); + generate_non_identity_h(params, sk_user, v, expiration_date, t_type); // Compute Pedersen commitments for private attributes (wallet secret and user's secret) let private_attributes = vec![sk_user.sk, v]; From 4957d9dbf223c2cf26d063a3b5c0c2e887933df5 Mon Sep 17 00:00:00 2001 From: aniampio Date: Mon, 12 Aug 2024 22:09:18 +0100 Subject: [PATCH 010/207] Fix clippy --- common/nym_offline_compact_ecash/src/scheme/withdrawal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 18666a35d6..2e9474566d 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -583,7 +583,7 @@ mod tests { // Generate the commitment and hash let (_, joined_commitment_hash, _) = - generate_non_identity_h(¶ms, &sk_user, v, expiration_date, t_type); + generate_non_identity_h(params, &sk_user, v, expiration_date, t_type); // Ensure that the joined_commitment_hash is not the identity element assert!( From f86e0886631a98b0638fe09e6fcbe5458d47adc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 26 Sep 2024 16:30:57 +0100 Subject: [PATCH 011/207] bugfix: correctly paginate through 'search_tx' endpoint --- .../nyxd/cosmwasm_client/client_traits/query_client.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index f63399ce18..47859ea0d1 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -218,17 +218,19 @@ pub trait CosmWasmClient: TendermintRpcClient { loop { let mut res = self - .tx_search(query.clone(), false, page, 100, Order::Ascending) + .tx_search(query.clone(), false, page, per_page, Order::Ascending) .await?; - results.append(&mut res.txs); // sanity check for if tendermint's maximum per_page was modified - // we don't want to accidentally be stuck in an infinite loop - if res.total_count == 0 || res.txs.is_empty() { + let early_break = res.total_count == 0 || res.txs.is_empty(); + results.append(&mut res.txs); + + if early_break { break; } - if res.total_count >= per_page { + if res.total_count > results.len() as u32 { page += 1 } else { break; From 894e0bd1bf58e6bf866e09b40b7aba71b2f84e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Thu, 26 Sep 2024 18:00:13 +0200 Subject: [PATCH 012/207] Add more conversions for responses of authenticator messages (#4929) * More conversions for responses * Expose version --- common/authenticator-requests/src/v1/mod.rs | 2 +- .../src/v2/conversion.rs | 105 ++++++++++++++++++ common/authenticator-requests/src/v2/mod.rs | 2 +- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/common/authenticator-requests/src/v1/mod.rs b/common/authenticator-requests/src/v1/mod.rs index 90363b4f89..2644abee60 100644 --- a/common/authenticator-requests/src/v1/mod.rs +++ b/common/authenticator-requests/src/v1/mod.rs @@ -10,4 +10,4 @@ pub use registration::{ClientMac, GatewayClient, InitMessage, Nonce}; #[cfg(feature = "verify")] pub use registration::HmacSha256; -const VERSION: u8 = 1; +pub const VERSION: u8 = 1; diff --git a/common/authenticator-requests/src/v2/conversion.rs b/common/authenticator-requests/src/v2/conversion.rs index 1c40a4e357..ccf87b538d 100644 --- a/common/authenticator-requests/src/v2/conversion.rs +++ b/common/authenticator-requests/src/v2/conversion.rs @@ -62,8 +62,113 @@ impl From for v2::registration::GatewayClient { } } +impl From for v1::registration::GatewayClient { + fn from(gw_client: v2::registration::GatewayClient) -> Self { + Self { + pub_key: gw_client.pub_key, + private_ip: gw_client.private_ip, + mac: gw_client.mac.into(), + } + } +} + impl From for v2::registration::ClientMac { fn from(mac: v1::registration::ClientMac) -> Self { Self::new(mac.to_vec()) } } + +impl From for v1::registration::ClientMac { + fn from(mac: v2::registration::ClientMac) -> Self { + Self::new(mac.to_vec()) + } +} + +impl From for v1::response::AuthenticatorResponse { + fn from(authenticator_response: v2::response::AuthenticatorResponse) -> Self { + Self { + version: authenticator_response.protocol.version, + data: authenticator_response.data.into(), + reply_to: authenticator_response.reply_to, + } + } +} + +impl From for v1::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v2::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => v1::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response.into(), + ), + v2::response::AuthenticatorResponseData::Registered(registered_response) => { + v1::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v1::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + } + } +} + +impl From for v1::response::PendingRegistrationResponse { + fn from(value: v2::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v1::response::RegisteredResponse { + fn from(value: v2::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v1::response::RemainingBandwidthResponse { + fn from(value: v2::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.map(Into::into), + } + } +} + +impl From for v1::registration::RegistrationData { + fn from(value: v2::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v1::registration::RegistredData { + fn from(value: v2::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ip: value.private_ip, + wg_port: value.wg_port, + } + } +} + +impl From for v1::registration::RemainingBandwidthData { + fn from(value: v2::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + suspended: false, + } + } +} diff --git a/common/authenticator-requests/src/v2/mod.rs b/common/authenticator-requests/src/v2/mod.rs index 29be89580d..3dd3373553 100644 --- a/common/authenticator-requests/src/v2/mod.rs +++ b/common/authenticator-requests/src/v2/mod.rs @@ -6,4 +6,4 @@ pub mod registration; pub mod request; pub mod response; -const VERSION: u8 = 2; +pub const VERSION: u8 = 2; From fabd48b7ea564df952abe058db93025a80a27f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Thu, 26 Sep 2024 18:44:21 +0200 Subject: [PATCH 013/207] Fix broken build after merge (#4937) --- common/authenticator-requests/src/v2/conversion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/authenticator-requests/src/v2/conversion.rs b/common/authenticator-requests/src/v2/conversion.rs index ccf87b538d..b8e16ac4ba 100644 --- a/common/authenticator-requests/src/v2/conversion.rs +++ b/common/authenticator-requests/src/v2/conversion.rs @@ -167,7 +167,7 @@ impl From for v1::registration::RegistredData { impl From for v1::registration::RemainingBandwidthData { fn from(value: v2::registration::RemainingBandwidthData) -> Self { Self { - available_bandwidth: value.available_bandwidth, + available_bandwidth: value.available_bandwidth as u64, suspended: false, } } From e08fc4894b96316b0c41ccf433e4371d97c5b45f Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:12:15 +0000 Subject: [PATCH 014/207] [DOCs/operators]: Release notes v2024.11-wedel (#4939) * finish release notes * add a note --- documentation/operators/src/changelog.md | 236 +++++++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/documentation/operators/src/changelog.md b/documentation/operators/src/changelog.md index 81dafbb6c2..a8a90456e2 100644 --- a/documentation/operators/src/changelog.md +++ b/documentation/operators/src/changelog.md @@ -2,6 +2,242 @@ This page displays a full list of all the changes during our release cycle from [`v2024.3-eclipse`](https://github.com/nymtech/nym/blob/nym-binaries-v2024.3-eclipse/CHANGELOG.md) onwards. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first. +## `v2024.11-wedel` + +- [Release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.11-wedel) +- [Release CHANGELOG.md](https://github.com/nymtech/nym/blob/nym-binaries-v2024.11-wedel/CHANGELOG.md) +- [`nym-node`](nodes/nym-node.md) version `1.1.8` + +```sh +Binary Name: nym-node +Build Timestamp: 2024-09-27T11:02:37.073944654Z +Build Version: 1.1.8 +Commit SHA: c3ec970a377adb25d57be5428551fada2ec55128 +Commit Date: 2024-09-26T08:24:53.000000000+02:00 +Commit Branch: master +rustc Version: 1.80.1 +rustc Channel: stable +cargo Profile: release +``` + + +### Features + +- [New Network Monitor](https://github.com/nymtech/nym/pull/4610): Monitors the Nym network by sending itself packages across the mixnet. Network monitor is running two tokio tasks, one manages mixnet clients and another manages monitoring itself. Monitor is designed to be driven externally, via an `HTTP api`. This means that it does not do any monitoring unless driven by something like [`locust`](https://locust.io/). This allows us to tailor the load externally, potentially distributing it across multiple monitors. Includes a dockerised setup for automatically spinning up monitor and driving it with locust. + - *Note: NNM is not deployed on mainnet yet!* + +- [Add get_mixnodes_described to validator_client](https://github.com/nymtech/nym/pull/4725) + +- [Remove deprecated mark_as_success and use new disarm](https://github.com/nymtech/nym/pull/4751): Update function name to keep terminology consistent with tokio `CancellationToken DropGuard`. + +- [Update peer refresh value](https://github.com/nymtech/nym/pull/4754): `lso` expose the value by moving it to wireguard types, and separate the refresh time to the database sync time, so that more probable and needed actions happen faster (refresh) and more improbable ones don't overload the system (peer suspended or stale) +~~~admonish example collapsible=true title='Testing steps performed' +- **Noted** that the constants `DEFAULT_PEER_TIMEOUT` and `DEFAULT_PEER_TIMEOUT_CHECK` have been moved to `common/wireguard-types/src/lib.rs` and are now being used across modules for consistency +- **Observed** that the `peer_controller.rs` now separates the in-memory updates from the storage sync operations to reduce system load +- **Identified** that in-memory updates of peer bandwidth usage happen every `DEFAULT_PEER_TIMEOUT_CHECK` (every 5 seconds), while storage updates occur every 5 * `DEFAULT_PEER_TIMEOUT_CHECK` (every 25 seconds) + +**Checked System Load and Performance:** + +- **Monitored** system resource usage (CPU, memory, I/O) during the test to assess the impact of the changes +- **Confirmed** that the separation of in-memory updates and storage syncs resulted in reduced system load, particularly I/O operations, compared to previous versions where storage updates occurred more frequently +- **Ensured** that the system remained responsive and no performance bottlenecks were introduced + +- **Efficiency Improvement:** The separation of in-memory updates and storage syncs effectively reduced unnecessary database writes, improving system efficiency without compromising data accuracy +~~~ + +- [Remove duplicate stat count for retransmissions](https://github.com/nymtech/nym/pull/4756) + +- [Make gateway latency check generic](https://github.com/nymtech/nym/pull/4759): Replace concrete gateway type with trait in latency check, so we can make use of it in the vpn client. +~~~admonish example collapsible=true title='Testing steps performed' +- Initialised new `nym-client` with the `--latency-based-selection` flag and ensured it still works as normal. +~~~ + +- [chore: remove repetitive words](https://github.com/nymtech/nym/pull/4763) + +- [Avoid race on ip and registration structures](https://github.com/nymtech/nym/pull/4766): To avoid a state where the ip is being cleared out before the registration is also cleared out, couple the two structures under the same lock, since they are anyway very inter-dependent. +~~~admonish example collapsible=true title='Testing steps performed' +1. - **Checked out** the release/2024.10-wedel branch containing the fix for the race condition on IP and registration structures + - **Deployed** the on a controlled test environment to prevent interference + +2. **Monitored Logs:** + + - **Enabled** debug logging to capture all events + - **Monitored** logs in real-time to observe the handling of concurrent registration requests + - **Checked** for any error messages, warnings, or indications of race conditions + +3. **Verified Client Responses:** + + - Ensured that all clients received appropriate responses: + - Successful registration with assigned IP and registration data + - Appropriate error messages if no IPs were available or if other issues occurred + - Confirmed that no clients were left in an inconsistent state (e.g., assigned an IP but not fully registered) + +4. **Validated Normal Operation:** + - **Conducted standard registration processes** with individual clients to confirm that regular functionality is unaffected via `nym-vpn-cli` + - Ensured that authenticated clients could communicate over the network as expected +~~~ + +- [Persist used wireguard private IPs](https://github.com/nymtech/nym/pull/4771) + +- [Enable dependabot version upgrades for root rust workspace](https://github.com/nymtech/nym/pull/4778) + +- [Fix clippy for `unwrap_or_default`](https://github.com/nymtech/nym/pull/4783): Fix nightly build for [beta toolchain](https://github.com/nymtech/nym/actions/runs/10552082396/job/29230401668) + +- [Update dependabot](https://github.com/nymtech/nym/pull/4796): Bump max number of dependabot rust PRs to 10. Add readme entry to workspace package. + +- [Run `cargo-autoinherit` for a few new crates](https://github.com/nymtech/nym/pull/4801): Run cargo-autoinherit for a few new crates - Sort crates list. + +- [Add `axum` server to `nym-api`](https://github.com/nymtech/nym/pull/4803): Summary PR to add axum functionality behind a feature flag `axum`, alongside rocket. + +- [Remove unused wireguard flag from SDK](https://github.com/nymtech/nym/pull/4823) + +- [Expose wireguard details on self described endpoint](https://github.com/nymtech/nym/pull/4825) +~~~admonish example collapsible=true title='Testing steps performed' +Wireguard details are now visible at the nym-node endpoint `/api/v1/gateway/client-interfaces` as well as on the nym-api self-described endpoint `/api/v1/gateways/described`, above the existing data displaying mixnet_websocket information. + +An example of what will be shown is: +```json + "wireguard": { + "port": 51822, + "public_key": "" + } +``` +~~~ + +- [Revamped ticketbook serialisation and exposed additional cli methods](https://github.com/nymtech/nym/pull/4827): `wip` branch that includes changes needed for `vpn-api` alongside additional `ecash utils` +~~~admonish example collapsible=true title='Testing steps performed' +Checked the following commands: +```sh +show-ticket-books # which displays the information about all ticketbooks associated to the client +import-ticket-book # which imports a normal ticketbook to the client alongside `--full` flag +``` + +On the cli, the following were added: `import-coin-index-signatures`, `import-expiration-date-signatures` and `import-master-verification-key`. +~~~ + +- [Run cargo autoinherit following last weeks dependabot updates](https://github.com/nymtech/nym/pull/4831) + +- [Remove serde_crate named import](https://github.com/nymtech/nym/pull/4832) + +- [Create nym-repo-setup debian package and nym-vpn meta package](https://github.com/nymtech/nym/pull/4837): Create nym-repo-setup debian package that sets up the nymtech debian repo on the system it's installed on. It does 2 things: + + 1. Copy the keyring to `/usr/share/keyrings/nymtech.gpg` + 2. Copy the repo spec to `/etc/apt/sources.list.d/nymtech.list` + - Also create a meta package `nym-vpn` which only purpose is to depend on the daemon and UI. + +~~~admonish example collapsible=true title='Usage' +1. Install with +```sh +sudo dpkg -i ./nym-repo-setup.deb +``` +2. Once it's installed, it should be possible to install the vpn client with +```sh +sudo apt install nym-vpnc +``` +3. To reemove the repo, use +```sh +sudo apt remove nym-repo-setup +``` + +NOTE: removing the repo will not remove any installed nym-vpn packages +~~~ + +~~~admonish example collapsible=true title='Testing steps performed' + +1. **Downloaded** the `nym-repo-setup.deb` package to a Debian-based test system + +2. **Installed** the repository setup package using the command: +```bash +sudo dpkg -i ./nym-repo-setup.deb +``` + +3. **Verified** that the GPG keyring was copied to `/usr/share/keyrings/nymtech.gpg`: +```bash +ls -l /usr/share/keyrings/nymtech.gpg +``` + +4. **Checked** that the repository specification was added to `/etc/apt/sources.list.d/nymtech.list`: +```bash +cat /etc/apt/sources.list.d/nymtech.list +``` + + 5. **Updated** the package list: +```bash +sudo apt update +``` + +6. **Installed** the VPN client meta-package: +```bash +sudo apt install nym-vpnc +``` + +7. **Confirmed** that the `nym-vpnc` package and its dependencies (daemon and UI) were installed successfully + +8. **Tested** the VPN client to ensure it operates as expected + +9. **Removed** the repository setup package: +```bash +sudo apt remove nym-repo-setup +``` + +10. **Verified** that the repository specification file `/etc/apt/sources.list.d/nymtech.list` was removed + +11. **Ensured** that the installed `nym-vpnc` packages remained installed and functional after removing the repo setup package +~~~ + +- [Use ecash credential type for bandwidth value](https://github.com/nymtech/nym/pull/4840) + +- [Start switching over jobs to arc-ubuntu-20.04](https://github.com/nymtech/nym/pull/4843) + +~~~admonish example collapsible=true title='`ci-binary-config-checker`' +``` + - ci-build-upload-binaries + - ci-build + - ci-cargo-deny + - ci-contracts-schema + - ci-contracts-upload-binaries + - ci-contracts + - ci-docs + - ci-nym-wallet-rust + - ci-sdk-wasm +``` +~~~ + +- [Move credential verification into common crate](https://github.com/nymtech/nym/pull/4853) + +- [Revert runner for `ci-docs`](https://github.com/nymtech/nym/pull/4855) + +- [Remove `golang` workaround in `ci-sdk-wasm`](https://github.com/nymtech/nym/pull/4858) + +- [Fix linux conditional in `ci-build.yml`](https://github.com/nymtech/nym/pull/4863) + +- [Disable push trigger and add missing paths in `ci-build`](https://github.com/nymtech/nym/pull/4864) + +- [chore: removed completed queued mixnet migration](https://github.com/nymtech/nym/pull/4865) + +- [Bump defguard to github latest version](https://github.com/nymtech/nym/pull/4872) + +- [Backport #4894 to fix ci](https://github.com/nymtech/nym/pull/4899) + +### Bugfix + +- [Fix test failure in ipr request size](https://github.com/nymtech/nym/pull/4844): Nightly build started failing due to a unit test using `now()`, changing the serialized size. Fixed to use a fixed date. + +- [Fix clippy for nym-wallet and latest rustc](https://github.com/nymtech/nym/pull/4845) + +- [Allow updating globally stored signatures](https://github.com/nymtech/nym/pull/4891) + +- [Bugfix/ticketbook false double spending](https://github.com/nymtech/nym/pull/4892) +~~~admonish example collapsible=true title='Testing steps performed' +Tested running a client in mixnet mode, with a standard ticketbook, as well as a client using an imported ticketbook. The double spending bug is no longer an issue, bandwidth is consumed properly, and upon consumption of one ticket another ticket is properly obtained. +~~~ + +### Operators Guide, Tooling & Updates + +- [WSS setup guide updates](https://github.com/nymtech/nym/commit/05d6652177fb77324f8c38b3d8a547d07e729fec): Operators setting up WSS and reverse proxy on Gateways have now cleaner and simpler guide to configure their VPS. + +- [Updat hostname instruction for WSS](https://github.com/nymtech/nym/commit/7146c4c012ba7012dc74edc8510bbf377dc32fba): Adding a hostname instruction for clarity + ## `nym-node` patch from `release/2024.10-caramello` - [Patch release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.10-caramello-patch) From d7eecd481c47d7958c199010bc76a430e24cc3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 14:13:09 +0100 Subject: [PATCH 015/207] decreased SOCKET_FAILURE_RESET the previous value of 2h was way too big. especially since it was quite likely for multiple failures to occur hourly during increased validator load when mixnet epoch was getting transitioned --- common/nyxd-scraper/src/scraper/subscriber.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/nyxd-scraper/src/scraper/subscriber.rs b/common/nyxd-scraper/src/scraper/subscriber.rs index 461d362eb5..c2b5bdbbfd 100644 --- a/common/nyxd-scraper/src/scraper/subscriber.rs +++ b/common/nyxd-scraper/src/scraper/subscriber.rs @@ -16,7 +16,7 @@ use url::Url; const MAX_FAILURES: usize = 10; const MAX_RECONNECTION_ATTEMPTS: usize = 8; -const SOCKET_FAILURE_RESET: Duration = Duration::hours(2); +const SOCKET_FAILURE_RESET: Duration = Duration::minutes(15); pub struct ChainSubscriber { cancel: CancellationToken, From 5e959924271b7083a3fa0b5865da148c69981561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 14:57:39 +0100 Subject: [PATCH 016/207] introduced cli command to process an individual block --- .../nyxd-scraper/src/block_processor/mod.rs | 10 ++- common/nyxd-scraper/src/error.rs | 2 +- common/nyxd-scraper/src/scraper/mod.rs | 74 +++++++++++++++---- common/nyxd-scraper/src/storage/manager.rs | 2 +- nym-validator-rewarder/src/cli/mod.rs | 5 ++ .../src/cli/process_block.rs | 32 ++++++++ 6 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 nym-validator-rewarder/src/cli/process_block.rs diff --git a/common/nyxd-scraper/src/block_processor/mod.rs b/common/nyxd-scraper/src/block_processor/mod.rs index 2b1748b633..baa6ffed7e 100644 --- a/common/nyxd-scraper/src/block_processor/mod.rs +++ b/common/nyxd-scraper/src/block_processor/mod.rs @@ -99,7 +99,15 @@ impl BlockProcessor { }) } - async fn process_block(&mut self, block: BlockToProcess) -> Result<(), ScraperError> { + pub fn with_pruning(mut self, pruning_options: PruningOptions) -> Self { + self.pruning_options = pruning_options; + self + } + + pub(super) async fn process_block( + &mut self, + block: BlockToProcess, + ) -> Result<(), ScraperError> { info!("processing block at height {}", block.height); let full_info = self.rpc_client.try_get_full_details(block).await?; diff --git a/common/nyxd-scraper/src/error.rs b/common/nyxd-scraper/src/error.rs index 079a28975d..9876536aa5 100644 --- a/common/nyxd-scraper/src/error.rs +++ b/common/nyxd-scraper/src/error.rs @@ -16,7 +16,7 @@ pub enum ScraperError { #[error("failed to perform startup SQL migration: {0}")] StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), - #[error("can't add any modules to the scraper as it's already running")] + #[error("the block scraper is already running")] ScraperAlreadyRunning, #[error("failed to establish websocket connection to {url}: {source}")] diff --git a/common/nyxd-scraper/src/scraper/mod.rs b/common/nyxd-scraper/src/scraper/mod.rs index 608731b489..140023a80f 100644 --- a/common/nyxd-scraper/src/scraper/mod.rs +++ b/common/nyxd-scraper/src/scraper/mod.rs @@ -1,8 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::block_processor::types::BlockToProcess; use crate::block_processor::BlockProcessor; -use crate::block_requester::BlockRequester; +use crate::block_requester::{BlockRequest, BlockRequester}; use crate::error::ScraperError; use crate::modules::{BlockModule, MsgModule, TxModule}; use crate::rpc_client::RpcClient; @@ -11,7 +12,9 @@ use crate::storage::ScraperStorage; use crate::PruningOptions; use std::path::PathBuf; use std::sync::Arc; -use tokio::sync::mpsc::{channel, unbounded_channel}; +use tokio::sync::mpsc::{ + channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender, +}; use tokio::sync::Notify; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; @@ -115,6 +118,7 @@ pub struct NyxdScraper { cancel_token: CancellationToken, startup_sync: Arc, pub storage: ScraperStorage, + rpc_client: RpcClient, } impl NyxdScraper { @@ -125,6 +129,7 @@ impl NyxdScraper { pub async fn new(config: Config) -> Result { config.pruning_options.validate()?; let storage = ScraperStorage::init(&config.database_path).await?; + let rpc_client = RpcClient::new(&config.rpc_url)?; Ok(NyxdScraper { config, @@ -132,6 +137,7 @@ impl NyxdScraper { cancel_token: CancellationToken::new(), startup_sync: Arc::new(Default::default()), storage, + rpc_client, }) } @@ -151,36 +157,74 @@ impl NyxdScraper { self.task_tracker.close(); } - pub async fn start(&self) -> Result<(), ScraperError> { - let (processing_tx, processing_rx) = unbounded_channel(); - let (req_tx, req_rx) = channel(5); + pub async fn process_single_block(&self, height: u32) -> Result<(), ScraperError> { + if !self.task_tracker.is_empty() { + return Err(ScraperError::ScraperAlreadyRunning); + } - let rpc_client = RpcClient::new(&self.config.rpc_url)?; + let (_, processing_rx) = unbounded_channel(); + let (req_tx, _) = channel(5); - // create the tasks - let block_requester = BlockRequester::new( + let block = self.rpc_client.get_basic_block_details(height).await?; + + let mut block_processor = self + .new_block_processor(req_tx.clone(), processing_rx) + .await? + .with_pruning(PruningOptions::nothing()); + block_processor.process_block(block.into()).await + } + + fn new_block_requester( + &self, + req_rx: Receiver, + processing_tx: UnboundedSender, + ) -> BlockRequester { + BlockRequester::new( self.cancel_token.clone(), - rpc_client.clone(), + self.rpc_client.clone(), req_rx, processing_tx.clone(), - ); - let block_processor = BlockProcessor::new( + ) + } + + async fn new_block_processor( + &self, + req_tx: Sender, + processing_rx: UnboundedReceiver, + ) -> Result { + BlockProcessor::new( self.config.pruning_options, self.cancel_token.clone(), self.startup_sync.clone(), processing_rx, req_tx, self.storage.clone(), - rpc_client, + self.rpc_client.clone(), ) - .await?; - let chain_subscriber = ChainSubscriber::new( + .await + } + + async fn new_chain_subscriber( + &self, + processing_tx: UnboundedSender, + ) -> Result { + ChainSubscriber::new( &self.config.websocket_url, self.cancel_token.clone(), self.task_tracker.clone(), processing_tx, ) - .await?; + .await + } + + pub async fn start(&self) -> Result<(), ScraperError> { + let (processing_tx, processing_rx) = unbounded_channel(); + let (req_tx, req_rx) = channel(5); + + // create the tasks + let block_requester = self.new_block_requester(req_rx, processing_tx.clone()); + let block_processor = self.new_block_processor(req_tx, processing_rx).await?; + let chain_subscriber = self.new_chain_subscriber(processing_tx).await?; // spawn them self.start_tasks(block_requester, block_processor, chain_subscriber); diff --git a/common/nyxd-scraper/src/storage/manager.rs b/common/nyxd-scraper/src/storage/manager.rs index 04af3e364b..780b6ad9c9 100644 --- a/common/nyxd-scraper/src/storage/manager.rs +++ b/common/nyxd-scraper/src/storage/manager.rs @@ -435,7 +435,7 @@ where trace!("update_last_processed"); let start = Instant::now(); - sqlx::query!("UPDATE metadata SET last_processed_height = ?", height) + sqlx::query!("UPDATE metadata SET last_processed_height = MAX(last_processed_height, ?)", height) .execute(executor) .await?; log_db_operation_time("update_last_processed", start); diff --git a/nym-validator-rewarder/src/cli/mod.rs b/nym-validator-rewarder/src/cli/mod.rs index 9fd6cf4ba8..4497a7994b 100644 --- a/nym-validator-rewarder/src/cli/mod.rs +++ b/nym-validator-rewarder/src/cli/mod.rs @@ -13,6 +13,7 @@ use url::Url; pub mod build_info; pub mod init; +pub mod process_block; pub mod run; pub mod upgrade_helpers; @@ -42,6 +43,7 @@ impl Cli { match self.command { Commands::Init(args) => init::execute(args), Commands::Run(args) => run::execute(args).await, + Commands::ProcessBlock(args) => process_block::execute(args).await, Commands::BuildInfo(args) => build_info::execute(args), } } @@ -97,6 +99,9 @@ pub(crate) enum Commands { /// Run the validator rewarder with the preconfigured settings. Run(run::Args), + /// Attempt to process a single block. + ProcessBlock(process_block::Args), + /// Show build information of this binary BuildInfo(build_info::Args), } diff --git a/nym-validator-rewarder/src/cli/process_block.rs b/nym-validator-rewarder/src/cli/process_block.rs new file mode 100644 index 0000000000..3fa92268d0 --- /dev/null +++ b/nym-validator-rewarder/src/cli/process_block.rs @@ -0,0 +1,32 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::{try_load_current_config, ConfigOverridableArgs}; +use crate::error::NymRewarderError; +use nyxd_scraper::NyxdScraper; +use std::path::PathBuf; + +#[derive(Debug, clap::Args)] +pub struct Args { + #[command(flatten)] + config_override: ConfigOverridableArgs, + + /// Height of the block we want to process + #[clap(long)] + height: u32, + + /// Specifies custom location for the configuration file of nym validators rewarder. + #[clap(long)] + custom_config_path: Option, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { + let config = + try_load_current_config(&args.custom_config_path)?.with_override(args.config_override); + + NyxdScraper::new(config.scraper_config()) + .await? + .process_single_block(args.height) + .await?; + Ok(()) +} From 457c478a036cfadeb008a7762cafb55ed8339b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 15:15:13 +0100 Subject: [PATCH 017/207] introduced cli command to process a block range --- .../nyxd-scraper/src/block_processor/mod.rs | 4 ++ common/nyxd-scraper/src/scraper/mod.rs | 67 ++++++++++++++++++- common/nyxd-scraper/src/storage/manager.rs | 9 ++- nym-validator-rewarder/src/cli/mod.rs | 5 ++ .../src/cli/process_block.rs | 2 +- .../src/cli/process_until.rs | 44 ++++++++++++ 6 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 nym-validator-rewarder/src/cli/process_until.rs diff --git a/common/nyxd-scraper/src/block_processor/mod.rs b/common/nyxd-scraper/src/block_processor/mod.rs index baa6ffed7e..f1b6142a4b 100644 --- a/common/nyxd-scraper/src/block_processor/mod.rs +++ b/common/nyxd-scraper/src/block_processor/mod.rs @@ -177,6 +177,10 @@ impl BlockProcessor { self.msg_modules = modules; } + pub(super) fn last_process_height(&self) -> u32 { + self.last_processed_height + } + async fn maybe_request_missing_blocks(&mut self) -> Result<(), ScraperError> { // we're still processing, so we're good if self.last_processed_at.elapsed() < MAX_MISSING_BLOCKS_DELAY { diff --git a/common/nyxd-scraper/src/scraper/mod.rs b/common/nyxd-scraper/src/scraper/mod.rs index 140023a80f..5c838de50a 100644 --- a/common/nyxd-scraper/src/scraper/mod.rs +++ b/common/nyxd-scraper/src/scraper/mod.rs @@ -10,6 +10,7 @@ use crate::rpc_client::RpcClient; use crate::scraper::subscriber::ChainSubscriber; use crate::storage::ScraperStorage; use crate::PruningOptions; +use futures::future::join_all; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::mpsc::{ @@ -18,7 +19,7 @@ use tokio::sync::mpsc::{ use tokio::sync::Notify; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; -use tracing::info; +use tracing::{error, info}; use url::Url; mod subscriber; @@ -158,6 +159,7 @@ impl NyxdScraper { } pub async fn process_single_block(&self, height: u32) -> Result<(), ScraperError> { + info!(height = height, "attempting to process a single block"); if !self.task_tracker.is_empty() { return Err(ScraperError::ScraperAlreadyRunning); } @@ -165,13 +167,74 @@ impl NyxdScraper { let (_, processing_rx) = unbounded_channel(); let (req_tx, _) = channel(5); + let mut block_processor = self + .new_block_processor(req_tx.clone(), processing_rx) + .await? + .with_pruning(PruningOptions::nothing()); + let block = self.rpc_client.get_basic_block_details(height).await?; + block_processor.process_block(block.into()).await + } + + pub async fn process_block_range( + &self, + starting_height: Option, + end_height: u32, + ) -> Result<(), ScraperError> { + if !self.task_tracker.is_empty() { + return Err(ScraperError::ScraperAlreadyRunning); + } + + let (_, processing_rx) = unbounded_channel(); + let (req_tx, _) = channel(5); + let mut block_processor = self .new_block_processor(req_tx.clone(), processing_rx) .await? .with_pruning(PruningOptions::nothing()); - block_processor.process_block(block.into()).await + + let starting_height = match starting_height { + Some(explicit) => explicit, + None => { + let last_processed = block_processor.last_process_height(); + if last_processed != 0 { + last_processed + } else { + self.rpc_client.current_block_height().await? as u32 + } + } + }; + + info!( + starting_height = starting_height, + end_height = end_height, + "attempting to process block range" + ); + + let range = (starting_height..=end_height).collect::>(); + + // the most likely bottleneck here are going to be the chain queries, + // so batch multiple requests + for batch in range.chunks(4) { + let batch_result = join_all( + batch + .iter() + .map(|height| self.rpc_client.get_basic_block_details(*height)), + ) + .await; + for result in batch_result { + match result { + Ok(block) => block_processor.process_block(block.into()).await?, + Err(err) => { + error!("failed to retrieve the block: {err}. stopping..."); + return Err(err); + } + } + } + } + + Ok(()) } fn new_block_requester( diff --git a/common/nyxd-scraper/src/storage/manager.rs b/common/nyxd-scraper/src/storage/manager.rs index 780b6ad9c9..fb40a065b8 100644 --- a/common/nyxd-scraper/src/storage/manager.rs +++ b/common/nyxd-scraper/src/storage/manager.rs @@ -435,9 +435,12 @@ where trace!("update_last_processed"); let start = Instant::now(); - sqlx::query!("UPDATE metadata SET last_processed_height = MAX(last_processed_height, ?)", height) - .execute(executor) - .await?; + sqlx::query!( + "UPDATE metadata SET last_processed_height = MAX(last_processed_height, ?)", + height + ) + .execute(executor) + .await?; log_db_operation_time("update_last_processed", start); Ok(()) diff --git a/nym-validator-rewarder/src/cli/mod.rs b/nym-validator-rewarder/src/cli/mod.rs index 4497a7994b..e986b5f4d2 100644 --- a/nym-validator-rewarder/src/cli/mod.rs +++ b/nym-validator-rewarder/src/cli/mod.rs @@ -14,6 +14,7 @@ use url::Url; pub mod build_info; pub mod init; pub mod process_block; +pub mod process_until; pub mod run; pub mod upgrade_helpers; @@ -44,6 +45,7 @@ impl Cli { Commands::Init(args) => init::execute(args), Commands::Run(args) => run::execute(args).await, Commands::ProcessBlock(args) => process_block::execute(args).await, + Commands::ProcessUntil(args) => process_until::execute(args).await, Commands::BuildInfo(args) => build_info::execute(args), } } @@ -102,6 +104,9 @@ pub(crate) enum Commands { /// Attempt to process a single block. ProcessBlock(process_block::Args), + /// Attempt to process multiple blocks until the provided height. + ProcessUntil(process_until::Args), + /// Show build information of this binary BuildInfo(build_info::Args), } diff --git a/nym-validator-rewarder/src/cli/process_block.rs b/nym-validator-rewarder/src/cli/process_block.rs index 3fa92268d0..7f395bca25 100644 --- a/nym-validator-rewarder/src/cli/process_block.rs +++ b/nym-validator-rewarder/src/cli/process_block.rs @@ -1,4 +1,4 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::cli::{try_load_current_config, ConfigOverridableArgs}; diff --git a/nym-validator-rewarder/src/cli/process_until.rs b/nym-validator-rewarder/src/cli/process_until.rs new file mode 100644 index 0000000000..43b235ec96 --- /dev/null +++ b/nym-validator-rewarder/src/cli/process_until.rs @@ -0,0 +1,44 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::{try_load_current_config, ConfigOverridableArgs}; +use crate::error::NymRewarderError; +use nyxd_scraper::NyxdScraper; +use std::path::PathBuf; + +#[derive(Debug, clap::Args)] +pub struct Args { + #[command(flatten)] + config_override: ConfigOverridableArgs, + + /// Optional starting height for processing the blocks. + /// If none is provided, the default behaviour will be applied. + #[clap(long)] + start_height: Option, + + /// Height of until we want to be processing the blocks + #[clap(long)] + stop_height: u32, + + /// Specifies custom location for the configuration file of nym validators rewarder. + #[clap(long)] + custom_config_path: Option, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { + if let Some(start_height) = args.start_height { + if start_height > args.stop_height { + eprintln!("the start height can't be larger than the stop height!"); + return Ok(()); + } + } + + let config = + try_load_current_config(&args.custom_config_path)?.with_override(args.config_override); + + NyxdScraper::new(config.scraper_config()) + .await? + .process_block_range(args.start_height, args.stop_height) + .await?; + Ok(()) +} From 74392a28862b8edc5d9b5607172bdcd7fac65f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 15:36:08 +0100 Subject: [PATCH 018/207] don't request useless blocks during startup sync --- common/nyxd-scraper/src/block_processor/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/common/nyxd-scraper/src/block_processor/mod.rs b/common/nyxd-scraper/src/block_processor/mod.rs index f1b6142a4b..acb0cbc375 100644 --- a/common/nyxd-scraper/src/block_processor/mod.rs +++ b/common/nyxd-scraper/src/block_processor/mod.rs @@ -10,6 +10,7 @@ use crate::rpc_client::RpcClient; use crate::storage::{persist_block, ScraperStorage}; use crate::PruningOptions; use futures::StreamExt; +use std::cmp::max; use std::collections::{BTreeMap, HashSet, VecDeque}; use std::ops::{Add, Range}; use std::sync::Arc; @@ -266,6 +267,7 @@ impl BlockProcessor { } if to_prune == 0 { + self.last_pruned_height = self.last_processed_height; return Ok(()); } @@ -365,7 +367,14 @@ impl BlockProcessor { self.maybe_prune_storage().await?; let latest_block = self.rpc_client.current_block_height().await? as u32; + if latest_block > self.last_processed_height && self.last_processed_height != 0 { + // in case we were offline for a while, + // make sure we don't request blocks we'd have to prune anyway + let keep_recent = self.pruning_options.strategy_keep_recent(); + let last_to_keep = latest_block - keep_recent; + self.last_processed_height = max(self.last_processed_height, last_to_keep); + let request_range = self.last_processed_height + 1..latest_block + 1; info!("we need to request {request_range:?} to resync"); self.request_missing_blocks(request_range).await?; From 27fb4ae0cc379bba2b84fc9a6f6c372bc510485c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 16:21:27 +0100 Subject: [PATCH 019/207] log error when rewarding fails due to missing blocks --- nym-validator-rewarder/src/error.rs | 4 ++++ nym-validator-rewarder/src/main.rs | 2 +- .../src/rewarder/block_signing/mod.rs | 18 ++++++++++++++---- nym-validator-rewarder/src/rewarder/epoch.rs | 18 ++++++++++++++++++ nym-validator-rewarder/src/rewarder/mod.rs | 15 ++++++++++++--- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index 4c72c6ef37..5e9de91cfb 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::RewardingRatios; +use crate::rewarder::Epoch; use nym_compact_ecash::error::CompactEcashError; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::error::NymAPIError; @@ -178,6 +179,9 @@ pub enum NymRewarderError { #[error("pruning.keep_recent must not be smaller than {min_to_keep}. got: {keep_recent}")] TooSmallKeepRecent { min_to_keep: u32, keep_recent: u32 }, + + #[error("there were no blocks processed within the epoch {epoch}")] + NoBlocksProcessedInEpoch { epoch: Epoch }, } #[derive(Debug)] diff --git a/nym-validator-rewarder/src/main.rs b/nym-validator-rewarder/src/main.rs index 9b6654c3d6..e8945316a5 100644 --- a/nym-validator-rewarder/src/main.rs +++ b/nym-validator-rewarder/src/main.rs @@ -14,7 +14,7 @@ use nym_network_defaults::setup_env; pub mod cli; pub mod config; pub mod error; -mod rewarder; +pub mod rewarder; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/nym-validator-rewarder/src/rewarder/block_signing/mod.rs b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs index d9747a9f7a..780b4efef3 100644 --- a/nym-validator-rewarder/src/rewarder/block_signing/mod.rs +++ b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs @@ -92,18 +92,28 @@ impl EpochSigning { let epoch_start = current_epoch.start_time; let epoch_end = current_epoch.end_time; - let first_block = self + + let Some(first_block) = self .nyxd_scraper .storage .get_first_block_height_after(epoch_start) .await? - .unwrap_or_default(); - let last_block = self + else { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: current_epoch, + }); + }; + + let Some(last_block) = self .nyxd_scraper .storage .get_last_block_height_before(epoch_end) .await? - .unwrap_or_default(); + else { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: current_epoch, + }); + }; // each validator MUST be online at some point during the first 20 blocks, otherwise they're not getting anything. let vp_range_end = min(first_block + 20, last_block); diff --git a/nym-validator-rewarder/src/rewarder/epoch.rs b/nym-validator-rewarder/src/rewarder/epoch.rs index bf74fc748c..c164a1735f 100644 --- a/nym-validator-rewarder/src/rewarder/epoch.rs +++ b/nym-validator-rewarder/src/rewarder/epoch.rs @@ -3,6 +3,7 @@ use crate::error::NymRewarderError; use sqlx::FromRow; +use std::fmt::{Display, Formatter}; use std::ops::Add; use std::time::Duration; use time::format_description::well_known::Rfc3339; @@ -34,6 +35,11 @@ impl Epoch { }) } + pub fn has_finished(&self) -> bool { + let now = OffsetDateTime::now_utc(); + self.end_time < now + } + pub fn until_end(&self) -> Duration { let now = OffsetDateTime::now_utc(); (self.end_time - now).try_into().unwrap_or_default() @@ -60,3 +66,15 @@ impl Epoch { self.end_time.format(&Rfc3339).unwrap() } } + +impl Display for Epoch { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}: {} - {}", + self.id, + self.start_rfc3339(), + self.end_rfc3339() + ) + } +} diff --git a/nym-validator-rewarder/src/rewarder/mod.rs b/nym-validator-rewarder/src/rewarder/mod.rs index 4829091f5c..21a53d8242 100644 --- a/nym-validator-rewarder/src/rewarder/mod.rs +++ b/nym-validator-rewarder/src/rewarder/mod.rs @@ -7,7 +7,6 @@ use crate::rewarder::block_signing::types::EpochSigningResults; use crate::rewarder::block_signing::EpochSigning; use crate::rewarder::credential_issuance::types::CredentialIssuanceResults; use crate::rewarder::credential_issuance::CredentialIssuance; -use crate::rewarder::epoch::Epoch; use crate::rewarder::nyxd_client::NyxdClient; use crate::rewarder::storage::RewarderStorage; use futures::future::{FusedFuture, OptionFuture}; @@ -28,6 +27,8 @@ mod nyxd_client; mod storage; mod tasks; +pub(crate) use crate::rewarder::epoch::Epoch; + pub struct RewardingResult { pub total_spent: Coin, pub rewarding_tx: Hash, @@ -47,15 +48,20 @@ impl EpochRewards { pub fn amounts(&self) -> Result)>, NymRewarderError> { let mut amounts = Vec::new(); - if let Ok(Some(signing)) = &self.signing { + match &self.signing { + Ok(Some(signing)) => { for (account, signing_amount) in signing.rewarding_amounts(&self.signing_budget) { if signing_amount[0].amount != 0 { amounts.push((account, signing_amount)) } } + } + Err(err) => error!("failed to determine rewards for block signing: {err}"), + _ => (), } - if let Ok(Some(credentials)) = &self.credentials { + match &self.credentials { + Ok(Some(credentials)) => { for (account, credential_amount) in credentials.rewarding_amounts(&self.credentials_budget) { @@ -63,6 +69,9 @@ impl EpochRewards { amounts.push((account, credential_amount)) } } + } + Err(err) => error!("failed to determine rewards for credential issuance: {err}"), + _ => (), } Ok(amounts) From d399161d31c671296f86e5c09a0564b23e9487a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 16:35:53 +0100 Subject: [PATCH 020/207] adjust 'process_until' command to allow empty stop height --- common/nyxd-scraper/src/scraper/mod.rs | 8 ++++++-- nym-validator-rewarder/src/cli/process_until.rs | 16 ++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/common/nyxd-scraper/src/scraper/mod.rs b/common/nyxd-scraper/src/scraper/mod.rs index 5c838de50a..9dddc15202 100644 --- a/common/nyxd-scraper/src/scraper/mod.rs +++ b/common/nyxd-scraper/src/scraper/mod.rs @@ -180,7 +180,7 @@ impl NyxdScraper { pub async fn process_block_range( &self, starting_height: Option, - end_height: u32, + end_height: Option, ) -> Result<(), ScraperError> { if !self.task_tracker.is_empty() { return Err(ScraperError::ScraperAlreadyRunning); @@ -194,6 +194,8 @@ impl NyxdScraper { .await? .with_pruning(PruningOptions::nothing()); + let current_height = self.rpc_client.current_block_height().await? as u32; + let starting_height = match starting_height { Some(explicit) => explicit, None => { @@ -201,11 +203,13 @@ impl NyxdScraper { if last_processed != 0 { last_processed } else { - self.rpc_client.current_block_height().await? as u32 + current_height } } }; + let end_height = end_height.unwrap_or(current_height); + info!( starting_height = starting_height, end_height = end_height, diff --git a/nym-validator-rewarder/src/cli/process_until.rs b/nym-validator-rewarder/src/cli/process_until.rs index 43b235ec96..47756447ea 100644 --- a/nym-validator-rewarder/src/cli/process_until.rs +++ b/nym-validator-rewarder/src/cli/process_until.rs @@ -16,9 +16,10 @@ pub struct Args { #[clap(long)] start_height: Option, - /// Height of until we want to be processing the blocks + /// Height of until we want to be processing the blocks. + /// If none is provided, the currrent block height will be used #[clap(long)] - stop_height: u32, + stop_height: Option, /// Specifies custom location for the configuration file of nym validators rewarder. #[clap(long)] @@ -26,11 +27,14 @@ pub struct Args { } pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { - if let Some(start_height) = args.start_height { - if start_height > args.stop_height { - eprintln!("the start height can't be larger than the stop height!"); - return Ok(()); + match (args.start_height, args.stop_height) { + (Some(start), Some(end)) => { + if start > end { + eprintln!("the start height can't be larger than the stop height!"); + return Ok(()); + } } + _ => (), } let config = From 76c30814701ac8eb487b6dfade8d45d340d46865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 16:36:28 +0100 Subject: [PATCH 021/207] introduced rewarding resync alongside recovery instructions --- common/nyxd-scraper/src/scraper/mod.rs | 12 +++- nym-validator-rewarder/src/rewarder/mod.rs | 66 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/common/nyxd-scraper/src/scraper/mod.rs b/common/nyxd-scraper/src/scraper/mod.rs index 9dddc15202..02afdafd84 100644 --- a/common/nyxd-scraper/src/scraper/mod.rs +++ b/common/nyxd-scraper/src/scraper/mod.rs @@ -208,7 +208,17 @@ impl NyxdScraper { } }; - let end_height = end_height.unwrap_or(current_height); + let end_height = match end_height { + Some(explicit) => explicit, + None => { + let last_processed = block_processor.last_process_height(); + if last_processed != 0 { + last_processed - 1 + } else { + current_height + } + } + }; info!( starting_height = starting_height, diff --git a/nym-validator-rewarder/src/rewarder/mod.rs b/nym-validator-rewarder/src/rewarder/mod.rs index 21a53d8242..dc7cf3bce8 100644 --- a/nym-validator-rewarder/src/rewarder/mod.rs +++ b/nym-validator-rewarder/src/rewarder/mod.rs @@ -288,6 +288,58 @@ impl Rewarder { self.current_epoch = self.current_epoch.next(); } + async fn ensure_has_epoch_blocks(&self) -> Result<(), NymRewarderError> { + // make sure we at least have a single block processed within the epoch + let epoch_start = self.current_epoch.start_time; + let epoch_end = self.current_epoch.end_time; + + if let Some(epoch_signing) = &self.epoch_signing { + if epoch_signing + .nyxd_scraper + .storage + .get_first_block_height_after(epoch_start) + .await? + .is_none() + { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: self.current_epoch, + }); + } + + if epoch_signing + .nyxd_scraper + .storage + .get_last_block_height_before(epoch_end) + .await? + .is_none() + { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: self.current_epoch, + }); + } + } + + Ok(()) + } + + async fn startup_resync(&mut self) -> Result<(), NymRewarderError> { + // no sync required + if !self.current_epoch.has_finished() { + return Ok(()); + } + + info!("attempting to distribute missed rewards"); + while self.current_epoch.has_finished() { + info!("processing epoch {}", self.current_epoch); + self.ensure_has_epoch_blocks().await?; + + // we need to perform rewarding from the 'current' epoch until the actual current epoch + self.handle_epoch_end().await + } + + Ok(()) + } + pub async fn run(mut self) -> Result<(), NymRewarderError> { info!("Starting nym validators rewarder"); @@ -315,6 +367,20 @@ impl Rewarder { let until_end = self.current_epoch.until_end(); + if let Err(err) = self.startup_resync().await { + error!("failed to perform startup sync: {err}"); + error!("if the failure was due to insufficient number of blocks, your course of action is as follows:"); + error!("(ideally it would have been automatically resolved in this very method, but that'd require some serious refactoring)"); + error!( + "1. determine height of the first block of the epoch (doesn't have to be exact)" + ); + error!("2. run the following subcommand of the rewarder: `nym-validator-rewarder process-until --start_height=$STARTING_BLOCK"); + error!("3. !!IMPORTANT!! go to config.toml and temporarily disable block pruning, i.e. `pruning.strategy=nothing`"); + error!("4. restart nym-validator-rewarder as normal until it sends missing rewards"); + error!("5. re-enable pruning and restart the nym-validator rewarder"); + return Err(err); + } + info!( "the initial epoch (id: {}) will finish in {} secs", self.current_epoch.id, From 07022314fc6a6c22377d4f7d5d87b9db607d0d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 16:51:45 +0100 Subject: [PATCH 022/207] fixed typos and formatting --- nym-validator-rewarder/src/rewarder/mod.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/nym-validator-rewarder/src/rewarder/mod.rs b/nym-validator-rewarder/src/rewarder/mod.rs index dc7cf3bce8..514262c53b 100644 --- a/nym-validator-rewarder/src/rewarder/mod.rs +++ b/nym-validator-rewarder/src/rewarder/mod.rs @@ -50,26 +50,26 @@ impl EpochRewards { match &self.signing { Ok(Some(signing)) => { - for (account, signing_amount) in signing.rewarding_amounts(&self.signing_budget) { - if signing_amount[0].amount != 0 { - amounts.push((account, signing_amount)) + for (account, signing_amount) in signing.rewarding_amounts(&self.signing_budget) { + if signing_amount[0].amount != 0 { + amounts.push((account, signing_amount)) + } } } - } Err(err) => error!("failed to determine rewards for block signing: {err}"), _ => (), } match &self.credentials { Ok(Some(credentials)) => { - for (account, credential_amount) in - credentials.rewarding_amounts(&self.credentials_budget) - { - if credential_amount[0].amount != 0 { - amounts.push((account, credential_amount)) + for (account, credential_amount) in + credentials.rewarding_amounts(&self.credentials_budget) + { + if credential_amount[0].amount != 0 { + amounts.push((account, credential_amount)) + } } } - } Err(err) => error!("failed to determine rewards for credential issuance: {err}"), _ => (), } @@ -374,7 +374,7 @@ impl Rewarder { error!( "1. determine height of the first block of the epoch (doesn't have to be exact)" ); - error!("2. run the following subcommand of the rewarder: `nym-validator-rewarder process-until --start_height=$STARTING_BLOCK"); + error!("2. run the following subcommand of the rewarder: `nym-validator-rewarder process-until --start-height=$STARTING_BLOCK"); error!("3. !!IMPORTANT!! go to config.toml and temporarily disable block pruning, i.e. `pruning.strategy=nothing`"); error!("4. restart nym-validator-rewarder as normal until it sends missing rewards"); error!("5. re-enable pruning and restart the nym-validator rewarder"); From 8aa5711beee67c2c83758bc48758a1ab9b627959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 17:48:56 +0100 Subject: [PATCH 023/207] fixed query for historical validator data --- .../src/rewarder/block_signing/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nym-validator-rewarder/src/rewarder/block_signing/mod.rs b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs index 780b4efef3..27f423c089 100644 --- a/nym-validator-rewarder/src/rewarder/block_signing/mod.rs +++ b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs @@ -49,7 +49,12 @@ impl EpochSigning { ) -> Result, NymRewarderError> { // first attempt to get it via the historical info. // if that fails, attempt to use current block information to at least get **something** - if let Some(validators) = self.nyxd_client.historical_info(height).await?.hist { + if let Ok(Some(validators)) = self + .nyxd_client + .historical_info(height) + .await + .map(|v| v.hist) + { Ok(validators.valset) } else { let mut page_request = None; @@ -63,6 +68,10 @@ impl EpochSigning { break; }; + if pagination.next_key.is_empty() { + break; + } + page_request = Some(PageRequest { key: pagination.next_key, offset: 0, From 784fae2204fbcc60e4bf1aaaf5ab331ac1957784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 17:53:55 +0100 Subject: [PATCH 024/207] fix logic for determining end height for processing block ranges --- common/nyxd-scraper/src/scraper/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/common/nyxd-scraper/src/scraper/mod.rs b/common/nyxd-scraper/src/scraper/mod.rs index 02afdafd84..1b3294c914 100644 --- a/common/nyxd-scraper/src/scraper/mod.rs +++ b/common/nyxd-scraper/src/scraper/mod.rs @@ -195,11 +195,14 @@ impl NyxdScraper { .with_pruning(PruningOptions::nothing()); let current_height = self.rpc_client.current_block_height().await? as u32; + let last_processed = block_processor.last_process_height(); let starting_height = match starting_height { + // always attempt to use whatever the user has provided Some(explicit) => explicit, None => { - let last_processed = block_processor.last_process_height(); + // otherwise, attempt to resume where we last stopped + // and if we haven't processed anything, start from the current height if last_processed != 0 { last_processed } else { @@ -209,10 +212,13 @@ impl NyxdScraper { }; let end_height = match end_height { + // always attempt to use whatever the user has provided Some(explicit) => explicit, None => { - let last_processed = block_processor.last_process_height(); - if last_processed != 0 { + // otherwise, attempt to either go from the start height to the height right + // before the final processed block held in the storage (in case there are gaps) + // or finally, just go to the current block height + if last_processed > starting_height { last_processed - 1 } else { current_height From 4ad52accc0061dc36effe563a38d255f7b25f7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 27 Sep 2024 18:10:59 +0100 Subject: [PATCH 025/207] fixed stabilised clippy issue --- nym-validator-rewarder/src/cli/process_until.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nym-validator-rewarder/src/cli/process_until.rs b/nym-validator-rewarder/src/cli/process_until.rs index 47756447ea..f1c15470d0 100644 --- a/nym-validator-rewarder/src/cli/process_until.rs +++ b/nym-validator-rewarder/src/cli/process_until.rs @@ -27,14 +27,11 @@ pub struct Args { } pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { - match (args.start_height, args.stop_height) { - (Some(start), Some(end)) => { - if start > end { - eprintln!("the start height can't be larger than the stop height!"); - return Ok(()); - } + if let (Some(start), Some(end)) = (args.start_height, args.stop_height) { + if start > end { + eprintln!("the start height can't be larger than the stop height!"); + return Ok(()); } - _ => (), } let config = From 676a909aee677a11c0294045164ec615de8ca9cd Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Mon, 30 Sep 2024 15:13:22 +0200 Subject: [PATCH 026/207] V2 performance monitoring feature flag (#4943) --- nym-api/Cargo.toml | 1 + nym-api/src/support/storage/manager.rs | 39 ++++++++++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 6f23a04747..194be8cc2d 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -131,6 +131,7 @@ nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa" [features] no-reward = [] +v2-performance = [] generate-ts = ["ts-rs"] axum = ["dep:axum", "axum-extra", diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index 62bb77921b..a7950e3252 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -395,18 +395,33 @@ impl StorageManager { start: i64, end: i64, ) -> Result, sqlx::Error> { - let result = sqlx::query!( - r#" - SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status - WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ? - "#, - id, - start, - end - ) - .fetch_one(&self.connection_pool) - .await?; - Ok(result.reliability) + if cfg!(feature = "v2-performance") { + let result = sqlx::query!( + r#" + SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status_v2 + WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ? + "#, + id, + start, + end + ) + .fetch_one(&self.connection_pool) + .await?; + Ok(result.reliability) + } else { + let result = sqlx::query!( + r#" + SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status + WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ? + "#, + id, + start, + end + ) + .fetch_one(&self.connection_pool) + .await?; + Ok(result.reliability) + } } pub(super) async fn get_gateway_average_reliability_in_interval( From 09a771f58f4d1a544ae0742c949bda1b513460ac Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Mon, 30 Sep 2024 15:40:33 +0200 Subject: [PATCH 027/207] Add "utoipa" feature to nym-node (#4945) --- nym-node/nym-node-http-api/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node/nym-node-http-api/Cargo.toml b/nym-node/nym-node-http-api/Cargo.toml index bf96afc08c..54db63a1ae 100644 --- a/nym-node/nym-node-http-api/Cargo.toml +++ b/nym-node/nym-node-http-api/Cargo.toml @@ -29,7 +29,7 @@ rand = { workspace = true } fastrand = { workspace = true } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } -nym-http-api-common = { path = "../../common/http-api-common" } +nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } nym-node-requests = { path = "../nym-node-requests", default-features = false, features = [ "openapi", ] } From 6030bf6c9583b9be24da078139478131e7d54225 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:40:54 +0200 Subject: [PATCH 028/207] build(deps): bump the patch-updates group across 1 directory with 9 updates (#4944) --- Cargo.lock | 165 +++++++++++++++++++++++------------------- Cargo.toml | 14 ++-- sdk/ffi/go/Cargo.toml | 2 +- 3 files changed, 98 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02e9018d17..a737a05462 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,9 +341,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -431,19 +431,19 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 0.1.2", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] [[package]] name = "axum" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", - "axum-core 0.4.3", + "axum-core 0.4.5", "bytes", "futures-util", "http 1.1.0", @@ -464,7 +464,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -489,9 +489,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -502,7 +502,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -510,12 +510,12 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" dependencies = [ - "axum 0.7.5", - "axum-core 0.4.3", + "axum 0.7.7", + "axum-core 0.4.5", "bytes", "futures-util", "headers", @@ -525,7 +525,7 @@ dependencies = [ "mime", "pin-project-lite", "serde", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -807,9 +807,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" dependencies = [ "serde", ] @@ -1009,9 +1009,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -1019,9 +1019,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -1031,11 +1031,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.28" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b378c786d3bde9442d2c6dd7e6080b2a818db2b96e30d6e7f1b6d224eb617d3" +checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e" dependencies = [ - "clap 4.5.17", + "clap 4.5.18", ] [[package]] @@ -1044,15 +1044,15 @@ version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d494102c8ff3951810c72baf96910b980fb065ca5d3101243e6a8dc19747c86b" dependencies = [ - "clap 4.5.17", + "clap 4.5.18", "clap_complete", ] [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1452,7 +1452,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.17", + "clap 4.5.18", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -2320,7 +2320,7 @@ name = "explorer-api" version = "1.1.40" dependencies = [ "chrono", - "clap 4.5.17", + "clap 4.5.18", "dotenvy", "humantime-serde", "isocountry", @@ -2488,9 +2488,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -3233,9 +3233,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -3246,7 +3246,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -4227,14 +4226,14 @@ version = "1.1.44" dependencies = [ "anyhow", "async-trait", - "axum 0.7.5", + "axum 0.7.7", "axum-extra", "bincode", "bip39", "bloomfilter", "bs58", "cfg-if", - "clap 4.5.17", + "clap 4.5.18", "console-subscriber", "cosmwasm-std", "cw-utils", @@ -4355,7 +4354,7 @@ dependencies = [ "bincode", "bs58", "bytes", - "clap 4.5.17", + "clap 4.5.18", "defguard_wireguard_rs", "fastrand 2.1.1", "futures", @@ -4433,7 +4432,7 @@ dependencies = [ name = "nym-bin-common" version = "0.6.0" dependencies = [ - "clap 4.5.17", + "clap 4.5.18", "clap_complete", "clap_complete_fig", "const-str", @@ -4475,7 +4474,7 @@ dependencies = [ "base64 0.22.1", "bip39", "bs58", - "clap 4.5.17", + "clap 4.5.18", "clap_complete", "clap_complete_fig", "dotenvy", @@ -4501,7 +4500,7 @@ dependencies = [ "bip39", "bs58", "cfg-if", - "clap 4.5.17", + "clap 4.5.18", "colored", "comfy-table", "cosmrs 0.17.0-pre", @@ -4553,7 +4552,7 @@ name = "nym-client" version = "1.1.41" dependencies = [ "bs58", - "clap 4.5.17", + "clap 4.5.18", "dirs 5.0.1", "futures", "log", @@ -4593,7 +4592,7 @@ dependencies = [ "base64 0.22.1", "bs58", "cfg-if", - "clap 4.5.17", + "clap 4.5.18", "comfy-table", "futures", "gloo-timers", @@ -4978,7 +4977,7 @@ name = "nym-data-observatory" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.7", "chrono", "nym-bin-common", "nym-network-defaults", @@ -5119,7 +5118,7 @@ dependencies = [ "async-trait", "bip39", "bs58", - "clap 4.5.17", + "clap 4.5.18", "colored", "dashmap", "defguard_wireguard_rs", @@ -5294,7 +5293,7 @@ dependencies = [ name = "nym-http-api-common" version = "0.1.0" dependencies = [ - "axum 0.7.5", + "axum 0.7.7", "bytes", "colored", "mime", @@ -5323,7 +5322,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bs58", - "clap 4.5.17", + "clap 4.5.18", "nym-bin-common", "nym-credential-storage", "nym-id", @@ -5365,7 +5364,7 @@ dependencies = [ "bincode", "bs58", "bytes", - "clap 4.5.17", + "clap 4.5.18", "etherparse", "futures", "log", @@ -5460,9 +5459,9 @@ name = "nym-mixnode" version = "1.1.37" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.7", "bs58", - "clap 4.5.17", + "clap 4.5.18", "colored", "cupid", "dirs 5.0.1", @@ -5562,8 +5561,8 @@ name = "nym-network-monitor" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.7.5", - "clap 4.5.17", + "axum 0.7.7", + "clap 4.5.18", "dashmap", "futures", "log", @@ -5595,7 +5594,7 @@ dependencies = [ "anyhow", "async-trait", "bs58", - "clap 4.5.17", + "clap 4.5.18", "dirs 5.0.1", "futures", "humantime-serde", @@ -5647,7 +5646,7 @@ dependencies = [ "bs58", "cargo_metadata 0.18.1", "celes", - "clap 4.5.17", + "clap 4.5.18", "colored", "cupid", "humantime-serde", @@ -5686,7 +5685,7 @@ dependencies = [ name = "nym-node-http-api" version = "0.1.0" dependencies = [ - "axum 0.7.5", + "axum 0.7.7", "axum-extra", "base64 0.22.1", "colored", @@ -5707,7 +5706,7 @@ dependencies = [ "thiserror", "time", "tokio", - "tower", + "tower 0.4.13", "tower-http", "tracing", "utoipa", @@ -5794,7 +5793,7 @@ name = "nym-nr-query" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.17", + "clap 4.5.18", "log", "nym-bin-common", "nym-network-defaults", @@ -5932,7 +5931,7 @@ name = "nym-socks5-client" version = "1.1.41" dependencies = [ "bs58", - "clap 4.5.17", + "clap 4.5.18", "log", "nym-bin-common", "nym-client-core", @@ -6351,7 +6350,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bip39", - "clap 4.5.17", + "clap 4.5.18", "cosmwasm-std", "futures", "humantime 2.1.0", @@ -6461,7 +6460,7 @@ version = "0.1.7" dependencies = [ "anyhow", "bytes", - "clap 4.5.17", + "clap 4.5.18", "dotenvy", "flate2", "futures", @@ -8626,7 +8625,7 @@ name = "ssl-inject" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.17", + "clap 4.5.18", "hex", "tokio", ] @@ -8861,9 +8860,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" dependencies = [ "filetime", "libc", @@ -9042,7 +9041,7 @@ dependencies = [ "anyhow", "bip39", "bs58", - "clap 4.5.17", + "clap 4.5.18", "console", "cw-utils", "dkg-bypass-contract", @@ -9082,18 +9081,18 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -9419,7 +9418,7 @@ dependencies = [ "prost 0.11.9", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -9445,6 +9444,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -9472,15 +9487,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -9841,7 +9856,7 @@ checksum = "21345172d31092fd48c47fd56c53d4ae9e41c4b1f559fb8c38c1ab1685fd919f" dependencies = [ "anyhow", "camino", - "clap 4.5.17", + "clap 4.5.18", "uniffi_bindgen", "uniffi_build", "uniffi_core", @@ -9858,7 +9873,7 @@ dependencies = [ "askama", "camino", "cargo_metadata 0.15.4", - "clap 4.5.17", + "clap 4.5.18", "fs-err", "glob", "goblin", @@ -10069,7 +10084,7 @@ version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943e0ff606c6d57d410fd5663a4d7c074ab2c5f14ab903b9514565e59fa1189e" dependencies = [ - "axum 0.7.5", + "axum 0.7.7", "mime_guess", "regex", "reqwest 0.12.4", diff --git a/Cargo.toml b/Cargo.toml index 7c0102e3c4..ab9b668e93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,9 +182,9 @@ aes-gcm-siv = "0.11.1" aead = "0.5.2" anyhow = "1.0.89" argon2 = "0.5.0" -async-trait = "0.1.82" +async-trait = "0.1.83" axum = "0.7.5" -axum-extra = "0.9.3" +axum-extra = "0.9.4" base64 = "0.22.1" bincode = "1.3.3" bip39 = { version = "2.0.0", features = ["zeroize"] } @@ -197,7 +197,7 @@ blake3 = "1.5.4" bloomfilter = "1.0.14" bs58 = "0.5.1" bytecodec = "0.4.15" -bytes = "1.7.1" +bytes = "1.7.2" cargo_metadata = "0.18.1" celes = "2.4.0" cfg-if = "1.0.0" @@ -205,7 +205,7 @@ chacha20 = "0.9.0" chacha20poly1305 = "0.10.1" chrono = "0.4.31" cipher = "0.4.3" -clap = "4.5.17" +clap = "4.5.18" clap_complete = "4.5" clap_complete_fig = "4.5" colored = "2.0" @@ -232,7 +232,7 @@ ed25519-dalek = "2.1" etherparse = "0.13.0" eyre = "0.6.9" fastrand = "2.1.1" -flate2 = "1.0.33" +flate2 = "1.0.34" futures = "0.3.28" generic-array = "0.14.7" getrandom = "0.2.10" @@ -307,9 +307,9 @@ subtle-encoding = "0.5" syn = "1" sysinfo = "0.30.13" tap = "1.0.1" -tar = "0.4.41" +tar = "0.4.42" tempfile = "3.5.0" -thiserror = "1.0.63" +thiserror = "1.0.64" time = "0.3.30" tokio = "1.39" tokio-stream = "0.1.16" diff --git a/sdk/ffi/go/Cargo.toml b/sdk/ffi/go/Cargo.toml index 18240848bc..e1486844db 100644 --- a/sdk/ffi/go/Cargo.toml +++ b/sdk/ffi/go/Cargo.toml @@ -21,7 +21,7 @@ tokio = { version = "1", features = ["full"] } lazy_static = "1.4.0" # error handling anyhow = "1.0.79" -thiserror = "1.0.56" +thiserror = "1.0.64" [build-dependencies] uniffi = { version = "0.25.2", features = ["build"] } From 1202a2f5f4f12d4a1a2e287f69e5f18ae64cc24e Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:42:11 +0000 Subject: [PATCH 029/207] [DOCs:/operators]: Update FAQ sphinx size (#4946) * updating sphinx size to develop branch from an outdated commit url * remove outdated graphs * add token page url --- documentation/operators/src/faq/general-faq.md | 2 +- documentation/operators/src/faq/nym-nodes-faq.md | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/documentation/operators/src/faq/general-faq.md b/documentation/operators/src/faq/general-faq.md index df8587872b..8692d7a9b4 100644 --- a/documentation/operators/src/faq/general-faq.md +++ b/documentation/operators/src/faq/general-faq.md @@ -25,7 +25,7 @@ We don't recommend this setup because it's really difficult to get a static IP a ### What's the Sphinx packet size? -The sizes are shown in the configs [here](https://github.com/nymtech/nym/blob/1ba6444e722e7757f1175a296bed6e31e25b8db8/common/nymsphinx/params/src/packet_sizes.rs#L12) (default is the one clients use, the others are for research purposes, not to be used in production as this would fragment the anonymity set). More info can be found [here](https://github.com/nymtech/nym/blob/4844ac953a12b29fa27688609ec193f1d560c996/common/nymsphinx/anonymous-replies/src/reply_surb.rs#L80). +The sizes are shown in the configs [here](https://github.com/nymtech/nym/blob/develop/common/nymsphinx/params/src/packet_sizes.rs#L32) (default is the one clients use, the others are for research purposes, not to be used in production as this would fragment the anonymity set). More info can be found [here](https://github.com/nymtech/nym/blob/develop/common/nymsphinx/anonymous-replies/src/reply_surb.rs#L80). ### Why a Mix Node and a Gateway cannot be bonded with the same wallet? diff --git a/documentation/operators/src/faq/nym-nodes-faq.md b/documentation/operators/src/faq/nym-nodes-faq.md index d0a65b2ec0..325782a32e 100644 --- a/documentation/operators/src/faq/nym-nodes-faq.md +++ b/documentation/operators/src/faq/nym-nodes-faq.md @@ -2,6 +2,8 @@ ### What determines the rewards when running a `nym-node --mode mixnode`? +> **Visit [nymtech.net/about/token](https://nymtech.net/about/token) to find live information, graphs and dashboards about NYM token.** + The stake required for a Mix Node to achieve maximum rewards is called Mix Node saturation point. This is calculated from the staking supply (all circulating supply + part of unlocked tokens). The target level of staking is to have 40% of the staking supply locked in Mix Nodes. The node stake saturation point, which we denote by Nsat, is given by the stake supply, target level of staking divided between the rewarded nodes. @@ -20,13 +22,3 @@ The rewarded nodes are the nodes which will receive some rewards by the end of t For more detailed calculation, read our blog post [Nym Token Economics update](https://blog.nymtech.net/nym-token-economics-update-fedff0ed5267). More info on staking can be found [here](https://blog.nymtech.net/staking-in-nym-introducing-mainnet-mixmining-f9bb1cbc7c36). And [here](https://blog.nymtech.net/want-to-stake-in-nym-here-is-how-to-choose-a-mix-node-to-delegate-nym-to-c3b862add165) is more info on how to choose a Mix Node for delegation. And finally an [update](https://blog.nymtech.net/quarterly-token-economic-parameter-update-b2862948710f) on token economics from July 2023. - - - - - -*More graphs and stats at [stats.notrustverify.ch](https://status.notrustverify.ch/d/CW3L7dVVk/nym-mixnet?orgId=1&from=1703074861988&to=1705666862004).* - - From 4c51a8975cef93cbfc2020a4db59fa2085916a1c Mon Sep 17 00:00:00 2001 From: durch Date: Tue, 1 Oct 2024 16:39:47 +0200 Subject: [PATCH 030/207] Initial stab --- nym-api.dockerfile | 7 +++++++ nym-api/entrypoint.sh | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 nym-api.dockerfile create mode 100755 nym-api/entrypoint.sh diff --git a/nym-api.dockerfile b/nym-api.dockerfile new file mode 100644 index 0000000000..7ee95379dc --- /dev/null +++ b/nym-api.dockerfile @@ -0,0 +1,7 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-api +RUN cargo build --release + +ENTRYPOINT ["/usr/src/nym/nym-api/entrypoint.sh"] diff --git a/nym-api/entrypoint.sh b/nym-api/entrypoint.sh new file mode 100755 index 0000000000..bf4c1942c3 --- /dev/null +++ b/nym-api/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +/usr/src/nym/target/release/nym-api init && /usr/src/nym/target/release/nym-api run \ No newline at end of file From 469f85fc49e766ccb869b364f55147637926e735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 7 Oct 2024 11:46:35 +0200 Subject: [PATCH 031/207] Switch over the last set of jobs to arc runners (#4938) * Switch over the last set of jobs to arc runners * Use dind runners * Disable matrix notification so we can use non-dind runners * wip * Add workflow_dispatch * Revert "wip" This reverts commit ae34efd32ed9ff838c05315a3610f6edb066a9cd. --- .github/workflows/cd-docs.yml | 23 +---------------------- .github/workflows/ci-build-ts.yml | 23 ++--------------------- .github/workflows/ci-docs.yml | 23 +---------------------- .github/workflows/ci-lint-typescript.yml | 24 ++---------------------- .github/workflows/publish-sdk-npm.yml | 2 +- 5 files changed, 7 insertions(+), 88 deletions(-) diff --git a/.github/workflows/cd-docs.yml b/.github/workflows/cd-docs.yml index c3b4f6a7b2..97150e29c8 100644 --- a/.github/workflows/cd-docs.yml +++ b/.github/workflows/cd-docs.yml @@ -5,7 +5,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Install Dependencies (Linux) @@ -99,24 +99,3 @@ jobs: run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} working-directory: dist/docs continue-on-error: false - - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: cd-docs - NYM_PROJECT_NAME: "Docs CD" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CD_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_DOCS }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - IS_SUCCESS: "${{ job.status == 'success' }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/ci-build-ts.yml b/.github/workflows/ci-build-ts.yml index 4c8dbe6b7c..934f855c5f 100644 --- a/.github/workflows/ci-build-ts.yml +++ b/.github/workflows/ci-build-ts.yml @@ -1,6 +1,7 @@ name: ci-build-ts on: + workflow_dispatch: pull_request: paths: - "ts-packages/**" @@ -9,7 +10,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Install rsync @@ -45,23 +46,3 @@ jobs: REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }} TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ts-${{ env.GITHUB_REF_SLUG }}-example EXCLUDE: "/dist/, /node_modules/" - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: ts-packages - NYM_PROJECT_NAME: "ts-packages" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - IS_SUCCESS: "${{ job.status == 'success' }}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 74b599d26d..602df75b44 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -10,7 +10,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Install Dependencies (Linux) @@ -70,24 +70,3 @@ jobs: REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }} TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/docs-${{ env.GITHUB_REF_SLUG }} EXCLUDE: "/node_modules/" - - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: ci-docs - NYM_PROJECT_NAME: "Docs CI" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "docs-${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_DOCS }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - IS_SUCCESS: "${{ job.status == 'success' }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/ci-lint-typescript.yml b/.github/workflows/ci-lint-typescript.yml index 165446bbd8..0f3aa78fa0 100644 --- a/.github/workflows/ci-lint-typescript.yml +++ b/.github/workflows/ci-lint-typescript.yml @@ -1,6 +1,7 @@ name: ci-lint-typescript on: + workflow_dispatch: pull_request: paths: - "ts-packages/**" @@ -14,7 +15,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: rlespinasse/github-slug-action@v3.x @@ -53,24 +54,3 @@ jobs: run: yarn lint - name: Typecheck with tsc run: yarn tsc - - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: ts-packages - NYM_PROJECT_NAME: "ts-packages" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - IS_SUCCESS: "${{ job.status == 'success' }}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/publish-sdk-npm.yml b/.github/workflows/publish-sdk-npm.yml index 583006b638..405cbcd022 100644 --- a/.github/workflows/publish-sdk-npm.yml +++ b/.github/workflows/publish-sdk-npm.yml @@ -4,7 +4,7 @@ on: jobs: publish: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 From e2d1806e49f336abd882066cfd1aa9af5eb2acef Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Mon, 7 Oct 2024 12:00:36 +0200 Subject: [PATCH 032/207] Extract packet processing from mixnode-common (#4949) * Extract packet processing from mixnode-common * Cleanup --- Cargo.lock | 6 + .../src/packet_processor/error.rs | 19 +- .../src/packet_processor/processor.rs | 308 +----------------- common/nymsphinx/framing/Cargo.toml | 7 + common/nymsphinx/framing/src/lib.rs | 1 + common/nymsphinx/framing/src/processing.rs | 284 ++++++++++++++++ contracts/Cargo.lock | 8 +- .../receiver/connection_handler.rs | 20 +- .../receiver/packet_processing.rs | 36 +- .../node/listener/connection_handler/mod.rs | 15 +- .../connection_handler/packet_processing.rs | 23 +- nym-wallet/Cargo.lock | 16 +- 12 files changed, 375 insertions(+), 368 deletions(-) create mode 100644 common/nymsphinx/framing/src/processing.rs diff --git a/Cargo.lock b/Cargo.lock index a737a05462..23f75f575f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6166,9 +6166,15 @@ name = "nym-sphinx-framing" version = "0.1.0" dependencies = [ "bytes", + "log", + "nym-metrics", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-forwarding", "nym-sphinx-params", "nym-sphinx-types", "thiserror", + "tokio", "tokio-util", ] diff --git a/common/mixnode-common/src/packet_processor/error.rs b/common/mixnode-common/src/packet_processor/error.rs index 405e10bb91..50ae179e37 100644 --- a/common/mixnode-common/src/packet_processor/error.rs +++ b/common/mixnode-common/src/packet_processor/error.rs @@ -2,30 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use nym_sphinx_acknowledgements::surb_ack::SurbAckRecoveryError; -use nym_sphinx_addressing::nodes::NymNodeRoutingAddressError; -use nym_sphinx_types::{NymPacketError, OutfoxError, SphinxError}; +use nym_sphinx_framing::processing::PacketProcessingError; use thiserror::Error; #[derive(Error, Debug)] pub enum MixProcessingError { - #[error("failed to process received packet: {0}")] - NymPacketProcessingError(#[from] NymPacketError), - - #[error("failed to process received sphinx packet: {0}")] - SphinxProcessingError(#[from] SphinxError), - - #[error("the forward hop address was malformed: {0}")] - InvalidForwardHopAddress(#[from] NymNodeRoutingAddressError), - - #[error("the final hop did not contain a SURB-Ack")] - NoSurbAckInFinalHop, - #[error("failed to recover the expected SURB-Ack packet: {0}")] MalformedSurbAck(#[from] SurbAckRecoveryError), #[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")] ReceivedOldTypeVpnPacket, - #[error("failed to process received outfox packet: {0}")] - OutfoxProcessingError(#[from] OutfoxError), + #[error("failed to process received Nym packet: {0}")] + NymPacketProcessingError(#[from] PacketProcessingError), } diff --git a/common/mixnode-common/src/packet_processor/processor.rs b/common/mixnode-common/src/packet_processor/processor.rs index 03a8f37880..7ab70cdce6 100644 --- a/common/mixnode-common/src/packet_processor/processor.rs +++ b/common/mixnode-common/src/packet_processor/processor.rs @@ -1,38 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::packet_processor::error::MixProcessingError; -use log::*; -use nym_metrics::nanos; -use nym_sphinx_acknowledgements::surb_ack::SurbAck; -use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; -use nym_sphinx_forwarding::packet::MixPacket; -use nym_sphinx_framing::packet::FramedNymPacket; -use nym_sphinx_params::{PacketSize, PacketType}; -use nym_sphinx_types::{ - Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymProcessedPacket, - PrivateKey, ProcessedPacket, -}; - use std::sync::Arc; -type ForwardAck = MixPacket; - -#[derive(Debug)] -pub struct ProcessedFinalHop { - pub destination: DestinationAddressBytes, - pub forward_ack: Option, - pub message: Vec, -} - -#[derive(Debug)] -pub enum MixProcessingResult { - /// Contains unwrapped data that should first get delayed before being sent to next hop. - ForwardHop(MixPacket, Option), - - /// Contains all data extracted out of the final hop packet that could be forwarded to the destination. - FinalHop(ProcessedFinalHop), -} +use nym_sphinx_types::PrivateKey; #[derive(Clone)] pub struct SphinxPacketProcessor { @@ -48,280 +19,7 @@ impl SphinxPacketProcessor { } } - /// Performs a fresh sphinx unwrapping using no cache. - fn perform_initial_packet_processing( - &self, - packet: NymPacket, - ) -> Result { - nanos!("perform_initial_packet_processing", { - packet.process(&self.sphinx_key).map_err(|err| { - debug!("Failed to unwrap NymPacket packet: {err}"); - MixProcessingError::NymPacketProcessingError(err) - }) - }) - } - - /// Takes the received framed packet and tries to unwrap it from the sphinx encryption. - fn perform_initial_unwrapping( - &self, - received: FramedNymPacket, - ) -> Result { - nanos!("perform_initial_unwrapping", { - let packet = received.into_inner(); - self.perform_initial_packet_processing(packet) - }) - } - - /// Processed received forward hop packet - tries to extract next hop address, sets delay - /// and packs all the data in a way that can be easily sent to the next hop. - fn process_forward_hop( - &self, - packet: NymPacket, - forward_address: NodeAddressBytes, - delay: SphinxDelay, - packet_type: PacketType, - ) -> Result { - let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?; - - let mix_packet = MixPacket::new(next_hop_address, packet, packet_type); - Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay))) - } - - /// Split data extracted from the final hop sphinx packet into a SURBAck and message - /// that should get delivered to a client. - fn split_hop_data_into_ack_and_message( - &self, - mut extracted_data: Vec, - packet_type: PacketType, - ) -> Result<(Vec, Vec), MixProcessingError> { - let ack_len = SurbAck::len(Some(packet_type)); - - // in theory it's impossible for this to fail since it managed to go into correct `match` - // branch at the caller - if extracted_data.len() < ack_len { - return Err(MixProcessingError::NoSurbAckInFinalHop); - } - - let message = extracted_data.split_off(ack_len); - let ack_data = extracted_data; - Ok((ack_data, message)) - } - - /// Tries to extract a SURBAck that could be sent back into the mix network and message - /// that should get delivered to a client from received Sphinx packet. - fn split_into_ack_and_message( - &self, - data: Vec, - packet_size: PacketSize, - packet_type: PacketType, - ) -> Result<(Option, Vec), MixProcessingError> { - match packet_size { - PacketSize::AckPacket | PacketSize::OutfoxAckPacket => { - trace!("received an ack packet!"); - Ok((None, data)) - } - PacketSize::RegularPacket - | PacketSize::ExtendedPacket8 - | PacketSize::ExtendedPacket16 - | PacketSize::ExtendedPacket32 - | PacketSize::OutfoxRegularPacket => { - trace!("received a normal packet!"); - let (ack_data, message) = - self.split_hop_data_into_ack_and_message(data, packet_type)?; - let (ack_first_hop, ack_packet) = - match SurbAck::try_recover_first_hop_packet(&ack_data, packet_type) { - Ok((first_hop, packet)) => (first_hop, packet), - Err(err) => { - info!("Failed to recover first hop from ack data: {err}"); - return Err(err.into()); - } - }; - let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type); - Ok((Some(forward_ack), message)) - } - } - } - - /// Processed received final hop packet - tries to extract SURBAck out of it (assuming the - /// packet itself is not an ACK) and splits it from the message that should get delivered - /// to the destination. - fn process_final_hop( - &self, - destination: DestinationAddressBytes, - payload: Vec, - packet_size: PacketSize, - packet_type: PacketType, - ) -> Result { - let (forward_ack, message) = - self.split_into_ack_and_message(payload, packet_size, packet_type)?; - - Ok(MixProcessingResult::FinalHop(ProcessedFinalHop { - destination, - forward_ack, - message, - })) - } - - /// Performs final processing for the unwrapped packet based on whether it was a forward hop - /// or a final hop. - fn perform_final_processing( - &self, - packet: NymProcessedPacket, - packet_size: PacketSize, - packet_type: PacketType, - ) -> Result { - match packet { - NymProcessedPacket::Sphinx(packet) => { - match packet { - ProcessedPacket::ForwardHop(packet, address, delay) => self - .process_forward_hop( - NymPacket::Sphinx(*packet), - address, - delay, - packet_type, - ), - // right now there's no use for the surb_id included in the header - probably it should get removed from the - // sphinx all together? - ProcessedPacket::FinalHop(destination, _, payload) => self.process_final_hop( - destination, - payload.recover_plaintext()?, - packet_size, - packet_type, - ), - } - } - NymProcessedPacket::Outfox(packet) => { - let next_address = *packet.next_address(); - let packet = packet.into_packet(); - if packet.is_final_hop() { - self.process_final_hop( - DestinationAddressBytes::from_bytes(next_address), - packet.recover_plaintext()?.to_vec(), - packet_size, - packet_type, - ) - } else { - let mix_packet = MixPacket::new( - NymNodeRoutingAddress::try_from_bytes(&next_address)?, - NymPacket::Outfox(packet), - PacketType::Outfox, - ); - Ok(MixProcessingResult::ForwardHop(mix_packet, None)) - } - } - } - } - - pub fn process_received( - &self, - received: FramedNymPacket, - ) -> Result { - // explicit packet size will help to correctly parse final hop - nanos!("process_received", { - let packet_size = received.packet_size(); - let packet_type = received.packet_type(); - - // unwrap the sphinx packet and if possible and appropriate, cache keys - let processed_packet = self.perform_initial_unwrapping(received)?; - - // for forward packets, extract next hop and set delay (but do NOT delay here) - // for final packets, extract SURBAck - let final_processing_result = - self.perform_final_processing(processed_packet, packet_size, packet_type); - - if final_processing_result.is_err() { - error!("{:?}", final_processing_result) - } - - final_processing_result - }) - } -} - -// TODO: what more could we realistically test here? -#[cfg(test)] -mod tests { - use super::*; - use nym_sphinx_types::crypto::keygen; - - fn fixture() -> SphinxPacketProcessor { - let local_keys = keygen(); - SphinxPacketProcessor::new(local_keys.0) - } - - #[tokio::test] - async fn splitting_hop_data_works_for_sufficiently_long_payload() { - let processor = fixture(); - - let short_data = vec![42u8]; - assert!(processor - .split_hop_data_into_ack_and_message(short_data, PacketType::Mix) - .is_err()); - - let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Mix))]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Mix) - .unwrap(); - assert_eq!(sufficient_data, ack); - assert!(data.is_empty()); - - let long_data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) * 5]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(long_data, PacketType::Mix) - .unwrap(); - assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Mix))); - assert_eq!(data.len(), SurbAck::len(Some(PacketType::Mix)) * 4) - } - - #[tokio::test] - async fn splitting_hop_data_works_for_sufficiently_long_payload_outfox() { - let processor = fixture(); - - let short_data = vec![42u8]; - assert!(processor - .split_hop_data_into_ack_and_message(short_data, PacketType::Outfox) - .is_err()); - - let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox))]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Outfox) - .unwrap(); - assert_eq!(sufficient_data, ack); - assert!(data.is_empty()); - - let long_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) * 5]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(long_data, PacketType::Outfox) - .unwrap(); - assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Outfox))); - assert_eq!(data.len(), SurbAck::len(Some(PacketType::Outfox)) * 4) - } - - #[tokio::test] - async fn splitting_into_ack_and_message_returns_whole_data_for_ack() { - let processor = fixture(); - - let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10]; - let (ack, message) = processor - .split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix) - .unwrap(); - assert!(ack.is_none()); - assert_eq!(data, message) - } - - #[tokio::test] - async fn splitting_into_ack_and_message_returns_whole_data_for_ack_outfox() { - let processor = fixture(); - - let data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) + 10]; - let (ack, message) = processor - .split_into_ack_and_message( - data.clone(), - PacketSize::OutfoxAckPacket, - PacketType::Outfox, - ) - .unwrap(); - assert!(ack.is_none()); - assert_eq!(data, message) + pub fn sphinx_key(&self) -> &PrivateKey { + &self.sphinx_key } } diff --git a/common/nymsphinx/framing/Cargo.toml b/common/nymsphinx/framing/Cargo.toml index 250c0dfd4f..03cff4ca9d 100644 --- a/common/nymsphinx/framing/Cargo.toml +++ b/common/nymsphinx/framing/Cargo.toml @@ -11,7 +11,14 @@ repository = { workspace = true } bytes = { workspace = true } tokio-util = { workspace = true, features = ["codec"] } thiserror = { workspace = true } +log = { workspace = true } nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] } nym-sphinx-params = { path = "../params", features = ["sphinx", "outfox"] } +nym-sphinx-forwarding = { path = "../forwarding" } +nym-metrics = { path = "../../nym-metrics" } +nym-sphinx-addressing = { path = "../addressing" } +nym-sphinx-acknowledgements = { path = "../acknowledgements" } +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } diff --git a/common/nymsphinx/framing/src/lib.rs b/common/nymsphinx/framing/src/lib.rs index 6c3fe39018..c0da04d8c2 100644 --- a/common/nymsphinx/framing/src/lib.rs +++ b/common/nymsphinx/framing/src/lib.rs @@ -3,3 +3,4 @@ pub mod codec; pub mod packet; +pub mod processing; diff --git a/common/nymsphinx/framing/src/processing.rs b/common/nymsphinx/framing/src/processing.rs new file mode 100644 index 0000000000..f9dd02dcfb --- /dev/null +++ b/common/nymsphinx/framing/src/processing.rs @@ -0,0 +1,284 @@ +use log::{debug, error, info, trace}; +use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError}; +use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nym_sphinx_params::{PacketSize, PacketType}; +use nym_sphinx_types::{ + Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError, + NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacket, SphinxError, +}; +use thiserror::Error; + +use crate::packet::FramedNymPacket; +use nym_metrics::nanos; +use nym_sphinx_forwarding::packet::MixPacket; + +#[derive(Debug)] +pub enum MixProcessingResult { + /// Contains unwrapped data that should first get delayed before being sent to next hop. + ForwardHop(MixPacket, Option), + + /// Contains all data extracted out of the final hop packet that could be forwarded to the destination. + FinalHop(ProcessedFinalHop), +} + +type ForwardAck = MixPacket; + +#[derive(Debug)] +pub struct ProcessedFinalHop { + pub destination: DestinationAddressBytes, + pub forward_ack: Option, + pub message: Vec, +} + +#[derive(Debug, Error)] +pub enum PacketProcessingError { + #[error("failed to process received packet: {0}")] + NymPacketProcessingError(#[from] NymPacketError), + + #[error("failed to process received sphinx packet: {0}")] + SphinxProcessingError(#[from] SphinxError), + + #[error("the forward hop address was malformed: {0}")] + InvalidForwardHopAddress(#[from] NymNodeRoutingAddressError), + + #[error("the final hop did not contain a SURB-Ack")] + NoSurbAckInFinalHop, + + #[error("failed to recover the expected SURB-Ack packet: {0}")] + MalformedSurbAck(#[from] SurbAckRecoveryError), + + #[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")] + ReceivedOldTypeVpnPacket, + + #[error("failed to process received outfox packet: {0}")] + OutfoxProcessingError(#[from] OutfoxError), +} + +pub fn process_framed_packet( + received: FramedNymPacket, + sphinx_key: &PrivateKey, +) -> Result { + nanos!("process_received", { + let packet_size = received.packet_size(); + let packet_type = received.packet_type(); + + // unwrap the sphinx packet and if possible and appropriate, cache keys + let processed_packet = perform_framed_unwrapping(received, sphinx_key)?; + + // for forward packets, extract next hop and set delay (but do NOT delay here) + // for final packets, extract SURBAck + let final_processing_result = + perform_final_processing(processed_packet, packet_size, packet_type); + + if final_processing_result.is_err() { + error!("{:?}", final_processing_result) + } + + final_processing_result + }) +} + +fn perform_framed_unwrapping( + received: FramedNymPacket, + sphinx_key: &PrivateKey, +) -> Result { + nanos!("perform_initial_unwrapping", { + let packet = received.into_inner(); + perform_framed_packet_processing(packet, sphinx_key) + }) +} + +fn perform_framed_packet_processing( + packet: NymPacket, + sphinx_key: &PrivateKey, +) -> Result { + nanos!("perform_initial_packet_processing", { + packet.process(sphinx_key).map_err(|err| { + debug!("Failed to unwrap NymPacket packet: {err}"); + PacketProcessingError::NymPacketProcessingError(err) + }) + }) +} + +fn perform_final_processing( + packet: NymProcessedPacket, + packet_size: PacketSize, + packet_type: PacketType, +) -> Result { + match packet { + NymProcessedPacket::Sphinx(packet) => { + match packet { + ProcessedPacket::ForwardHop(packet, address, delay) => { + process_forward_hop(NymPacket::Sphinx(*packet), address, delay, packet_type) + } + // right now there's no use for the surb_id included in the header - probably it should get removed from the + // sphinx all together? + ProcessedPacket::FinalHop(destination, _, payload) => process_final_hop( + destination, + payload.recover_plaintext()?, + packet_size, + packet_type, + ), + } + } + NymProcessedPacket::Outfox(packet) => { + let next_address = *packet.next_address(); + let packet = packet.into_packet(); + if packet.is_final_hop() { + process_final_hop( + DestinationAddressBytes::from_bytes(next_address), + packet.recover_plaintext()?.to_vec(), + packet_size, + packet_type, + ) + } else { + let mix_packet = MixPacket::new( + NymNodeRoutingAddress::try_from_bytes(&next_address)?, + NymPacket::Outfox(packet), + PacketType::Outfox, + ); + Ok(MixProcessingResult::ForwardHop(mix_packet, None)) + } + } + } +} + +fn process_final_hop( + destination: DestinationAddressBytes, + payload: Vec, + packet_size: PacketSize, + packet_type: PacketType, +) -> Result { + let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?; + + Ok(MixProcessingResult::FinalHop(ProcessedFinalHop { + destination, + forward_ack, + message, + })) +} + +fn split_into_ack_and_message( + data: Vec, + packet_size: PacketSize, + packet_type: PacketType, +) -> Result<(Option, Vec), PacketProcessingError> { + match packet_size { + PacketSize::AckPacket | PacketSize::OutfoxAckPacket => { + trace!("received an ack packet!"); + Ok((None, data)) + } + PacketSize::RegularPacket + | PacketSize::ExtendedPacket8 + | PacketSize::ExtendedPacket16 + | PacketSize::ExtendedPacket32 + | PacketSize::OutfoxRegularPacket => { + trace!("received a normal packet!"); + let (ack_data, message) = split_hop_data_into_ack_and_message(data, packet_type)?; + let (ack_first_hop, ack_packet) = + match SurbAck::try_recover_first_hop_packet(&ack_data, packet_type) { + Ok((first_hop, packet)) => (first_hop, packet), + Err(err) => { + info!("Failed to recover first hop from ack data: {err}"); + return Err(err.into()); + } + }; + let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type); + Ok((Some(forward_ack), message)) + } + } +} + +fn split_hop_data_into_ack_and_message( + mut extracted_data: Vec, + packet_type: PacketType, +) -> Result<(Vec, Vec), PacketProcessingError> { + let ack_len = SurbAck::len(Some(packet_type)); + + // in theory it's impossible for this to fail since it managed to go into correct `match` + // branch at the caller + if extracted_data.len() < ack_len { + return Err(PacketProcessingError::NoSurbAckInFinalHop); + } + + let message = extracted_data.split_off(ack_len); + let ack_data = extracted_data; + Ok((ack_data, message)) +} + +fn process_forward_hop( + packet: NymPacket, + forward_address: NodeAddressBytes, + delay: SphinxDelay, + packet_type: PacketType, +) -> Result { + let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?; + + let mix_packet = MixPacket::new(next_hop_address, packet, packet_type); + Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay))) +} + +// TODO: what more could we realistically test here? +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn splitting_hop_data_works_for_sufficiently_long_payload() { + let short_data = vec![42u8]; + assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Mix).is_err()); + + let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Mix))]; + let (ack, data) = + split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Mix).unwrap(); + assert_eq!(sufficient_data, ack); + assert!(data.is_empty()); + + let long_data: Vec = vec![42u8; SurbAck::len(Some(PacketType::Mix)) * 5]; + let (ack, data) = split_hop_data_into_ack_and_message(long_data, PacketType::Mix).unwrap(); + assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Mix))); + assert_eq!(data.len(), SurbAck::len(Some(PacketType::Mix)) * 4) + } + + #[tokio::test] + async fn splitting_hop_data_works_for_sufficiently_long_payload_outfox() { + let short_data = vec![42u8]; + assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Outfox).is_err()); + + let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox))]; + let (ack, data) = + split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Outfox) + .unwrap(); + assert_eq!(sufficient_data, ack); + assert!(data.is_empty()); + + let long_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) * 5]; + let (ack, data) = + split_hop_data_into_ack_and_message(long_data, PacketType::Outfox).unwrap(); + assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Outfox))); + assert_eq!(data.len(), SurbAck::len(Some(PacketType::Outfox)) * 4) + } + + #[tokio::test] + async fn splitting_into_ack_and_message_returns_whole_data_for_ack() { + let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10]; + let (ack, message) = + split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix) + .unwrap(); + assert!(ack.is_none()); + assert_eq!(data, message) + } + + #[tokio::test] + async fn splitting_into_ack_and_message_returns_whole_data_for_ack_outfox() { + let data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) + 10]; + let (ack, message) = split_into_ack_and_message( + data.clone(), + PacketSize::OutfoxAckPacket, + PacketType::Outfox, + ) + .unwrap(); + assert!(ack.is_none()); + assert_eq!(data, message) + } +} diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 2f57ea58ef..6185826bbf 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1935,18 +1935,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index 4ee18c77d5..5eb0b0c136 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -8,10 +8,10 @@ use futures::channel::mpsc::SendError; use futures::StreamExt; use nym_gateway_storage::{error::StorageError, Storage}; use nym_mixnet_client::forwarder::MixForwardingSender; -use nym_mixnode_common::packet_processor::processor::ProcessedFinalHop; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::ProcessedFinalHop; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; use std::collections::HashMap; @@ -21,6 +21,8 @@ use tokio::net::TcpStream; use tokio_util::codec::Framed; use tracing::*; +use super::packet_processing::process_packet; + // defines errors that warrant a panic if not thrown in the context of a shutdown #[derive(Debug, Error)] enum CriticalPacketProcessingError { @@ -184,14 +186,14 @@ impl ConnectionHandler { // question: can it also be per connection vs global? // - let processed_final_hop = match self.packet_processor.process_received(framed_sphinx_packet) - { - Err(err) => { - debug!("We failed to process received sphinx packet - {err}"); - return Ok(()); - } - Ok(processed_final_hop) => processed_final_hop, - }; + let processed_final_hop = + match process_packet(framed_sphinx_packet, self.packet_processor.sphinx_key()) { + Err(err) => { + debug!("We failed to process received sphinx packet - {err}"); + return Ok(()); + } + Ok(processed_final_hop) => processed_final_hop, + }; self.handle_processed_packet(processed_final_hop).await } diff --git a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs index 7ac90bb174..4f16c2451f 100644 --- a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -3,18 +3,24 @@ use nym_crypto::asymmetric::encryption; use nym_mixnode_common::packet_processor::error::MixProcessingError; -pub use nym_mixnode_common::packet_processor::processor::MixProcessingResult; -use nym_mixnode_common::packet_processor::processor::{ProcessedFinalHop, SphinxPacketProcessor}; +use nym_mixnode_common::packet_processor::processor::SphinxPacketProcessor; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::{ + process_framed_packet, MixProcessingResult, PacketProcessingError, ProcessedFinalHop, +}; +use nym_sphinx::PrivateKey; use thiserror::Error; #[derive(Error, Debug)] pub enum GatewayProcessingError { #[error("failed to process received mix packet - {0}")] - PacketProcessingError(#[from] MixProcessingError), + PacketProcessing(#[from] MixProcessingError), #[error("received a forward hop mix packet")] - ForwardHopReceivedError, + ForwardHopReceived, + + #[error("failed to process received sphinx packet: {0}")] + NymPacketProcessing(#[from] PacketProcessingError), } // PacketProcessor contains all data required to correctly unwrap and store sphinx packets @@ -24,21 +30,23 @@ pub struct PacketProcessor { } impl PacketProcessor { + pub fn sphinx_key(&self) -> &PrivateKey { + self.inner_processor.sphinx_key() + } + pub(crate) fn new(encryption_key: &encryption::PrivateKey) -> Self { PacketProcessor { inner_processor: SphinxPacketProcessor::new(encryption_key.into()), } } +} - pub(crate) fn process_received( - &self, - received: FramedNymPacket, - ) -> Result { - match self.inner_processor.process_received(received)? { - MixProcessingResult::ForwardHop(..) => { - Err(GatewayProcessingError::ForwardHopReceivedError) - } - MixProcessingResult::FinalHop(processed_final) => Ok(processed_final), - } +pub(crate) fn process_packet( + received: FramedNymPacket, + sphinx_key: &nym_sphinx::PrivateKey, +) -> Result { + match process_framed_packet(received, sphinx_key)? { + MixProcessingResult::ForwardHop(..) => Err(GatewayProcessingError::ForwardHopReceived), + MixProcessingResult::FinalHop(processed_final) => Ok(processed_final), } } diff --git a/mixnode/src/node/listener/connection_handler/mod.rs b/mixnode/src/node/listener/connection_handler/mod.rs index f4144c4f67..0b76762667 100644 --- a/mixnode/src/node/listener/connection_handler/mod.rs +++ b/mixnode/src/node/listener/connection_handler/mod.rs @@ -1,9 +1,7 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::listener::connection_handler::packet_processing::{ - MixProcessingResult, PacketProcessor, -}; +use crate::node::listener::connection_handler::packet_processing::PacketProcessor; use crate::node::packet_delayforwarder::PacketDelayForwardSender; use crate::node::TaskClient; use futures::StreamExt; @@ -13,7 +11,9 @@ use nym_metrics::nanos; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::MixProcessingResult; use nym_sphinx::Delay as SphinxDelay; +use packet_processing::process_received_packet; use std::net::SocketAddr; use tokio::net::TcpStream; use tokio::time::Instant; @@ -38,6 +38,10 @@ impl ConnectionHandler { } } + pub fn packet_processor(&self) -> &PacketProcessor { + &self.packet_processor + } + fn delay_and_forward_packet(&self, mix_packet: MixPacket, delay: Option) { // determine instant at which packet should get forwarded. this way we minimise effect of // being stuck in the queue [of the channel] to get inserted into the delay queue @@ -60,7 +64,10 @@ impl ConnectionHandler { // all processing such, key caching, etc. was done. // however, if it was a forward hop, we still need to delay it nanos!("handle_received_packet", { - match self.packet_processor.process_received(framed_sphinx_packet) { + self.packet_processor + .node_stats_update_sender() + .report_received(); + match process_received_packet(framed_sphinx_packet, self.packet_processor().inner()) { Err(err) => debug!("We failed to process received sphinx packet - {err}"), Ok(res) => match res { MixProcessingResult::ForwardHop(forward_packet, delay) => { diff --git a/mixnode/src/node/listener/connection_handler/packet_processing.rs b/mixnode/src/node/listener/connection_handler/packet_processing.rs index f849632f0f..9e6742a6b8 100644 --- a/mixnode/src/node/listener/connection_handler/packet_processing.rs +++ b/mixnode/src/node/listener/connection_handler/packet_processing.rs @@ -4,13 +4,13 @@ use crate::node::node_statistics; use nym_crypto::asymmetric::encryption; use nym_mixnode_common::packet_processor::error::MixProcessingError; -pub use nym_mixnode_common::packet_processor::processor::MixProcessingResult; use nym_mixnode_common::packet_processor::processor::SphinxPacketProcessor; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::{process_framed_packet, MixProcessingResult}; // PacketProcessor contains all data required to correctly unwrap and forward sphinx packets #[derive(Clone)] -pub struct PacketProcessor { +pub(crate) struct PacketProcessor { /// Responsible for performing unwrapping inner_processor: SphinxPacketProcessor, @@ -29,11 +29,18 @@ impl PacketProcessor { } } - pub(crate) fn process_received( - &self, - received: FramedNymPacket, - ) -> Result { - self.node_stats_update_sender.report_received(); - self.inner_processor.process_received(received) + pub fn inner(&self) -> &SphinxPacketProcessor { + &self.inner_processor + } + + pub fn node_stats_update_sender(&self) -> &node_statistics::UpdateSender { + &self.node_stats_update_sender } } + +pub fn process_received_packet( + packet: FramedNymPacket, + inner_processor: &SphinxPacketProcessor, +) -> Result { + Ok(process_framed_packet(packet, inner_processor.sphinx_key())?) +} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 3813f1ba4c..f65e796297 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -173,9 +173,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -1637,9 +1637,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -5760,18 +5760,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", From 143b336978e5f25d4f142d2adaf2fd06e7f97b51 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Mon, 7 Oct 2024 17:19:17 +0200 Subject: [PATCH 033/207] expose autheticator address along other address in node-details (#4953) --- nym-node/src/node/helpers.rs | 6 ++++++ nym-node/src/node/mod.rs | 1 + 2 files changed, 7 insertions(+) diff --git a/nym-node/src/node/helpers.rs b/nym-node/src/node/helpers.rs index 9fc7c310c9..3055578757 100644 --- a/nym-node/src/node/helpers.rs +++ b/nym-node/src/node/helpers.rs @@ -38,6 +38,7 @@ pub(crate) struct DisplayDetails { pub(crate) exit_network_requester_address: String, pub(crate) exit_ip_packet_router_address: String, + pub(crate) exit_authenticator_address: String, } impl Display for DisplayDetails { @@ -64,6 +65,11 @@ impl Display for DisplayDetails { "exit ip packet router address: {}", self.exit_ip_packet_router_address )?; + writeln!( + f, + "exit authenticator address: {}", + self.exit_authenticator_address + )?; Ok(()) } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index ce832617aa..3bb5fe83ca 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -522,6 +522,7 @@ impl NymNode { x25519_wireguard_key: self.x25519_wireguard_key().to_base58_string(), exit_network_requester_address: self.exit_network_requester_address().to_string(), exit_ip_packet_router_address: self.exit_ip_packet_router_address().to_string(), + exit_authenticator_address: self.exit_authenticator_address().to_string(), } } From 92e9da7be5ab2e7e47750b51a7e1fc011903fed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 8 Oct 2024 11:18:49 +0100 Subject: [PATCH 034/207] extended CosmWasmClient with AllContractState query --- .../client-libs/validator-client/Cargo.toml | 1 + .../client_traits/query_client.rs | 44 ++++++++++++++++--- .../src/nyxd/cosmwasm_client/types.rs | 23 +++++++++- common/serde-helpers/Cargo.toml | 2 + common/serde-helpers/src/lib.rs | 14 ++++++ 5 files changed, 77 insertions(+), 7 deletions(-) diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 6e8dd55772..d17b100672 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -20,6 +20,7 @@ nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } +nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } nym-http-api-client = { path = "../../../common/http-api-client" } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 47859ea0d1..5cea0a1ba2 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -5,7 +5,7 @@ use crate::nyxd; use crate::nyxd::coin::Coin; use crate::nyxd::cosmwasm_client::helpers::{create_pagination, next_page_key}; use crate::nyxd::cosmwasm_client::types::{ - Account, CodeDetails, Contract, ContractCodeId, SequenceResponse, SimulateResponse, + Account, CodeDetails, Contract, ContractCodeId, Model, SequenceResponse, SimulateResponse, }; use crate::nyxd::error::NyxdError; use crate::nyxd::Query; @@ -21,11 +21,11 @@ use cosmrs::proto::cosmos::tx::v1beta1::{ SimulateRequest, SimulateResponse as ProtoSimulateResponse, }; use cosmrs::proto::cosmwasm::wasm::v1::{ - QueryCodeRequest, QueryCodeResponse, QueryCodesRequest, QueryCodesResponse, - QueryContractHistoryRequest, QueryContractHistoryResponse, QueryContractInfoRequest, - QueryContractInfoResponse, QueryContractsByCodeRequest, QueryContractsByCodeResponse, - QueryRawContractStateRequest, QueryRawContractStateResponse, QuerySmartContractStateRequest, - QuerySmartContractStateResponse, + QueryAllContractStateRequest, QueryAllContractStateResponse, QueryCodeRequest, + QueryCodeResponse, QueryCodesRequest, QueryCodesResponse, QueryContractHistoryRequest, + QueryContractHistoryResponse, QueryContractInfoRequest, QueryContractInfoResponse, + QueryContractsByCodeRequest, QueryContractsByCodeResponse, QueryRawContractStateRequest, + QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse, }; use cosmrs::tendermint::{block, chain, Hash}; use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; @@ -444,6 +444,38 @@ pub trait CosmWasmClient: TendermintRpcClient { .collect::>()?) } + async fn query_all_contract_state(&self, address: &AccountId) -> Result, NyxdError> { + let path = Some("/cosmwasm.wasm.v1.Query/AllContractState".to_owned()); + + let mut models = Vec::new(); + let mut pagination = None; + + loop { + let req = QueryAllContractStateRequest { + address: address.to_string(), + pagination, + }; + + let mut res = self + .make_abci_query::<_, QueryAllContractStateResponse>(path.clone(), req) + .await?; + + let empty_response = res.models.is_empty(); + models.append(&mut res.models); + + if empty_response { + break; + } + if let Some(next_key) = next_page_key(res.pagination) { + pagination = Some(create_pagination(next_key)) + } else { + break; + } + } + + Ok(models.into_iter().map(Into::into).collect()) + } + async fn query_contract_raw( &self, address: &AccountId, diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs index 564a17441e..26003c1d29 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs @@ -27,13 +27,34 @@ use cosmrs::vesting::{ }; use cosmrs::{AccountId, Any, Coin as CosmosCoin}; use prost::Message; -use serde::Serialize; +use serde::{Deserialize, Serialize}; pub use cosmrs::abci::GasInfo; pub use cosmrs::abci::MsgResponse; pub type ContractCodeId = u64; +// yet another thing to put in cosmrs +#[derive(Serialize, Deserialize)] +pub struct Model { + #[serde(with = "nym_serde_helpers::hex")] + pub key: Vec, + + #[serde(with = "nym_serde_helpers::base64")] + pub value: Vec, +} + +// follow the cosmwasm serialisation format, i.e. hex for key and base64 for value + +impl From for Model { + fn from(model: cosmrs::proto::cosmwasm::wasm::v1::Model) -> Self { + Model { + key: model.key, + value: model.value, + } + } +} + #[derive(Serialize)] pub struct EmptyMsg {} diff --git a/common/serde-helpers/Cargo.toml b/common/serde-helpers/Cargo.toml index a45ed0afbd..bc9de1862e 100644 --- a/common/serde-helpers/Cargo.toml +++ b/common/serde-helpers/Cargo.toml @@ -13,11 +13,13 @@ license.workspace = true [dependencies] serde = { workspace = true } +hex = { workspace = true, optional = true } bs58 = { workspace = true, optional = true } base64 = { workspace = true, optional = true } time = { workspace = true, features = ["formatting", "parsing"], optional = true } [features] +hex = ["dep:hex"] bs58 = ["dep:bs58"] base64 = ["dep:base64"] date = ["time"] \ No newline at end of file diff --git a/common/serde-helpers/src/lib.rs b/common/serde-helpers/src/lib.rs index fd37be86b8..07ad83face 100644 --- a/common/serde-helpers/src/lib.rs +++ b/common/serde-helpers/src/lib.rs @@ -32,6 +32,20 @@ pub mod bs58 { } } +#[cfg(feature = "hex")] +pub mod hex { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&::hex::encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + ::hex::decode(&s).map_err(serde::de::Error::custom) + } +} + #[cfg(feature = "date")] pub mod date { use serde::ser::Error; From a21052b72e14e25d30ae39e9236ec912351de123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 8 Oct 2024 11:19:01 +0100 Subject: [PATCH 035/207] exposed contract state query to nym-cli --- Cargo.lock | 2 + common/commands/src/validator/cosmwasm/mod.rs | 5 ++- .../validator/cosmwasm/raw_contract_state.rs | 39 +++++++++++++++++++ nym-wallet/Cargo.lock | 2 + tools/nym-cli/src/validator/cosmwasm/mod.rs | 24 +++++++----- 5 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 common/commands/src/validator/cosmwasm/raw_contract_state.rs diff --git a/Cargo.lock b/Cargo.lock index 23f75f575f..03b9714297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5898,6 +5898,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -6334,6 +6335,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost 0.12.6", "reqwest 0.12.4", diff --git a/common/commands/src/validator/cosmwasm/mod.rs b/common/commands/src/validator/cosmwasm/mod.rs index 43dcf08951..2fbd63a535 100644 --- a/common/commands/src/validator/cosmwasm/mod.rs +++ b/common/commands/src/validator/cosmwasm/mod.rs @@ -7,13 +7,14 @@ pub mod execute_contract; pub mod generators; pub mod init_contract; pub mod migrate_contract; +pub mod raw_contract_state; pub mod upload_contract; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] pub struct Cosmwasm { #[clap(subcommand)] - pub command: Option, + pub command: CosmwasmCommands, } #[derive(Debug, Subcommand)] @@ -28,4 +29,6 @@ pub enum CosmwasmCommands { Migrate(crate::validator::cosmwasm::migrate_contract::Args), /// Execute a WASM smart contract method Execute(crate::validator::cosmwasm::execute_contract::Args), + /// Obtain raw contract state of a cosmwasm smart contract + RawContractState(crate::validator::cosmwasm::raw_contract_state::Args), } diff --git a/common/commands/src/validator/cosmwasm/raw_contract_state.rs b/common/commands/src/validator/cosmwasm/raw_contract_state.rs new file mode 100644 index 0000000000..29771b7274 --- /dev/null +++ b/common/commands/src/validator/cosmwasm/raw_contract_state.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::QueryClient; +use clap::Parser; +use cosmrs::AccountId; +use log::trace; +use nym_validator_client::nyxd::CosmWasmClient; +use std::fs; +use std::fs::File; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long, value_parser)] + #[clap(help = "The address of contract to get the state of")] + pub contract: AccountId, + + #[clap(short, long)] + #[clap(help = "Output file for the retrieved contract state")] + pub output: PathBuf, +} + +pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { + trace!("args: {args:?}"); + + let output = File::create(&args.output)?; + let raw = client.query_all_contract_state(&args.contract).await?; + + serde_json::to_writer(output, &raw)?; + println!( + "wrote {} key-value from {} pairs into '{}'", + raw.len(), + args.contract, + fs::canonicalize(args.output)?.display() + ); + + Ok(()) +} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index f65e796297..951f240165 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3361,6 +3361,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -3449,6 +3450,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.4", diff --git a/tools/nym-cli/src/validator/cosmwasm/mod.rs b/tools/nym-cli/src/validator/cosmwasm/mod.rs index 28f197eedb..d078f498ab 100644 --- a/tools/nym-cli/src/validator/cosmwasm/mod.rs +++ b/tools/nym-cli/src/validator/cosmwasm/mod.rs @@ -1,4 +1,4 @@ -use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; pub(crate) mod generators; @@ -9,14 +9,14 @@ pub(crate) async fn execute( network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match cosmwasm.command { - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Upload(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Upload(args) => { nym_cli_commands::validator::cosmwasm::upload_contract::upload( args, create_signing_client(global_args, network_details)?, ) .await } - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Init(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Init(args) => { nym_cli_commands::validator::cosmwasm::init_contract::init( args, create_signing_client(global_args, network_details)?, @@ -25,24 +25,30 @@ pub(crate) async fn execute( .await } - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::GenerateInitMessage( - generator, - )) => generators::execute(generator).await?, - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Migrate(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::GenerateInitMessage(generator) => { + generators::execute(generator).await? + } + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Migrate(args) => { nym_cli_commands::validator::cosmwasm::migrate_contract::migrate( args, create_signing_client(global_args, network_details)?, ) .await } - Some(nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Execute(args)) => { + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::Execute(args) => { nym_cli_commands::validator::cosmwasm::execute_contract::execute( args, create_signing_client(global_args, network_details)?, ) .await } - _ => unreachable!(), + nym_cli_commands::validator::cosmwasm::CosmwasmCommands::RawContractState(args) => { + nym_cli_commands::validator::cosmwasm::raw_contract_state::execute( + args, + create_query_client(network_details)?, + ) + .await? + } } Ok(()) } From c16746a47b83ac63abffadd3f721028ab3c7475b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 8 Oct 2024 15:06:41 +0100 Subject: [PATCH 036/207] introduced internal tool for importing contract states --- Cargo.lock | 37 ++ Cargo.toml | 2 + .../internal/contract-state-importer/Makefile | 2 + .../importer-cli/Cargo.toml | 26 + .../importer-cli/src/main.rs | 477 ++++++++++++++++++ .../importer-contract/Cargo.toml | 24 + .../importer-contract/Makefile | 5 + .../importer-contract/src/contract.rs | 45 ++ .../importer-contract/src/lib.rs | 8 + .../importer-contract/src/msg.rs | 20 + 10 files changed, 646 insertions(+) create mode 100644 tools/internal/contract-state-importer/Makefile create mode 100644 tools/internal/contract-state-importer/importer-cli/Cargo.toml create mode 100644 tools/internal/contract-state-importer/importer-cli/src/main.rs create mode 100644 tools/internal/contract-state-importer/importer-contract/Cargo.toml create mode 100644 tools/internal/contract-state-importer/importer-contract/Makefile create mode 100644 tools/internal/contract-state-importer/importer-contract/src/contract.rs create mode 100644 tools/internal/contract-state-importer/importer-contract/src/lib.rs create mode 100644 tools/internal/contract-state-importer/importer-contract/src/msg.rs diff --git a/Cargo.lock b/Cargo.lock index 03b9714297..82a9a1df21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "base85" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36915bbaca237c626689b5bd14d02f2ba7a5a359d30a2a08be697392e3718079" +dependencies = [ + "thiserror", +] + [[package]] name = "basic-toml" version = "0.1.9" @@ -3299,6 +3308,34 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "importer-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "clap 4.5.18", + "dirs 5.0.1", + "importer-contract", + "nym-bin-common", + "nym-network-defaults", + "nym-validator-client", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "importer-contract" +version = "0.1.0" +dependencies = [ + "base85", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", +] + [[package]] name = "indenter" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index ab9b668e93..19f0ef8f00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,8 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", "tools/echo-server", + "tools/internal/contract-state-importer/importer-cli", + "tools/internal/contract-state-importer/importer-contract", ] default-members = [ diff --git a/tools/internal/contract-state-importer/Makefile b/tools/internal/contract-state-importer/Makefile new file mode 100644 index 0000000000..8e375fdf6a --- /dev/null +++ b/tools/internal/contract-state-importer/Makefile @@ -0,0 +1,2 @@ +build-importer-contract: + $(MAKE) -C importer-contract build \ No newline at end of file diff --git a/tools/internal/contract-state-importer/importer-cli/Cargo.toml b/tools/internal/contract-state-importer/importer-cli/Cargo.toml new file mode 100644 index 0000000000..45a2302209 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "importer-cli" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +bip39 = { workspace = true } +clap = { workspace = true, features = ["derive"] } +dirs = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } +tracing = { workspace = true } + +importer-contract = { path = "../importer-contract" } +nym-validator-client = { path = "../../../../common/client-libs/validator-client" } +nym-bin-common = { path = "../../../../common/bin-common", features = ["basic_tracing"] } +nym-network-defaults = { path = "../../../../common/network-defaults" } diff --git a/tools/internal/contract-state-importer/importer-cli/src/main.rs b/tools/internal/contract-state-importer/importer-cli/src/main.rs new file mode 100644 index 0000000000..2caaa2e09b --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/main.rs @@ -0,0 +1,477 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow::bail; +use clap::ArgGroup; +use clap::{Args, Parser, Subcommand}; +use importer_contract::contract::EmptyMessage; +use importer_contract::{base85, ExecuteMsg}; +use nym_bin_common::bin_info; +use nym_bin_common::logging::setup_tracing_logger; +use nym_network_defaults::{setup_env, NymNetworkDetails}; +use nym_validator_client::nyxd::cosmwasm_client::types::{InstantiateOptions, Model}; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; +use serde::{Deserialize, Serialize}; +use std::env::current_dir; +use std::fs; +use std::fs::{create_dir_all, File}; +use std::io::Read; +use std::path::PathBuf; +use std::sync::OnceLock; +use tracing::{debug, info}; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub struct Cli { + /// Path pointing to an env file that configures the CLI. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + #[clap(long)] + pub(crate) mnemonic: bip39::Mnemonic, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Serialize, Deserialize)] +pub struct CachedState { + pub importer_address: AccountId, + pub state_imported: bool, +} + +impl CachedState { + pub fn save(&self) -> anyhow::Result<()> { + let path = cached_state_file(); + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + let file = File::create(&path)?; + serde_json::to_writer_pretty(file, self)?; + + info!("saved cached details to {}", path.display()); + Ok(()) + } + + pub fn load() -> anyhow::Result { + let file = File::open(cached_state_file())?; + Ok(serde_json::from_reader(&file)?) + } +} + +fn cached_state_file() -> PathBuf { + dirs::cache_dir() + .unwrap() + .join("contract-state-importer") + .join(".state.json") +} + +// this only works if the cli is called from somewhere within the nym directory +// (which realistically is going to be the case most of the time) +fn importer_contract_path(explicit: Option) -> anyhow::Result { + if let Some(explicit) = explicit { + return Ok(explicit); + } + + for ancestor in current_dir()?.ancestors() { + debug!("checking {:?}", fs::canonicalize(ancestor)); + for content in ancestor.read_dir()? { + let dir_entry = content?; + let Ok(name) = dir_entry.file_name().into_string() else { + continue; + }; + + if name == "target" { + let maybe_contract_path = dir_entry + .path() + .join("wasm32-unknown-unknown") + .join("release") + .join("importer_contract.wasm"); + + if maybe_contract_path.exists() { + return Ok(maybe_contract_path); + } + } + } + } + + bail!("could not find importer_contract.wasm") +} + +fn importer_contract_address(explicit: Option) -> anyhow::Result { + if let Some(explicit) = explicit { + return Ok(explicit); + } + + let state = CachedState::load()?; + Ok(state.importer_address) +} + +#[derive(Args, Clone)] +pub struct PrepareArgs { + /// Path to the .wasm file with the importer contract + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" + #[clap(long)] + pub importer_contract_path: Option, +} + +#[derive(Args, Clone)] +pub struct SetStateArgs { + /// Explicit address of the initialised importer contract. + /// If not set, the value from the cached state will be attempted to be used + #[clap(long)] + pub importer_contract_address: Option, + + /// Path to the file containing state dump of a cosmwasm contract + #[clap(long)] + pub raw_state: PathBuf, +} + +#[derive(Args, Clone)] +#[clap(group(ArgGroup::new("contract").required(true)))] +pub struct SwapContractArgs { + /// Explicit address of the initialised importer contract. + /// If not set, the value from the cached state will be attempted to be used + #[clap(long)] + pub importer_contract_address: Option, + + /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_code_id: Option, + + /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_path: Option, + + /// The custom migrate message used for migrating into target contract. + /// If none is provided an empty object will be used instead, i.e. '{}' + #[clap(long)] + pub migrate_msg: Option, +} + +#[derive(Args, Clone)] +#[clap(group(ArgGroup::new("contract").required(true)))] +pub struct InitialiseWithStateArgs { + /// Path to the .wasm file with the importer contract + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" + #[clap(long)] + pub importer_contract_path: Option, + + /// Path to the file containing state dump of a cosmwasm contract + #[clap(long)] + pub raw_state: PathBuf, + + /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_code_id: Option, + + /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_path: Option, + + #[clap(long)] + pub migrate_msg: Option, +} + +#[derive(Subcommand)] +pub(crate) enum Commands { + /// Upload and instantiates the importer contract + PrepareContract(PrepareArgs), + + /// Set the state of the previously instantiated importer contract with the provided state dump + SetState(SetStateArgs), + + /// Swap the importer contract code with the one corresponding to the previously uploaded state dump + SwapContract(SwapContractArgs), + + /// Combines the functionalities of `prepare-contract`, `set-state` and `swap-contract` + InitialiseWithState(InitialiseWithStateArgs), +} + +async fn create_importer_contract( + explicit_contract_path: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result { + info!("attempting to create the importer contract"); + + let importer_path = importer_contract_path(explicit_contract_path)?; + info!( + "going to use the following importer contract: '{}'", + fs::canonicalize(&importer_path)?.display() + ); + + let mut data = Vec::new(); + File::open(importer_path)?.read_to_end(&mut data)?; + + let res = client.upload(data, "", None).await?; + let importer_code_id = res.code_id; + info!( + " ✅ uploaded the importer contract in {}", + res.transaction_hash + ); + + let res = client + .instantiate( + importer_code_id, + &EmptyMessage {}, + "importer-contract".into(), + "", + Some(InstantiateOptions::default().with_admin(client.address())), + None, + ) + .await?; + let importer_address = res.contract_address; + info!( + " ✅ instantiated the importer contract in {}", + res.transaction_hash + ); + + info!("IMPORTER CONTRACT ADDRESS: {importer_address}"); + + CachedState { + importer_address: importer_address.clone(), + state_imported: false, + } + .save()?; + + Ok(importer_address) +} + +async fn execute_prepare_contract( + args: PrepareArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + create_importer_contract(args.importer_contract_path, &client).await?; + + Ok(()) +} + +fn approximate_size(pair: &Model) -> usize { + base85::encode(&pair.key).len() + base85::encode(&pair.value).len() +} + +fn models_to_exec(data: Vec) -> ExecuteMsg { + let pairs = data + .into_iter() + .map(|kv| (kv.key, kv.value)) + .collect::>(); + + pairs.into() +} + +fn split_into_importable_execute_msgs( + kv_pairs: Vec, + approximate_max_chunk: usize, +) -> Vec { + let mut chunks: Vec = Vec::new(); + + let mut current_wip_chunk = Vec::new(); + let mut current_chunk_size = 0; + for kv in kv_pairs { + if current_chunk_size + approximate_size(&kv) > approximate_max_chunk { + let taken = std::mem::take(&mut current_wip_chunk); + chunks.push(models_to_exec(taken)); + current_chunk_size = 0; + } + current_chunk_size += approximate_size(&kv); + current_wip_chunk.push(kv); + } + + if !current_wip_chunk.is_empty() { + chunks.push(models_to_exec(current_wip_chunk)) + } + chunks +} + +async fn set_importer_state( + state_dump_path: PathBuf, + explicit_importer_address: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + info!("attempting to set the importer contract state"); + + // this is the value that we found to be optimal during v1->v2 mixnet migration + const MAX_CHUNK_SIZE: usize = 350 * 1000; + + let importer_address = importer_contract_address(explicit_importer_address)?; + + if let Ok(state) = CachedState::load() { + if state.state_imported && state.importer_address == importer_address { + bail!("the state has already been imported for {importer_address}") + } + } + + let dump_file = File::open(state_dump_path)?; + info!("attempting to decode the state dump. for bigger contracts this might take a while..."); + let kv_pairs: Vec = serde_json::from_reader(&dump_file)?; + + info!("there are {} key-value pairs to import", kv_pairs.len()); + info!("attempting to split them into {MAX_CHUNK_SIZE}B chunks ExecuteMsgs..."); + + let chunks = split_into_importable_execute_msgs(kv_pairs, MAX_CHUNK_SIZE); + info!("obtained {} execute msgs", chunks.len()); + + let total = chunks.len(); + for (i, msg) in chunks.into_iter().enumerate() { + info!("executing message {}/{total}...", i + 1); + let res = client + .execute( + &importer_address, + &msg, + None, + "importing contract state", + Vec::new(), + ) + .await?; + info!(" ✅ OK: {}", res.transaction_hash); + } + + info!("Finished migrating storage to {importer_address}!"); + + CachedState { + importer_address, + state_imported: true, + } + .save()?; + + Ok(()) +} + +async fn execute_set_state( + args: SetStateArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + set_importer_state(args.raw_state, args.importer_contract_address, &client).await?; + + Ok(()) +} + +async fn swap_contract( + target_code_id: Option, + target_contract_path: Option, + explicit_importer_address: Option, + migrate_msg: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + info!("attempting to swap the contract code"); + + let importer_address = importer_contract_address(explicit_importer_address)?; + + if let Ok(state) = CachedState::load() { + if !state.state_imported && state.importer_address == importer_address { + bail!("the state hasn't been imported for {importer_address}") + } + } + + // one of those must have been set via clap + let code_id = match target_code_id { + Some(explicit) => explicit, + None => { + // upload the contract + let mut data = Vec::new(); + File::open(target_contract_path.unwrap())?.read_to_end(&mut data)?; + + let res = client.upload(data, "", None).await?; + info!( + " ✅ uploaded the target contract in {}", + res.transaction_hash + ); + res.code_id + } + }; + + let migrate_msg = migrate_msg.unwrap_or(serde_json::Value::Object(Default::default())); + let res = client + .migrate( + &importer_address, + code_id, + &migrate_msg, + "migrating into target contract", + None, + ) + .await?; + info!( + " ✅ migrated into the target contract: {}", + res.transaction_hash + ); + + Ok(()) +} + +async fn execute_swap_contract( + args: SwapContractArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + swap_contract( + args.target_contract_code_id, + args.target_contract_path, + args.importer_contract_address, + args.migrate_msg, + &client, + ) + .await?; + + Ok(()) +} + +async fn initialise_with_state( + args: InitialiseWithStateArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + let importer_address = create_importer_contract(args.importer_contract_path, &client).await?; + set_importer_state(args.raw_state, Some(importer_address.clone()), &client).await?; + swap_contract( + args.target_contract_code_id, + args.target_contract_path, + Some(importer_address.clone()), + args.migrate_msg, + &client, + ) + .await?; + + info!("the contract is ready at {importer_address}"); + + Ok(()) +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + let network_details = NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + let nyxd_url = network_details + .endpoints + .first() + .expect("network details are not defined") + .nyxd_url + .as_str(); + + let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + client_config, + nyxd_url, + self.mnemonic, + )?; + match self.command { + Commands::PrepareContract(args) => execute_prepare_contract(args, client).await, + Commands::SetState(args) => execute_set_state(args, client).await, + Commands::SwapContract(args) => execute_swap_contract(args, client).await, + Commands::InitialiseWithState(args) => initialise_with_state(args, client).await, + } + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + setup_env(cli.config_env_file.as_ref()); + + setup_tracing_logger(); + cli.execute().await +} diff --git a/tools/internal/contract-state-importer/importer-contract/Cargo.toml b/tools/internal/contract-state-importer/importer-contract/Cargo.toml new file mode 100644 index 0000000000..741b9e7e2e --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "importer-contract" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +readme.workspace = true + + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +base85 = "2.0.0" +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +cosmwasm-schema = { workspace = true } + +[features] +default = ["library"] +library = [] \ No newline at end of file diff --git a/tools/internal/contract-state-importer/importer-contract/Makefile b/tools/internal/contract-state-importer/importer-contract/Makefile new file mode 100644 index 0000000000..fdc2752a81 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/Makefile @@ -0,0 +1,5 @@ +all: build + +build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown --no-default-features + wasm-opt --signext-lowering -O ../../../../target/wasm32-unknown-unknown/release/importer_contract.wasm -o ../../../../target/wasm32-unknown-unknown/release/importer_contract.wasm \ No newline at end of file diff --git a/tools/internal/contract-state-importer/importer-contract/src/contract.rs b/tools/internal/contract-state-importer/importer-contract/src/contract.rs new file mode 100644 index 0000000000..27b241fbba --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/src/contract.rs @@ -0,0 +1,45 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ExecuteMsg; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError}; + +#[cw_serde] +pub struct EmptyMessage {} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn instantiate( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn execute( + deps: DepsMut<'_>, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + for (key, value) in msg.pairs { + let key = base85::decode(&key).unwrap(); + let value = base85::decode(&value).unwrap(); + deps.storage.set(&key, &value); + } + + Ok(Default::default()) +} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn query(_: Deps<'_>, _: Env, _: EmptyMessage) -> Result { + Ok(Default::default()) +} + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: EmptyMessage) -> Result { + Ok(Default::default()) +} diff --git a/tools/internal/contract-state-importer/importer-contract/src/lib.rs b/tools/internal/contract-state-importer/importer-contract/src/lib.rs new file mode 100644 index 0000000000..5f736d0af3 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/src/lib.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod contract; +pub mod msg; + +pub use base85; +pub use msg::ExecuteMsg; diff --git a/tools/internal/contract-state-importer/importer-contract/src/msg.rs b/tools/internal/contract-state-importer/importer-contract/src/msg.rs new file mode 100644 index 0000000000..e629d9588d --- /dev/null +++ b/tools/internal/contract-state-importer/importer-contract/src/msg.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct ExecuteMsg { + pub pairs: Vec<(String, String)>, +} + +impl From, Vec)>> for ExecuteMsg { + fn from(raw: Vec<(Vec, Vec)>) -> Self { + ExecuteMsg { + pairs: raw + .into_iter() + .map(|(k, v)| (base85::encode(&k), base85::encode(&v))) + .collect(), + } + } +} From 5e0d1bb14e88d12df465f1a011bb3ec24dcd080b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 8 Oct 2024 15:30:57 +0100 Subject: [PATCH 037/207] swapped base85 for base85rs with more compatible license --- Cargo.lock | 11 ++++------- .../contract-state-importer/importer-cli/src/main.rs | 4 ++-- .../importer-contract/Cargo.toml | 2 +- .../importer-contract/src/contract.rs | 4 ++-- .../importer-contract/src/lib.rs | 2 +- .../importer-contract/src/msg.rs | 2 +- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82a9a1df21..00d23fa76e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,13 +577,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] -name = "base85" -version = "2.0.0" +name = "base85rs" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36915bbaca237c626689b5bd14d02f2ba7a5a359d30a2a08be697392e3718079" -dependencies = [ - "thiserror", -] +checksum = "87678d33a2af71f019ed11f52db246ca6c5557edee2cccbe689676d1ad9c6b5a" [[package]] name = "basic-toml" @@ -3330,7 +3327,7 @@ dependencies = [ name = "importer-contract" version = "0.1.0" dependencies = [ - "base85", + "base85rs", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", diff --git a/tools/internal/contract-state-importer/importer-cli/src/main.rs b/tools/internal/contract-state-importer/importer-cli/src/main.rs index 2caaa2e09b..842217363c 100644 --- a/tools/internal/contract-state-importer/importer-cli/src/main.rs +++ b/tools/internal/contract-state-importer/importer-cli/src/main.rs @@ -5,7 +5,7 @@ use anyhow::bail; use clap::ArgGroup; use clap::{Args, Parser, Subcommand}; use importer_contract::contract::EmptyMessage; -use importer_contract::{base85, ExecuteMsg}; +use importer_contract::{base85rs, ExecuteMsg}; use nym_bin_common::bin_info; use nym_bin_common::logging::setup_tracing_logger; use nym_network_defaults::{setup_env, NymNetworkDetails}; @@ -255,7 +255,7 @@ async fn execute_prepare_contract( } fn approximate_size(pair: &Model) -> usize { - base85::encode(&pair.key).len() + base85::encode(&pair.value).len() + base85rs::encode(&pair.key).len() + base85rs::encode(&pair.value).len() } fn models_to_exec(data: Vec) -> ExecuteMsg { diff --git a/tools/internal/contract-state-importer/importer-contract/Cargo.toml b/tools/internal/contract-state-importer/importer-contract/Cargo.toml index 741b9e7e2e..7952b197c2 100644 --- a/tools/internal/contract-state-importer/importer-contract/Cargo.toml +++ b/tools/internal/contract-state-importer/importer-contract/Cargo.toml @@ -14,7 +14,7 @@ readme.workspace = true crate-type = ["cdylib", "rlib"] [dependencies] -base85 = "2.0.0" +base85rs = "0.1.3" cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } cosmwasm-schema = { workspace = true } diff --git a/tools/internal/contract-state-importer/importer-contract/src/contract.rs b/tools/internal/contract-state-importer/importer-contract/src/contract.rs index 27b241fbba..c47a31f75b 100644 --- a/tools/internal/contract-state-importer/importer-contract/src/contract.rs +++ b/tools/internal/contract-state-importer/importer-contract/src/contract.rs @@ -26,8 +26,8 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { for (key, value) in msg.pairs { - let key = base85::decode(&key).unwrap(); - let value = base85::decode(&value).unwrap(); + let key = base85rs::decode(&key).unwrap(); + let value = base85rs::decode(&value).unwrap(); deps.storage.set(&key, &value); } diff --git a/tools/internal/contract-state-importer/importer-contract/src/lib.rs b/tools/internal/contract-state-importer/importer-contract/src/lib.rs index 5f736d0af3..d1ca431ab0 100644 --- a/tools/internal/contract-state-importer/importer-contract/src/lib.rs +++ b/tools/internal/contract-state-importer/importer-contract/src/lib.rs @@ -4,5 +4,5 @@ pub mod contract; pub mod msg; -pub use base85; +pub use base85rs; pub use msg::ExecuteMsg; diff --git a/tools/internal/contract-state-importer/importer-contract/src/msg.rs b/tools/internal/contract-state-importer/importer-contract/src/msg.rs index e629d9588d..bbdce88fcc 100644 --- a/tools/internal/contract-state-importer/importer-contract/src/msg.rs +++ b/tools/internal/contract-state-importer/importer-contract/src/msg.rs @@ -13,7 +13,7 @@ impl From, Vec)>> for ExecuteMsg { ExecuteMsg { pairs: raw .into_iter() - .map(|(k, v)| (base85::encode(&k), base85::encode(&v))) + .map(|(k, v)| (base85rs::encode(&k), base85rs::encode(&v))) .collect(), } } From e59a9a59b6428b6bb9b0014c76093e72b2b67fed Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Wed, 9 Oct 2024 13:15:38 +0200 Subject: [PATCH 038/207] feat: add env feature to clap and change variables to be available as env variables as well --- nym-api/Cargo.toml | 2 +- nym-api/src/support/cli/init.rs | 17 +++++++++-------- nym-api/src/support/cli/mod.rs | 4 ++-- nym-api/src/support/cli/run.rs | 17 +++++++++-------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 194be8cc2d..f408e1f3ba 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -22,7 +22,7 @@ bip39 = { workspace = true } bincode.workspace = true bloomfilter = { workspace = true } cfg-if = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive", "env"] } console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" dirs = { workspace = true } futures = { workspace = true } diff --git a/nym-api/src/support/cli/init.rs b/nym-api/src/support/cli/init.rs index 10ce41b6f9..ca67b43c12 100644 --- a/nym-api/src/support/cli/init.rs +++ b/nym-api/src/support/cli/init.rs @@ -9,27 +9,27 @@ use anyhow::bail; pub(crate) struct Args { /// Id of the nym-api we want to initialise. if unspecified, a default value will be used. /// default: "default" - #[clap(long, default_value = "default")] + #[clap(long, default_value = "default", env)] pub(crate) id: String, /// Specifies whether network monitoring is enabled on this API /// default: false - #[clap(short = 'm', long)] + #[clap(short = 'm', long, env)] pub(crate) enable_monitor: bool, /// Specifies whether network rewarding is enabled on this API /// default: false - #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic")] + #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic", env)] pub(crate) enable_rewarding: bool, /// Endpoint to nyxd instance used for contract information. /// default: http://localhost:26657 - #[clap(long)] + #[clap(long, env)] pub(crate) nyxd_validator: Option, /// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions /// default: None - #[clap(long)] + #[clap(long, env)] pub(crate) mnemonic: Option, /// Flag to indicate whether credential signer authority is enabled on this API @@ -38,18 +38,19 @@ pub(crate) struct Args { long, requires = "mnemonic", requires = "announce_address", - alias = "enable_coconut" + alias = "enable_coconut", + env )] pub(crate) enable_zk_nym: bool, /// Announced address that is going to be put in the DKG contract where zk-nym clients will connect /// to obtain their credentials /// default: None - #[clap(long)] + #[clap(long, env)] pub(crate) announce_address: Option, /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement - #[clap(long, requires = "enable_monitor")] + #[clap(long, requires = "enable_monitor", env)] pub(crate) monitor_credentials_mode: bool, // #[clap(short, long, default_value_t = OutputFormat::default())] // output: OutputFormat, diff --git a/nym-api/src/support/cli/mod.rs b/nym-api/src/support/cli/mod.rs index 6667adccc6..483361a758 100644 --- a/nym-api/src/support/cli/mod.rs +++ b/nym-api/src/support/cli/mod.rs @@ -20,11 +20,11 @@ fn pretty_build_info_static() -> &'static str { #[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] pub(crate) struct Cli { /// Path pointing to an env file that configures the Nym API. - #[clap(short, long)] + #[clap(short, long, env)] pub(crate) config_env_file: Option, /// A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops) - #[clap(long)] + #[clap(long, env)] pub(crate) no_banner: bool, #[clap(subcommand)] diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index a19d2c1c29..a12a1dc81c 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -9,27 +9,27 @@ use cfg_if::cfg_if; pub(crate) struct Args { /// Id of the nym-api we want to run.if unspecified, a default value will be used. /// default: "default" - #[clap(long, default_value = "default")] + #[clap(long, default_value = "default", env)] pub(crate) id: String, /// Specifies whether network monitoring is enabled on this API /// default: None - config value will be used instead - #[clap(short = 'm', long)] + #[clap(short = 'm', long, env)] pub(crate) enable_monitor: Option, /// Specifies whether network rewarding is enabled on this API /// default: None - config value will be used instead - #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic")] + #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic", env)] pub(crate) enable_rewarding: Option, /// Endpoint to nyxd instance used for contract information. /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env)] pub(crate) nyxd_validator: Option, /// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env)] pub(crate) mnemonic: Option, /// Flag to indicate whether coconut signer authority is enabled on this API @@ -38,19 +38,20 @@ pub(crate) struct Args { long, requires = "mnemonic", requires = "announce_address", - alias = "enable_coconut" + alias = "enable_coconut", + env )] pub(crate) enable_zk_nym: Option, /// Announced address that is going to be put in the DKG contract where zk-nym clients will connect /// to obtain their credentials /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env)] pub(crate) announce_address: Option, /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env)] pub(crate) monitor_credentials_mode: Option, } From a1e75e1dfffa87a87770fd99b55a21d13ec6b198 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Wed, 9 Oct 2024 13:24:28 +0200 Subject: [PATCH 039/207] change env var naming to be consistent with nym-node --- nym-api/src/support/cli/init.rs | 26 ++++++++++++++++++-------- nym-api/src/support/cli/mod.rs | 4 ++-- nym-api/src/support/cli/run.rs | 22 ++++++++++++++-------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/nym-api/src/support/cli/init.rs b/nym-api/src/support/cli/init.rs index ca67b43c12..f3c47eb6a5 100644 --- a/nym-api/src/support/cli/init.rs +++ b/nym-api/src/support/cli/init.rs @@ -9,27 +9,33 @@ use anyhow::bail; pub(crate) struct Args { /// Id of the nym-api we want to initialise. if unspecified, a default value will be used. /// default: "default" - #[clap(long, default_value = "default", env)] + #[clap(long, default_value = "default", env = "NYMAPI_ID_ARG")] pub(crate) id: String, /// Specifies whether network monitoring is enabled on this API /// default: false - #[clap(short = 'm', long, env)] + #[clap(short = 'm', long, env = "NYMAPI_ENABLE_MONITOR_ARG")] pub(crate) enable_monitor: bool, /// Specifies whether network rewarding is enabled on this API /// default: false - #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic", env)] + #[clap( + short = 'r', + long, + requires = "enable_monitor", + requires = "mnemonic", + env = "NYMAPI_ENABLE_REWARDING_ARG" + )] pub(crate) enable_rewarding: bool, /// Endpoint to nyxd instance used for contract information. /// default: http://localhost:26657 - #[clap(long, env)] + #[clap(long, env = "NYMAPI_NYXD_VALIDATOR_ARG")] pub(crate) nyxd_validator: Option, /// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions /// default: None - #[clap(long, env)] + #[clap(long, env = "NYMAPI_MNEMONIC_ARG")] pub(crate) mnemonic: Option, /// Flag to indicate whether credential signer authority is enabled on this API @@ -39,18 +45,22 @@ pub(crate) struct Args { requires = "mnemonic", requires = "announce_address", alias = "enable_coconut", - env + env = "NYMAPI_ENABLE_ZK_NYM_ARG" )] pub(crate) enable_zk_nym: bool, /// Announced address that is going to be put in the DKG contract where zk-nym clients will connect /// to obtain their credentials /// default: None - #[clap(long, env)] + #[clap(long, env = "NYMAPI_ANNOUNCE_ADDRESS_NYM_ARG")] pub(crate) announce_address: Option, /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement - #[clap(long, requires = "enable_monitor", env)] + #[clap( + long, + requires = "enable_monitor", + env = "NYMAPI_MONITOR_CREDENTIALS_MODE_ARG" + )] pub(crate) monitor_credentials_mode: bool, // #[clap(short, long, default_value_t = OutputFormat::default())] // output: OutputFormat, diff --git a/nym-api/src/support/cli/mod.rs b/nym-api/src/support/cli/mod.rs index 483361a758..7ba0b6411f 100644 --- a/nym-api/src/support/cli/mod.rs +++ b/nym-api/src/support/cli/mod.rs @@ -20,11 +20,11 @@ fn pretty_build_info_static() -> &'static str { #[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] pub(crate) struct Cli { /// Path pointing to an env file that configures the Nym API. - #[clap(short, long, env)] + #[clap(short, long, env = "NYMAPI_CONFIG_ENV_FILE_ARG")] pub(crate) config_env_file: Option, /// A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops) - #[clap(long, env)] + #[clap(long, env = "NYMAPI_NO_BANNER_ARG")] pub(crate) no_banner: bool, #[clap(subcommand)] diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index a12a1dc81c..e5e1849ac9 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -9,27 +9,33 @@ use cfg_if::cfg_if; pub(crate) struct Args { /// Id of the nym-api we want to run.if unspecified, a default value will be used. /// default: "default" - #[clap(long, default_value = "default", env)] + #[clap(long, default_value = "default", env = "NYMAPI_ID_ARG")] pub(crate) id: String, /// Specifies whether network monitoring is enabled on this API /// default: None - config value will be used instead - #[clap(short = 'm', long, env)] + #[clap(short = 'm', long, env = "NYMAPI_ENABLE_MONITOR_ARG")] pub(crate) enable_monitor: Option, /// Specifies whether network rewarding is enabled on this API /// default: None - config value will be used instead - #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic", env)] + #[clap( + short = 'r', + long, + requires = "enable_monitor", + requires = "mnemonic", + env = "NYMAPI_ENABLE_REWARDING_ARG" + )] pub(crate) enable_rewarding: Option, /// Endpoint to nyxd instance used for contract information. /// default: None - config value will be used instead - #[clap(long, env)] + #[clap(long, env = "NYMAPI_NYXD_VALIDATOR_ARG")] pub(crate) nyxd_validator: Option, /// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions /// default: None - config value will be used instead - #[clap(long, env)] + #[clap(long, env = "NYMAPI_MNEMONIC_ARG")] pub(crate) mnemonic: Option, /// Flag to indicate whether coconut signer authority is enabled on this API @@ -39,19 +45,19 @@ pub(crate) struct Args { requires = "mnemonic", requires = "announce_address", alias = "enable_coconut", - env + env = "NYMAPI_ENABLE_ZK_NYM_ARG" )] pub(crate) enable_zk_nym: Option, /// Announced address that is going to be put in the DKG contract where zk-nym clients will connect /// to obtain their credentials /// default: None - config value will be used instead - #[clap(long, env)] + #[clap(long, env = "NYMAPI_ANNOUNCE_ADDRESS_ARG")] pub(crate) announce_address: Option, /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement /// default: None - config value will be used instead - #[clap(long, env)] + #[clap(long, env = "NYMAPI_MONITOR_CREDENTIALS_MODE_ARG")] pub(crate) monitor_credentials_mode: Option, } From 51b511b27ea7d81e9fe8f5f7e9de6623c80f3b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 28 Mar 2024 11:36:36 +0000 Subject: [PATCH 040/207] Rebased the branch one more time WIP; rebasing Another branch squash Squashing the v3 branch changing min pledge amounts logic for adding new nymnode into the contract converting mixnode/gateway bonding into nym-node bonding logic for migrating gateways into nymnodes ibid for mixnodes further nym-node work + fixed most existing unit tests forbid nymnode migration with pending cost params changes preassign nodeid for gateways changing role assignment and epoch progression changing role assignment and epoch progression optional custom http port logic for unbonding a nym-node updating Delegation struct logic for increasing pledge of either mixnode or nymnode logic for decreasing pledge of either mixnode or a nym node logic for changing cost params of either mixnode or a nym node wip initialise nymnodes storage fixing transaction tests fixed naive family tests reward-compatibility related works resolving delegation events introduced rewarded set metadata another iteration of restoring old tests updated rewarding part of nym-api parking the branch unparking the branch wip purged families added 'ExitGateway' role passing explicit work factor for rewarding function remove legacy layers storage wip: node description queries added announced ports to self-described api step1 in gruelling journey of adding node_id to gateways ensure epoch work never goes above 1.0 changed active set to contain role distribution [theoretically] sending rewarding messages for the new rewarded set [theoretically] assigning new rewarded set reimplementing more nym-api features remove legacy types re-implement legacy network monitor restoring further routes + minor refactor of NodeStatusCache skimmed routes now return legacy nodes alongside nym-nodes seemingly restored all functionalities in nym-api removing more legacy things from the contract initial contract cleanup added nym-api endpoints to return generic annotations regardless of type updated simulator to use new rewarding parameters more contract cleanup made existing mixnet contract tests compile extra validation of nym-node bonding parameters fixed additional compilation issues fixed nym-api v3 database migration failure added additional nym-node contract queries updated the schema made additional delegation/rewards queries compatible with both legacy mixnodes and nym-nodes fixing existing unit tests in mixnet contract wip resolved first batch of 500 compiler errors re-deprecating routes making wallet's rust backend compile fixed non-determinism in contract + nym-api build fixes to the build populating cotracts-cache with nym-nodes data more missing nymnodes queries temp mixnet contract methods + restored result submission in nym-api allow deprecated routes submitting correct results for mixnode results removed deprecated re-export of AxumAppState and removed smurf naming moved axum modules into support::http cleaning up nym-api warnings determine entry gateways before exits exposed transaction to update nym-node config missing memo for updating node config new routes added routes to swagger and fixed relative paths fixed some macro derivations added nym-node commands to nym-cli --- Cargo.lock | 11 +- Cargo.toml | 1 + .../topology_control/geo_aware_provider.rs | 18 +- common/client-core/src/error.rs | 10 - common/client-core/src/init/helpers.rs | 28 +- common/client-core/src/init/mod.rs | 2 +- common/client-core/src/init/types.rs | 21 +- .../validator-client/src/client.rs | 23 +- .../validator-client/src/nym_api/mod.rs | 41 +- .../contract_traits/mixnet_query_client.rs | 511 +- .../contract_traits/mixnet_signing_client.rs | 369 +- .../contract_traits/vesting_query_client.rs | 10 +- .../contract_traits/vesting_signing_client.rs | 76 +- .../validator-client/src/nyxd/error.rs | 17 + .../validator/cosmwasm/generators/mixnet.rs | 24 +- .../mixnet/delegators/delegate_to_mixnode.rs | 6 +- .../delegate_to_multiple_mixnodes.rs | 56 +- .../delegators/migrate_vested_delegation.rs | 4 +- .../delegators/query_for_delegations.rs | 6 +- .../rewards/claim_delegator_reward.rs | 4 +- .../rewards/vesting_claim_delegator_reward.rs | 4 +- .../delegators/undelegate_from_mixnode.rs | 6 +- .../delegators/vesting_delegate_to_mixnode.rs | 4 +- .../vesting_undelegate_from_mixnode.rs | 4 +- .../validator/mixnet/operators/gateway/mod.rs | 3 + .../operators/gateway/nymnode_migration.rs | 56 + .../mixnet/operators/mixnode/bond_mixnode.rs | 21 +- .../mixnode/families/create_family.rs | 25 - .../create_family_join_permit_sign_payload.rs | 72 - .../operators/mixnode/families/join_family.rs | 34 - .../mixnode/families/kick_family_member.rs | 40 - .../mixnode/families/leave_family.rs | 29 - .../mixnet/operators/mixnode/families/mod.rs | 35 - .../mixnode/mixnode_bonding_sign_payload.rs | 15 +- .../validator/mixnet/operators/mixnode/mod.rs | 6 +- .../operators/mixnode/nymnode_migration.rs | 19 + .../mixnode/settings/update_cost_params.rs | 62 +- .../operators/mixnode/vesting_bond_mixnode.rs | 16 +- .../src/validator/mixnet/operators/mod.rs | 7 +- .../mixnet/operators/nymnode/bond_nymnode.rs | 89 + .../operators/nymnode/keys/decode_node_key.rs | 20 + .../mixnet/operators/nymnode/keys/mod.rs | 19 + .../validator/mixnet/operators/nymnode/mod.rs | 43 + .../nymnode/nymnode_bonding_sign_payload.rs | 90 + .../nymnode/pledge/decrease_pledge.rs | 29 + .../nymnode/pledge/increase_pledge.rs | 29 + .../mixnet/operators/nymnode/pledge/mod.rs | 22 + .../nymnode/rewards/claim_operator_reward.rs | 21 + .../mixnet/operators/nymnode/rewards/mod.rs | 19 + .../mixnet/operators/nymnode/settings/mod.rs | 22 + .../nymnode/settings/update_config.rs | 50 + .../nymnode/settings/update_cost_params.rs | 71 + .../operators/nymnode/unbond_nymnode.rs | 22 + .../mixnet/query/query_all_gateways.rs | 2 +- .../contracts-common/src/types.rs | 94 +- .../mixnet-contract/Cargo.toml | 1 + .../mixnet-contract/src/constants.rs | 3 + .../mixnet-contract/src/delegation.rs | 47 +- .../mixnet-contract/src/error.rs | 147 +- .../mixnet-contract/src/events.rs | 245 +- .../mixnet-contract/src/families.rs | 189 - .../mixnet-contract/src/gateway.rs | 19 +- .../mixnet-contract/src/helpers.rs | 164 +- .../mixnet-contract/src/interval.rs | 54 +- .../mixnet-contract/src/lib.rs | 23 +- .../mixnet-contract/src/mixnode.rs | 207 +- .../mixnet-contract/src/msg.rs | 461 +- .../mixnet-contract/src/nym_node.rs | 575 ++ .../mixnet-contract/src/pending_events.rs | 87 +- .../mixnet-contract/src/reward_params.rs | 208 +- .../mixnet-contract/src/rewarding/helpers.rs | 8 + .../mixnet-contract/src/rewarding/mod.rs | 14 +- .../src/rewarding/simulator/mod.rs | 160 +- .../src/rewarding/simulator/simulated_node.rs | 16 +- .../mixnet-contract/src/signing_types.rs | 67 +- .../mixnet-contract/src/types.rs | 229 +- .../vesting-contract/src/error.rs | 6 +- .../vesting-contract/src/lib.rs | 6 +- .../vesting-contract/src/messages.rs | 47 +- .../vesting-contract/src/types.rs | 6 +- common/node-tester-utils/src/error.rs | 4 +- common/node-tester-utils/src/lib.rs | 2 +- common/node-tester-utils/src/message.rs | 18 +- common/node-tester-utils/src/node.rs | 54 +- common/node-tester-utils/src/tester.rs | 32 +- common/nymsphinx/src/receiver.rs | 16 +- common/topology/src/gateway.rs | 120 +- common/topology/src/lib.rs | 175 +- common/topology/src/mix.rs | 61 +- common/topology/src/serde.rs | 41 +- common/types/src/delegation.rs | 12 +- common/types/src/deprecated.rs | 4 +- common/types/src/lib.rs | 3 + common/types/src/mixnode.rs | 47 +- common/types/src/monitoring.rs | 49 +- common/types/src/nym_node.rs | 94 + common/types/src/pending_events.rs | 33 +- common/wasm/client-core/src/helpers.rs | 8 +- contracts/Cargo.lock | 6 +- contracts/Cargo.toml | 3 +- .../src/support/setup.rs | 8 +- contracts/mixnet/Cargo.toml | 1 + contracts/mixnet/artifacts/checksums.txt | 1 - contracts/mixnet/artifacts/mixnode.wasm | Bin 130585 -> 0 bytes .../mixnet/schema/nym-mixnet-contract.json | 5108 ++++++++++++----- contracts/mixnet/schema/raw/execute.json | 733 +-- contracts/mixnet/schema/raw/instantiate.json | 50 +- contracts/mixnet/schema/raw/query.json | 672 ++- .../raw/response_to_get_all_delegations.json | 6 +- ...et_bonded_mixnode_details_by_identity.json | 92 +- .../response_to_get_delegation_details.json | 15 +- ...response_to_get_delegator_delegations.json | 6 +- .../raw/response_to_get_epoch_status.json | 36 +- .../raw/response_to_get_mix_node_bonds.json | 20 +- .../response_to_get_mix_nodes_detailed.json | 92 +- .../response_to_get_mixnode_delegations.json | 6 +- .../raw/response_to_get_mixnode_details.json | 92 +- ...onse_to_get_mixnode_rewarding_details.json | 14 +- .../raw/response_to_get_node_delegations.json | 116 + ...esponse_to_get_node_rewarding_details.json | 155 + ...response_to_get_node_stake_saturation.json | 46 + .../response_to_get_nym_node_bonds_paged.json | 134 + .../raw/response_to_get_nym_node_details.json | 299 + ..._get_nym_node_details_by_identity_key.json | 297 + ...ponse_to_get_nym_nodes_detailed_paged.json | 297 + .../raw/response_to_get_owned_mixnode.json | 92 +- .../raw/response_to_get_owned_nym_node.json | 301 + ...ponse_to_get_pending_delegator_reward.json | 7 +- .../response_to_get_pending_epoch_event.json | 158 +- .../response_to_get_pending_epoch_events.json | 158 +- ...esponse_to_get_pending_interval_event.json | 96 +- ...sponse_to_get_pending_interval_events.json | 96 +- ...e_to_get_pending_node_operator_reward.json | 79 + ...sponse_to_get_pending_operator_reward.json | 7 +- ...sponse_to_get_preassigned_gateway_ids.json | 46 + ...response_to_get_rewarded_set_metadata.json | 114 + .../raw/response_to_get_rewarding_params.json | 52 +- .../raw/response_to_get_role_assignment.json | 26 + .../raw/response_to_get_stake_saturation.json | 2 +- .../schema/raw/response_to_get_state.json | 17 +- .../raw/response_to_get_state_params.json | 17 +- .../response_to_get_unbonded_nym_node.json | 72 + ...onded_nym_nodes_by_identity_key_paged.json | 71 + ...get_unbonded_nym_nodes_by_owner_paged.json | 71 + ...ponse_to_get_unbonded_nym_nodes_paged.json | 71 + contracts/mixnet/src/compat/helpers.rs | 144 + contracts/mixnet/src/compat/mod.rs | 7 + contracts/mixnet/src/compat/queries.rs | 76 + contracts/mixnet/src/compat/storage_traits.rs | 48 + contracts/mixnet/src/compat/transactions.rs | 95 + contracts/mixnet/src/constants.rs | 84 +- contracts/mixnet/src/contract.rs | 330 +- contracts/mixnet/src/delegations/helpers.rs | 15 +- contracts/mixnet/src/delegations/queries.rs | 284 +- contracts/mixnet/src/delegations/storage.rs | 8 +- .../mixnet/src/delegations/transactions.rs | 134 +- contracts/mixnet/src/families/mod.rs | 7 - contracts/mixnet/src/families/queries.rs | 116 - .../mixnet/src/families/signature_helpers.rs | 41 - contracts/mixnet/src/families/storage.rs | 102 - contracts/mixnet/src/families/transactions.rs | 291 - contracts/mixnet/src/gateways/mod.rs | 1 - contracts/mixnet/src/gateways/queries.rs | 84 +- .../mixnet/src/gateways/signature_helpers.rs | 51 - contracts/mixnet/src/gateways/storage.rs | 12 +- contracts/mixnet/src/gateways/transactions.rs | 177 +- contracts/mixnet/src/interval/helpers.rs | 27 +- .../mixnet/src/interval/pending_events.rs | 590 +- contracts/mixnet/src/interval/queries.rs | 124 +- contracts/mixnet/src/interval/storage.rs | 98 +- contracts/mixnet/src/interval/transactions.rs | 903 +-- contracts/mixnet/src/lib.rs | 8 +- .../src/mixnet_contract_settings/queries.rs | 5 +- .../src/mixnet_contract_settings/storage.rs | 22 +- .../mixnet_contract_settings/transactions.rs | 12 +- contracts/mixnet/src/mixnodes/helpers.rs | 147 +- contracts/mixnet/src/mixnodes/mod.rs | 1 - contracts/mixnet/src/mixnodes/queries.rs | 81 +- .../mixnet/src/mixnodes/signature_helpers.rs | 59 - contracts/mixnet/src/mixnodes/storage.rs | 153 +- contracts/mixnet/src/mixnodes/transactions.rs | 478 +- contracts/mixnet/src/nodes/helpers.rs | 183 + contracts/mixnet/src/nodes/mod.rs | 8 + contracts/mixnet/src/nodes/queries.rs | 259 + .../mixnet/src/nodes/signature_helpers.rs | 38 + contracts/mixnet/src/nodes/storage/helpers.rs | 152 + contracts/mixnet/src/nodes/storage/mod.rs | 128 + contracts/mixnet/src/nodes/transactions.rs | 264 + contracts/mixnet/src/queued_migrations.rs | 301 + contracts/mixnet/src/rewards/helpers.rs | 108 +- contracts/mixnet/src/rewards/queries.rs | 306 +- contracts/mixnet/src/rewards/storage.rs | 126 +- contracts/mixnet/src/rewards/transactions.rs | 1458 +++-- contracts/mixnet/src/support/helpers.rs | 96 +- contracts/mixnet/src/support/legacy.rs | 2 + contracts/mixnet/src/support/mod.rs | 4 + .../mixnet/src/support/tests/fixtures.rs | 42 +- contracts/mixnet/src/support/tests/legacy.rs | 65 + .../mixnet/src/support/tests/messages.rs | 36 - contracts/mixnet/src/support/tests/mod.rs | 896 ++- contracts/mixnet/src/testing/legacy.rs | 12 + contracts/mixnet/src/testing/mod.rs | 4 + contracts/mixnet/src/vesting_migration.rs | 4 +- contracts/vesting/src/queries.rs | 10 +- contracts/vesting/src/storage.rs | 12 +- .../vesting/src/traits/bonding_account.rs | 6 +- .../vesting/src/traits/delegating_account.rs | 14 +- contracts/vesting/src/traits/mod.rs | 2 - contracts/vesting/src/traits/node_families.rs | 32 - contracts/vesting/src/transactions.rs | 53 +- .../src/vesting/account/delegating_account.rs | 14 +- .../account/mixnode_bonding_account.rs | 6 +- contracts/vesting/src/vesting/account/mod.rs | 19 +- .../src/vesting/account/node_families.rs | 71 - contracts/vesting/src/vesting/mod.rs | 4 +- explorer-api/explorer-api-requests/src/lib.rs | 6 +- explorer-api/src/gateways/models.rs | 21 +- explorer-api/src/mix_node/delegations.rs | 6 +- explorer-api/src/mix_node/econ_stats.rs | 4 +- explorer-api/src/mix_node/http.rs | 14 +- explorer-api/src/mix_node/models.rs | 28 +- explorer-api/src/mix_nodes/location.rs | 4 +- explorer-api/src/mix_nodes/models.rs | 56 +- explorer-api/src/mix_nodes/utils.rs | 14 +- explorer-api/src/ping/http.rs | 4 +- explorer-api/src/ping/models.rs | 10 +- explorer-api/src/state.rs | 4 +- explorer-api/src/tasks.rs | 15 +- nym-api/Cargo.toml | 47 +- nym-api/Rocket.toml | 7 - nym-api/build.rs | 38 +- .../migrations/20240726120000_v3_changes.sql | 24 + nym-api/nym-api-requests/Cargo.toml | 7 +- nym-api/nym-api-requests/src/legacy.rs | 92 + nym-api/nym-api-requests/src/lib.rs | 1 + nym-api/nym-api-requests/src/models.rs | 268 +- nym-api/nym-api-requests/src/nym_nodes.rs | 44 +- .../src/circulating_supply_api/cache/mod.rs | 17 +- .../circulating_supply_api/cache/refresher.rs | 1 + .../src/circulating_supply_api/handlers.rs | 14 +- nym-api/src/circulating_supply_api/mod.rs | 28 +- nym-api/src/ecash/api_routes/aggregation.rs | 2 +- .../src/ecash/api_routes/aggregation_axum.rs | 6 +- nym-api/src/ecash/api_routes/handlers.rs | 4 +- nym-api/src/ecash/api_routes/helpers.rs | 1 - nym-api/src/ecash/api_routes/issued_axum.rs | 4 +- nym-api/src/ecash/api_routes/mod.rs | 22 +- .../ecash/api_routes/partial_signing_axum.rs | 5 +- nym-api/src/ecash/api_routes/spending.rs | 6 +- nym-api/src/ecash/api_routes/spending_axum.rs | 11 +- nym-api/src/ecash/client.rs | 1 + nym-api/src/ecash/comm.rs | 1 + nym-api/src/ecash/dkg/controller/keys.rs | 19 +- nym-api/src/ecash/dkg/controller/mod.rs | 7 +- nym-api/src/ecash/dkg/dealing.rs | 2 +- nym-api/src/ecash/dkg/key_derivation.rs | 3 +- nym-api/src/ecash/dkg/key_finalization.rs | 1 + nym-api/src/ecash/dkg/key_validation.rs | 1 + nym-api/src/ecash/dkg/public_key.rs | 3 +- nym-api/src/ecash/dkg/state/mod.rs | 2 +- nym-api/src/ecash/error.rs | 44 +- nym-api/src/ecash/mod.rs | 51 +- nym-api/src/ecash/state/helpers.rs | 23 +- nym-api/src/ecash/state/local.rs | 3 +- nym-api/src/ecash/state/mod.rs | 11 +- nym-api/src/ecash/storage/manager.rs | 1 + nym-api/src/ecash/storage/mod.rs | 2 + nym-api/src/epoch_operations/error.rs | 12 +- .../epoch_operations/event_reconciliation.rs | 13 +- nym-api/src/epoch_operations/helpers.rs | 151 +- nym-api/src/epoch_operations/mod.rs | 146 +- .../rewarded_set_assignment.rs | 418 +- nym-api/src/epoch_operations/rewarding.rs | 67 +- .../epoch_operations/transition_beginning.rs | 9 +- nym-api/src/main.rs | 171 +- nym-api/src/network/handlers.rs | 10 +- nym-api/src/network/mod.rs | 24 +- nym-api/src/network/models.rs | 6 +- nym-api/src/network_monitor/mod.rs | 38 +- .../monitor/gateways_pinger.rs | 2 +- nym-api/src/network_monitor/monitor/mod.rs | 9 +- .../src/network_monitor/monitor/preparer.rs | 347 +- .../src/network_monitor/monitor/processor.rs | 2 +- .../src/network_monitor/monitor/receiver.rs | 1 + nym-api/src/network_monitor/monitor/sender.rs | 2 +- .../monitor/summary_producer.rs | 22 +- nym-api/src/network_monitor/test_packet.rs | 2 +- nym-api/src/network_monitor/test_route/mod.rs | 16 +- nym-api/src/node_describe_cache/mod.rs | 437 +- .../src/node_describe_cache/query_helpers.rs | 242 + nym-api/src/node_status_api/cache/data.rs | 15 +- .../cache/inclusion_probabilities.rs | 23 +- nym-api/src/node_status_api/cache/mod.rs | 98 +- .../src/node_status_api/cache/node_sets.rs | 186 +- .../src/node_status_api/cache/refresher.rs | 64 +- nym-api/src/node_status_api/handlers/mod.rs | 8 +- .../handlers/network_monitor.rs | 61 +- .../src/node_status_api/handlers/unstable.rs | 13 +- .../handlers/without_monitor.rs | 183 +- nym-api/src/node_status_api/helpers.rs | 412 +- .../src/node_status_api/helpers_deprecated.rs | 405 -- nym-api/src/node_status_api/mod.rs | 98 +- nym-api/src/node_status_api/models.rs | 424 +- .../src/node_status_api/reward_estimate.rs | 34 +- .../src/node_status_api/routes_deprecated.rs | 279 +- nym-api/src/node_status_api/uptime_updater.rs | 5 +- nym-api/src/node_status_api/utils.rs | 10 +- nym-api/src/nym_contract_cache/cache/data.rs | 35 +- nym-api/src/nym_contract_cache/cache/mod.rs | 322 +- .../src/nym_contract_cache/cache/refresher.rs | 116 +- nym-api/src/nym_contract_cache/handlers.rs | 147 +- .../src/nym_contract_cache/legacy_helpers.rs | 2 + nym-api/src/nym_contract_cache/mod.rs | 41 +- nym-api/src/nym_contract_cache/routes.rs | 97 +- nym-api/src/nym_nodes/handlers.rs | 95 - nym-api/src/nym_nodes/handlers/legacy.rs | 97 + nym-api/src/nym_nodes/handlers/mod.rs | 281 + .../unstable.rs} | 297 +- nym-api/src/nym_nodes/mod.rs | 60 +- nym-api/src/nym_nodes/routes.rs | 97 +- nym-api/src/nym_nodes/unstable_routes.rs | 288 +- nym-api/src/status/handlers.rs | 6 +- nym-api/src/status/mod.rs | 26 +- nym-api/src/status/routes.rs | 2 +- nym-api/src/support/caching/cache.rs | 20 +- nym-api/src/support/caching/refresher.rs | 10 +- nym-api/src/support/cli/run.rs | 271 +- nym-api/src/support/config/helpers.rs | 2 +- nym-api/src/support/config/mod.rs | 37 +- nym-api/src/support/config/persistence.rs | 15 +- nym-api/src/support/config/template.rs | 21 +- nym-api/src/support/http/helpers.rs | 12 +- nym-api/src/support/http/mod.rs | 247 +- nym-api/src/support/http/openapi.rs | 122 +- nym-api/src/{v2 => support/http}/router.rs | 31 +- nym-api/src/support/http/state.rs | 104 + nym-api/src/support/http/unstable_routes.rs | 11 + nym-api/src/support/nyxd/mod.rs | 119 +- nym-api/src/support/storage/manager.rs | 494 +- nym-api/src/support/storage/mod.rs | 309 +- nym-api/src/support/storage/models.rs | 30 +- nym-api/src/v2/api_docs.rs | 85 - nym-api/src/v2/mod.rs | 313 - nym-api/src/v3_migration.rs | 82 + nym-node/nym-node-requests/src/api/client.rs | 6 +- .../src/api/v1/node/models.rs | 24 + nym-node/src/cli/commands/migrate.rs | 2 + nym-node/src/config/mixnode.rs | 8 + nym-node/src/config/mod.rs | 8 + .../src/config/old_configs/old_config_v3.rs | 2 + nym-node/src/config/template.rs | 12 + nym-node/src/node/mod.rs | 6 +- nym-wallet/Cargo.lock | 31 +- nym-wallet/nym-wallet-types/src/admin.rs | 28 +- nym-wallet/src-tauri/src/error.rs | 3 + .../src-tauri/src/operations/helpers.rs | 9 +- .../src-tauri/src/operations/mixnet/bond.rs | 46 +- .../src/operations/mixnet/delegate.rs | 35 +- .../src/operations/mixnet/rewards.rs | 25 +- .../src/operations/nym_api/status.rs | 12 +- .../signatures/ed25519_signing_payload.rs | 8 +- .../src/operations/simulate/mixnet.rs | 40 +- .../src/operations/simulate/vesting.rs | 17 +- .../src-tauri/src/operations/vesting/bond.rs | 6 +- .../src/operations/vesting/delegate.rs | 6 +- .../src/operations/vesting/migrate.rs | 2 +- .../src/operations/vesting/rewards.rs | 4 +- nym-wallet/src-tauri/src/utils.rs | 19 +- nym-wallet/src/requests/queries.ts | 58 +- nym-wallet/src/types/rust/AppEnv.ts | 8 +- nym-wallet/src/types/rust/AppVersion.ts | 6 +- nym-wallet/src/types/rust/Interval.ts | 11 +- nym-wallet/src/types/rust/Network.ts | 2 +- .../src/types/rust/OperatingCostRange.ts | 7 +- .../src/types/rust/ProfitMarginRange.ts | 5 +- nym-wallet/src/types/rust/StateParams.ts | 14 +- nym-wallet/src/types/rust/ValidatorUrl.ts | 5 +- nym-wallet/src/types/rust/ValidatorUrls.ts | 6 +- .../examples/custom_topology_provider.rs | 20 +- .../examples/manually_overwrite_topology.rs | 14 +- .../dkg-bypass-contract/Makefile | 2 +- .../testnet-manager/src/manager/local_apis.rs | 4 +- .../src/manager/local_nodes.rs | 98 +- .../src/manager/network_init.rs | 9 +- .../testnet-manager/src/manager/node.rs | 6 +- .../mixnet/operators/gateways/mod.rs | 26 +- .../mixnet/operators/gateways/settings/mod.rs | 1 + .../mixnet/operators/mixnodes/families/mod.rs | 47 - .../mixnet/operators/mixnodes/keys/mod.rs | 1 + .../mixnet/operators/mixnodes/mod.rs | 40 +- .../mixnet/operators/mixnodes/rewards/mod.rs | 1 + .../mixnet/operators/mixnodes/settings/mod.rs | 3 +- .../src/validator/mixnet/operators/mod.rs | 23 +- .../mixnet/operators/nymnodes/keys.rs | 15 + .../mixnet/operators/nymnodes/mod.rs | 49 + .../mixnet/operators/nymnodes/pledge.rs | 22 + .../mixnet/operators/nymnodes/rewards.rs | 19 + .../mixnet/operators/nymnodes/settings.rs | 22 + tools/ts-rs-cli/src/main.rs | 18 +- .../types/src/types/rust/NodeRewarding.ts | 4 + ts-packages/types/src/types/rust/NymNode.ts | 3 + .../types/src/types/rust/NymNodeBond.ts | 4 + .../types/src/types/rust/NymNodeDetails.ts | 6 + .../src/types/rust/PendingNodeChanges.ts | 3 + ts-packages/types/src/types/rust/index.ts | 4 + 405 files changed, 23134 insertions(+), 13138 deletions(-) create mode 100644 common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs delete mode 100644 common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs delete mode 100644 common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs delete mode 100644 common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs delete mode 100644 common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs delete mode 100644 common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs delete mode 100644 common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs create mode 100644 common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/mod.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs create mode 100644 common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs delete mode 100644 common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs create mode 100644 common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs create mode 100644 common/types/src/nym_node.rs delete mode 100644 contracts/mixnet/artifacts/checksums.txt delete mode 100644 contracts/mixnet/artifacts/mixnode.wasm create mode 100644 contracts/mixnet/schema/raw/response_to_get_node_delegations.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_nym_node_details.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_role_assignment.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json create mode 100644 contracts/mixnet/src/compat/helpers.rs create mode 100644 contracts/mixnet/src/compat/mod.rs create mode 100644 contracts/mixnet/src/compat/queries.rs create mode 100644 contracts/mixnet/src/compat/storage_traits.rs create mode 100644 contracts/mixnet/src/compat/transactions.rs delete mode 100644 contracts/mixnet/src/families/mod.rs delete mode 100644 contracts/mixnet/src/families/queries.rs delete mode 100644 contracts/mixnet/src/families/signature_helpers.rs delete mode 100644 contracts/mixnet/src/families/storage.rs delete mode 100644 contracts/mixnet/src/families/transactions.rs delete mode 100644 contracts/mixnet/src/gateways/signature_helpers.rs delete mode 100644 contracts/mixnet/src/mixnodes/signature_helpers.rs create mode 100644 contracts/mixnet/src/nodes/helpers.rs create mode 100644 contracts/mixnet/src/nodes/mod.rs create mode 100644 contracts/mixnet/src/nodes/queries.rs create mode 100644 contracts/mixnet/src/nodes/signature_helpers.rs create mode 100644 contracts/mixnet/src/nodes/storage/helpers.rs create mode 100644 contracts/mixnet/src/nodes/storage/mod.rs create mode 100644 contracts/mixnet/src/nodes/transactions.rs create mode 100644 contracts/mixnet/src/support/legacy.rs create mode 100644 contracts/mixnet/src/support/tests/legacy.rs delete mode 100644 contracts/mixnet/src/support/tests/messages.rs create mode 100644 contracts/mixnet/src/testing/legacy.rs delete mode 100644 contracts/vesting/src/traits/node_families.rs delete mode 100644 contracts/vesting/src/vesting/account/node_families.rs delete mode 100644 nym-api/Rocket.toml create mode 100644 nym-api/migrations/20240726120000_v3_changes.sql create mode 100644 nym-api/nym-api-requests/src/legacy.rs create mode 100644 nym-api/src/node_describe_cache/query_helpers.rs delete mode 100644 nym-api/src/node_status_api/helpers_deprecated.rs create mode 100644 nym-api/src/nym_contract_cache/legacy_helpers.rs delete mode 100644 nym-api/src/nym_nodes/handlers.rs create mode 100644 nym-api/src/nym_nodes/handlers/legacy.rs create mode 100644 nym-api/src/nym_nodes/handlers/mod.rs rename nym-api/src/nym_nodes/{handlers_unstable.rs => handlers/unstable.rs} (52%) rename nym-api/src/{v2 => support/http}/router.rs (81%) create mode 100644 nym-api/src/support/http/state.rs create mode 100644 nym-api/src/support/http/unstable_routes.rs delete mode 100644 nym-api/src/v2/api_docs.rs delete mode 100644 nym-api/src/v2/mod.rs create mode 100644 nym-api/src/v3_migration.rs delete mode 100644 tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs create mode 100644 tools/nym-cli/src/validator/mixnet/operators/nymnodes/keys.rs create mode 100644 tools/nym-cli/src/validator/mixnet/operators/nymnodes/mod.rs create mode 100644 tools/nym-cli/src/validator/mixnet/operators/nymnodes/pledge.rs create mode 100644 tools/nym-cli/src/validator/mixnet/operators/nymnodes/rewards.rs create mode 100644 tools/nym-cli/src/validator/mixnet/operators/nymnodes/settings.rs create mode 100644 ts-packages/types/src/types/rust/NodeRewarding.ts create mode 100644 ts-packages/types/src/types/rust/NymNode.ts create mode 100644 ts-packages/types/src/types/rust/NymNodeBond.ts create mode 100644 ts-packages/types/src/types/rust/NymNodeDetails.ts create mode 100644 ts-packages/types/src/types/rust/PendingNodeChanges.ts diff --git a/Cargo.lock b/Cargo.lock index 00d23fa76e..11ed1c4400 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,6 @@ dependencies = [ "humantime-serde", "itertools 0.13.0", "k256", - "log", "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", @@ -4305,26 +4304,22 @@ dependencies = [ "nym-node-requests", "nym-node-tester-utils", "nym-pemstore", + "nym-serde-helpers", "nym-sphinx", "nym-task", "nym-topology", "nym-types", "nym-validator-client", "nym-vesting-contract-common", - "okapi", "pin-project", "rand", "rand_chacha", "reqwest 0.12.4", - "rocket", - "rocket_cors", - "rocket_okapi", "schemars", "serde", "serde_json", "sha2 0.9.9", "sqlx", - "tap", "tempfile", "thiserror", "time", @@ -4333,7 +4328,6 @@ dependencies = [ "tokio-util", "tower-http", "tracing", - "tracing-subscriber", "ts-rs", "url", "utoipa", @@ -4356,9 +4350,9 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", - "rocket", "schemars", "serde", "serde_json", @@ -5473,6 +5467,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "cw2", "humantime-serde", "log", diff --git a/Cargo.toml b/Cargo.toml index 19f0ef8f00..81804b89da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -275,6 +275,7 @@ parking_lot = "0.12.3" pem = "0.8" petgraph = "0.6.5" pin-project = "1.0" +pin-project-lite = "0.2.14" pretty_env_logger = "0.4.0" publicsuffix = "2.2.3" quote = "1" diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 96d52fd8d5..9967e1a705 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -3,11 +3,11 @@ use log::{debug, error}; use nym_explorer_client::{ExplorerClient, PrettyDetailedMixNodeBond}; use nym_network_defaults::var_names::EXPLORER_API; use nym_topology::{ - nym_topology_from_detailed, + nym_topology_from_basic_info, provider_trait::{async_trait, TopologyProvider}, NymTopology, }; -use nym_validator_client::client::MixId; +use nym_validator_client::client::NodeId; use rand::{prelude::SliceRandom, thread_rng}; use std::collections::HashMap; use tap::TapOptional; @@ -39,10 +39,10 @@ fn create_explorer_client() -> Option { fn group_mixnodes_by_country_code( mixnodes: Vec, -) -> HashMap> { +) -> HashMap> { mixnodes .into_iter() - .fold(HashMap::>::new(), |mut acc, m| { + .fold(HashMap::>::new(), |mut acc, m| { if let Some(ref location) = m.location { let country_code = location.two_letter_iso_country_code.clone(); let group_code = CountryGroup::new(country_code.as_str()); @@ -53,7 +53,7 @@ fn group_mixnodes_by_country_code( }) } -fn log_mixnode_distribution(mixnodes: &HashMap>) { +fn log_mixnode_distribution(mixnodes: &HashMap>) { let mixnode_distribution = mixnodes .iter() .map(|(k, v)| format!("{}: {}", k, v.len())) @@ -110,7 +110,7 @@ impl GeoAwareTopologyProvider { } async fn get_topology(&self) -> Option { - let mixnodes = match self.validator_client.get_cached_active_mixnodes().await { + let mixnodes = match self.validator_client.get_basic_mixnodes(None).await { Err(err) => { error!("failed to get network mixnodes - {err}"); return None; @@ -118,7 +118,7 @@ impl GeoAwareTopologyProvider { Ok(mixes) => mixes, }; - let gateways = match self.validator_client.get_cached_gateways().await { + let gateways = match self.validator_client.get_basic_gateways(None).await { Err(err) => { error!("failed to get network gateways - {err}"); return None; @@ -182,10 +182,10 @@ impl GeoAwareTopologyProvider { let mixnodes = mixnodes .into_iter() - .filter(|m| filtered_mixnode_ids.contains(&m.mix_id())) + .filter(|m| filtered_mixnode_ids.contains(&m.node_id)) .collect::>(); - let topology = nym_topology_from_detailed(mixnodes, gateways) + let topology = nym_topology_from_basic_info(&mixnodes, &gateways) .filter_system_version(&self.client_version); // TODO: return real error type diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 653256df1f..4d412902e0 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -187,16 +187,6 @@ pub enum ClientCoreError { source: Ed25519RecoveryError, }, - #[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {err}")] - MalformedGatewayOwnerAccountAddress { - gateway_id: String, - - raw_owner: String, - - // just use the string formatting as opposed to underlying type to avoid having to import cosmrs - err: String, - }, - #[error( "the listening address of gateway {gateway_id} ({raw_listener}) is malformed: {source}" )] diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index c6d17fc16d..ac2b01bf6f 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -53,7 +53,7 @@ pub trait ConnectableGateway { fn is_wss(&self) -> bool; } -impl ConnectableGateway for gateway::Node { +impl ConnectableGateway for gateway::LegacyNode { fn identity(&self) -> &identity::PublicKey { self.identity() } @@ -82,7 +82,7 @@ pub async fn current_gateways( rng: &mut R, nym_apis: &[Url], user_agent: Option, -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> { let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; @@ -94,14 +94,14 @@ pub async fn current_gateways( log::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_cached_described_gateways().await?; + let gateways = client.get_basic_gateways(None).await?; log::debug!("Found {} gateways", gateways.len()); log::trace!("Gateways: {:#?}", gateways); let valid_gateways = gateways - .into_iter() + .iter() .filter_map(|gateway| gateway.try_into().ok()) - .collect::>(); + .collect::>(); log::debug!("Ater checking validity: {}", valid_gateways.len()); log::trace!("Valid gateways: {:#?}", valid_gateways); @@ -118,7 +118,7 @@ pub async fn current_gateways( pub async fn current_mixnodes( rng: &mut R, nym_apis: &[Url], -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> { let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; @@ -126,11 +126,11 @@ pub async fn current_mixnodes( log::trace!("Fetching list of mixnodes from: {nym_api}"); - let mixnodes = client.get_cached_mixnodes().await?; + let mixnodes = client.get_basic_mixnodes(None).await?; let valid_mixnodes = mixnodes - .into_iter() - .filter_map(|mixnode| (&mixnode.bond_information).try_into().ok()) - .collect::>(); + .iter() + .filter_map(|mixnode| mixnode.try_into().ok()) + .collect::>(); // we were always filtering by version so I'm not removing that 'feature' let filtered_mixnodes = valid_mixnodes.filter_by_version(env!("CARGO_PKG_VERSION")); @@ -273,9 +273,9 @@ fn filter_by_tls( pub(super) fn uniformly_random_gateway( rng: &mut R, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], must_use_tls: bool, -) -> Result { +) -> Result { filter_by_tls(gateways, must_use_tls)? .choose(rng) .ok_or(ClientCoreError::NoGatewaysOnNetwork) @@ -284,9 +284,9 @@ pub(super) fn uniformly_random_gateway( pub(super) fn get_specified_gateway( gateway_identity: IdentityKeyRef, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], must_use_tls: bool, -) -> Result { +) -> Result { log::debug!("Requesting specified gateway: {}", gateway_identity); let user_gateway = identity::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 479d73be1a..8e93babbf2 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -50,7 +50,7 @@ async fn setup_new_gateway( key_store: &K, details_store: &D, selection_specification: GatewaySelectionSpecification, - available_gateways: Vec, + available_gateways: Vec, ) -> Result where K: KeyStore, diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 2ba78dd6d1..1aa4a0d24b 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -18,7 +18,6 @@ use nym_validator_client::client::IdentityKey; use nym_validator_client::nyxd::AccountId; use serde::Serialize; use std::fmt::Display; -use std::str::FromStr; use std::sync::Arc; use time::OffsetDateTime; use url::Url; @@ -39,7 +38,7 @@ pub enum SelectedGateway { impl SelectedGateway { pub fn from_topology_node( - node: gateway::Node, + node: gateway::LegacyNode, must_use_tls: bool, ) -> Result { let gateway_listener = if must_use_tls { @@ -51,20 +50,6 @@ impl SelectedGateway { node.clients_address() }; - let gateway_owner_address = node - .owner - .as_ref() - .map(|raw_owner| { - AccountId::from_str(raw_owner).map_err(|source| { - ClientCoreError::MalformedGatewayOwnerAccountAddress { - gateway_id: node.identity_key.to_base58_string(), - raw_owner: raw_owner.clone(), - err: source.to_string(), - } - }) - }) - .transpose()?; - let gateway_listener = Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { gateway_id: node.identity_key.to_base58_string(), @@ -74,7 +59,7 @@ impl SelectedGateway { Ok(SelectedGateway::Remote { gateway_id: node.identity_key, - gateway_owner_address, + gateway_owner_address: None, gateway_listener, }) } @@ -215,7 +200,7 @@ pub enum GatewaySetup { specification: GatewaySelectionSpecification, // TODO: seems to be a bit inefficient to pass them by value - available_gateways: Vec, + available_gateways: Vec, }, ReuseConnection { diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 32cfd9ae2f..eee67ba509 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -17,11 +17,12 @@ use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; -use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated}; +use nym_api_requests::legacy::LegacyGatewayBondWithId; use nym_api_requests::models::{ GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; +use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; @@ -31,7 +32,7 @@ use url::Url; pub use crate::nym_api::NymApiClientExt; pub use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, MixId, + mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId, }; // re-export the type to not break existing imports @@ -239,7 +240,9 @@ impl Client { Ok(self.nym_api.get_active_mixnodes_detailed().await?) } - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { + pub async fn get_cached_gateways( + &self, + ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } @@ -321,13 +324,15 @@ impl NymApiClient { Ok(self.nym_api.get_mixnodes().await?) } - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { + pub async fn get_cached_gateways( + &self, + ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } pub async fn get_cached_described_gateways( &self, - ) -> Result, ValidatorClientError> { + ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways_described().await?) } @@ -344,7 +349,7 @@ impl NymApiClient { pub async fn get_mixnode_core_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { Ok(self @@ -355,21 +360,21 @@ impl NymApiClient { pub async fn get_mixnode_status( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_status(mix_id).await?) } pub async fn get_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_reward_estimation(mix_id).await?) } pub async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_stake_saturation(mix_id).await?) } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 1ebfea7284..6c5337afaf 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -10,8 +10,8 @@ use nym_api_requests::ecash::models::{ VerifyEcashTicketBody, }; use nym_api_requests::ecash::VerificationKeyResponse; -use nym_api_requests::models::DescribedMixNode; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; +use nym_api_requests::legacy::LegacyGatewayBondWithId; +use nym_api_requests::models::LegacyDescribedMixNode; pub use nym_api_requests::{ ecash::{ models::{ @@ -23,19 +23,20 @@ pub use nym_api_requests::{ VerifyEcashCredentialBody, }, models::{ - ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, + ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, - StakeSaturationResponse, UptimeResponse, + LegacyDescribedGateway, MixNodeBondAnnotated, MixnodeCoreStatusResponse, + MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }, + nym_nodes::{CachedNodesResponse, SkimmedNode}, }; pub use nym_coconut_dkg_common::types::EpochId; use nym_contracts_common::IdentityKey; pub use nym_http_api_client::Client; use nym_http_api_client::{ApiClient, NO_PARAMS}; use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use nym_mixnet_contract_common::{IdentityKeyRef, NodeId}; use time::format_description::BorrowedFormatItem; use time::Date; @@ -95,12 +96,12 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_gateways(&self) -> Result, NymAPIError> { + async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } - async fn get_gateways_described(&self) -> Result, NymAPIError> { + async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], NO_PARAMS, @@ -108,7 +109,7 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnodes_described(&self) -> Result, NymAPIError> { + async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], NO_PARAMS, @@ -194,7 +195,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_report( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -228,7 +229,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_history( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -309,7 +310,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_core_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { if let Some(since) = since { @@ -341,7 +342,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_status( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -358,7 +359,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -375,7 +376,7 @@ pub trait NymApiClientExt: ApiClient { async fn compute_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, request_body: &ComputeRewardEstParam, ) -> Result { self.post_json( @@ -394,7 +395,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -411,7 +412,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_inclusion_probability( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -426,7 +427,7 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnode_avg_uptime(&self, mix_id: MixId) -> Result { + async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ routes::API_VERSION, @@ -440,7 +441,7 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { + async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], NO_PARAMS, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 47b67d8f16..2f1c3cf0e6 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -7,29 +7,35 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; +use nym_api_requests::models::StakeSaturationResponse; use nym_contracts_common::signing::Nonce; +use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; +use nym_mixnet_contract_common::nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeOwnershipResponse, + NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, PagedNymNodeDetailsResponse, + PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, UnbondedNodeResponse, + UnbondedNymNode, +}; +use nym_mixnet_contract_common::reward_params::WorkFactor; use nym_mixnet_contract_common::{ delegation, - delegation::{MixNodeDelegationResponse, OwnerProxySubKey}, - families::{Family, FamilyHead}, + delegation::{NodeDelegationResponse, OwnerProxySubKey}, mixnode::{ - MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixStakeSaturationResponse, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, + PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, }, reward_params::{Performance, RewardingParams}, rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, - Delegation, EpochEventId, EpochStatus, FamilyByHeadResponse, FamilyByLabelResponse, - FamilyMembersByHeadResponse, FamilyMembersByLabelResponse, GatewayBond, GatewayBondResponse, - GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, LayerDistribution, - MixId, MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, NumberOfPendingEventsResponse, PagedAllDelegationsResponse, - PagedDelegatorDelegationsResponse, PagedFamiliesResponse, PagedGatewayResponse, - PagedMembersResponse, PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, - PagedRewardedSetResponse, PendingEpochEvent, PendingEpochEventResponse, - PendingEpochEventsResponse, PendingIntervalEvent, PendingIntervalEventResponse, - PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, RewardedSetNodeStatus, - UnbondedMixnode, + Delegation, EpochEventId, EpochStatus, GatewayBond, GatewayBondResponse, + GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, + MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, + NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, + PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, + PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent, + PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent, + PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, + RewardedSet, UnbondedMixnode, }; use serde::Deserialize; @@ -91,56 +97,11 @@ pub trait MixnetQueryClient { .await } - async fn get_rewarded_set_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSet { limit, start_after }) - .await - } - - async fn get_all_node_families_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetAllFamiliesPaged { limit, start_after }) - .await - } - - async fn get_all_family_members_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetAllMembersPaged { limit, start_after }) - .await - } - - async fn get_family_members_by_head + Send>( - &self, - head: S, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyMembersByHead { head: head.into() }) - .await - } - - async fn get_family_members_by_label + Send>( - &self, - label: S, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyMembersByLabel { - label: label.into(), - }) - .await - } - // mixnode-related: async fn get_mixnode_bonds_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodeBonds { limit, start_after }) @@ -149,26 +110,26 @@ pub trait MixnetQueryClient { async fn get_mixnodes_detailed_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodesDetailed { limit, start_after }) .await } - async fn get_unbonded_paged( + async fn get_unbonded_mixnodes_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after }) .await } - async fn get_unbonded_by_owner_paged( + async fn get_unbonded_mixnodes_by_owner_paged( &self, owner: &AccountId, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByOwner { @@ -179,10 +140,10 @@ pub trait MixnetQueryClient { .await } - async fn get_unbonded_by_identity_paged( + async fn get_unbonded_mixnodes_by_identity_paged( &self, identity_key: IdentityKeyRef<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { @@ -205,7 +166,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_details( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDetails { mix_id }) .await @@ -223,7 +184,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_rewarding_details( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeRewardingDetails { mix_id }) .await @@ -231,24 +192,24 @@ pub trait MixnetQueryClient { async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, - ) -> Result { + mix_id: NodeId, + ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetStakeSaturation { mix_id }) .await } async fn get_unbonded_mixnode_information( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodeInformation { mix_id }) .await } - async fn get_layer_distribution(&self) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetLayerDistribution {}) - .await - } + // async fn get_layer_distribution(&self) -> Result { + // self.query_mixnet_contract(MixnetQueryMsg::GetRoleDistribution {}) + // .await + // } // gateway-related: @@ -281,17 +242,142 @@ pub trait MixnetQueryClient { .await } + async fn get_preassigned_gateway_ids_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPreassignedGatewayIds { start_after, limit }) + .await + } + + // nym-nodes related: + async fn get_nymnode_bonds_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeBondsPaged { limit, start_after }) + .await + } + + async fn get_nymnodes_detailed_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodesDetailedPaged { limit, start_after }) + .await + } + + async fn get_unbonded_nymnodes_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesPaged { limit, start_after }) + .await + } + + async fn get_unbonded_nymnodes_by_owner_paged( + &self, + owner: &AccountId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner: owner.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_unbonded_nymnodes_by_identity_paged( + &self, + identity_key: IdentityKeyRef<'_>, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key: identity_key.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_owned_nymnode( + &self, + address: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetOwnedNymNode { + address: address.to_string(), + }) + .await + } + + async fn get_nymnode_details( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetails { node_id }) + .await + } + + async fn get_nymnode_details_by_identity( + &self, + node_identity: IdentityKey, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetailsByIdentityKey { node_identity }) + .await + } + + async fn get_nymnode_rewarding_details( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeRewardingDetails { node_id }) + .await + } + + async fn get_node_stake_saturation( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeStakeSaturation { node_id }) + .await + } + + async fn get_unbonded_nymnode_information( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNode { node_id }) + .await + } + + async fn get_role_assignment(&self, role: Role) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRoleAssignment { role }) + .await + } + + async fn get_rewarded_set_metadata(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSetMetadata {}) + .await + } + // delegation-related: /// Gets list of all delegations towards particular mixnode on particular page. async fn get_mixnode_delegations_paged( &self, - mix_id: MixId, + node_id: NodeId, start_after: Option, limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDelegations { - mix_id, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeDelegations { + node_id, start_after, limit, }) @@ -302,7 +388,7 @@ pub trait MixnetQueryClient { async fn get_delegator_delegations_paged( &self, delegator: &AccountId, - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetDelegatorDelegations { @@ -316,12 +402,12 @@ pub trait MixnetQueryClient { /// Checks value of delegation of given client towards particular mixnode. async fn get_delegation_details( &self, - mix_id: MixId, + node_id: NodeId, delegator: &AccountId, proxy: Option, - ) -> Result { + ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetDelegationDetails { - mix_id, + node_id, delegator: delegator.to_string(), proxy, }) @@ -351,21 +437,21 @@ pub trait MixnetQueryClient { async fn get_pending_mixnode_operator_reward( &self, - mix_id: MixId, + node_id: NodeId, ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id }) + self.query_mixnet_contract(MixnetQueryMsg::GetPendingNodeOperatorReward { node_id }) .await } async fn get_pending_delegator_reward( &self, delegator: &AccountId, - mix_id: MixId, + node_id: NodeId, proxy: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetPendingDelegatorReward { address: delegator.to_string(), - mix_id, + node_id, proxy, }) .await @@ -374,12 +460,14 @@ pub trait MixnetQueryClient { // given the provided performance, estimate the reward at the end of the current epoch async fn get_estimated_current_epoch_operator_reward( &self, - mix_id: MixId, + node_id: NodeId, estimated_performance: Performance, + estimated_work: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, }) .await } @@ -388,15 +476,15 @@ pub trait MixnetQueryClient { async fn get_estimated_current_epoch_delegator_reward( &self, delegator: &AccountId, - mix_id: MixId, - proxy: Option, + node_id: NodeId, estimated_performance: Performance, + estimated_work: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { address: delegator.to_string(), - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, }) .await } @@ -450,22 +538,6 @@ pub trait MixnetQueryClient { }) .await } - - async fn get_node_family_by_label( - &self, - label: String, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByLabel { label }) - .await - } - - async fn get_node_family_by_head( - &self, - head: String, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByHead { head }) - .await - } } // extension trait to the query client to deal with the paged queries @@ -473,18 +545,35 @@ pub trait MixnetQueryClient { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait PagedMixnetQueryClient: MixnetQueryClient { - async fn get_all_node_families(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_node_families_paged, families) + async fn get_all_nymnode_bonds(&self) -> Result, NyxdError> { + collect_paged!(self, get_nymnode_bonds_paged, nodes) } - async fn get_all_family_members(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_family_members_paged, members) + async fn get_all_nymnodes_detailed(&self) -> Result, NyxdError> { + collect_paged!(self, get_nymnodes_detailed_paged, nodes) } - async fn get_all_rewarded_set_mixnodes( + async fn get_all_unbonded_nymnodes(&self) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_nymnodes_paged, nodes) + } + + async fn get_all_unbonded_nymnodes_by_owner( &self, - ) -> Result, NyxdError> { - collect_paged!(self, get_rewarded_set_paged, nodes) + owner: &AccountId, + ) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_nymnodes_by_owner_paged, nodes, owner) + } + + async fn get_all_unbonded_nymnodes_by_identity( + &self, + identity_key: IdentityKeyRef<'_>, + ) -> Result, NyxdError> { + collect_paged!( + self, + get_unbonded_nymnodes_by_identity_paged, + nodes, + identity_key + ) } async fn get_all_mixnode_bonds(&self) -> Result, NyxdError> { @@ -495,31 +584,40 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { collect_paged!(self, get_mixnodes_detailed_paged, nodes) } - async fn get_all_unbonded_mixnodes(&self) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_paged, nodes) + async fn get_all_unbonded_mixnodes(&self) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_mixnodes_paged, nodes) } async fn get_all_unbonded_mixnodes_by_owner( &self, owner: &AccountId, - ) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_by_owner_paged, nodes, owner) + ) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_mixnodes_by_owner_paged, nodes, owner) } async fn get_all_unbonded_mixnodes_by_identity( &self, identity_key: IdentityKeyRef<'_>, - ) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_by_identity_paged, nodes, identity_key) + ) -> Result, NyxdError> { + collect_paged!( + self, + get_unbonded_mixnodes_by_identity_paged, + nodes, + identity_key + ) } async fn get_all_gateways(&self) -> Result, NyxdError> { collect_paged!(self, get_gateways_paged, nodes) } + async fn get_all_preassigned_gateway_ids(&self) -> Result, NyxdError> { + collect_paged!(self, get_preassigned_gateway_ids_paged, ids) + } + async fn get_all_single_mixnode_delegations( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, NyxdError> { collect_paged!(self, get_mixnode_delegations_paged, delegations, mix_id) } @@ -554,6 +652,65 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { #[async_trait] impl PagedMixnetQueryClient for T where T: MixnetQueryClient {} +// extension help to provide extra functionalities based on existing queries: +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait MixnetQueryClientExt: MixnetQueryClient { + async fn get_rewarded_set(&self) -> Result { + let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message)); + + let metadata = self.get_rewarded_set_metadata().await?; + if !metadata.metadata.fully_assigned { + return error_response("the rewarded set hasn't been fully assigned for this epoch"); + } + let expected_epoch_id = metadata.metadata.epoch_id; + + // if we have to query those things more frequently, we could do it concurrently, + // but as it stands now, it happens so infrequently it might as well be sequential + let entry = self.get_role_assignment(Role::EntryGateway).await?; + if entry.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'entry' returned unexpected epoch_id"); + } + + let exit = self.get_role_assignment(Role::ExitGateway).await?; + if exit.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'exit' returned unexpected epoch_id"); + } + + let layer1 = self.get_role_assignment(Role::Layer1).await?; + if layer1.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer1' returned unexpected epoch_id"); + } + + let layer2 = self.get_role_assignment(Role::Layer2).await?; + if layer2.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer2' returned unexpected epoch_id"); + } + + let layer3 = self.get_role_assignment(Role::Layer3).await?; + if layer3.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer3' returned unexpected epoch_id"); + } + + let standby = self.get_role_assignment(Role::Standby).await?; + if standby.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'standby' returned unexpected epoch_id"); + } + + Ok(RewardedSet { + entry_gateways: entry.nodes, + exit_gateways: exit.nodes, + layer1: layer1.nodes, + layer2: layer2.nodes, + layer3: layer3.nodes, + standby: standby.nodes, + }) + } +} + +#[async_trait] +impl MixnetQueryClientExt for T where T: MixnetQueryClient {} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl MixnetQueryClient for C @@ -576,6 +733,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_mixnet_contract_common::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -585,24 +743,6 @@ mod tests { ) -> u32 { match msg { MixnetQueryMsg::Admin {} => client.admin().ignore(), - MixnetQueryMsg::GetAllFamiliesPaged { limit, start_after } => client - .get_all_family_members_paged(start_after, limit) - .ignore(), - MixnetQueryMsg::GetAllMembersPaged { limit, start_after } => client - .get_all_family_members_paged(start_after, limit) - .ignore(), - MixnetQueryMsg::GetFamilyByHead { head } => { - client.get_node_family_by_head(head).ignore() - } - MixnetQueryMsg::GetFamilyByLabel { label } => { - client.get_node_family_by_label(label).ignore() - } - MixnetQueryMsg::GetFamilyMembersByHead { head } => { - client.get_family_members_by_head(head).ignore() - } - MixnetQueryMsg::GetFamilyMembersByLabel { label } => { - client.get_family_members_by_label(label).ignore() - } MixnetQueryMsg::GetContractVersion {} => client.get_mixnet_contract_version().ignore(), MixnetQueryMsg::GetCW2ContractVersion {} => { client.get_mixnet_contract_cw2_version().ignore() @@ -617,31 +757,28 @@ mod tests { MixnetQueryMsg::GetCurrentIntervalDetails {} => { client.get_current_interval_details().ignore() } - MixnetQueryMsg::GetRewardedSet { limit, start_after } => { - client.get_rewarded_set_paged(start_after, limit).ignore() - } MixnetQueryMsg::GetMixNodeBonds { limit, start_after } => { client.get_mixnode_bonds_paged(start_after, limit).ignore() } MixnetQueryMsg::GetMixNodesDetailed { limit, start_after } => client .get_mixnodes_detailed_paged(start_after, limit) .ignore(), - MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after } => { - client.get_unbonded_paged(start_after, limit).ignore() - } + MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after } => client + .get_unbonded_mixnodes_paged(start_after, limit) + .ignore(), MixnetQueryMsg::GetUnbondedMixNodesByOwner { owner, limit, start_after, } => client - .get_unbonded_by_owner_paged(&owner.parse().unwrap(), start_after, limit) + .get_unbonded_mixnodes_by_owner_paged(&owner.parse().unwrap(), start_after, limit) .ignore(), MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { identity_key, limit, start_after, } => client - .get_unbonded_by_identity_paged(&identity_key, start_after, limit) + .get_unbonded_mixnodes_by_identity_paged(&identity_key, start_after, limit) .ignore(), MixnetQueryMsg::GetOwnedMixnode { address } => { client.get_owned_mixnode(&address.parse().unwrap()).ignore() @@ -661,7 +798,6 @@ mod tests { MixnetQueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => client .get_mixnode_details_by_identity(mix_identity) .ignore(), - MixnetQueryMsg::GetLayerDistribution {} => client.get_layer_distribution().ignore(), MixnetQueryMsg::GetGateways { start_after, limit } => { client.get_gateways_paged(start_after, limit).ignore() } @@ -671,8 +807,8 @@ mod tests { MixnetQueryMsg::GetOwnedGateway { address } => { client.get_owned_gateway(&address.parse().unwrap()).ignore() } - MixnetQueryMsg::GetMixnodeDelegations { - mix_id, + MixnetQueryMsg::GetNodeDelegations { + node_id: mix_id, start_after, limit, } => client @@ -686,7 +822,7 @@ mod tests { .get_delegator_delegations_paged(&delegator.parse().unwrap(), start_after, limit) .ignore(), MixnetQueryMsg::GetDelegationDetails { - mix_id, + node_id: mix_id, delegator, proxy, } => client @@ -698,33 +834,38 @@ mod tests { MixnetQueryMsg::GetPendingOperatorReward { address } => client .get_pending_operator_reward(&address.parse().unwrap()) .ignore(), - MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id } => { + MixnetQueryMsg::GetPendingNodeOperatorReward { node_id: mix_id } => { client.get_pending_mixnode_operator_reward(mix_id).ignore() } MixnetQueryMsg::GetPendingDelegatorReward { address, - mix_id, + node_id: mix_id, proxy, } => client .get_pending_delegator_reward(&address.parse().unwrap(), mix_id, proxy) .ignore(), MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, } => client - .get_estimated_current_epoch_operator_reward(mix_id, estimated_performance) + .get_estimated_current_epoch_operator_reward( + node_id, + estimated_performance, + estimated_work, + ) .ignore(), MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, } => client .get_estimated_current_epoch_delegator_reward( &address.parse().unwrap(), - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, ) .ignore(), MixnetQueryMsg::GetPendingEpochEvents { limit, start_after } => client @@ -745,6 +886,50 @@ mod tests { MixnetQueryMsg::GetSigningNonce { address } => { client.get_signing_nonce(&address.parse().unwrap()).ignore() } + QueryMsg::GetPreassignedGatewayIds { start_after, limit } => client + .get_preassigned_gateway_ids_paged(start_after, limit) + .ignore(), + QueryMsg::GetNymNodeBondsPaged { limit, start_after } => { + client.get_nymnode_bonds_paged(limit, start_after).ignore() + } + QueryMsg::GetNymNodesDetailedPaged { limit, start_after } => client + .get_nymnodes_detailed_paged(limit, start_after) + .ignore(), + QueryMsg::GetUnbondedNymNode { node_id } => { + client.get_unbonded_nymnode_information(node_id).ignore() + } + QueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => client + .get_unbonded_nymnodes_paged(limit, start_after) + .ignore(), + QueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner, + limit, + start_after, + } => client + .get_unbonded_nymnodes_by_owner_paged(&owner.parse().unwrap(), limit, start_after) + .ignore(), + QueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key, + limit, + start_after, + } => client + .get_unbonded_nymnodes_by_identity_paged(&identity_key, limit, start_after) + .ignore(), + QueryMsg::GetOwnedNymNode { address } => { + client.get_owned_nymnode(&address.parse().unwrap()).ignore() + } + QueryMsg::GetNymNodeDetails { node_id } => client.get_nymnode_details(node_id).ignore(), + QueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => client + .get_nymnode_details_by_identity(node_identity) + .ignore(), + QueryMsg::GetNodeRewardingDetails { node_id } => { + client.get_nymnode_rewarding_details(node_id).ignore() + } + QueryMsg::GetNodeStakeSaturation { node_id } => { + client.get_node_stake_saturation(node_id).ignore() + } + QueryMsg::GetRoleAssignment { role } => client.get_role_assignment(role).ignore(), + QueryMsg::GetRewardedSetMetadata {} => client.get_rewarded_set_metadata().ignore(), } } } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index f84becb55f..5a7b8fae26 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -10,13 +10,15 @@ use crate::signing::signer::OfflineSigner; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use nym_mixnet_contract_common::reward_params::{IntervalRewardingParamsUpdate, Performance}; +use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_mixnet_contract_common::reward_params::{ + ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters, +}; use nym_mixnet_contract_common::{ - ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, Layer, LayerAssignment, MixId, - MixNode, + ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, MixNode, NodeId, NymNode, + RoleAssignment, }; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -70,14 +72,14 @@ pub trait MixnetSigningClient { async fn update_active_set_size( &self, - active_set_size: u32, + update: ActiveSetUpdate, force_immediately: bool, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::UpdateActiveSetSize { - active_set_size, + MixnetExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, }, vec![], @@ -126,37 +128,6 @@ pub trait MixnetSigningClient { .await } - async fn advance_current_epoch( - &self, - new_rewarded_set: Vec, - expected_active_set_size: u32, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size, - }, - vec![], - ) - .await - } - - async fn assign_node_layer( - &self, - mix_id: MixId, - layer: Layer, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::AssignNodeLayer { mix_id, layer }, - vec![], - ) - .await - } - async fn reconcile_epoch_events( &self, limit: Option, @@ -170,126 +141,21 @@ pub trait MixnetSigningClient { .await } - // family related - async fn create_family( + async fn assign_roles( &self, - label: String, + assignment: RoleAssignment, fee: Option, ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::CreateFamily { label }, vec![]) + self.execute_mixnet_contract(fee, MixnetExecuteMsg::AssignRoles { assignment }, vec![]) .await } - async fn create_family_on_behalf( - &self, - owner_address: String, - label: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - }, - vec![], - ) - .await - } - - async fn join_family( - &self, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::JoinFamily { - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn join_family_on_behalf( - &self, - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn leave_family( - &self, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::LeaveFamily { family_head }, vec![]) - .await - } - - async fn leave_family_on_behalf( - &self, - member_address: String, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - }, - vec![], - ) - .await - } - - async fn kick_family_member( - &self, - member: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::KickFamilyMember { member }, vec![]) - .await - } - - async fn kick_family_member_on_behalf( - &self, - head_address: String, - member: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - }, - vec![], - ) - .await - } // mixnode-related: async fn bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -310,7 +176,7 @@ pub trait MixnetSigningClient { &self, owner: AccountId, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -409,14 +275,14 @@ pub trait MixnetSigningClient { .await } - async fn update_mixnode_cost_params( + async fn update_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs }, + MixnetExecuteMsg::UpdateCostParams { new_costs }, vec![], ) .await @@ -425,7 +291,7 @@ pub trait MixnetSigningClient { async fn update_mixnode_cost_params_on_behalf( &self, owner: AccountId, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -559,26 +425,75 @@ pub trait MixnetSigningClient { .await } - // delegation-related: + // nym-node related: + async fn migrate_legacy_mixnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::MigrateMixnode {}, vec![]) + .await + } - async fn delegate_to_mixnode( + async fn migrate_legacy_gateway( &self, - mix_id: MixId, - amount: Coin, + cost_params: Option, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::DelegateToMixnode { mix_id }, - vec![amount], + MixnetExecuteMsg::MigrateGateway { cost_params }, + vec![], ) .await } + async fn bond_nymnode( + &self, + node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + }, + vec![pledge], + ) + .await + } + + async fn unbond_nymnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondNymNode {}, vec![]) + .await + } + + async fn update_nymnode_config( + &self, + update: NodeConfigUpdate, + fee: Option, + ) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UpdateNodeConfig { update }, vec![]) + .await + } + + // delegation-related: + + async fn delegate( + &self, + node_id: NodeId, + amount: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::Delegate { node_id }, vec![amount]) + .await + } + async fn delegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: MixId, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result { @@ -593,23 +508,19 @@ pub trait MixnetSigningClient { .await } - async fn undelegate_from_mixnode( + async fn undelegate( &self, - mix_id: MixId, + node_id: NodeId, fee: Option, ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::UndelegateFromMixnode { mix_id }, - vec![], - ) - .await + self.execute_mixnet_contract(fee, MixnetExecuteMsg::Undelegate { node_id }, vec![]) + .await } async fn undelegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -625,18 +536,15 @@ pub trait MixnetSigningClient { // reward-related - async fn reward_mixnode( + async fn reward_node( &self, - mix_id: MixId, - performance: Performance, + node_id: NodeId, + params: NodeRewardingParameters, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::RewardMixnode { - mix_id, - performance, - }, + MixnetExecuteMsg::RewardNode { node_id, params }, vec![], ) .await @@ -664,12 +572,12 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward( &self, - mix_id: MixId, + node_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::WithdrawDelegatorReward { mix_id }, + MixnetExecuteMsg::WithdrawDelegatorReward { node_id }, vec![], ) .await @@ -678,7 +586,7 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward_on_behalf( &self, owner: AccountId, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -699,7 +607,7 @@ pub trait MixnetSigningClient { async fn migrate_vested_delegation( &self, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -761,6 +669,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; + use nym_mixnet_contract_common::ExecuteMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -770,56 +679,17 @@ mod tests { ) { match msg { MixnetExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(), - MixnetExecuteMsg::AssignNodeLayer { mix_id, layer } => { - client.assign_node_layer(mix_id, layer, None).ignore() - } - MixnetExecuteMsg::CreateFamily { label } => client.create_family(label, None).ignore(), - MixnetExecuteMsg::JoinFamily { - join_permit, - family_head, - } => client.join_family(join_permit, family_head, None).ignore(), - MixnetExecuteMsg::LeaveFamily { family_head } => { - client.leave_family(family_head, None).ignore() - } - MixnetExecuteMsg::KickFamilyMember { member } => { - client.kick_family_member(member, None).ignore() - } - MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - } => client - .create_family_on_behalf(owner_address, label, None) - .ignore(), - MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - } => client - .join_family_on_behalf(member_address, join_permit, family_head, None) - .ignore(), - MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - } => client - .leave_family_on_behalf(member_address, family_head, None) - .ignore(), - MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - } => client - .kick_family_member_on_behalf(head_address, member, None) - .ignore(), MixnetExecuteMsg::UpdateRewardingValidatorAddress { address } => client .update_rewarding_validator_address(address.parse().unwrap(), None) .ignore(), MixnetExecuteMsg::UpdateContractStateParams { updated_parameters } => client .update_contract_state_params(updated_parameters, None) .ignore(), - MixnetExecuteMsg::UpdateActiveSetSize { - active_set_size, + MixnetExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, } => client - .update_active_set_size(active_set_size, force_immediately, None) + .update_active_set_size(update, force_immediately, None) .ignore(), MixnetExecuteMsg::UpdateRewardingParams { updated_params, @@ -842,12 +712,6 @@ mod tests { MixnetExecuteMsg::BeginEpochTransition {} => { client.begin_epoch_transition(None).ignore() } - MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size, - } => client - .advance_current_epoch(new_rewarded_set, expected_active_set_size, None) - .ignore(), MixnetExecuteMsg::ReconcileEpochEvents { limit } => { client.reconcile_epoch_events(limit, None).ignore() } @@ -887,8 +751,8 @@ mod tests { MixnetExecuteMsg::UnbondMixnodeOnBehalf { owner } => client .unbond_mixnode_on_behalf(owner.parse().unwrap(), None) .ignore(), - MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - client.update_mixnode_cost_params(new_costs, None).ignore() + MixnetExecuteMsg::UpdateCostParams { new_costs } => { + client.update_cost_params(new_costs, None).ignore() } MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => client .update_mixnode_cost_params_on_behalf(owner.parse().unwrap(), new_costs, None) @@ -928,29 +792,28 @@ mod tests { MixnetExecuteMsg::UpdateGatewayConfigOnBehalf { new_config, owner } => client .update_gateway_config_on_behalf(owner.parse().unwrap(), new_config, None) .ignore(), - MixnetExecuteMsg::DelegateToMixnode { mix_id } => client - .delegate_to_mixnode(mix_id, mock_coin(), None) - .ignore(), + MixnetExecuteMsg::Delegate { node_id: mix_id } => { + client.delegate(mix_id, mock_coin(), None).ignore() + } MixnetExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => client .delegate_to_mixnode_on_behalf(delegate.parse().unwrap(), mix_id, mock_coin(), None) .ignore(), - MixnetExecuteMsg::UndelegateFromMixnode { mix_id } => { - client.undelegate_from_mixnode(mix_id, None).ignore() + MixnetExecuteMsg::Undelegate { node_id: mix_id } => { + client.undelegate(mix_id, None).ignore() } MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => client .undelegate_to_mixnode_on_behalf(delegate.parse().unwrap(), mix_id, None) .ignore(), - MixnetExecuteMsg::RewardMixnode { - mix_id, - performance, - } => client.reward_mixnode(mix_id, performance, None).ignore(), + MixnetExecuteMsg::RewardNode { node_id, params } => { + client.reward_node(node_id, params, None).ignore() + } MixnetExecuteMsg::WithdrawOperatorReward {} => { client.withdraw_operator_reward(None).ignore() } MixnetExecuteMsg::WithdrawOperatorRewardOnBehalf { owner } => client .withdraw_operator_reward_on_behalf(owner.parse().unwrap(), None) .ignore(), - MixnetExecuteMsg::WithdrawDelegatorReward { mix_id } => { + MixnetExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { client.withdraw_delegator_reward(mix_id, None).ignore() } MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => client @@ -963,6 +826,32 @@ mod tests { client.migrate_vested_delegation(mix_id, None).ignore() } + ExecuteMsg::AssignRoles { assignment } => { + client.assign_roles(assignment, None).ignore() + } + ExecuteMsg::MigrateMixnode {} => client.migrate_legacy_mixnode(None).ignore(), + ExecuteMsg::MigrateGateway { cost_params } => { + client.migrate_legacy_gateway(cost_params, None).ignore() + } + ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + } => client + .bond_nymnode(node, cost_params, owner_signature, mock_coin(), None) + .ignore(), + ExecuteMsg::UnbondNymNode {} => client.unbond_nymnode(None).ignore(), + ExecuteMsg::UpdateNodeConfig { update } => { + client.update_nymnode_config(update, None).ignore() + } + + ExecuteMsg::TestingUncheckedBondLegacyMixnode { .. } => { + todo!("purposely not implemented") + } + ExecuteMsg::TestingUncheckedBondLegacyGateway { .. } => { + todo!("purposely not implemented") + } + #[cfg(feature = "contract-testing")] MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => { client.testing_resolve_all_pending_events(None).ignore() diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs index fa6bd643fa..02eaf8c4b8 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs @@ -9,7 +9,7 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_vesting_contract_common::{ messages::QueryMsg as VestingQueryMsg, Account, AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, DelegationTimesResponse, @@ -238,7 +238,7 @@ pub trait VestingQueryClient { async fn get_vesting_delegation( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, block_timestamp_secs: u64, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetDelegation { @@ -252,7 +252,7 @@ pub trait VestingQueryClient { async fn get_total_delegation_amount( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetTotalDelegationAmount { address: address.to_string(), @@ -264,7 +264,7 @@ pub trait VestingQueryClient { async fn get_delegation_timestamps( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetDelegationTimes { address: address.to_string(), @@ -275,7 +275,7 @@ pub trait VestingQueryClient { async fn get_all_vesting_delegations_paged( &self, - start_after: Option<(u32, MixId, u64)>, + start_after: Option<(u32, NodeId, u64)>, limit: Option, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetAllDelegations { start_after, limit }) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs index 07472c4262..2f50016f91 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs @@ -9,10 +9,9 @@ use crate::signing::signer::OfflineSigner; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use nym_mixnet_contract_common::{Gateway, MixNode, NodeId}; use nym_vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; use nym_vesting_contract_common::{PledgeCap, VestingSpecification}; @@ -28,7 +27,7 @@ pub trait VestingSigningClient { async fn vesting_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_vesting_contract( @@ -124,7 +123,7 @@ pub trait VestingSigningClient { async fn vesting_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -204,7 +203,7 @@ pub trait VestingSigningClient { async fn vesting_track_undelegation( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result { @@ -222,7 +221,7 @@ pub trait VestingSigningClient { async fn vesting_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, fee: Option, @@ -241,7 +240,7 @@ pub trait VestingSigningClient { async fn vesting_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, fee: Option, ) -> Result { @@ -301,7 +300,7 @@ pub trait VestingSigningClient { async fn vesting_withdraw_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_vesting_contract( @@ -354,50 +353,6 @@ pub trait VestingSigningClient { ) .await } - - async fn vesting_create_family( - &self, - label: String, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::CreateFamily { label }, vec![]) - .await - } - - async fn vesting_join_family( - &self, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_vesting_contract( - fee, - VestingExecuteMsg::JoinFamily { - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn vesting_leave_family( - &self, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::LeaveFamily { family_head }, vec![]) - .await - } - - async fn vesting_kick_family_member( - &self, - member: String, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::KickFamilyMember { member }, vec![]) - .await - } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -446,21 +401,6 @@ mod tests { msg: VestingExecuteMsg, ) { match msg { - VestingExecuteMsg::CreateFamily { label } => { - client.vesting_create_family(label, None).ignore() - } - VestingExecuteMsg::JoinFamily { - join_permit, - family_head, - } => client - .vesting_join_family(join_permit, family_head, None) - .ignore(), - VestingExecuteMsg::LeaveFamily { family_head } => { - client.vesting_leave_family(family_head, None).ignore() - } - VestingExecuteMsg::KickFamilyMember { member } => { - client.vesting_kick_family_member(member, None).ignore() - } VestingExecuteMsg::TrackReward { amount, address } => client .vesting_track_reward(amount.into(), address, None) .ignore(), diff --git a/common/client-libs/validator-client/src/nyxd/error.rs b/common/client-libs/validator-client/src/nyxd/error.rs index c71ed596ad..e4b2f1b9e5 100644 --- a/common/client-libs/validator-client/src/nyxd/error.rs +++ b/common/client-libs/validator-client/src/nyxd/error.rs @@ -154,6 +154,23 @@ pub enum NyxdError { #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")] MalformedResponseData { got: usize, expected: usize }, + + #[error( + "one of the extension query for {contract} failed with the following message: {message}" + )] + ExtensionQueryFailure { contract: String, message: String }, +} + +impl NyxdError { + pub fn extension_query_failure( + contract: impl Into, + message: impl Into, + ) -> Self { + NyxdError::ExtensionQueryFailure { + contract: contract.into(), + message: message.into(), + } + } } // The purpose of parsing the abci query result is that we want to generate the `pretty_log` if diff --git a/common/commands/src/validator/cosmwasm/generators/mixnet.rs b/common/commands/src/validator/cosmwasm/generators/mixnet.rs index afd21fde9a..7f97809e36 100644 --- a/common/commands/src/validator/cosmwasm/generators/mixnet.rs +++ b/common/commands/src/validator/cosmwasm/generators/mixnet.rs @@ -4,6 +4,7 @@ use clap::Parser; use cosmwasm_std::Decimal; use log::{debug, info}; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; use nym_mixnet_contract_common::{ InitialRewardingParams, InstantiateMsg, OperatingCostRange, Percent, ProfitMarginRange, }; @@ -56,11 +57,17 @@ pub struct Args { #[clap(long, default_value_t = 2)] pub interval_pool_emission: u64, - #[clap(long, default_value_t = 240)] - pub rewarded_set_size: u32, + #[clap(long, default_value_t = 50)] + pub(crate) entry_gateways: u32, - #[clap(long, default_value_t = 240)] - pub active_set_size: u32, + #[clap(long, default_value_t = 70)] + pub(crate) exit_gateways: u32, + + #[clap(long, default_value_t = 120)] + pub(crate) mixnodes: u32, + + #[clap(long, default_value_t = 0)] + pub(crate) standby: u32, #[clap(long, default_value_t = Percent::zero())] pub minimum_profit_margin_percent: Percent, @@ -95,8 +102,13 @@ pub async fn generate(args: Args) { .expect("active_set_work_factor can't be converted to Decimal"), interval_pool_emission: Percent::from_percentage_value(args.interval_pool_emission) .expect("interval_pool_emission can't be converted to Percent"), - rewarded_set_size: args.rewarded_set_size, - active_set_size: args.active_set_size, + + rewarded_set_params: RewardedSetParams { + entry_gateways: args.entry_gateways, + exit_gateways: args.exit_gateways, + mixnodes: args.mixnodes, + standby: args.standby, + }, }; debug!("initial_rewarding_params: {:?}", initial_rewarding_params); diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs index 54b2516d6e..dc8a2d591f 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::{Coin, MixId}; +use nym_mixnet_contract_common::{Coin, NodeId}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, @@ -43,7 +43,7 @@ pub async fn delegate_to_mixnode(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); let res = client - .delegate_to_mixnode(mix_id, coin.into(), None) + .delegate(mix_id, coin.into(), None) .await .expect("failed to delegate to mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs index 787d27a56e..77d59d6bb3 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs @@ -1,21 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::fs::OpenOptions; - use clap::Parser; use comfy_table::Table; use csv::WriterBuilder; use log::info; use nym_mixnet_contract_common::ExecuteMsg; -use nym_mixnet_contract_common::ExecuteMsg::{DelegateToMixnode, UndelegateFromMixnode}; - -use nym_mixnet_contract_common::PendingEpochEventKind::{Delegate, Undelegate}; +use nym_mixnet_contract_common::PendingEpochEventKind; use nym_validator_client::nyxd::contract_traits::{NymContractsProvider, PagedMixnetQueryClient}; use nym_validator_client::nyxd::Coin; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::fs::OpenOptions; use crate::context::SigningClient; use crate::utils::pretty_coin; @@ -40,7 +37,7 @@ pub struct Args { #[derive(Debug)] pub struct InputFileRow { - pub mix_id: String, + pub node_id: String, pub amount: Coin, } #[derive(Debug)] @@ -76,7 +73,7 @@ impl InputFileReader { } rows.push(InputFileRow { - mix_id, + node_id: mix_id, amount: Coin { amount: micro_nym_amount, denom: "unym".to_string(), @@ -140,8 +137,10 @@ async fn fetch_delegation_data( let mut pending_delegation_map: HashMap = HashMap::new(); for delegation in delegations { - existing_delegation_map - .insert(delegation.mix_id.to_string(), Coin::from(delegation.amount)); + existing_delegation_map.insert( + delegation.node_id.to_string(), + Coin::from(delegation.amount), + ); } // Look for pending delegate / undelegate events which might be of interest to us @@ -155,27 +154,27 @@ async fn fetch_delegation_data( for event in pending_events { match event.event.kind { // If a pending undelegate tx is found, remove it from delegation map - Undelegate { owner, mix_id, .. } => { + PendingEpochEventKind::Undelegate { owner, node_id, .. } => { if owner == address.as_ref() - && existing_delegation_map.contains_key(&mix_id.to_string()) + && existing_delegation_map.contains_key(&node_id.to_string()) { - existing_delegation_map.remove(&mix_id.to_string()); + existing_delegation_map.remove(&node_id.to_string()); } } // If a pending delegation event is found, gather them to consolidate later - Delegate { + PendingEpochEventKind::Delegate { owner, - mix_id, + node_id, amount, .. } => { if owner == address.as_ref() { let mut amount = Coin::from(amount); - if let Some(pending_record) = pending_delegation_map.get(&mix_id.to_string()) { + if let Some(pending_record) = pending_delegation_map.get(&node_id.to_string()) { amount.amount += pending_record.amount; } - pending_delegation_map.insert(mix_id.to_string(), amount); + pending_delegation_map.insert(node_id.to_string(), amount); } } _ => {} @@ -217,7 +216,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { for row in &records.rows { let input_amount = row.amount.amount; let existing_delegation_amount = existing_delegation_map - .get(&row.mix_id) + .get(&row.node_id) .map_or(0, |coin| coin.amount); match existing_delegation_amount.cmp(&input_amount) { @@ -229,25 +228,26 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { amount: input_amount - existing_delegation_amount, denom: row.amount.denom.clone(), }; - let mix_id = row.mix_id.clone().parse::().unwrap(); - delegation_msgs.push((DelegateToMixnode { mix_id }, vec![difference.clone()])); + let node_id = row.node_id.clone().parse::().unwrap(); + delegation_msgs.push((ExecuteMsg::Delegate { node_id }, vec![difference.clone()])); delegation_table.add_row(&[ - row.mix_id.clone(), + row.node_id.clone(), pretty_coin(&row.amount), pretty_coin(&difference), ]); } Ordering::Greater => { - let mix_id = row.mix_id.clone().parse::().unwrap(); + let node_id = row.node_id.clone().parse::().unwrap(); let coins: Vec = vec![]; - undelegation_msgs.push((UndelegateFromMixnode { mix_id }, coins)); - undelegation_table.add_row(&[row.mix_id.clone()]); + undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins)); + undelegation_table.add_row(&[row.node_id.clone()]); if row.amount.amount > 0 { - delegation_msgs.push((DelegateToMixnode { mix_id }, vec![row.amount.clone()])); + delegation_msgs + .push((ExecuteMsg::Delegate { node_id }, vec![row.amount.clone()])); delegation_table.add_row(&[ - row.mix_id.clone(), + row.node_id.clone(), pretty_coin(&row.amount), pretty_coin(&row.amount), ]); diff --git a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs index 8523e63639..8ac1ab9c07 100644 --- a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs +++ b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs index b1a223b603..b05f44b6fb 100644 --- a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -66,7 +66,7 @@ async fn print_delegations(delegations: Vec, client: &SigningClientW for delegation in delegations { table.add_row(vec![ to_iso_timestamp(delegation.height as u32, client).await, - delegation.mix_id.to_string(), + delegation.node_id.to_string(), pretty_cosmwasm_coin(&delegation.amount), delegation .proxy @@ -93,7 +93,7 @@ async fn print_delegation_events(events: Vec, client: &Signin match event.event.kind { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, proxy, .. @@ -110,7 +110,7 @@ async fn print_delegation_events(events: Vec, client: &Signin } PendingEpochEventKind::Undelegate { owner, - mix_id, + node_id: mix_id, proxy, .. } => { diff --git a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs index 327a3c0fd3..83426a316e 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs index aa6b8dbe44..d36c4a7b17 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs index cadb6c272f..19593ed5ce 100644 --- a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, @@ -36,7 +36,7 @@ pub async fn undelegate_from_mixnode(args: Args, client: SigningClient) { }; let res = client - .undelegate_from_mixnode(mix_id, None) + .undelegate(mix_id, None) .await .expect("failed to remove stake from mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs index 45c4ffa5c3..3fa3fd7cee 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -4,7 +4,7 @@ use clap::Parser; use log::info; -use nym_mixnet_contract_common::{Coin, MixId}; +use nym_mixnet_contract_common::{Coin, NodeId}; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; @@ -13,7 +13,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs index 13bae0b7a0..9daf8691d3 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -3,7 +3,7 @@ use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; @@ -12,7 +12,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/operators/gateway/mod.rs b/common/commands/src/validator/mixnet/operators/gateway/mod.rs index 8c4b8753c3..dcc81efbcc 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/mod.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/mod.rs @@ -5,6 +5,7 @@ use clap::{Args, Subcommand}; pub mod bond_gateway; pub mod gateway_bonding_sign_payload; +pub mod nymnode_migration; pub mod settings; pub mod unbond_gateway; pub mod vesting_bond_gateway; @@ -31,4 +32,6 @@ pub enum MixnetOperatorsGatewayCommands { VestingUnbond(vesting_unbond_gateway::Args), /// Create base58-encoded payload required for producing valid bonding signature. CreateGatewayBondingSignPayload(gateway_bonding_sign_payload::Args), + /// Migrate the gateway into a Nym Node + MigrateToNymnode(nymnode_migration::Args), } diff --git a/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs new file mode 100644 index 0000000000..a6b22a2d4b --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs @@ -0,0 +1,56 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::info; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, +} + +pub async fn migrate_to_nymnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let cost_params = + if args.profit_margin_percent.is_some() || args.interval_operating_cost.is_some() { + Some(NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }) + } else { + None + }; + + let res = client + .migrate_legacy_gateway(cost_params, None) + .await + .expect("failed to migrate gateway!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs index 862a797566..410ce91d28 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -1,20 +1,21 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::context::SigningClient; use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; - use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Coin, MixNodeCostParams, Percent}; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; use nym_validator_client::nyxd::CosmWasmCoin; -use crate::context::SigningClient; - #[derive(Debug, Parser)] pub struct Args { #[clap(long)] @@ -42,7 +43,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -85,14 +86,18 @@ pub async fn bond_mixnode(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs deleted file mode 100644 index 6e2c664a35..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// Label that is going to be used for creating the family - #[arg(long)] - pub family_label: String, -} - -pub async fn create_family(args: Args, client: SigningClient) { - info!("Create family"); - - let res = client - .create_family(args.family_label, None) - .await - .expect("failed to create family"); - - info!("Family creation result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs deleted file mode 100644 index ac397150ea..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::QueryClient; -use crate::utils::DataWrapper; -use clap::Parser; -use cosmrs::AccountId; -use log::info; -use nym_bin_common::output_format::OutputFormat; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::construct_family_join_permit; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// Account address (i.e. owner of the family head) which will be used for issuing the permit - #[arg(long)] - pub address: AccountId, - - // might as well validate the value when parsing the arguments - /// Identity of the member for whom we're issuing the permit - #[arg(long)] - pub member: identity::PublicKey, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub async fn create_family_join_permit_sign_payload(args: Args, client: QueryClient) { - info!("Create family join permit sign payload"); - - // get the address of our mixnode to recover the family head information - let Some(mixnode) = client - .get_owned_mixnode(&args.address) - .await - .unwrap() - .mixnode_details - else { - eprintln!("{} does not seem to even own a mixnode!", args.address); - return; - }; - - // make sure this mixnode is actually a family head - if client - .get_node_family_by_head(mixnode.bond_information.identity().to_string()) - .await - .unwrap() - .family - .is_none() - { - eprintln!("{} does not even seem to own a family!", args.address); - return; - } - - let nonce = match client.get_signing_nonce(&args.address).await { - Ok(nonce) => nonce, - Err(err) => { - eprint!( - "failed to query for the signing nonce of {}: {err}", - args.address - ); - return; - } - }; - - let head = FamilyHead::new(mixnode.bond_information.identity()); - - let payload = construct_family_join_permit(nonce, head, args.member.to_base58_string()); - let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); - println!("{}", args.output.format(&wrapper)) -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs deleted file mode 100644 index 08b4c471c0..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_contracts_common::signing::MessageSignature; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The head of the family that we intend to join - #[arg(long)] - pub family_head: identity::PublicKey, - - /// Permission, as provided by the family head, for joining the family - #[arg(long)] - pub join_permit: MessageSignature, -} - -pub async fn join_family(args: Args, client: SigningClient) { - info!("Join family"); - - let family_head = FamilyHead::new(args.family_head.to_base58_string()); - - let res = client - .join_family(args.join_permit, family_head, None) - .await - .expect("failed to join family"); - - info!("Family join result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs deleted file mode 100644 index 27b5d71d7d..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The member of the family that we intend to kick - #[arg(long)] - pub member: identity::PublicKey, - - /// Indicates whether the family was created (and managed) via the vesting contract - #[arg(long)] - pub with_vesting_account: bool, -} - -pub async fn kick_family_member(args: Args, client: SigningClient) { - info!("Leave family"); - - let member = args.member.to_base58_string(); - - let res = if args.with_vesting_account { - client - .vesting_kick_family_member(member, None) - .await - .expect("failed to kick family member with vesting account") - } else { - client - .kick_family_member(member, None) - .await - .expect("failed to kick family member") - }; - - info!("Family leave result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs deleted file mode 100644 index d9c31e3933..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The head of the family that we intend to leave - #[arg(long)] - pub family_head: identity::PublicKey, -} - -pub async fn leave_family(args: Args, client: SigningClient) { - info!("Leave family"); - - let family_head = FamilyHead::new(args.family_head.to_base58_string()); - - let res = client - .leave_family(family_head, None) - .await - .expect("failed to leave family"); - - info!("Family leave result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs deleted file mode 100644 index c343ef977f..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use clap::{Args, Subcommand}; - -pub mod create_family; -pub mod create_family_join_permit_sign_payload; -pub mod join_family; -pub mod kick_family_member; -pub mod leave_family; - -#[derive(Debug, Args)] -#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] -pub struct MixnetOperatorsMixnodeFamilies { - #[clap(subcommand)] - pub command: MixnetOperatorsMixnodeFamiliesCommands, -} - -#[derive(Debug, Subcommand)] -pub enum MixnetOperatorsMixnodeFamiliesCommands { - /// Create family - CreateFamily(create_family::Args), - - /// Join family - JoinFamily(join_family::Args), - - /// Leave family, - LeaveFamily(leave_family::Args), - - /// Kick family member - KickFamilyMember(kick_family_member::Args), - - /// Create a message payload that is required to get signed in order to obtain a permit for joining family - CreateFamilyJoinPermitSignPayload(create_family_join_permit_sign_payload::Args), -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs index 3eda67a2e9..7525657547 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs @@ -8,7 +8,8 @@ use cosmwasm_std::{Coin, Uint128}; use nym_bin_common::output_format::OutputFormat; use nym_contracts_common::Percent; use nym_mixnet_contract_common::{ - construct_legacy_mixnode_bonding_sign_payload, MixNodeCostParams, + construct_legacy_mixnode_bonding_sign_payload, NodeCostParams, + DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, }; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, @@ -40,7 +41,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -75,14 +76,18 @@ pub async fn create_payload(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs index abb5060e9b..3244a189bc 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -5,10 +5,10 @@ use clap::{Args, Subcommand}; pub mod bond_mixnode; pub mod decrease_pledge; -pub mod families; pub mod keys; pub mod migrate_vested_mixnode; pub mod mixnode_bonding_sign_payload; +pub mod nymnode_migration; pub mod pledge_more; pub mod rewards; pub mod settings; @@ -33,8 +33,6 @@ pub enum MixnetOperatorsMixnodeCommands { Rewards(rewards::MixnetOperatorsMixnodeRewards), /// Manage your mixnode settings stored in the directory Settings(settings::MixnetOperatorsMixnodeSettings), - /// Operations for mixnode families - Families(families::MixnetOperatorsMixnodeFamilies), /// Bond to a mixnode Bond(bond_mixnode::Args), /// Unbond from a mixnode @@ -55,4 +53,6 @@ pub enum MixnetOperatorsMixnodeCommands { DecreasePledgeVesting(vesting_decrease_pledge::Args), /// Migrate the mixnode to use liquid tokens MigrateVestedNode(migrate_vested_mixnode::Args), + /// Migrate the mixnode into a Nym Node + MigrateToNymnode(nymnode_migration::Args), } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs new file mode 100644 index 0000000000..fe46e2c11a --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn migrate_to_nymnode(_args: Args, client: SigningClient) { + let res = client + .migrate_legacy_mixnode(None) + .await + .expect("failed to migrate mixnode!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs index 7c09ba2946..c246bc8fba 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::SigningClient; +use anyhow::{anyhow, bail}; use clap::Parser; use cosmwasm_std::Uint128; use log::info; -use nym_mixnet_contract_common::{MixNodeCostParams, Percent}; +use nym_mixnet_contract_common::{ + NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::nyxd::CosmWasmCoin; @@ -24,62 +27,45 @@ pub struct Args { pub interval_operating_cost: Option, } -pub async fn update_cost_params(args: Args, client: SigningClient) { +pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { let denom = client.current_chain_details().mix_denom.base.as_str(); - fn convert_to_percent(value: u64) -> Percent { - Percent::from_percentage_value(value).expect("Invalid value") - } + let default_profit_margin = + Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT).unwrap(); - let default_profit_margin: Percent = convert_to_percent(20); + let mix_details = client + .get_owned_mixnode(&client.address()) + .await? + .mixnode_details + .ok_or_else(|| anyhow!("the client does not own any mixnodes"))?; + let current_parameters = mix_details.rewarding_details.cost_params; - let mixownership_response = match client.get_owned_mixnode(&client.address()).await { - Ok(response) => response, - Err(_) => { - eprintln!("Failed to obtain owned mixnode"); - return; - } - }; - - let mix_id = match mixownership_response.mixnode_details { - Some(details) => details.bond_information.mix_id, - None => { - eprintln!("Failed to obtain mixnode details"); - return; - } - }; - - let rewarding_response = match client.get_mixnode_rewarding_details(mix_id).await { - Ok(details) => details, - Err(_) => { - eprintln!("Failed to obtain rewarding details"); - return; - } - }; - - let profit_margin_percent = rewarding_response - .rewarding_details + let profit_margin_percent = current_parameters .map(|rd| rd.cost_params.profit_margin_percent) .unwrap_or(default_profit_margin); let profit_margin_value = args .profit_margin_percent - .map(|pm| convert_to_percent(pm as u64)) - .unwrap_or(profit_margin_percent); + .map(|pm| Percent::from_percentage_value(pm as u64)) + .unwrap_or(profit_margin_percent)?; - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: profit_margin_value, interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; info!("Starting mixnode params updating!"); let res = client - .update_mixnode_cost_params(cost_params, None) + .update_cost_params(cost_params, None) .await .expect("failed to update cost params"); - info!("Cost params result: {:?}", res) + info!("Cost params result: {:?}", res); + Ok(()) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index 415f9fe2b3..d55d6463e7 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -6,7 +6,9 @@ use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Coin, MixNodeCostParams}; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_mixnet_contract_common::{MixNode, Percent}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, @@ -40,7 +42,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -84,14 +86,18 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mod.rs b/common/commands/src/validator/mixnet/operators/mod.rs index cde9dc3754..cf6e2d9d5f 100644 --- a/common/commands/src/validator/mixnet/operators/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mod.rs @@ -6,6 +6,7 @@ use clap::{Args, Subcommand}; pub mod gateway; pub mod identity_key; pub mod mixnode; +pub mod nymnode; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] @@ -17,9 +18,11 @@ pub struct MixnetOperators { #[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub enum MixnetOperatorsCommands { - /// Manage your mixnode + /// Manage your Nym Node + Nymnode(nymnode::MixnetOperatorsNymNode), + /// Manage your legacy mixnode Mixnode(mixnode::MixnetOperatorsMixnode), - /// Manage your gateway + /// Manage your legacy gateway Gateway(gateway::MixnetOperatorsGateway), /// Sign messages using your private identity key IdentityKey(identity_key::MixnetOperatorsIdentityKey), diff --git a/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs new file mode 100644 index 0000000000..acde3d39e3 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs @@ -0,0 +1,89 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::{info, warn}; +use nym_contracts_common::signing::MessageSignature; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub signature: MessageSignature, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long)] + pub force: bool, +} + +pub async fn bond_nymnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting nym node bonding!"); + + // if we're trying to bond less than 1 token + if args.amount < 1_000_000 && !args.force { + warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom); + return; + } + + let nymnode = nym_mixnet_contract_common::NymNode { + host: args.host, + custom_http_port: args.http_api_port, + identity_key: args.identity_key, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + let res = client + .bond_nymnode(nymnode, cost_params, args.signature, coin.into(), None) + .await + .expect("failed to bond nymnode!"); + + info!("Bonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs b/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs new file mode 100644 index 0000000000..04b6d7c9ea --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use base64::Engine; +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(short, long)] + pub key: String, +} + +pub fn decode_node_key(args: Args) { + let b64_decoded = base64::prelude::BASE64_STANDARD + .decode(args.key) + .expect("failed to decode base64 string"); + let b58_encoded = bs58::encode(&b64_decoded).into_string(); + + println!("{b58_encoded}") +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs new file mode 100644 index 0000000000..1f0b5d4c21 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod decode_node_key; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeKeys { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeKeysCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeKeysCommands { + /// Decode a Nym Node key + DecodeNodeKey(decode_node_key::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/mod.rs new file mode 100644 index 0000000000..e2cd7890a7 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/mod.rs @@ -0,0 +1,43 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod bond_nymnode; +pub mod keys; +pub mod nymnode_bonding_sign_payload; +pub mod pledge; +pub mod rewards; +pub mod settings; +pub mod unbond_nymnode; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNode { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeCommands { + /// Operations for Nym Node keys + Keys(keys::MixnetOperatorsNymNodeKeys), + + /// Manage your Nym Node operator rewards + Rewards(rewards::MixnetOperatorsNymNodeRewards), + + /// Manage your Nym Node settings stored in the directory + Settings(settings::MixnetOperatorsNymNodeSettings), + + /// Manage your Nym Node pledge + Pledge(pledge::MixnetOperatorsNymNodePledge), + + /// Bond to a Nym Node + Bond(bond_nymnode::Args), + + /// Unbond from a Nym Node + Unbond(unbond_nymnode::Args), + + /// Create base58-encoded payload required for producing valid bonding signature. + CreateNodeBondingSignPayload(nymnode_bonding_sign_payload::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs new file mode 100644 index 0000000000..e3b66e65be --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs @@ -0,0 +1,90 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::{account_id_to_cw_addr, DataWrapper}; +use clap::Parser; +use cosmwasm_std::{Coin, Uint128}; +use nym_bin_common::output_format::OutputFormat; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + construct_nym_node_bonding_sign_payload, NodeCostParams, + DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub custom_http_api_port: Option, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long, default_value_t = OutputFormat::default())] + pub output: OutputFormat, +} + +pub async fn create_payload(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let mixnode = nym_mixnet_contract_common::NymNode { + host: args.host, + custom_http_port: args.custom_http_api_port, + identity_key: args.identity_key, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + let nonce = match client.get_signing_nonce(&client.address()).await { + Ok(nonce) => nonce, + Err(err) => { + eprint!( + "failed to query for the signing nonce of {}: {err}", + client.address() + ); + return; + } + }; + + let address = account_id_to_cw_addr(&client.address()); + + let payload = + construct_nym_node_bonding_sign_payload(nonce, address, coin, mixnode, cost_params); + let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); + println!("{}", args.output.format(&wrapper)) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs new file mode 100644 index 0000000000..a299ddbe65 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::Coin; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub decrease_by: u128, +} + +pub async fn decrease_pledge(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting to decrease pledge"); + + let coin = Coin::new(args.decrease_by, denom); + + let res = client + .pledge_more(coin.into(), None) + .await + .expect("failed to decrease pledge!"); + + info!("decreasing pledge: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs new file mode 100644 index 0000000000..09b94bc5d5 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::Coin; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub amount: u128, +} + +pub async fn increase_pledge(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting to pledge more"); + + let coin = Coin::new(args.amount, denom); + + let res = client + .pledge_more(coin.into(), None) + .await + .expect("failed to pledge more!"); + + info!("pledging more: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs new file mode 100644 index 0000000000..188e024857 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod decrease_pledge; +pub mod increase_pledge; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodePledge { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodePledgeCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodePledgeCommands { + /// Increase current pledge + Increase(increase_pledge::Args), + /// decrease current pledge + Decrease(decrease_pledge::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs new file mode 100644 index 0000000000..a8f157f661 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs @@ -0,0 +1,21 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn claim_operator_reward(_args: Args, client: SigningClient) { + info!("Claim operator reward"); + + let res = client + .withdraw_operator_reward(None) + .await + .expect("failed to claim operator reward"); + + info!("Claiming operator reward: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs new file mode 100644 index 0000000000..166124751c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod claim_operator_reward; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeRewards { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeRewardsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeRewardsCommands { + /// Claim rewards + Claim(claim_operator_reward::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs new file mode 100644 index 0000000000..8c5661188c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod update_config; +pub mod update_cost_params; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeSettings { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeSettingsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeSettingsCommands { + /// Update Nym Node configuration + UpdateConfig(update_config::Args), + /// Update Nym Node cost parameters + UpdateCostParameters(update_cost_params::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs new file mode 100644 index 0000000000..5ba5bf52fa --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: Option, + + // ideally this would have been `Option>`, but not sure if clap would have recognised it + #[clap(long)] + pub custom_http_port: Option, + + // equivalent to setting `custom_http_port` to `None` + #[clap(long)] + pub restore_default_http_port: bool, +} + +pub async fn update_config(args: Args, client: SigningClient) { + info!("Update nym node config!"); + + if client + .get_owned_nymnode(&client.address()) + .await + .expect("failed to query the chain for nym node details") + .details + .is_none() + { + log::warn!("this operator does not own a nym node to update"); + return; + } + + let update = NodeConfigUpdate { + host: args.host, + custom_http_port: args.custom_http_port, + restore_default_http_port: args.restore_default_http_port, + }; + + let res = client + .update_nymnode_config(update, None) + .await + .expect("updating nym node config"); + + info!("nym node config updated: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs new file mode 100644 index 0000000000..b7168f243a --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use anyhow::anyhow; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::info; +use nym_mixnet_contract_common::{ + NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap( + long, + help = "input your profit margin as follows; (so it would be 20, rather than 0.2)" + )] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, +} + +pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let default_profit_margin = + Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT).unwrap(); + + let node_details = client + .get_owned_nymnode(&client.address()) + .await? + .details + .ok_or_else(|| anyhow!("the client does not own any nodes"))?; + let current_parameters = node_details.rewarding_details.cost_params; + + let profit_margin_percent = current_parameters + .map(|rd| rd.cost_params.profit_margin_percent) + .unwrap_or(default_profit_margin); + + let profit_margin_value = args + .profit_margin_percent + .map(|pm| Percent::from_percentage_value(pm as u64)) + .unwrap_or(profit_margin_percent)?; + + let cost_params = NodeCostParams { + profit_margin_percent: profit_margin_value, + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + info!("Starting nym node params updating!"); + let res = client + .update_cost_params(cost_params, None) + .await + .expect("failed to update cost params"); + + info!("Cost params result: {:?}", res); + Ok(()) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs new file mode 100644 index 0000000000..fbcef6bfd7 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn unbond_nymnode(_args: Args, client: SigningClient) { + info!("Starting Nym Node unbonding!"); + + let res = client + .unbond_nymnode(None) + .await + .expect("failed to unbond Nym Node!"); + + info!("Unbonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index 16f0e44285..cfe66ffd6c 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -39,7 +39,7 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) { node.owner.to_string(), node.gateway.host.to_string(), pretty_cosmwasm_coin(&node.pledge_amount), - node.gateway.version, + node.gateway.version.clone(), ]); } diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index e14cb3501a..30a01c17a9 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -8,7 +8,7 @@ use cosmwasm_std::Uint128; use serde::de::Error; use serde::{Deserialize, Deserializer}; use std::fmt::{self, Display, Formatter}; -use std::ops::Mul; +use std::ops::{Deref, Mul}; use std::str::FromStr; use thiserror::Error; @@ -23,7 +23,7 @@ pub fn truncate_decimal(amount: Decimal) -> Uint128 { #[derive(Error, Debug)] pub enum ContractsCommonError { #[error("Provided percent value ({0}) is greater than 100%")] - InvalidPercent(Decimal), + InvalidPercent(String), #[error("{source}")] StdErr { @@ -41,7 +41,7 @@ pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); impl Percent { pub fn new(value: Decimal) -> Result { if value > Decimal::one() { - Err(ContractsCommonError::InvalidPercent(value)) + Err(ContractsCommonError::InvalidPercent(value.to_string())) } else { Ok(Percent(value)) } @@ -51,11 +51,15 @@ impl Percent { self.0 == Decimal::zero() } - pub fn zero() -> Self { + pub fn is_hundred(&self) -> bool { + self == &Self::hundred() + } + + pub const fn zero() -> Self { Self(Decimal::zero()) } - pub fn hundred() -> Self { + pub const fn hundred() -> Self { Self(Decimal::one()) } @@ -117,6 +121,70 @@ impl Mul for Percent { } } +impl Deref for Percent { + type Target = Decimal; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// this is not implemented via From traits due to its naive nature and loss of precision +#[cfg(not(target_arch = "wasm32"))] +pub trait NaiveFloat { + fn naive_to_f64(&self) -> f64; + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized; +} + +#[cfg(not(target_arch = "wasm32"))] +impl NaiveFloat for Percent { + fn naive_to_f64(&self) -> f64 { + use cosmwasm_std::Fraction; + + // note: this conversion loses precision with too many decimal places, + // but for the purposes of displaying basic performance, that's not an issue + self.numerator().u128() as f64 / self.denominator().u128() as f64 + } + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized, + { + // we are only interested in positive values between 0 and 1 + if !(0. ..=1.).contains(&val) { + return Err(ContractsCommonError::InvalidPercent(val.to_string())); + } + + fn gcd(mut x: u64, mut y: u64) -> u64 { + while y > 0 { + let rem = x % y; + x = y; + y = rem; + } + + x + } + + fn to_rational(x: f64) -> (u64, u64) { + let log = x.log2().floor(); + if log >= 0.0 { + (x as u64, 1) + } else { + let num: u64 = (x / f64::EPSILON) as _; + let den: u64 = (1.0 / f64::EPSILON) as _; + let gcd = gcd(num, den); + (num / gcd, den / gcd) + } + } + + let (n, d) = to_rational(val); + Percent::new(Decimal::from_ratio(n, d)) + } +} + // implement custom Deserialize because we want to validate Percent has the correct range fn de_decimal_percent<'de, D>(deserializer: D) -> Result where @@ -204,6 +272,7 @@ impl ContractBuildInformation { #[cfg(test)] mod tests { use super::*; + use cosmwasm_std::Fraction; #[test] fn percent_serde() { @@ -243,4 +312,19 @@ mod tests { let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); assert_eq!(p.round_to_integer(), 100); } + + #[test] + fn naive_float_conversion() { + // around 15 decimal places is the maximum precision we can handle + // which is still way more than enough for what we use it for + let float: f64 = "0.546295475423853".parse().unwrap(); + let percent: Percent = "0.546295475423853".parse().unwrap(); + + assert_eq!(float, percent.naive_to_f64()); + + let epsilon = Decimal::from_ratio(1u64, 1000000000000000u64); + let converted = Percent::naive_try_from_f64(float).unwrap(); + + assert!(converted.0 - converted.0 < epsilon); + } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 58335f5c5e..ca87cb9b95 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -12,6 +12,7 @@ repository = { workspace = true } bs58 = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } +cw-storage-plus.workspace = true cw-controllers = { workspace = true } cw2 = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs index 2269c20cd7..c68d1fc31f 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs @@ -5,6 +5,9 @@ use cosmwasm_std::{Decimal, Uint128}; pub const TOKEN_SUPPLY: Uint128 = Uint128::new(1_000_000_000_000_000); +pub const DEFAULT_INTERVAL_OPERATING_COST_AMOUNT: u128 = 40_000_000; +pub const DEFAULT_PROFIT_MARGIN_PERCENT: u64 = 20; + // I'm still not 100% sure how to feel about existence of this file // This is equivalent of representing our display coin with 6 decimal places. // I'm using this one as opposed to "Decimal::one()", as this provides us with higher accuracy diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index f30dc63a73..5c7a733728 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -3,14 +3,14 @@ use crate::constants::TOKEN_SUPPLY; use crate::helpers::IntoBaseDecimal; -use crate::{Addr, MixId}; +use crate::{Addr, NodeId}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal, StdResult}; // just use a string representation of those so that we wouldn't need to bother with decoding bytes // and trying to figure out whether they're valid, etc pub type OwnerProxySubKey = String; -pub type StorageKey = (MixId, OwnerProxySubKey); +pub type StorageKey = (NodeId, OwnerProxySubKey); // throughout the contract we ensure that our proxy can ONLY ever be the vesting contract // thus this method is equivalent to either using the existing address (for when there's no proxy) @@ -40,8 +40,9 @@ pub struct Delegation { /// Address of the owner of this delegation. pub owner: Addr, - /// Id of the MixNode that this delegation was performed against. - pub mix_id: MixId, + /// Id of the Node that this delegation was performed against. + #[serde(alias = "mix_id")] + pub node_id: NodeId, // Note to UI/UX devs: there's absolutely no point in displaying this value to the users, // it would serve them no purpose. It's only used for calculating rewards @@ -56,12 +57,13 @@ pub struct Delegation { /// Proxy address used to delegate the funds on behalf of another address pub proxy: Option, + // TODO: perhaps add a field to indicate if it was made against old mixnode with #[serde(default)]? } impl Delegation { pub fn new( owner: Addr, - mix_id: MixId, + node_id: NodeId, cumulative_reward_ratio: Decimal, amount: Coin, height: u64, @@ -73,7 +75,7 @@ impl Delegation { Delegation { owner, - mix_id, + node_id, cumulative_reward_ratio, amount, height, @@ -82,7 +84,7 @@ impl Delegation { } pub fn generate_storage_key( - mix_id: MixId, + mix_id: NodeId, owner_address: &Addr, proxy: Option<&Addr>, ) -> StorageKey { @@ -92,7 +94,7 @@ impl Delegation { // this function might seem a bit redundant, but I'd rather explicitly keep it around in case // some types change in the future pub fn generate_storage_key_with_subkey( - mix_id: MixId, + mix_id: NodeId, owner_proxy_subkey: OwnerProxySubKey, ) -> StorageKey { (mix_id, owner_proxy_subkey) @@ -107,13 +109,13 @@ impl Delegation { } pub fn storage_key(&self) -> StorageKey { - Self::generate_storage_key(self.mix_id, &self.owner, self.proxy.as_ref()) + Self::generate_storage_key(self.node_id, &self.owner, self.proxy.as_ref()) } } -/// Response containing paged list of all delegations made towards particular mixnode. +/// Response containing paged list of all delegations made towards particular node. #[cw_serde] -pub struct PagedMixNodeDelegationsResponse { +pub struct PagedNodeDelegationsResponse { /// Each individual delegation made. pub delegations: Vec, @@ -121,9 +123,9 @@ pub struct PagedMixNodeDelegationsResponse { pub start_next_after: Option, } -impl PagedMixNodeDelegationsResponse { +impl PagedNodeDelegationsResponse { pub fn new(delegations: Vec, start_next_after: Option) -> Self { - PagedMixNodeDelegationsResponse { + PagedNodeDelegationsResponse { delegations, start_next_after, } @@ -137,13 +139,13 @@ pub struct PagedDelegatorDelegationsResponse { pub delegations: Vec, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option<(MixId, OwnerProxySubKey)>, + pub start_next_after: Option<(NodeId, OwnerProxySubKey)>, } impl PagedDelegatorDelegationsResponse { pub fn new( delegations: Vec, - start_next_after: Option<(MixId, OwnerProxySubKey)>, + start_next_after: Option<(NodeId, OwnerProxySubKey)>, ) -> Self { PagedDelegatorDelegationsResponse { delegations, @@ -154,19 +156,24 @@ impl PagedDelegatorDelegationsResponse { /// Response containing delegation details. #[cw_serde] -pub struct MixNodeDelegationResponse { +pub struct NodeDelegationResponse { /// If the delegation exists, this field contains its detailed information. pub delegation: Option, /// Flag indicating whether the node towards which the delegation was made is still bonded in the network. + #[deprecated(note = "this field will be removed. use .node_still_bonded instead")] pub mixnode_still_bonded: bool, + + pub node_still_bonded: bool, } -impl MixNodeDelegationResponse { - pub fn new(delegation: Option, mixnode_still_bonded: bool) -> Self { - MixNodeDelegationResponse { +impl NodeDelegationResponse { + pub fn new(delegation: Option, node_still_bonded: bool) -> Self { + #[allow(deprecated)] + NodeDelegationResponse { delegation, - mixnode_still_bonded, + mixnode_still_bonded: node_still_bonded, + node_still_bonded, } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 12f2030acc..9a67023224 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,7 +1,10 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{EpochEventId, EpochState, IdentityKey, MixId, OperatingCostRange, ProfitMarginRange}; +use crate::nym_node::Role; +use crate::{ + EpochEventId, EpochState, IntervalEventId, NodeId, OperatingCostRange, ProfitMarginRange, +}; use contracts_common::signing::verifier::ApiVerifierError; use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; @@ -34,6 +37,16 @@ pub enum MixnetContractError { #[error("Not enough funds sent for node pledge. (received {received}, minimum {minimum})")] InsufficientPledge { received: Coin, minimum: Coin }, + #[error( + "the provided value for node host is too long. it must not be longer than 255 characters" + )] + HostTooLong, + + #[error( + "the provided node identity public key is not a correctly encoded base58 slice of 32 bytes" + )] + InvalidPubKey, + #[error("Attempted to reduce node pledge ({current}{denom} - {decrease_by}{denom}) below the minimum amount: {minimum}{denom}")] InvalidPledgeReduction { current: Uint128, @@ -45,11 +58,19 @@ pub enum MixnetContractError { #[error("A pledge change is already pending in this epoch. The event id: {pending_event_id}")] PendingPledgeChange { pending_event_id: EpochEventId }, + #[error( + "A cost params change is already pending in this epoch. The event id: {pending_event_id}" + )] + PendingParamsChange { pending_event_id: IntervalEventId }, + #[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")] InsufficientDelegation { received: Coin, minimum: Coin }, + #[error("Node ({node_id}) does not exist")] + NymNodeBondNotFound { node_id: NodeId }, + #[error("Mixnode ({mix_id}) does not exist")] - MixNodeBondNotFound { mix_id: MixId }, + MixNodeBondNotFound { mix_id: NodeId }, #[error("{owner} does not seem to own any mixnodes")] NoAssociatedMixNodeBond { owner: Addr }, @@ -57,12 +78,18 @@ pub enum MixnetContractError { #[error("{owner} does not seem to own any gateways")] NoAssociatedGatewayBond { owner: Addr }, + #[error("{owner} does not seem to own any nodes")] + NoAssociatedNodeBond { owner: Addr }, + #[error("This address has already bonded a mixnode")] AlreadyOwnsMixnode, #[error("This address has already bonded a gateway")] AlreadyOwnsGateway, + #[error("This address has already bonded a nym-node")] + AlreadyOwnsNymNode, + #[error("Gateway with this identity already exists. Its owner is {owner}")] DuplicateGateway { owner: Addr }, @@ -103,32 +130,41 @@ pub enum MixnetContractError { epoch_end: i64, }, - #[error("Mixnode {mix_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] - MixnodeAlreadyRewarded { - mix_id: MixId, + #[error("attempted to reward a gateway node - this has not been fully integrated yet")] + GatewayRewarding, + + #[error("node {node_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] + NodeAlreadyRewarded { + node_id: NodeId, absolute_epoch_id: u32, }, + #[error("node {node_id} hasn't been assigned the role of {role} for this epoch")] + IncorrectEpochRole { node_id: NodeId, role: Role }, + #[error("Mixnode {mix_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] MixnodeNotInRewardedSet { - mix_id: MixId, + mix_id: NodeId, absolute_epoch_id: u32, }, #[error("Mixnode {mix_id} is currently in the process of unbonding")] - MixnodeIsUnbonding { mix_id: MixId }, + MixnodeIsUnbonding { mix_id: NodeId }, + + #[error("Node {node_id} is currently in the process of unbonding")] + NodeIsUnbonding { node_id: NodeId }, #[error("Mixnode {mix_id} has already unbonded")] - MixnodeHasUnbonded { mix_id: MixId }, + MixnodeHasUnbonded { mix_id: NodeId }, #[error("The contract has ended up in a state that was deemed impossible: {comment}")] InconsistentState { comment: String }, #[error( - "Could not find any delegation information associated with mixnode {mix_id} for {address} (proxy: {proxy:?})" + "Could not find any delegation information associated with node {node_id} for {address} (proxy: {proxy:?})" )] - NoMixnodeDelegationFound { - mix_id: MixId, + NodeDelegationNotFound { + node_id: NodeId, address: String, proxy: Option, }, @@ -136,63 +172,18 @@ pub enum MixnetContractError { #[error("Provided message to update rewarding params did not contain any updates")] EmptyParamsChangeMsg, - #[error("Provided active set size is bigger than the rewarded set")] + #[error("one of the roles in the new active set is empty")] + EmptyRoleAssignment, + + #[error("the number of mixnodes in the rewarded set is not divisible by the number of mix-layers (3)")] + UnevenLayerAssignment, + + #[error("provided active set is bigger than the rewarded set")] InvalidActiveSetSize, - #[error("Provided rewarded set size is smaller than the active set")] - InvalidRewardedSetSize, - - #[error("Provided active set size is zero")] - ZeroActiveSet, - - #[error("Provided rewarded set size is zero")] - ZeroRewardedSet, - - #[error("Received unexpected value for the active set. Got: {received}, expected: {expected}")] - UnexpectedActiveSetSize { received: u32, expected: u32 }, - - #[error("Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}")] - UnexpectedRewardedSetSize { received: u32, expected: u32 }, - - #[error("Mixnode {mix_id} appears multiple times in the provided rewarded set update!")] - DuplicateRewardedSetNode { mix_id: MixId }, - - #[error("Family with head {head} does not exist!")] - FamilyDoesNotExist { head: String }, - - #[error("Family with label {label} does not exist!")] - FamilyLabelDoesNotExist { label: String }, - - #[error("Family with label '{0}' already exists")] - FamilyWithLabelExists(String), - #[error("Invalid layer expected 1, 2 or 3, got {0}")] InvalidLayer(u8), - #[error("Head already has a family")] - FamilyCanHaveOnlyOne, - - #[error("Already member of family {0}")] - AlreadyMemberOfFamily(String), - - #[error("Can't join own family, family head {head}, member {member}")] - CantJoinOwnFamily { - head: IdentityKey, - member: IdentityKey, - }, - - #[error("Can't leave own family, family head {head}, member {member}")] - CantLeaveOwnFamily { - head: IdentityKey, - member: IdentityKey, - }, - - #[error("{member} is not a member of family {head}")] - NotAMember { - head: IdentityKey, - member: IdentityKey, - }, - #[error("Feature is not yet implemented")] NotImplemented, @@ -219,13 +210,28 @@ pub enum MixnetContractError { #[error("attempted to reward mixnode out of order. Attempted to reward {attempted_to_reward} while last rewarded was {last_rewarded}.")] RewardingOutOfOrder { - last_rewarded: MixId, - attempted_to_reward: MixId, + last_rewarded: NodeId, + attempted_to_reward: NodeId, }, #[error("the epoch is currently not in the 'event reconciliation' state. (the state is {current_state})")] EpochNotInEventReconciliationState { current_state: EpochState }, + #[error( + "the epoch is currently not in the 'role assignment' state. (the state is {current_state})" + )] + EpochNotInRoleAssignmentState { current_state: EpochState }, + + #[error("unexpected role assignment. got: {got} while expected: {expected}")] + UnexpectedRoleAssignment { expected: Role, got: Role }, + + #[error("attempted to assign an invalid number of nodes for a role of {role}. got {assigned}, but the maximum allowed is {allowed}")] + IllegalRoleCount { + role: Role, + assigned: u32, + allowed: u32, + }, + #[error("the epoch is currently not in the 'epoch advancement' state. (the state is {current_state})")] EpochNotInAdvancementState { current_state: EpochState }, @@ -258,6 +264,17 @@ pub enum MixnetContractError { provided: Uint128, range: OperatingCostRange, }, + + #[error( + "currently it's not possible to migrate nodes bonded with vesting tokens into a nym-node. please perform vesting->liquid migration first." + )] + VestingNodeMigration, + + #[error("value {got} does not correspond to any known node role")] + UnknownRoleRepresentation { got: u8 }, + + #[error("the total work for this epoch seems to be bigger than 1.0!")] + TotalWorkAboveOne, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index 7ebb604225..e0b7f5f506 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -1,11 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::MixnetContractError; use crate::gateway::GatewayConfigUpdate; -use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate}; +use crate::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use crate::nym_node::Role; +use crate::reward_params::{ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate}; use crate::rewarding::RewardDistribution; -use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId}; +use crate::{BlockHeight, ContractStateParams, EpochId, IdentityKeyRef, Interval, NodeId}; pub use contracts_common::events::*; use cosmwasm_std::{Addr, Coin, Decimal, Event}; use std::fmt::Display; @@ -14,6 +16,11 @@ pub const EVENT_VERSION_PREFIX: &str = "v2_"; pub enum MixnetEventType { MixnodeBonding, + NymNodeBonding, + NymNodeUnbonding, + PendingNymNodeUnbonding, + GatewayMigration, + MixnodeMigration, PendingPledgeIncrease, PledgeIncrease, PendingPledgeDecrease, @@ -23,9 +30,9 @@ pub enum MixnetEventType { PendingMixnodeUnbonding, MixnodeUnbonding, MixnodeConfigUpdate, - PendingMixnodeCostParamsUpdate, - MixnodeCostParamsUpdate, - MixnodeRewarding, + PendingCostParamsUpdate, + CostParamsUpdate, + NodeRewarding, WithdrawDelegatorReward, WithdrawOperatorReward, PendingActiveSetUpdate, @@ -41,6 +48,7 @@ pub enum MixnetEventType { RewardingValidatorUpdate, BeginEpochTransition, AdvanceEpoch, + RoleAssignment, ExecutePendingEpochEvents, ExecutePendingIntervalEvents, ReconcilePendingEvents, @@ -59,6 +67,11 @@ impl Display for MixnetEventType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let event_name = match self { MixnetEventType::MixnodeBonding => "mixnode_bonding", + MixnetEventType::NymNodeBonding => "nymnode_bonding", + MixnetEventType::NymNodeUnbonding => "nymnode_unbonding", + MixnetEventType::PendingNymNodeUnbonding => "pending_nymnode_unbonding", + MixnetEventType::GatewayMigration => "gateway_migration", + MixnetEventType::MixnodeMigration => "mixnode_migration", MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase", MixnetEventType::PledgeIncrease => "pledge_increase", MixnetEventType::PendingPledgeDecrease => "pending_pledge_decrease", @@ -68,9 +81,9 @@ impl Display for MixnetEventType { MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", MixnetEventType::MixnodeConfigUpdate => "mixnode_config_update", MixnetEventType::MixnodeUnbonding => "mixnode_unbonding", - MixnetEventType::PendingMixnodeCostParamsUpdate => "pending_mixnode_cost_params_update", - MixnetEventType::MixnodeCostParamsUpdate => "mixnode_cost_params_update", - MixnetEventType::MixnodeRewarding => "mix_rewarding", + MixnetEventType::PendingCostParamsUpdate => "pending_cost_params_update", + MixnetEventType::CostParamsUpdate => "cost_params_update", + MixnetEventType::NodeRewarding => "node_rewarding", MixnetEventType::WithdrawDelegatorReward => "withdraw_delegator_reward", MixnetEventType::WithdrawOperatorReward => "withdraw_operator_reward", MixnetEventType::PendingActiveSetUpdate => "pending_active_set_update", @@ -87,6 +100,7 @@ impl Display for MixnetEventType { MixnetEventType::RewardingValidatorUpdate => "rewarding_validator_address_update", MixnetEventType::BeginEpochTransition => "beginning_epoch_transition", MixnetEventType::AdvanceEpoch => "advance_epoch", + MixnetEventType::RoleAssignment => "role_assignment", MixnetEventType::ExecutePendingEpochEvents => "execute_pending_epoch_events", MixnetEventType::ExecutePendingIntervalEvents => "execute_pending_interval_events", MixnetEventType::ReconcilePendingEvents => "reconcile_pending_events", @@ -103,6 +117,7 @@ impl Display for MixnetEventType { // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; pub const AMOUNT_KEY: &str = "amount"; +pub const ERROR_MESSAGE_KEY: &str = "error_message"; // event-specific attributes @@ -113,16 +128,14 @@ pub const UNIT_REWARD_KEY: &str = "unit_reward"; // bonding/unbonding pub const MIX_ID_KEY: &str = "mix_id"; +pub const NODE_ID_KEY: &str = "node_id"; pub const NODE_IDENTITY_KEY: &str = "identity"; -pub const ASSIGNED_LAYER_KEY: &str = "assigned_layer"; // settings change -pub const OLD_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "old_minimum_mixnode_pledge"; -pub const OLD_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "old_minimum_gateway_pledge"; +pub const OLD_MINIMUM_PLEDGE_KEY: &str = "old_minimum_pledge"; pub const OLD_MINIMUM_DELEGATION_KEY: &str = "old_minimum_delegation"; -pub const NEW_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "new_minimum_mixnode_pledge"; -pub const NEW_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "new_minimum_gateway_pledge"; +pub const NEW_MINIMUM_PLEDGE_KEY: &str = "new_minimum_pledge"; pub const NEW_MINIMUM_DELEGATION_KEY: &str = "new_minimum_delegation"; pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address"; @@ -147,11 +160,16 @@ pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance"; // rewarded set update -pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size"; +pub const NUM_MIXNODES_KEY: &str = "num_mixnodes"; +pub const NUM_ENTRIES_KEY: &str = "num_entry_gateways"; +pub const NUM_EXITS_KEY: &str = "num_exit_gateways"; pub const CURRENT_EPOCH_KEY: &str = "current_epoch"; pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch"; +pub const ROLE_KEY: &str = "role"; +pub const NODE_COUNT_KEY: &str = "node_count"; + // interval pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed"; pub const EVENT_CREATION_HEIGHT_KEY: &str = "created_at"; @@ -163,7 +181,7 @@ pub fn new_delegation_event( created_at: BlockHeight, delegator: &Addr, amount: &Coin, - mix_id: MixId, + mix_id: NodeId, unit_reward: Decimal, ) -> Event { Event::new(MixnetEventType::Delegation) @@ -174,45 +192,57 @@ pub fn new_delegation_event( .add_attribute(UNIT_REWARD_KEY, unit_reward.to_string()) } -pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::Delegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: MixId) -> Event { +pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: NodeId) -> Event { Event::new(MixnetEventType::PendingDelegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: MixId) -> Event { +pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: NodeId) -> Event { Event::new(MixnetEventType::WithdrawOperatorReward) .add_attribute(OWNER_KEY, owner.as_str()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_withdraw_delegator_reward_event(delegator: &Addr, amount: Coin, mix_id: MixId) -> Event { +pub fn new_withdraw_delegator_reward_event( + delegator: &Addr, + amount: Coin, + mix_id: NodeId, +) -> Event { Event::new(MixnetEventType::WithdrawDelegatorReward) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_active_set_update_event(created_at: BlockHeight, new_size: u32) -> Event { +pub fn new_active_set_update_failure(err: MixnetContractError) -> Event { + Event::new(MixnetEventType::ActiveSetUpdate).add_attribute(ERROR_MESSAGE_KEY, err.to_string()) +} + +pub fn new_active_set_update_event(created_at: BlockHeight, update: ActiveSetUpdate) -> Event { Event::new(MixnetEventType::ActiveSetUpdate) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute(NUM_MIXNODES_KEY, update.mixnodes.to_string()) + .add_attribute(NUM_ENTRIES_KEY, update.entry_gateways.to_string()) + .add_attribute(NUM_EXITS_KEY, update.exit_gateways.to_string()) } pub fn new_pending_active_set_update_event( - new_size: u32, + update: ActiveSetUpdate, approximate_time_remaining_secs: i64, ) -> Event { Event::new(MixnetEventType::PendingActiveSetUpdate) - .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute(NUM_MIXNODES_KEY, update.mixnodes.to_string()) + .add_attribute(NUM_ENTRIES_KEY, update.entry_gateways.to_string()) + .add_attribute(NUM_EXITS_KEY, update.exit_gateways.to_string()) .add_attribute( APPROXIMATE_TIME_LEFT_SECS_KEY, approximate_time_remaining_secs.to_string(), @@ -221,7 +251,6 @@ pub fn new_pending_active_set_update_event( pub fn new_rewarding_params_update_event( created_at: BlockHeight, - update: IntervalRewardingParamsUpdate, updated: IntervalRewardParams, ) -> Event { @@ -252,30 +281,19 @@ pub fn new_pending_rewarding_params_update_event( ) } -pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::Undelegation) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::PendingUndelegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_gateway_bonding_event( - owner: &Addr, - amount: &Coin, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(MixnetEventType::GatewayBonding) - .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(AMOUNT_KEY, amount.to_string()) -} - pub fn new_gateway_unbonding_event( owner: &Addr, amount: &Coin, @@ -287,49 +305,85 @@ pub fn new_gateway_unbonding_event( .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_bonding_event( +pub fn new_nym_node_bonding_event( owner: &Addr, amount: &Coin, identity: IdentityKeyRef<'_>, - mix_id: MixId, - assigned_layer: Layer, + node_id: NodeId, ) -> Event { - // coin implements Display trait and we use that implementation here - Event::new(MixnetEventType::MixnodeBonding) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + Event::new(MixnetEventType::NymNodeBonding) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_attribute(ASSIGNED_LAYER_KEY, assigned_layer) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pending_pledge_increase_event(mix_id: MixId, amount: &Coin) -> Event { +pub fn new_nym_node_unbonding_event(created_at: BlockHeight, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NymNodeUnbonding) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) +} + +pub fn new_pending_nym_node_unbonding_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::PendingNymNodeUnbonding) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_migrated_gateway_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::GatewayMigration) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_migrated_mixnode_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::MixnodeMigration) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_pending_pledge_increase_event(node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PendingPledgeIncrease) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pledge_increase_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pledge_increase_event(created_at: BlockHeight, node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PledgeIncrease) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pending_pledge_decrease_event(mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pending_pledge_decrease_event(node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PendingPledgeDecrease) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pledge_decrease_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pledge_decrease_event(created_at: BlockHeight, node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PledgeDecrease) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event { +pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: NodeId) -> Event { Event::new(MixnetEventType::MixnodeUnbonding) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) @@ -338,7 +392,7 @@ pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Ev pub fn new_pending_mixnode_unbonding_event( owner: &Addr, identity: IdentityKeyRef<'_>, - mix_id: MixId, + mix_id: NodeId, ) -> Event { Event::new(MixnetEventType::PendingMixnodeUnbonding) .add_attribute(MIX_ID_KEY, mix_id.to_string()) @@ -347,7 +401,7 @@ pub fn new_pending_mixnode_unbonding_event( } pub fn new_mixnode_config_update_event( - mix_id: MixId, + mix_id: NodeId, owner: &Addr, update: &MixNodeConfigUpdate, ) -> Event { @@ -363,23 +417,18 @@ pub fn new_gateway_config_update_event(owner: &Addr, update: &GatewayConfigUpdat .add_attribute(UPDATED_GATEWAY_CONFIG_KEY, update.to_inline_json()) } -pub fn new_mixnode_pending_cost_params_update_event( - mix_id: MixId, - owner: &Addr, - new_costs: &MixNodeCostParams, -) -> Event { - Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) - .add_attribute(OWNER_KEY, owner) +pub fn new_pending_cost_params_update_event(mix_id: NodeId, new_costs: &NodeCostParams) -> Event { + Event::new(MixnetEventType::PendingCostParamsUpdate) + .add_attribute(NODE_ID_KEY, mix_id.to_string()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) } -pub fn new_mixnode_cost_params_update_event( +pub fn new_cost_params_update_event( created_at: BlockHeight, - mix_id: MixId, - new_costs: &MixNodeCostParams, + mix_id: NodeId, + new_costs: &NodeCostParams, ) -> Event { - Event::new(MixnetEventType::MixnodeCostParamsUpdate) + Event::new(MixnetEventType::CostParamsUpdate) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) @@ -397,37 +446,25 @@ pub fn new_settings_update_event( ) -> Event { let mut event = Event::new(MixnetEventType::ContractSettingsUpdate); - if old_params.minimum_mixnode_pledge != new_params.minimum_mixnode_pledge { + if old_params.minimum_pledge != new_params.minimum_pledge { event = event .add_attribute( - OLD_MINIMUM_MIXNODE_PLEDGE_KEY, - old_params.minimum_mixnode_pledge.to_string(), + OLD_MINIMUM_PLEDGE_KEY, + old_params.minimum_pledge.to_string(), ) .add_attribute( - NEW_MINIMUM_MIXNODE_PLEDGE_KEY, - new_params.minimum_mixnode_pledge.to_string(), + NEW_MINIMUM_PLEDGE_KEY, + new_params.minimum_pledge.to_string(), ) } - if old_params.minimum_gateway_pledge != new_params.minimum_gateway_pledge { - event = event - .add_attribute( - OLD_MINIMUM_GATEWAY_PLEDGE_KEY, - old_params.minimum_gateway_pledge.to_string(), - ) - .add_attribute( - NEW_MINIMUM_GATEWAY_PLEDGE_KEY, - new_params.minimum_gateway_pledge.to_string(), - ) - } - - if old_params.minimum_mixnode_delegation != new_params.minimum_mixnode_delegation { - if let Some(ref old) = old_params.minimum_mixnode_delegation { + if old_params.minimum_delegation != new_params.minimum_delegation { + if let Some(ref old) = old_params.minimum_delegation { event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, old.to_string()) } else { event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, "None") } - if let Some(ref new) = new_params.minimum_mixnode_delegation { + if let Some(ref new) = new_params.minimum_delegation { event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, new.to_string()) } else { event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, "None") @@ -437,41 +474,41 @@ pub fn new_settings_update_event( event } -pub fn new_not_found_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) +pub fn new_not_found_node_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) } -pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) +pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_VALUE) } pub fn new_mix_rewarding_event( interval: Interval, - mix_id: MixId, + node_id: NodeId, reward_distribution: RewardDistribution, prior_delegates: Decimal, prior_unit_reward: Decimal, ) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) .add_attribute(PRIOR_DELEGATES_KEY, prior_delegates.to_string()) .add_attribute(PRIOR_UNIT_REWARD_KEY, prior_unit_reward.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute( OPERATOR_REWARD_KEY, reward_distribution.operator.to_string(), @@ -489,13 +526,15 @@ pub fn new_epoch_transition_start_event(current_interval: Interval) -> Event { ) } -pub fn new_advance_epoch_event(interval: Interval, rewarded_nodes: u32) -> Event { +pub fn new_assigned_role_event(role: Role, nodes: u32) -> Event { + Event::new(MixnetEventType::RoleAssignment) + .add_attribute(ROLE_KEY, role.to_string()) + .add_attribute(NODE_COUNT_KEY, nodes.to_string()) +} + +pub fn new_advance_epoch_event(epoch_id: EpochId) -> Event { Event::new(MixnetEventType::AdvanceEpoch) - .add_attribute( - NEW_CURRENT_EPOCH_KEY, - interval.current_epoch_absolute_id().to_string(), - ) - .add_attribute(REWARDED_SET_NODES_KEY, rewarded_nodes.to_string()) + .add_attribute(NEW_CURRENT_EPOCH_KEY, epoch_id.to_string()) } pub fn new_pending_epoch_events_execution_event(executed: u32) -> Event { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs deleted file mode 100644 index 58f74f4e08..0000000000 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{IdentityKey, IdentityKeyRef}; -use cosmwasm_schema::cw_serde; -use schemars::JsonSchema; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -/// A group of mixnodes associated with particular staking entity. -/// When defined all nodes belonging to the same family will be prioritised to be put onto the same layer. -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeFamily.ts") -)] -#[cw_serde] -pub struct Family { - /// Owner of this family. - head: FamilyHead, - - /// Optional proxy (i.e. vesting contract address) used when creating the family. - proxy: Option, - - /// Human readable label for this family. - label: String, -} - -/// Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58). -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeFamilyHead.ts") -)] -#[derive(Debug, Clone, Eq, PartialEq, JsonSchema)] -pub struct FamilyHead(IdentityKey); - -impl Serialize for FamilyHead { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.0.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for FamilyHead { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let inner = IdentityKey::deserialize(deserializer)?; - Ok(FamilyHead(inner)) - } -} - -impl FromStr for FamilyHead { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - // theoretically we should be verifying whether it's a valid base58 value - // (or even better, whether it's a valid ed25519 public key), but definition of - // `FamilyHead` might change later - Ok(FamilyHead(IdentityKey::from_str(s)?)) - } -} - -impl Display for FamilyHead { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl FamilyHead { - pub fn new>(identity: S) -> Self { - FamilyHead(identity.into()) - } - - pub fn identity(&self) -> IdentityKeyRef<'_> { - &self.0 - } -} - -impl Family { - pub fn new(head: FamilyHead, label: String) -> Self { - Family { - head, - proxy: None, - label, - } - } - - #[allow(dead_code)] - pub fn head(&self) -> &FamilyHead { - &self.head - } - - pub fn head_identity(&self) -> IdentityKeyRef<'_> { - self.head.identity() - } - - #[allow(dead_code)] - pub fn proxy(&self) -> Option<&String> { - self.proxy.as_ref() - } - - pub fn label(&self) -> &str { - &self.label - } -} - -/// Response containing paged list of all families registered in the contract. -#[cw_serde] -pub struct PagedFamiliesResponse { - /// The families registered in the contract. - pub families: Vec, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} - -/// Response containing paged list of all family members (of ALL families) registered in the contract. -#[cw_serde] -pub struct PagedMembersResponse { - /// The members alongside their family heads. - pub members: Vec<(IdentityKey, FamilyHead)>, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} - -/// Response containing family information. -#[cw_serde] -pub struct FamilyByHeadResponse { - /// The family head used for the query. - pub head: FamilyHead, - - /// If applicable, the family associated with the provided head. - pub family: Option, -} - -/// Response containing family information. -#[cw_serde] -pub struct FamilyByLabelResponse { - /// The family label used for the query. - pub label: String, - - /// If applicable, the family associated with the provided label. - pub family: Option, -} - -/// Response containing family members information. -#[cw_serde] -pub struct FamilyMembersByHeadResponse { - /// The family head used for the query. - pub head: FamilyHead, - - /// All members belonging to the specified family. - pub members: Vec, -} - -/// Response containing family members information. -#[cw_serde] -pub struct FamilyMembersByLabelResponse { - /// The family label used for the query. - pub label: String, - - /// All members belonging to the specified family. - pub members: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn family_head_serde() { - let dummy = FamilyHead::new("foomp"); - - let ser_str = serde_json_wasm::to_string(&dummy).unwrap(); - let de_str: FamilyHead = serde_json_wasm::from_str(&ser_str).unwrap(); - assert_eq!(dummy, de_str); - - let ser_bytes = serde_json_wasm::to_vec(&dummy).unwrap(); - let de_bytes: FamilyHead = serde_json_wasm::from_slice(&ser_bytes).unwrap(); - assert_eq!(dummy, de_bytes); - } -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index 9d75b66c22..f819bedb50 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{IdentityKey, SphinxKey}; +use crate::{IdentityKey, NodeId, SphinxKey}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; use std::cmp::Ordering; @@ -200,6 +200,23 @@ pub struct GatewayBondResponse { pub gateway: Option, } +#[cw_serde] +pub struct PreassignedId { + /// The identity key (base58-encoded ed25519 public key) of the gateway. + pub identity: IdentityKey, + + /// The id pre-assigned to this gateway + pub node_id: NodeId, +} + +#[cw_serde] +pub struct PreassignedGatewayIdsResponse { + pub ids: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs index fc0a05892b..31e8e820d4 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -1,7 +1,14 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; +use crate::error::MixnetContractError; +use crate::mixnode::PendingMixNodeChanges; +use crate::{ + EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond, + NymNodeDetails, PendingNodeChanges, +}; +use contracts_common::IdentityKeyRef; +use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option) { let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128)); @@ -31,3 +38,158 @@ where }) } } + +pub trait NodeDetails { + type Bond: NodeBond; + type PendingChanges: PendingChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges); + fn rewarding_info(&self) -> &NodeRewarding; + fn bond_info(&self) -> &Self::Bond; + fn pending_changes(&self) -> &Self::PendingChanges; +} + +pub trait NodeBond { + fn node_id(&self) -> NodeId; + + fn is_unbonding(&self) -> bool; + + fn identity(&self) -> IdentityKeyRef; + + fn original_pledge(&self) -> &Coin; + + fn ensure_bonded(&self) -> Result<(), MixnetContractError> { + if self.is_unbonding() { + return Err(MixnetContractError::NodeIsUnbonding { + node_id: self.node_id(), + }); + } + Ok(()) + } +} + +pub trait PendingChanges { + fn pending_pledge_changes(&self) -> Option; + + fn pending_cost_params_changes(&self) -> Option; + + fn ensure_no_pending_pledge_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pending_pledge_changes() { + return Err(MixnetContractError::PendingPledgeChange { pending_event_id }); + } + Ok(()) + } + + fn ensure_no_pending_params_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pending_cost_params_changes() { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) + } +} + +impl NodeDetails for MixNodeDetails { + type Bond = MixNodeBond; + type PendingChanges = PendingMixNodeChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges) { + ( + self.bond_information, + self.rewarding_details, + self.pending_changes, + ) + } + + fn rewarding_info(&self) -> &NodeRewarding { + &self.rewarding_details + } + + fn bond_info(&self) -> &Self::Bond { + &self.bond_information + } + + fn pending_changes(&self) -> &Self::PendingChanges { + &self.pending_changes + } +} + +impl NodeBond for MixNodeBond { + fn node_id(&self) -> NodeId { + self.mix_id + } + + fn is_unbonding(&self) -> bool { + self.is_unbonding + } + + fn identity(&self) -> IdentityKeyRef { + self.identity() + } + + fn original_pledge(&self) -> &Coin { + self.original_pledge() + } +} + +impl PendingChanges for PendingMixNodeChanges { + fn pending_pledge_changes(&self) -> Option { + self.pledge_change + } + + fn pending_cost_params_changes(&self) -> Option { + self.cost_params_change + } +} + +impl NodeDetails for NymNodeDetails { + type Bond = NymNodeBond; + type PendingChanges = PendingNodeChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges) { + ( + self.bond_information, + self.rewarding_details, + self.pending_changes, + ) + } + + fn rewarding_info(&self) -> &NodeRewarding { + &self.rewarding_details + } + + fn bond_info(&self) -> &Self::Bond { + &self.bond_information + } + + fn pending_changes(&self) -> &Self::PendingChanges { + &self.pending_changes + } +} + +impl NodeBond for NymNodeBond { + fn node_id(&self) -> NodeId { + self.node_id + } + + fn is_unbonding(&self) -> bool { + self.is_unbonding + } + + fn identity(&self) -> IdentityKeyRef { + self.identity() + } + + fn original_pledge(&self) -> &Coin { + &self.original_pledge + } +} + +impl PendingChanges for PendingNodeChanges { + fn pending_pledge_changes(&self) -> Option { + self.pledge_change + } + + fn pending_cost_params_changes(&self) -> Option { + self.cost_params_change + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index d5382a29e6..067a3aa50e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::MixnetContractError; -use crate::MixId; +use crate::nym_node::Role; +use crate::NodeId; use cosmwasm_schema::cw_serde; use cosmwasm_schema::schemars::gen::SchemaGenerator; use cosmwasm_schema::schemars::schema::{InstanceType, Schema, SchemaObject}; @@ -86,7 +87,7 @@ impl EpochStatus { pub fn update_last_rewarded( &mut self, - new_last_rewarded: MixId, + new_last_rewarded: NodeId, ) -> Result { match &mut self.state { EpochState::Rewarding { @@ -109,7 +110,7 @@ impl EpochStatus { } } - pub fn last_rewarded(&self) -> Result { + pub fn last_rewarded(&self) -> Result { match self.state { EpochState::Rewarding { last_rewarded, .. } => Ok(last_rewarded), state => Err(MixnetContractError::UnexpectedNonRewardingEpochState { @@ -127,12 +128,23 @@ impl EpochStatus { Ok(()) } - pub fn ensure_is_in_advancement_state(&self) -> Result<(), MixnetContractError> { - if !matches!(self.state, EpochState::AdvancingEpoch) { - return Err(MixnetContractError::EpochNotInAdvancementState { + pub fn ensure_is_in_expected_role_assignment_state( + &self, + caller: Role, + ) -> Result<(), MixnetContractError> { + let EpochState::RoleAssignment { next } = self.state else { + return Err(MixnetContractError::EpochNotInRoleAssignmentState { current_state: self.state, }); + }; + + if caller != next { + return Err(MixnetContractError::UnexpectedRoleAssignment { + expected: next, + got: caller, + }); } + Ok(()) } @@ -147,10 +159,6 @@ impl EpochStatus { pub fn is_reconciling(&self) -> bool { matches!(self.state, EpochState::ReconcilingEvents) } - - pub fn is_advancing(&self) -> bool { - matches!(self.state, EpochState::AdvancingEpoch) - } } /// The state of the current rewarding epoch. @@ -167,10 +175,10 @@ pub enum EpochState { #[serde(alias = "Rewarding")] Rewarding { /// The id of the last node that has already received its rewards. - last_rewarded: MixId, + last_rewarded: NodeId, /// The id of the last node that's going to be rewarded before progressing into the next state. - final_node_id: MixId, + final_node_id: NodeId, // total_rewarded: u32, }, @@ -179,10 +187,9 @@ pub enum EpochState { #[serde(alias = "ReconcilingEvents")] ReconcilingEvents, - /// Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, - /// all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set. - #[serde(alias = "AdvancingEpoch")] - AdvancingEpoch, + /// Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, + /// all issued actions got resolved and node roles should now be assigned before advancing into the next epoch. + RoleAssignment { next: Role }, } impl Display for EpochState { @@ -197,7 +204,9 @@ impl Display for EpochState { "mix rewarding (last rewarded: {last_rewarded}, final node: {final_node_id})" ), EpochState::ReconcilingEvents => write!(f, "event reconciliation"), - EpochState::AdvancingEpoch => write!(f, "advancing epoch"), + EpochState::RoleAssignment { next } => { + write!(f, "role assignment with next assignment for: {next}") + } } } } @@ -359,6 +368,17 @@ impl Interval { self.current_epoch_end_unix_timestamp() <= env.block.time.seconds() as i64 } + pub fn ensure_current_epoch_is_over(&self, env: &Env) -> Result<(), MixnetContractError> { + if !self.is_current_epoch_over(&env) { + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: self.current_epoch_start_unix_timestamp(), + epoch_end: self.current_epoch_end_unix_timestamp(), + }); + } + Ok(()) + } + pub fn secs_until_current_epoch_end(&self, env: &Env) -> i64 { if self.is_current_epoch_over(env) { 0 diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 50a5e90aec..fca9f6bfcf 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -3,32 +3,30 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] -mod constants; +pub mod constants; pub mod delegation; pub mod error; pub mod events; -pub mod families; pub mod gateway; pub mod helpers; pub mod interval; pub mod mixnode; pub mod msg; +pub mod nym_node; pub mod pending_events; pub mod reward_params; pub mod rewarding; pub mod signing_types; pub mod types; +pub use constants::*; pub use contracts_common::types::*; pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction}; pub use delegation::{ Delegation, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, -}; -pub use families::{ - Family, FamilyByHeadResponse, FamilyByLabelResponse, FamilyHead, FamilyMembersByHeadResponse, - FamilyMembersByLabelResponse, PagedFamiliesResponse, PagedMembersResponse, + PagedNodeDelegationsResponse, }; pub use gateway::{ Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse, @@ -38,11 +36,12 @@ pub use interval::{ CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId, }; pub use mixnode::{ - Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails, - MixNodeRewarding, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, PagedMixnodeBondsResponse, RewardedSetNodeStatus, UnbondedMixnode, + LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails, + MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams, + NodeRewarding, PagedMixnodeBondsResponse, UnbondedMixnode, }; pub use msg::*; +pub use nym_node::{NymNode, NymNodeBond, NymNodeDetails, PendingNodeChanges}; pub use pending_events::{ EpochEventId, IntervalEventId, NumberOfPendingEventsResponse, PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingEpochEventResponse, @@ -50,8 +49,6 @@ pub use pending_events::{ PendingIntervalEventKind, PendingIntervalEventResponse, PendingIntervalEventsResponse, }; pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams}; -pub use rewarding::{ - EstimatedCurrentEpochRewardResponse, PagedRewardedSetResponse, PendingRewardResponse, -}; +pub use rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}; pub use signing_types::*; pub use types::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 8772763150..779df2e125 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -7,42 +7,18 @@ use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE}; use crate::error::MixnetContractError; use crate::helpers::IntoBaseDecimal; -use crate::reward_params::{NodeRewardParams, RewardingParams}; +use crate::reward_params::{NodeRewardingParameters, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; use crate::{ - Delegation, EpochEventId, EpochId, IdentityKey, MixId, OperatingCostRange, Percent, - ProfitMarginRange, SphinxKey, + Delegation, EpochEventId, EpochId, IdentityKey, IntervalEventId, NodeId, OperatingCostRange, + Percent, ProfitMarginRange, SphinxKey, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; use serde_repr::{Deserialize_repr, Serialize_repr}; -/// Current state of given node in the rewarded set. -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardedSetNodeStatus.ts") -)] -#[cw_serde] -#[derive(Copy)] -pub enum RewardedSetNodeStatus { - /// Node that is currently active, i.e. is expected to be used by clients for mixing packets. - #[serde(alias = "Active")] - Active, - - /// Node that is currently in standby, i.e. it's present in the rewarded set but is not active. - #[serde(alias = "Standby")] - Standby, -} - -impl RewardedSetNodeStatus { - pub fn is_active(&self) -> bool { - matches!(self, RewardedSetNodeStatus::Active) - } -} - /// Full details associated with given mixnode. #[cw_serde] pub struct MixNodeDetails { @@ -50,7 +26,7 @@ pub struct MixNodeDetails { pub bond_information: MixNodeBond, /// Details used for computation of rewarding related data. - pub rewarding_details: MixNodeRewarding, + pub rewarding_details: NodeRewarding, /// Adjustments to the mixnode that are ought to happen during future epoch transitions. #[serde(default)] @@ -60,7 +36,7 @@ pub struct MixNodeDetails { impl MixNodeDetails { pub fn new( bond_information: MixNodeBond, - rewarding_details: MixNodeRewarding, + rewarding_details: NodeRewarding, pending_changes: PendingMixNodeChanges, ) -> Self { MixNodeDetails { @@ -70,14 +46,10 @@ impl MixNodeDetails { } } - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.bond_information.mix_id } - pub fn layer(&self) -> Layer { - self.bond_information.layer - } - pub fn is_unbonding(&self) -> bool { self.bond_information.is_unbonding } @@ -106,10 +78,11 @@ impl MixNodeDetails { } } +// currently this struct is shared between mixnodes and nymnodes #[cw_serde] -pub struct MixNodeRewarding { +pub struct NodeRewarding { /// Information provided by the operator that influence the cost function. - pub cost_params: MixNodeCostParams, + pub cost_params: NodeCostParams, /// Total pledge and compounded reward earned by the node operator. pub operator: Decimal, @@ -120,7 +93,7 @@ pub struct MixNodeRewarding { /// Cumulative reward earned by the "unit delegation" since the block 0. pub total_unit_reward: Decimal, - /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + /// Value of the theoretical "unit delegation" that has delegated to this node at block 0. pub unit_delegation: Decimal, /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt @@ -133,9 +106,9 @@ pub struct MixNodeRewarding { pub unique_delegations: u32, } -impl MixNodeRewarding { +impl NodeRewarding { pub fn initialise_new( - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, initial_pledge: &Coin, current_epoch: EpochId, ) -> Result { @@ -144,7 +117,7 @@ impl MixNodeRewarding { "pledge cannot be larger than the token supply" ); - Ok(MixNodeRewarding { + Ok(NodeRewarding { cost_params, operator: initial_pledge.amount.into_base_decimal()?, delegates: Decimal::zero(), @@ -155,6 +128,15 @@ impl MixNodeRewarding { }) } + pub fn normalise_cost_function( + &mut self, + allowed_profit_margin: ProfitMarginRange, + allowed_operating_cost: OperatingCostRange, + ) { + self.normalise_profit_margin(allowed_profit_margin); + self.normalise_operating_cost(allowed_operating_cost) + } + pub fn normalise_profit_margin(&mut self, allowed_range: ProfitMarginRange) { self.cost_params.profit_margin_percent = allowed_range.normalise(self.cost_params.profit_margin_percent) @@ -257,23 +239,18 @@ impl MixNodeRewarding { pub fn node_reward( &self, - reward_params: &RewardingParams, - node_params: NodeRewardParams, + global_params: &RewardingParams, + node_params: NodeRewardingParameters, ) -> Decimal { - let work = if node_params.in_active_set { - reward_params.active_node_work() - } else { - reward_params.standby_node_work() - }; + let work = node_params.work_factor; + let alpha = global_params.interval.sybil_resistance; - let alpha = reward_params.interval.sybil_resistance; - - reward_params.interval.epoch_reward_budget - * node_params.performance.value() - * self.bond_saturation(reward_params) + global_params.interval.epoch_reward_budget + * node_params.performance + * self.bond_saturation(global_params) * (work - + alpha.value() * self.pledge_saturation(reward_params) - / reward_params.dec_rewarded_set_size()) + + alpha.value() * self.pledge_saturation(global_params) + / global_params.dec_rewarded_set_size()) / (Decimal::one() + alpha.value()) } @@ -285,7 +262,7 @@ impl MixNodeRewarding { epochs_in_interval: u32, ) -> RewardDistribution { let node_cost = - self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance.value(); + self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance; // check if profit is positive if node_reward > node_cost { @@ -315,7 +292,7 @@ impl MixNodeRewarding { pub fn calculate_epoch_reward( &self, reward_params: &RewardingParams, - node_params: NodeRewardParams, + node_params: NodeRewardingParameters, epochs_in_interval: u32, ) -> RewardDistribution { let node_reward = self.node_reward(reward_params, node_params); @@ -341,7 +318,7 @@ impl MixNodeRewarding { pub fn epoch_rewarding( &mut self, reward_params: &RewardingParams, - node_params: NodeRewardParams, + node_params: NodeRewardingParameters, epochs_in_interval: u32, absolute_epoch_id: EpochId, ) { @@ -492,13 +469,30 @@ impl MixNodeRewarding { amount / self.delegates } } + + /// Returns a copy of `Self` with zeroed operator value + pub fn clear_operator(&self) -> NodeRewarding { + let mut zeroed = self.clone(); + zeroed.operator = Decimal::zero(); + zeroed + } } /// Basic mixnode information provided by the node operator. -#[cw_serde] +// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want +// with the removal of explicit .layer field +#[derive( + ::cosmwasm_schema::serde::Serialize, + ::cosmwasm_schema::serde::Deserialize, + ::std::clone::Clone, + ::std::fmt::Debug, + ::std::cmp::PartialEq, + ::cosmwasm_schema::schemars::JsonSchema, +)] +#[schemars(crate = "::cosmwasm_schema::schemars")] pub struct MixNodeBond { /// Unique id assigned to the bonded mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// Address of the owner of this mixnode. pub owner: Addr, @@ -506,9 +500,9 @@ pub struct MixNodeBond { /// Original amount pledged by the operator of this node. pub original_pledge: Coin, - /// Layer assigned to this mixnode. - pub layer: Layer, - + // REMOVED (but might be needed due to legacy things, idk yet) + // /// Layer assigned to this mixnode. + // pub layer: Layer, /// Information provided by the operator for the purposes of bonding. pub mix_node: MixNode, @@ -525,26 +519,6 @@ pub struct MixNodeBond { } impl MixNodeBond { - pub fn new( - mix_id: MixId, - owner: Addr, - original_pledge: Coin, - layer: Layer, - mix_node: MixNode, - bonding_height: u64, - ) -> Self { - MixNodeBond { - mix_id, - owner, - original_pledge, - layer, - mix_node, - proxy: None, - bonding_height, - is_unbonding: false, - } - } - pub fn identity(&self) -> &str { &self.mix_node.identity_key } @@ -595,21 +569,21 @@ pub struct MixNode { /// The cost parameters, or the cost function, defined for the particular mixnode that influences /// how the rewards should be split between the node operator and its delegators. #[cw_serde] -pub struct MixNodeCostParams { - /// The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator. +pub struct NodeCostParams { + /// The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator. pub profit_margin_percent: Percent, - /// Operating cost of the associated mixnode per the entire interval. + /// Operating cost of the associated node per the entire interval. pub interval_operating_cost: Coin, } -impl MixNodeCostParams { +impl NodeCostParams { pub fn to_inline_json(&self) -> String { serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } -impl MixNodeCostParams { +impl NodeCostParams { pub fn epoch_operating_cost(&self, epochs_in_interval: u32) -> Decimal { Decimal::from_ratio(self.interval_operating_cost.amount, epochs_in_interval) } @@ -628,38 +602,39 @@ impl MixNodeCostParams { Deserialize_repr, JsonSchema, )] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[repr(u8)] -pub enum Layer { +pub enum LegacyMixLayer { One = 1, Two = 2, Three = 3, } -impl From for String { - fn from(layer: Layer) -> Self { +impl From for String { + fn from(layer: LegacyMixLayer) -> Self { (layer as u8).to_string() } } -impl TryFrom for Layer { +impl TryFrom for LegacyMixLayer { type Error = MixnetContractError; - fn try_from(i: u8) -> Result { + fn try_from(i: u8) -> Result { match i { - 1 => Ok(Layer::One), - 2 => Ok(Layer::Two), - 3 => Ok(Layer::Three), + 1 => Ok(LegacyMixLayer::One), + 2 => Ok(LegacyMixLayer::Two), + 3 => Ok(LegacyMixLayer::Three), _ => Err(MixnetContractError::InvalidLayer(i)), } } } -impl From for u8 { - fn from(layer: Layer) -> u8 { +impl From for u8 { + fn from(layer: LegacyMixLayer) -> u8 { match layer { - Layer::One => 1, - Layer::Two => 2, - Layer::Three => 3, + LegacyMixLayer::One => 1, + LegacyMixLayer::Two => 2, + LegacyMixLayer::Three => 3, } } } @@ -673,13 +648,15 @@ impl From for u8 { #[derive(Default, Copy)] pub struct PendingMixNodeChanges { pub pledge_change: Option, - // pub cost_params_change: Option, + #[serde(default)] + pub cost_params_change: Option, } impl PendingMixNodeChanges { pub fn new_empty() -> PendingMixNodeChanges { PendingMixNodeChanges { pledge_change: None, + cost_params_change: None, } } } @@ -740,11 +717,11 @@ pub struct PagedMixnodeBondsResponse { pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodeBondsResponse { - pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { + pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { PagedMixnodeBondsResponse { nodes, per_page, @@ -766,14 +743,14 @@ pub struct PagedMixnodesDetailsResponse { pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodesDetailsResponse { pub fn new( nodes: Vec, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedMixnodesDetailsResponse { nodes, @@ -787,21 +764,21 @@ impl PagedMixnodesDetailsResponse { #[cw_serde] pub struct PagedUnbondedMixnodesResponse { /// The past ids of unbonded mixnodes alongside their basic information such as the owner or the identity key. - pub nodes: Vec<(MixId, UnbondedMixnode)>, + pub nodes: Vec<(NodeId, UnbondedMixnode)>, /// Maximum number of entries that could be included in a response. `per_page <= nodes.len()` // this field is rather redundant and should be deprecated. pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedUnbondedMixnodesResponse { pub fn new( - nodes: Vec<(MixId, UnbondedMixnode)>, + nodes: Vec<(NodeId, UnbondedMixnode)>, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedUnbondedMixnodesResponse { nodes, @@ -825,7 +802,7 @@ pub struct MixOwnershipResponse { #[cw_serde] pub struct MixnodeDetailsResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there exists a mixnode with the provided id, this field contains its detailed information. pub mixnode_details: Option, @@ -845,17 +822,17 @@ pub struct MixnodeDetailsByIdentityResponse { #[cw_serde] pub struct MixnodeRewardingDetailsResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there exists a mixnode with the provided id, this field contains its rewarding information. - pub rewarding_details: Option, + pub rewarding_details: Option, } /// Response containing basic information of an unbonded mixnode with the provided id. #[cw_serde] pub struct UnbondedMixnodeResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there existed a mixnode with the provided id, this field contains its basic information. pub unbonded_info: Option, @@ -863,9 +840,9 @@ pub struct UnbondedMixnodeResponse { /// Response containing the current state of the stake saturation of a mixnode with the provided id. #[cw_serde] -pub struct StakeSaturationResponse { +pub struct MixStakeSaturationResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// The current stake saturation of this node that is indirectly used in reward calculation formulas. /// Note that it can't be larger than 1. diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 863fdc0327..39a81b93c5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -3,15 +3,17 @@ use crate::delegation::{self, OwnerProxySubKey}; use crate::error::MixnetContractError; -use crate::families::FamilyHead; use crate::gateway::{Gateway, GatewayConfigUpdate}; use crate::helpers::IntoBaseDecimal; -use crate::mixnode::{Layer, MixNode, MixNodeConfigUpdate, MixNodeCostParams}; +use crate::mixnode::{MixNode, MixNodeConfigUpdate, NodeCostParams}; +use crate::nym_node::{NodeConfigUpdate, Role}; use crate::pending_events::{EpochEventId, IntervalEventId}; use crate::reward_params::{ - IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, + ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate, NodeRewardingParameters, + Performance, RewardedSetParams, RewardingParams, WorkFactor, }; -use crate::types::{ContractStateParams, LayerAssignment, MixId}; +use crate::types::{ContractStateParams, NodeId}; +use crate::{NymNode, RoleAssignment}; use crate::{OperatingCostRange, ProfitMarginRange}; use contracts_common::{signing::MessageSignature, IdentityKey, Percent}; use cosmwasm_schema::cw_serde; @@ -21,28 +23,31 @@ use std::time::Duration; #[cfg(feature = "schema")] use crate::{ delegation::{ - MixNodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, + NodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedNodeDelegationsResponse, }, - families::{ - FamilyByHeadResponse, FamilyByLabelResponse, FamilyMembersByHeadResponse, - FamilyMembersByLabelResponse, PagedFamiliesResponse, PagedMembersResponse, + gateway::{ + GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse, + PreassignedGatewayIdsResponse, }, - gateway::{GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse}, interval::{CurrentIntervalResponse, EpochStatus}, mixnode::{ - MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, - MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse, + MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse, + PagedMixnodesDetailsResponse, PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, + }, + nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, }, pending_events::{ NumberOfPendingEventsResponse, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse, }, - rewarding::{ - EstimatedCurrentEpochRewardResponse, PagedRewardedSetResponse, PendingRewardResponse, - }, - types::{ContractState, LayerDistribution}, + rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, + types::ContractState, }; #[cfg(feature = "schema")] use contracts_common::{signing::Nonce, ContractBuildInformation}; @@ -76,8 +81,7 @@ pub struct InitialRewardingParams { pub active_set_work_factor: Decimal, pub interval_pool_emission: Percent, - pub rewarded_set_size: u32, - pub active_set_size: u32, + pub rewarded_set_params: RewardedSetParams, } impl InitialRewardingParams { @@ -88,8 +92,11 @@ impl InitialRewardingParams { let epoch_reward_budget = self.initial_reward_pool / epochs_in_interval.into_base_decimal()? * self.interval_pool_emission; - let stake_saturation_point = - self.initial_staking_supply / self.rewarded_set_size.into_base_decimal()?; + let stake_saturation_point = self.initial_staking_supply + / self + .rewarded_set_params + .rewarded_set_size() + .into_base_decimal()?; Ok(RewardingParams { interval: IntervalRewardParams { @@ -102,8 +109,7 @@ impl InitialRewardingParams { active_set_work_factor: self.active_set_work_factor, interval_pool_emission: self.interval_pool_emission, }, - rewarded_set_size: self.rewarded_set_size, - active_set_size: self.active_set_size, + rewarded_set: self.rewarded_set_params, }) } } @@ -115,45 +121,6 @@ pub enum ExecuteMsg { admin: String, }, - AssignNodeLayer { - mix_id: MixId, - layer: Layer, - }, - // Families - /// Only owner of the node can crate the family with node as head - CreateFamily { - label: String, - }, - /// Family head needs to sign the joining node IdentityKey - JoinFamily { - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamily { - family_head: FamilyHead, - }, - KickFamilyMember { - member: IdentityKey, - }, - CreateFamilyOnBehalf { - owner_address: String, - label: String, - }, - /// Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family - JoinFamilyOnBehalf { - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamilyOnBehalf { - member_address: String, - family_head: FamilyHead, - }, - KickFamilyMemberOnBehalf { - head_address: String, - member: IdentityKey, - }, - // state/sys-params-related UpdateRewardingValidatorAddress { address: String, @@ -161,8 +128,8 @@ pub enum ExecuteMsg { UpdateContractStateParams { updated_parameters: ContractStateParams, }, - UpdateActiveSetSize { - active_set_size: u32, + UpdateActiveSetDistribution { + update: ActiveSetUpdate, force_immediately: bool, }, UpdateRewardingParams { @@ -174,25 +141,24 @@ pub enum ExecuteMsg { epoch_duration_secs: u64, force_immediately: bool, }, + BeginEpochTransition {}, - AdvanceCurrentEpoch { - new_rewarded_set: Vec, - // families_in_layer: HashMap, - expected_active_set_size: u32, - }, ReconcileEpochEvents { limit: Option, }, + AssignRoles { + assignment: RoleAssignment, + }, // mixnode-related: BondMixnode { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, }, BondMixnodeOnBehalf { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, owner: String, }, @@ -211,11 +177,15 @@ pub enum ExecuteMsg { UnbondMixnodeOnBehalf { owner: String, }, - UpdateMixnodeCostParams { - new_costs: MixNodeCostParams, + #[serde( + alias = "UpdateMixnodeCostParams", + alias = "update_mixnode_cost_params" + )] + UpdateCostParams { + new_costs: NodeCostParams, }, UpdateMixnodeCostParamsOnBehalf { - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, owner: String, }, UpdateMixnodeConfig { @@ -225,6 +195,7 @@ pub enum ExecuteMsg { new_config: MixNodeConfigUpdate, owner: String, }, + MigrateMixnode {}, // gateway-related: BondGateway { @@ -247,44 +218,64 @@ pub enum ExecuteMsg { new_config: GatewayConfigUpdate, owner: String, }, + MigrateGateway { + cost_params: Option, + }, + + // nym-node related: + BondNymNode { + node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + }, + UnbondNymNode {}, + UpdateNodeConfig { + update: NodeConfigUpdate, + }, // delegation-related: - DelegateToMixnode { - mix_id: MixId, + #[serde(alias = "DelegateToMixnode", alias = "delegate_to_mixnode")] + Delegate { + #[serde(alias = "mix_id")] + node_id: NodeId, }, DelegateToMixnodeOnBehalf { - mix_id: MixId, + mix_id: NodeId, delegate: String, }, - UndelegateFromMixnode { - mix_id: MixId, + #[serde(alias = "UndelegateFromMixnode", alias = "undelegate_from_mixnode")] + Undelegate { + #[serde(alias = "mix_id")] + node_id: NodeId, }, UndelegateFromMixnodeOnBehalf { - mix_id: MixId, + mix_id: NodeId, delegate: String, }, // reward-related - RewardMixnode { - mix_id: MixId, - performance: Performance, + RewardNode { + #[serde(alias = "mix_id")] + node_id: NodeId, + params: NodeRewardingParameters, }, WithdrawOperatorReward {}, WithdrawOperatorRewardOnBehalf { owner: String, }, WithdrawDelegatorReward { - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, }, WithdrawDelegatorRewardOnBehalf { - mix_id: MixId, + mix_id: NodeId, owner: String, }, // vesting migration: MigrateVestedMixNode {}, MigrateVestedDelegation { - mix_id: MixId, + mix_id: NodeId, }, // testing-only @@ -292,47 +283,31 @@ pub enum ExecuteMsg { TestingResolveAllPendingEvents { limit: Option, }, + + // TO BE REMOVED BEFORE MERGING + TestingUncheckedBondLegacyMixnode { + node: MixNode, + }, + TestingUncheckedBondLegacyGateway { + node: Gateway, + }, } +const bad_const: &str = "clippy will yell so that i'd remember to remove those legacy bonding ops"; + impl ExecuteMsg { pub fn default_memo(&self) -> String { match self { ExecuteMsg::UpdateAdmin { admin } => format!("updating contract admin to {admin}"), - ExecuteMsg::AssignNodeLayer { mix_id, layer } => { - format!("assigning mix {mix_id} for layer {layer:?}") - } - ExecuteMsg::CreateFamily { .. } => "crating node family with".to_string(), - ExecuteMsg::JoinFamily { family_head, .. } => { - format!("joining family {family_head}") - } - ExecuteMsg::LeaveFamily { family_head, .. } => { - format!("leaving family {family_head}") - } - ExecuteMsg::KickFamilyMember { member, .. } => { - format!("kicking {member} from family") - } - ExecuteMsg::CreateFamilyOnBehalf { .. } => "crating node family with".to_string(), - ExecuteMsg::JoinFamilyOnBehalf { family_head, .. } => { - format!("joining family {family_head}") - } - ExecuteMsg::LeaveFamilyOnBehalf { family_head, .. } => { - format!("leaving family {family_head}") - } - ExecuteMsg::KickFamilyMemberOnBehalf { member, .. } => { - format!("kicking {member} from family") - } ExecuteMsg::UpdateRewardingValidatorAddress { address } => { format!("updating rewarding validator to {address}") } ExecuteMsg::UpdateContractStateParams { .. } => { "updating mixnet state parameters".into() } - ExecuteMsg::UpdateActiveSetSize { - active_set_size, - force_immediately, - } => format!( - "updating active set size to {active_set_size}. forced: {force_immediately}" - ), + ExecuteMsg::UpdateActiveSetDistribution { + force_immediately, .. + } => format!("updating active set distribution. forced: {force_immediately}"), ExecuteMsg::UpdateRewardingParams { force_immediately, .. } => format!("updating mixnet rewarding parameters. forced: {force_immediately}"), @@ -340,7 +315,6 @@ impl ExecuteMsg { force_immediately, .. } => format!("updating mixnet interval configuration. forced: {force_immediately}"), ExecuteMsg::BeginEpochTransition {} => "beginning epoch transition".into(), - ExecuteMsg::AdvanceCurrentEpoch { .. } => "advancing current epoch".into(), ExecuteMsg::ReconcileEpochEvents { .. } => "reconciling epoch events".into(), ExecuteMsg::BondMixnode { mix_node, .. } => { format!("bonding mixnode {}", mix_node.identity_key) @@ -356,7 +330,7 @@ impl ExecuteMsg { } ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), - ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), + ExecuteMsg::UpdateCostParams { .. } => "updating mixnode cost parameters".into(), ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } => { "updating mixnode cost parameters on behalf".into() } @@ -376,25 +350,22 @@ impl ExecuteMsg { ExecuteMsg::UpdateGatewayConfigOnBehalf { .. } => { "updating gateway configuration on behalf".into() } - ExecuteMsg::DelegateToMixnode { mix_id } => format!("delegating to mixnode {mix_id}"), + ExecuteMsg::Delegate { node_id: mix_id } => format!("delegating to mixnode {mix_id}"), ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, .. } => { format!("delegating to mixnode {mix_id} on behalf") } - ExecuteMsg::UndelegateFromMixnode { mix_id } => { + ExecuteMsg::Undelegate { node_id: mix_id } => { format!("removing delegation from mixnode {mix_id}") } ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, .. } => { format!("removing delegation from mixnode {mix_id} on behalf") } - ExecuteMsg::RewardMixnode { - mix_id, - performance, - } => format!("rewarding mixnode {mix_id} for performance {performance}"), + ExecuteMsg::RewardNode { node_id, .. } => format!("rewarding node {node_id}"), ExecuteMsg::WithdrawOperatorReward { .. } => "withdrawing operator reward".into(), ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } => { "withdrawing operator reward on behalf".into() } - ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + ExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { format!("withdrawing delegator reward from mixnode {mix_id}") } ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => { @@ -402,11 +373,19 @@ impl ExecuteMsg { } ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(), ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(), + ExecuteMsg::AssignRoles { .. } => "assigning epoch roles".into(), + ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(), + ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(), + ExecuteMsg::BondNymNode { .. } => "bonding nym-node".into(), + ExecuteMsg::UnbondNymNode { .. } => "unbonding nym-node".into(), + ExecuteMsg::UpdateNodeConfig { .. } => "updating node config".into(), #[cfg(feature = "contract-testing")] ExecuteMsg::TestingResolveAllPendingEvents { .. } => { "resolving all pending events".into() } + ExecuteMsg::TestingUncheckedBondLegacyMixnode { .. } => "todo!".into(), + ExecuteMsg::TestingUncheckedBondLegacyGateway { .. } => "todo!".into(), } } } @@ -417,43 +396,6 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] Admin {}, - // families - /// Gets the list of families registered in this contract. - #[cfg_attr(feature = "schema", returns(PagedFamiliesResponse))] - GetAllFamiliesPaged { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - - /// Gets the list of all family members registered in this contract. - #[cfg_attr(feature = "schema", returns(PagedMembersResponse))] - GetAllMembersPaged { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - - /// Attempts to lookup family information given the family head. - #[cfg_attr(feature = "schema", returns(FamilyByHeadResponse))] - GetFamilyByHead { head: String }, - - /// Attempts to lookup family information given the family label. - #[cfg_attr(feature = "schema", returns(FamilyByLabelResponse))] - GetFamilyByLabel { label: String }, - - /// Attempts to retrieve family members given the family head. - #[cfg_attr(feature = "schema", returns(FamilyMembersByHeadResponse))] - GetFamilyMembersByHead { head: String }, - - /// Attempts to retrieve family members given the family label. - #[cfg_attr(feature = "schema", returns(FamilyMembersByLabelResponse))] - GetFamilyMembersByLabel { label: String }, - // state/sys-params-related /// Gets build information of this contract, such as the commit hash used for the build or rustc version. #[cfg_attr(feature = "schema", returns(ContractBuildInformation))] @@ -488,16 +430,6 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(CurrentIntervalResponse))] GetCurrentIntervalDetails {}, - /// Gets the current list of mixnodes in the rewarded set. - #[cfg_attr(feature = "schema", returns(PagedRewardedSetResponse))] - GetRewardedSet { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - // mixnode-related: /// Gets the basic list of all currently bonded mixnodes. #[cfg_attr(feature = "schema", returns(PagedMixnodeBondsResponse))] @@ -506,7 +438,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the detailed list of all currently bonded mixnodes. @@ -516,7 +448,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes. @@ -526,20 +458,20 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes that belonged to a particular owner. #[cfg_attr(feature = "schema", returns(PagedUnbondedMixnodesResponse))] GetUnbondedMixNodesByOwner { - /// The address of the owner of the the mixnodes used for the query. + /// The address of the owner of the mixnodes used for the query. owner: String, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes that used the particular identity key. @@ -552,7 +484,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the detailed mixnode information belonging to the particular owner. @@ -566,28 +498,28 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(MixnodeDetailsResponse))] GetMixnodeDetails { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the rewarding information of a mixnode with the provided id. #[cfg_attr(feature = "schema", returns(MixnodeRewardingDetailsResponse))] GetMixnodeRewardingDetails { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the stake saturation of a mixnode with the provided id. - #[cfg_attr(feature = "schema", returns(StakeSaturationResponse))] + #[cfg_attr(feature = "schema", returns(MixStakeSaturationResponse))] GetStakeSaturation { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the basic information of an unbonded mixnode with the provided id. #[cfg_attr(feature = "schema", returns(UnbondedMixnodeResponse))] GetUnbondedMixNodeInformation { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the detailed mixnode information of a node given its current identity key. @@ -597,10 +529,6 @@ pub enum QueryMsg { mix_identity: IdentityKey, }, - /// Gets the current layer configuration of the mix network. - #[cfg_attr(feature = "schema", returns(LayerDistribution))] - GetLayerDistribution {}, - // gateway-related: /// Gets the basic list of all currently bonded gateways. #[cfg_attr(feature = "schema", returns(PagedGatewayResponse))] @@ -626,12 +554,128 @@ pub enum QueryMsg { address: String, }, - // delegation-related: - /// Gets all delegations associated with particular mixnode - #[cfg_attr(feature = "schema", returns(PagedMixNodeDelegationsResponse))] - GetMixnodeDelegations { + /// Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes + #[cfg_attr(feature = "schema", returns(PreassignedGatewayIdsResponse))] + GetPreassignedGatewayIds { + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + }, + + // nym-node-related: + /// Gets the basic list of all currently bonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedNymNodeBondsResponse))] + GetNymNodeBondsPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the detailed list of all currently bonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedNymNodeDetailsResponse))] + GetNymNodesDetailedPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic information of an unbonded nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(UnbondedNodeResponse))] + GetUnbondedNymNode { /// Id of the node to query. - mix_id: MixId, + node_id: NodeId, + }, + + /// Gets the basic list of all unbonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic list of all unbonded nymnodes that belonged to a particular owner. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesByOwnerPaged { + /// The address of the owner of the nym-node used for the query + owner: String, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic list of all unbonded nymnodes that used the particular identity key. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesByIdentityKeyPaged { + /// The identity key (base58-encoded ed25519 public key) of the node used for the query. + identity_key: IdentityKey, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the detailed nymnode information belonging to the particular owner. + #[cfg_attr(feature = "schema", returns(NodeOwnershipResponse))] + GetOwnedNymNode { + /// Address of the node owner to use for the query. + address: String, + }, + + /// Gets the detailed nymnode information of a node with the provided id. + #[cfg_attr(feature = "schema", returns(NodeDetailsResponse))] + GetNymNodeDetails { + /// Id of the node to query. + node_id: NodeId, + }, + + /// Gets the detailed nym-node information given its current identity key. + #[cfg_attr(feature = "schema", returns(NodeDetailsByIdentityResponse))] + GetNymNodeDetailsByIdentityKey { + /// The identity key (base58-encoded ed25519 public key) of the nym-node used for the query. + node_identity: IdentityKey, + }, + + /// Gets the rewarding information of a nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(NodeRewardingDetailsResponse))] + GetNodeRewardingDetails { + /// Id of the node to query. + node_id: NodeId, + }, + + /// Gets the stake saturation of a nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(StakeSaturationResponse))] + GetNodeStakeSaturation { + /// Id of the node to query. + node_id: NodeId, + }, + + #[cfg_attr(feature = "schema", returns(EpochAssignmentResponse))] + GetRoleAssignment { role: Role }, + + #[cfg_attr(feature = "schema", returns(RolesMetadataResponse))] + GetRewardedSetMetadata {}, + + // delegation-related: + /// Gets all delegations associated with particular node + #[cfg_attr(feature = "schema", returns(PagedNodeDelegationsResponse))] + GetNodeDelegations { + /// Id of the node to query. + #[serde(alias = "mix_id")] + node_id: NodeId, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. start_after: Option, @@ -649,17 +693,18 @@ pub enum QueryMsg { delegator: String, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, }, /// Gets delegation information associated with particular mixnode - delegator pair - #[cfg_attr(feature = "schema", returns(MixNodeDelegationResponse))] + #[cfg_attr(feature = "schema", returns(NodeDelegationResponse))] GetDelegationDetails { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The address of the owner of the delegation. delegator: String, @@ -689,9 +734,14 @@ pub enum QueryMsg { /// Gets the reward amount accrued by the particular mixnode that has not yet been claimed. #[cfg_attr(feature = "schema", returns(PendingRewardResponse))] - GetPendingMixNodeOperatorReward { + #[serde( + alias = "GetPendingMixNodeOperatorReward", + alias = "get_pending_mix_node_operator_reward" + )] + GetPendingNodeOperatorReward { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, }, /// Gets the reward amount accrued by the particular delegator that has not yet been claimed. @@ -701,7 +751,8 @@ pub enum QueryMsg { address: String, /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// Entity who made the delegation on behalf of the owner. /// If present, it's most likely the address of the vesting contract. @@ -712,10 +763,14 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(EstimatedCurrentEpochRewardResponse))] GetEstimatedCurrentEpochOperatorReward { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The estimated performance for the current epoch of the given node. estimated_performance: Performance, + + /// The estimated work for the current epoch of the given node. + estimated_work: Option, }, /// Given the provided node performance, attempt to estimate the delegator reward for the current epoch. @@ -725,14 +780,14 @@ pub enum QueryMsg { address: String, /// Id of the node to query. - mix_id: MixId, - - /// Entity who made the delegation on behalf of the owner. - /// If present, it's most likely the address of the vesting contract. - proxy: Option, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The estimated performance for the current epoch of the given node. estimated_performance: Performance, + + /// The estimated work for the current epoch of the given node. + estimated_work: Option, }, // interval-related diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs new file mode 100644 index 0000000000..fa5db3b39c --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -0,0 +1,575 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::MixnetContractError; +use crate::{EpochEventId, EpochId, Gateway, IntervalEventId, MixNode, NodeId, NodeRewarding}; +use contracts_common::IdentityKey; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin, Decimal, StdError, StdResult}; +use cw_storage_plus::{IntKey, Key, KeyDeserialize, PrimaryKey}; +use std::fmt::{Display, Formatter}; + +#[cw_serde] +#[derive(PartialOrd, Copy, Hash, Eq)] +#[repr(u8)] +pub enum Role { + #[serde(rename = "eg", alias = "entry", alias = "entry_gateway")] + EntryGateway = 0, + + #[serde(rename = "l1", alias = "layer1")] + Layer1 = 1, + + #[serde(rename = "l2", alias = "layer2")] + Layer2 = 2, + + #[serde(rename = "l3", alias = "layer3")] + Layer3 = 3, + + #[serde(rename = "xg", alias = "exit", alias = "exit_gateway")] + ExitGateway = 4, + + #[serde(rename = "stb", alias = "standby")] + Standby = 128, +} + +impl TryFrom for Role { + type Error = MixnetContractError; + fn try_from(value: u8) -> Result { + match value { + n if n == Role::EntryGateway as u8 => Ok(Role::EntryGateway), + n if n == Role::Layer1 as u8 => Ok(Role::Layer1), + n if n == Role::Layer2 as u8 => Ok(Role::Layer2), + n if n == Role::Layer3 as u8 => Ok(Role::Layer3), + n if n == Role::ExitGateway as u8 => Ok(Role::ExitGateway), + n if n == Role::Standby as u8 => Ok(Role::Standby), + n => Err(MixnetContractError::UnknownRoleRepresentation { got: n }), + } + } +} + +impl<'a> PrimaryKey<'a> for Role { + type Prefix = >::Prefix; + type SubPrefix = >::SubPrefix; + type Suffix = >::Suffix; + type SuperSuffix = >::SuperSuffix; + + fn key(&self) -> Vec { + // I'm not sure why it wasn't possible to delegate the call to + // `(*self as u8).key()` directly... + // I guess because of the `Key::Ref(&'a [u8])` variant? + vec![Key::Val8((*self as u8).to_cw_bytes())] + } + + fn joined_key(&self) -> Vec { + (*self as u8).joined_key() + } + + fn joined_extra_key(&self, key: &[u8]) -> Vec { + (*self as u8).joined_extra_key(key) + } +} + +impl KeyDeserialize for Role { + type Output = Role; + + fn from_vec(value: Vec) -> StdResult { + let u8_key: ::Output = ::from_vec(value)?; + Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string())) + } + + fn from_slice(value: &[u8]) -> StdResult { + let u8_key: ::Output = ::from_slice(value)?; + Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string())) + } +} + +impl Role { + pub fn first() -> Role { + Role::ExitGateway + } + + pub fn next(&self) -> Option { + // roles have to be assigned in the following order: + // exit -> entry -> l1 -> l2 -> l3 -> standby + match self { + Role::ExitGateway => Some(Role::EntryGateway), + Role::EntryGateway => Some(Role::Layer1), + Role::Layer1 => Some(Role::Layer2), + Role::Layer2 => Some(Role::Layer3), + Role::Layer3 => Some(Role::Standby), + Role::Standby => None, + } + } + + pub fn is_first(&self) -> bool { + self == &Role::first() + } + + pub fn is_standby(&self) -> bool { + matches!(self, Role::Standby) + } +} + +impl Display for Role { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Role::Layer1 => write!(f, "mix layer 1"), + Role::Layer2 => write!(f, "mix layer 2"), + Role::Layer3 => write!(f, "mix layer 3"), + Role::EntryGateway => write!(f, "entry gateway"), + Role::ExitGateway => write!(f, "exit gateway"), + Role::Standby => write!(f, "standby"), + } + } +} + +/// Metadata associated with the rewarded set. +#[cw_serde] +#[derive(Default, Copy)] +pub struct RewardedSetMetadata { + /// Epoch that this data corresponds to. + pub epoch_id: EpochId, + + /// Indicates whether all roles got assigned to the set for this epoch. + pub fully_assigned: bool, + + /// Metadata for the 'EntryGateway' role + pub entry_gateway_metadata: RoleMetadata, + + /// Metadata for the 'ExitGateway' role + pub exit_gateway_metadata: RoleMetadata, + + /// Metadata for the 'Layer1' role + pub layer1_metadata: RoleMetadata, + + /// Metadata for the 'Layer2' role + pub layer2_metadata: RoleMetadata, + + /// Metadata for the 'Layer3' role + pub layer3_metadata: RoleMetadata, + + /// Metadata for the 'Standby' role + pub standby_metadata: RoleMetadata, +} + +impl RewardedSetMetadata { + pub fn new(epoch_id: EpochId) -> Self { + RewardedSetMetadata { + epoch_id, + fully_assigned: false, + entry_gateway_metadata: Default::default(), + exit_gateway_metadata: Default::default(), + layer1_metadata: Default::default(), + layer2_metadata: Default::default(), + layer3_metadata: Default::default(), + standby_metadata: Default::default(), + } + } + + pub fn set_role_count(&mut self, role: Role, num_nodes: u32) { + match role { + Role::EntryGateway => self.entry_gateway_metadata.num_nodes = num_nodes, + Role::Layer1 => self.layer1_metadata.num_nodes = num_nodes, + Role::Layer2 => self.layer2_metadata.num_nodes = num_nodes, + Role::Layer3 => self.layer3_metadata.num_nodes = num_nodes, + Role::ExitGateway => self.exit_gateway_metadata.num_nodes = num_nodes, + Role::Standby => self.standby_metadata.num_nodes = num_nodes, + } + } + + pub fn set_highest_id(&mut self, highest_id: NodeId, role: Role) { + match role { + Role::EntryGateway => self.entry_gateway_metadata.highest_id = highest_id, + Role::Layer1 => self.layer1_metadata.highest_id = highest_id, + Role::Layer2 => self.layer2_metadata.highest_id = highest_id, + Role::Layer3 => self.layer3_metadata.highest_id = highest_id, + Role::ExitGateway => self.exit_gateway_metadata.highest_id = highest_id, + Role::Standby => self.standby_metadata.highest_id = highest_id, + } + } + + // important note: this currently does **NOT** include gateway role as they're not being rewarded + // and the metadata is primarily used for data lookup during epoch transition + pub fn highest_rewarded_id(&self) -> NodeId { + let mut highest = 0; + if self.layer1_metadata.highest_id > highest { + highest = self.layer1_metadata.highest_id; + } + if self.layer2_metadata.highest_id > highest { + highest = self.layer2_metadata.highest_id; + } + if self.layer3_metadata.highest_id > highest { + highest = self.layer3_metadata.highest_id; + } + if self.standby_metadata.highest_id > highest { + highest = self.standby_metadata.highest_id; + } + + highest + } +} + +/// Metadata associated with particular node role. +#[cw_serde] +#[derive(Default, Copy)] +pub struct RoleMetadata { + /// Highest, also latest, node-id of a node assigned this role. + pub highest_id: NodeId, + + /// Number of nodes assigned this particular role. + pub num_nodes: u32, +} + +/// Full details associated with given node. +#[cw_serde] +pub struct NymNodeDetails { + /// Basic bond information of this node, such as owner address, original pledge, etc. + pub bond_information: NymNodeBond, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the node that are scheduled to happen during future epoch/interval transitions. + pub pending_changes: PendingNodeChanges, +} + +impl NymNodeDetails { + pub fn new( + bond_information: NymNodeBond, + rewarding_details: NodeRewarding, + pending_changes: PendingNodeChanges, + ) -> Self { + NymNodeDetails { + bond_information, + rewarding_details, + pending_changes, + } + } + + pub fn node_id(&self) -> NodeId { + self.bond_information.node_id + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } + + pub fn original_pledge(&self) -> &Coin { + &self.bond_information.original_pledge + } + + pub fn pending_operator_reward(&self) -> Coin { + let pledge = self.original_pledge(); + self.rewarding_details.pending_operator_reward(pledge) + } + + pub fn pending_detailed_operator_reward(&self) -> StdResult { + let pledge = self.original_pledge(); + self.rewarding_details + .pending_detailed_operator_reward(pledge) + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } + + pub fn pending_pledge_change(&self) -> Option { + self.pending_changes.pledge_change + } +} + +/// +#[cw_serde] +pub struct NymNodeBond { + /// Unique id assigned to the bonded node. + pub node_id: NodeId, + + /// Address of the owner of this nym-node. + pub owner: Addr, + + /// Original amount pledged by the operator of this node. + pub original_pledge: Coin, + + /// Block height at which this nym-node has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, + + /// Information provided by the operator for the purposes of bonding. + pub node: NymNode, +} + +impl NymNodeBond { + pub fn new( + node_id: NodeId, + owner: Addr, + original_pledge: Coin, + node: impl Into, + bonding_height: u64, + ) -> NymNodeBond { + Self { + node_id, + owner, + original_pledge, + bonding_height, + is_unbonding: false, + node: node.into(), + } + } + + pub fn identity(&self) -> &str { + &self.node.identity_key + } + + pub fn ensure_bonded(&self) -> Result<(), MixnetContractError> { + if self.is_unbonding { + return Err(MixnetContractError::NodeIsUnbonding { + node_id: self.node_id, + }); + } + Ok(()) + } +} + +/// Information provided by the node operator during bonding that are used to allow other entities to use the services of this node. +#[cw_serde] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NymNode.ts") +)] +pub struct NymNode { + /// Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com + /// that is used to discover other capabilities of this node. + pub host: String, + + /// Allow specifying custom port for accessing the http, and thus self-described, api + /// of this node for the capabilities discovery. + pub custom_http_port: Option, + + /// Base58-encoded ed25519 EdDSA public key. + pub identity_key: IdentityKey, + // TODO: I don't think we want to include sphinx keys here, + // given we want to rotate them and keeping that in sync with contract will be a PITA +} + +impl NymNode { + /// Perform naive validation of the attached identity key - makes sure it's correctly encoded + /// and has 32 bytes (as expected from ed25519). we're not, however, checking if it's a valid curve point + pub fn naive_ensure_valid_pubkey(&self) -> Result<(), MixnetContractError> { + let decoded = bs58::decode(&self.identity_key) + .into_vec() + .map_err(|_| MixnetContractError::InvalidPubKey)?; + if decoded.len() != 32 { + return Err(MixnetContractError::InvalidPubKey); + } + Ok(()) + } + + /// Makes sure the provided host's length is at most 255 characters to prevent abuse. + pub fn ensure_host_in_range(&self) -> Result<(), MixnetContractError> { + if self.host.len() > 255 { + return Err(MixnetContractError::HostTooLong); + } + Ok(()) + } +} + +impl From for NymNode { + fn from(value: MixNode) -> Self { + NymNode { + host: value.host, + custom_http_port: Some(value.http_api_port), + identity_key: value.identity_key, + } + } +} + +impl From for NymNode { + fn from(value: Gateway) -> Self { + NymNode { + host: value.host, + custom_http_port: None, + identity_key: value.identity_key, + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NodeConfigUpdate.ts") +)] +#[cw_serde] +#[derive(Default)] +pub struct NodeConfigUpdate { + pub host: Option, + // ideally this would have been `Option>`, but not sure if json would have recognised it + pub custom_http_port: Option, + + // equivalent to setting `custom_http_port` to `None` + #[serde(default)] + pub restore_default_http_port: bool, +} + +#[cw_serde] +#[derive(Default, Copy)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingNodeChanges.ts") +)] +pub struct PendingNodeChanges { + pub pledge_change: Option, + pub cost_params_change: Option, +} + +impl PendingNodeChanges { + pub fn new_empty() -> PendingNodeChanges { + PendingNodeChanges { + pledge_change: None, + cost_params_change: None, + } + } + + pub fn ensure_no_pending_pledge_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pledge_change { + return Err(MixnetContractError::PendingPledgeChange { pending_event_id }); + } + Ok(()) + } + + pub fn ensure_no_pending_params_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.cost_params_change { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) + } +} + +/// Basic information of a node that used to be part of the nym network but has already unbonded. +#[cw_serde] +pub struct UnbondedNymNode { + /// Base58-encoded ed25519 EdDSA public key. + pub identity_key: IdentityKey, + + /// NodeId assigned to this node. + pub node_id: NodeId, + + /// Address of the owner of this nym node. + pub owner: Addr, + + /// Block height at which this nym node has unbonded. + pub unbonding_height: u64, +} + +/// Response containing rewarding information of a node with the provided id. +#[cw_serde] +pub struct NodeRewardingDetailsResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// If there exists a node with the provided id, this field contains its rewarding information. + pub rewarding_details: Option, +} + +/// Response containing details of a node belonging to the particular owner. +#[cw_serde] +pub struct NodeOwnershipResponse { + /// Validated address of the node owner. + pub address: Addr, + + /// If the provided address owns a nym-node, this field contains its detailed information. + pub details: Option, +} + +/// Response containing details of a node with the provided id. +#[cw_serde] +pub struct NodeDetailsResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// If there exists a node with the provided id, this field contains its detailed information. + pub details: Option, +} + +/// Response containing details of a bonded node with the provided identity key. +#[cw_serde] +pub struct NodeDetailsByIdentityResponse { + /// The identity key (base58-encoded ed25519 public key) of the node. + pub identity_key: IdentityKey, + + /// If there exists a bonded node with the provided identity key, this field contains its detailed information. + pub details: Option, +} + +/// Response containing the current state of the stake saturation of a node with the provided id. +#[cw_serde] +pub struct StakeSaturationResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// The current stake saturation of this node that is indirectly used in reward calculation formulas. + /// Note that it can't be larger than 1. + pub current_saturation: Option, + + /// The current, absolute, stake saturation of this node. + /// Note that as the name suggests it can be larger than 1. + /// However, anything beyond that value has no effect on the total node reward. + pub uncapped_saturation: Option, +} + +/// Response containing paged list of all nym-nodes that have ever unbonded. +#[cw_serde] +pub struct PagedUnbondedNymNodesResponse { + /// Basic information of the node such as the owner or the identity key. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +/// Response containing basic information of an unbonded nym-node with the provided id. +#[cw_serde] +pub struct UnbondedNodeResponse { + /// Id of the requested nym-node. + pub node_id: NodeId, + + /// If there existed a nym-node with the provided id, this field contains its basic information. + pub details: Option, +} + +#[cw_serde] +pub struct PagedNymNodeBondsResponse { + /// The nym node bond information present in the contract. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +#[cw_serde] +pub struct PagedNymNodeDetailsResponse { + /// All nym-node details stored in the contract. + /// Apart from the basic bond information it also contains details required for all future reward calculation + /// as well as any pending changes requested by the operator. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +#[cw_serde] +pub struct EpochAssignmentResponse { + /// Epoch that this data corresponds to. + pub epoch_id: EpochId, + + pub nodes: Vec, +} + +#[cw_serde] +pub struct RolesMetadataResponse { + pub metadata: RewardedSetMetadata, +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index 8d97e20494..72266d01d8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -1,9 +1,9 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnode::MixNodeCostParams; -use crate::reward_params::IntervalRewardingParamsUpdate; -use crate::{BlockHeight, MixId}; +use crate::mixnode::NodeCostParams; +use crate::reward_params::{ActiveSetUpdate, IntervalRewardingParamsUpdate}; +use crate::{BlockHeight, NodeId}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; @@ -35,7 +35,7 @@ pub struct PendingEpochEventData { pub enum PendingEpochEventKind { // can't just pass the `Delegation` struct here as it's impossible to determine // `cumulative_reward_ratio` ahead of time - /// Request to create a delegation towards particular mixnode. + /// Request to create a delegation towards particular node. /// Note that if such delegation already exists, it will get updated with the provided token amount. #[serde(alias = "Delegate")] #[non_exhaustive] @@ -43,8 +43,9 @@ pub enum PendingEpochEventKind { /// The address of the owner of the delegation. owner: Addr, - /// The id of the mixnode used for the delegation. - mix_id: MixId, + /// The id of the node used for the delegation. + #[serde(alias = "mix_id")] + node_id: NodeId, /// The amount of tokens to use for the delegation. amount: Coin, @@ -54,15 +55,16 @@ pub enum PendingEpochEventKind { proxy: Option, }, - /// Request to remove delegation from particular mixnode. + /// Request to remove delegation from particular node. #[serde(alias = "Undelegate")] #[non_exhaustive] Undelegate { /// The address of the owner of the delegation. owner: Addr, - /// The id of the mixnode used for the delegation. - mix_id: MixId, + /// The id of the node used for the delegation. + #[serde(alias = "mix_id")] + node_id: NodeId, /// Entity who made the delegation on behalf of the owner. /// If present, it's most likely the address of the vesting contract. @@ -70,20 +72,38 @@ pub enum PendingEpochEventKind { }, /// Request to pledge more tokens (by the node operator) towards its node. - #[serde(alias = "PledgeMore")] - PledgeMore { - /// The id of the mixnode that will have its pledge updated. - mix_id: MixId, + NymNodePledgeMore { + /// The id of the nym node that will have its pledge updated. + node_id: NodeId, - /// The amount of additional tokens to use by the pledge. + /// The amount of additional tokens to use in the pledge. + amount: Coin, + }, + + /// Request to pledge more tokens (by the node operator) towards its node. + #[serde(alias = "PledgeMore")] + MixnodePledgeMore { + /// The id of the mixnode that will have its pledge updated. + mix_id: NodeId, + + /// The amount of additional tokens to use in the pledge. amount: Coin, }, + /// Request to decrease amount of pledged tokens (by the node operator) from its node. + NymNodeDecreasePledge { + /// The id of the nym node that will have its pledge updated. + node_id: NodeId, + + /// The amount of tokens that should be removed from the pledge. + decrease_by: Coin, + }, + /// Request to decrease amount of pledged tokens (by the node operator) from its node. #[serde(alias = "DecreasePledge")] - DecreasePledge { + MixnodeDecreasePledge { /// The id of the mixnode that will have its pledge updated. - mix_id: MixId, + mix_id: NodeId, /// The amount of tokens that should be removed from the pledge. decrease_by: Coin, @@ -93,15 +113,17 @@ pub enum PendingEpochEventKind { #[serde(alias = "UnbondMixnode")] UnbondMixnode { /// The id of the mixnode that will get unbonded. - mix_id: MixId, + mix_id: NodeId, }, - /// Request to update the current size of the active set. - #[serde(alias = "UpdateActiveSetSize")] - UpdateActiveSetSize { - /// The new desired size of the active set. - new_size: u32, + /// Request to unbond a nym node and completely remove it from the network. + UnbondNymNode { + /// The id of the node that will get unbonded. + node_id: NodeId, }, + + /// Request to update the current active set. + UpdateActiveSet { update: ActiveSetUpdate }, } impl PendingEpochEventKind { @@ -112,19 +134,19 @@ impl PendingEpochEventKind { } } - pub fn new_delegate(owner: Addr, mix_id: MixId, amount: Coin) -> Self { + pub fn new_delegate(owner: Addr, node_id: NodeId, amount: Coin) -> Self { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id, amount, proxy: None, } } - pub fn new_undelegate(owner: Addr, mix_id: MixId) -> Self { + pub fn new_undelegate(owner: Addr, node_id: NodeId) -> Self { PendingEpochEventKind::Undelegate { owner, - mix_id, + node_id, proxy: None, } } @@ -166,10 +188,19 @@ pub enum PendingIntervalEventKind { #[serde(alias = "ChangeMixCostParams")] ChangeMixCostParams { /// The id of the mixnode that will have its cost parameters updated. - mix_id: MixId, + mix_id: NodeId, /// The new updated cost function of this mixnode. - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, + }, + + /// Request to update cost parameters of given nym node. + ChangeNymNodeCostParams { + /// The id of the nym node that will have its cost parameters updated. + node_id: NodeId, + + /// The new updated cost function of this nym node. + new_costs: NodeCostParams, }, /// Request to update the underlying rewarding parameters used by the system diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 4928bd6a1e..da6e2d3dae 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::helpers::IntoBaseDecimal; +use crate::nym_node::Role; use crate::{error::MixnetContractError, Percent}; use cosmwasm_schema::cw_serde; use cosmwasm_std::Decimal; pub type Performance = Percent; +pub type WorkFactor = Decimal; /// Parameters required by the mix-mining reward distribution that do not change during an interval. #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -86,23 +88,15 @@ pub struct RewardingParams { /// Parameters that should remain unchanged throughout an interval. pub interval: IntervalRewardParams, - // while the rewarded set size can change between epochs to accommodate for bandwidth demands, - // the active set size should be unchanged between epochs and should only be adjusted between - // intervals. However, it makes more sense to keep both of those values together as they're - // very strongly related to each other. - /// The expected number of mixnodes in the rewarded set (i.e. active + standby). - pub rewarded_set_size: u32, - - /// The expected number of mixnodes in the active set. - pub active_set_size: u32, + pub rewarded_set: RewardedSetParams, } impl RewardingParams { - pub fn active_node_work(&self) -> Decimal { + pub fn active_node_work(&self) -> WorkFactor { self.interval.active_set_work_factor * self.standby_node_work() } - pub fn standby_node_work(&self) -> Decimal { + pub fn standby_node_work(&self) -> WorkFactor { let f = self.interval.active_set_work_factor; let k = self.dec_rewarded_set_size(); let one = Decimal::one(); @@ -113,27 +107,33 @@ impl RewardingParams { one / (f * k - (f - one) * k_r) } + pub fn rewarded_set_size(&self) -> u32 { + self.rewarded_set.rewarded_set_size() + } + + pub fn active_set_size(&self) -> u32 { + self.rewarded_set.active_set_size() + } + pub fn dec_rewarded_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - self.rewarded_set_size.into_base_decimal().unwrap() + self.rewarded_set_size().into_base_decimal().unwrap() } pub fn dec_active_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - self.active_set_size.into_base_decimal().unwrap() + self.active_set_size().into_base_decimal().unwrap() } fn dec_standby_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - (self.rewarded_set_size - self.active_set_size) - .into_base_decimal() - .unwrap() + self.rewarded_set.standby.into_base_decimal().unwrap() } pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) { @@ -146,19 +146,35 @@ impl RewardingParams { * self.interval.interval_pool_emission; } - pub fn try_change_active_set_size( - &mut self, - new_active_set_size: u32, + pub fn validate_active_set_update( + &self, + update: ActiveSetUpdate, ) -> Result<(), MixnetContractError> { - if new_active_set_size == 0 { - return Err(MixnetContractError::ZeroActiveSet); - } + update.ensure_non_empty()?; + let active_set_size = update.active_set_size(); - if new_active_set_size > self.rewarded_set_size { + if active_set_size > self.rewarded_set_size() { return Err(MixnetContractError::InvalidActiveSetSize); } - self.active_set_size = new_active_set_size; + Ok(()) + } + + pub fn try_change_active_set( + &mut self, + update: ActiveSetUpdate, + ) -> Result<(), MixnetContractError> { + self.validate_active_set_update(update)?; + let active_set_size = update.active_set_size(); + let rewarded_set_size = self.rewarded_set_size(); + + // safety: due to validation we know that the active_set_size <= rewarded_set_size + let new_standby = rewarded_set_size - active_set_size; + + self.rewarded_set.exit_gateways = update.exit_gateways; + self.rewarded_set.entry_gateways = update.entry_gateways; + self.rewarded_set.mixnodes = update.mixnodes; + self.rewarded_set.standby = new_standby; Ok(()) } @@ -201,16 +217,10 @@ impl RewardingParams { self.interval.interval_pool_emission = interval_pool_emission; } - if let Some(rewarded_set_size) = updates.rewarded_set_size { - if rewarded_set_size == 0 { - return Err(MixnetContractError::ZeroRewardedSet); - } - if rewarded_set_size < self.active_set_size { - return Err(MixnetContractError::InvalidRewardedSetSize); - } - + if let Some(rewarded_set_update) = updates.rewarded_set_params { + rewarded_set_update.ensure_valid()?; recompute_saturation_point = true; - self.rewarded_set_size = rewarded_set_size; + self.rewarded_set = rewarded_set_update; } if recompute_epoch_budget { @@ -221,31 +231,99 @@ impl RewardingParams { if recompute_saturation_point { self.interval.stake_saturation_point = - self.interval.staking_supply / self.rewarded_set_size.into_base_decimal()? + self.interval.staking_supply / self.rewarded_set_size().into_base_decimal()? } Ok(()) } } -// TODO: possibly refactor this -/// Parameters used for rewarding particular mixnode. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/RewardedSetParams.ts") +)] #[cw_serde] #[derive(Copy)] -pub struct NodeRewardParams { - /// Performance of the particular node in the current epoch. - pub performance: Percent, +pub struct RewardedSetParams { + /// The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + pub entry_gateways: u32, - /// Flag indicating whether the node has been in the active set during the epoch. - pub in_active_set: bool, + /// The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + pub exit_gateways: u32, + + /// The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + pub mixnodes: u32, + + /// Number of nodes in the 'standby' set. (i.e. [`Role::Standby`]) + pub standby: u32, } -impl NodeRewardParams { - pub fn new(performance: Percent, in_active_set: bool) -> Self { - NodeRewardParams { - performance, - in_active_set, +impl RewardedSetParams { + pub fn active_set_size(&self) -> u32 { + self.entry_gateways + self.exit_gateways + self.mixnodes + } + + pub fn rewarded_set_size(&self) -> u32 { + self.active_set_size() + self.standby + } + + pub fn ensure_valid(&self) -> Result<(), MixnetContractError> { + if self.entry_gateways == 0 || self.exit_gateways == 0 || self.mixnodes == 0 { + return Err(MixnetContractError::EmptyRoleAssignment); } + if self.mixnodes % 3 != 0 { + return Err(MixnetContractError::UnevenLayerAssignment); + } + Ok(()) + } + + pub fn maximum_role_count(&self, role: Role) -> u32 { + match role { + Role::EntryGateway => self.entry_gateways, + Role::Layer1 | Role::Layer2 | Role::Layer3 => self.mixnodes / 3, + Role::ExitGateway => self.exit_gateways, + Role::Standby => self.standby, + } + } + + pub fn ensure_role_count(&self, role: Role, assigned: u32) -> Result<(), MixnetContractError> { + let allowed = self.maximum_role_count(role); + + if assigned > allowed { + return Err(MixnetContractError::IllegalRoleCount { + role, + assigned, + allowed, + }); + } + + Ok(()) + } +} + +/// Parameters used for rewarding particular node. +#[cw_serde] +#[derive(Copy)] +pub struct NodeRewardingParameters { + /// Performance of the particular node in the current epoch. + pub performance: Performance, + + /// Amount of work performed by this node in the current epoch + /// also known as 'omega' in the paper + pub work_factor: WorkFactor, +} + +impl NodeRewardingParameters { + pub fn new(performance: Performance, work_factor: WorkFactor) -> Self { + NodeRewardingParameters { + performance, + work_factor, + } + } + + pub fn is_zero(&self) -> bool { + self.performance.is_zero() || self.work_factor.is_zero() } } @@ -282,8 +360,8 @@ pub struct IntervalRewardingParamsUpdate { /// Defines the new value of the interval pool emission rate. pub interval_pool_emission: Option, - /// Defines the new size of the rewarded set. - pub rewarded_set_size: Option, + /// Defines the parameters of the rewarded set. + pub rewarded_set_params: Option, } impl IntervalRewardingParamsUpdate { @@ -295,10 +373,42 @@ impl IntervalRewardingParamsUpdate { || self.sybil_resistance_percent.is_some() || self.active_set_work_factor.is_some() || self.interval_pool_emission.is_some() - || self.rewarded_set_size.is_some() + || self.rewarded_set_params.is_some() } pub fn to_inline_json(&self) -> String { serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } + +/// Specification on how the active set should be updated. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/ActiveSetUpdate.ts") +)] +#[cw_serde] +#[derive(Copy, Default)] +pub struct ActiveSetUpdate { + /// The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + pub entry_gateways: u32, + + /// The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + pub exit_gateways: u32, + + /// The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + pub mixnodes: u32, +} + +impl ActiveSetUpdate { + pub fn active_set_size(&self) -> u32 { + self.entry_gateways + self.exit_gateways + self.mixnodes + } + + pub fn ensure_non_empty(&self) -> Result<(), MixnetContractError> { + if self.entry_gateways == 0 || self.exit_gateways == 0 || self.mixnodes == 0 { + return Err(MixnetContractError::EmptyRoleAssignment); + } + Ok(()) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs index 4d9ca75459..d9c0bb5ea9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -18,3 +18,11 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { truncate_decimal(reward) } + +pub fn legacy_standby_work_factor() -> Decimal { + todo!() +} + +pub fn legacy_active_work_factor() -> Decimal { + todo!() +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index 4ef9cd433a..18bdc3e562 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -1,7 +1,6 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{MixId, RewardedSetNodeStatus}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal}; @@ -63,7 +62,10 @@ pub struct PendingRewardResponse { /// The associated mixnode is still fully bonded, meaning it is neither unbonded /// nor in the process of unbonding that would have finished at the epoch transition. + #[deprecated(note = "this field will be removed. use .node_still_fully_bonded instead")] pub mixnode_still_fully_bonded: bool, + + pub node_still_fully_bonded: bool, } /// Response containing estimation of node rewards for the current epoch. @@ -99,13 +101,3 @@ impl EstimatedCurrentEpochRewardResponse { } } } - -/// Response containing paged list of all mixnodes in the rewarded set. -#[cw_serde] -pub struct PagedRewardedSetResponse { - /// Nodes in the current rewarded set. - pub nodes: Vec<(MixId, RewardedSetNodeStatus)>, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs index 0cc4d06dee..46326a1712 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -3,23 +3,21 @@ use crate::error::MixnetContractError; use crate::helpers::IntoBaseDecimal; -use crate::reward_params::NodeRewardParams; +use crate::reward_params::{NodeRewardingParameters, WorkFactor}; use crate::rewarding::simulator::simulated_node::SimulatedNode; use crate::rewarding::RewardDistribution; -use crate::{ - Delegation, Interval, IntervalRewardParams, MixId, MixNodeCostParams, RewardingParams, -}; +use crate::{Delegation, Interval, IntervalRewardParams, NodeCostParams, NodeId, RewardingParams}; use cosmwasm_std::{Coin, Decimal}; use std::collections::BTreeMap; pub mod simulated_node; pub struct Simulator { - pub nodes: BTreeMap, + pub nodes: BTreeMap, pub system_rewarding_params: RewardingParams, pub interval: Interval, - next_mix_id: MixId, + next_mix_id: NodeId, pending_reward_pool_emission: Decimal, } @@ -34,6 +32,14 @@ impl Simulator { } } + pub fn legacy_standby_work_factor(&self) -> WorkFactor { + self.system_rewarding_params.standby_node_work() + } + + pub fn legacy_active_work_factor(&self) -> WorkFactor { + self.system_rewarding_params.active_node_work() + } + fn advance_epoch(&mut self) -> Result<(), MixnetContractError> { let updated = self.interval.advance_epoch(); @@ -53,7 +59,7 @@ impl Simulator { let stake_saturation_point = staking_supply / self .system_rewarding_params - .rewarded_set_size + .rewarded_set_size() .into_base_decimal()?; let updated_params = RewardingParams { @@ -67,8 +73,7 @@ impl Simulator { active_set_work_factor: old.active_set_work_factor, interval_pool_emission: old.interval_pool_emission, }, - rewarded_set_size: self.system_rewarding_params.rewarded_set_size, - active_set_size: self.system_rewarding_params.active_set_size, + rewarded_set: self.system_rewarding_params.rewarded_set, }; self.system_rewarding_params = updated_params; @@ -82,8 +87,8 @@ impl Simulator { pub fn bond( &mut self, pledge: Coin, - cost_params: MixNodeCostParams, - ) -> Result { + cost_params: NodeCostParams, + ) -> Result { let mix_id = self.next_mix_id; self.nodes.insert( @@ -105,7 +110,7 @@ impl Simulator { &mut self, delegator: S, delegation: Coin, - mix_id: MixId, + mix_id: NodeId, ) -> Result<(), MixnetContractError> { let node = self .nodes @@ -119,7 +124,7 @@ impl Simulator { pub fn undelegate>( &mut self, delegator: S, - mix_id: MixId, + mix_id: NodeId, ) -> Result<(Coin, Coin), MixnetContractError> { let node = self .nodes @@ -130,7 +135,7 @@ impl Simulator { pub fn simulate_epoch_single_node( &mut self, - params: NodeRewardParams, + params: NodeRewardingParameters, ) -> Result { assert_eq!(self.nodes.len(), 1); @@ -148,8 +153,8 @@ impl Simulator { pub fn simulate_epoch( &mut self, - node_params: &BTreeMap, - ) -> Result, MixnetContractError> { + node_params: &BTreeMap, + ) -> Result, MixnetContractError> { let mut params_keys = node_params.keys().copied().collect::>(); params_keys.sort_unstable(); let mut node_keys = self.nodes.keys().copied().collect::>(); @@ -185,7 +190,7 @@ impl Simulator { &self, delegation: &Delegation, ) -> Result { - Ok(self.nodes[&delegation.mix_id] + Ok(self.nodes[&delegation.node_id] .rewarding_details .determine_delegation_reward(delegation)?) } @@ -206,7 +211,7 @@ impl Simulator { // assume node state doesn't change in the interval (kinda unrealistic) pub fn simulate_full_interval( &mut self, - node_params: &BTreeMap, + node_params: &BTreeMap, ) -> Result<(), MixnetContractError> { for _ in 0..self.interval.epochs_in_interval() { self.simulate_epoch(node_params)?; @@ -219,6 +224,7 @@ impl Simulator { mod tests { use super::*; use crate::helpers::compare_decimals; + use crate::reward_params::RewardedSetParams; use crate::Percent; use cosmwasm_std::testing::mock_env; use std::time::Duration; @@ -226,6 +232,7 @@ mod tests { #[cfg(test)] mod single_node_case { use super::*; + use crate::reward_params::RewardedSetParams; use crate::rewarding::helpers::truncate_reward_amount; use cosmwasm_std::coin; @@ -237,15 +244,23 @@ mod tests { let profit_margin = Percent::from_percentage_value(10).unwrap(); let interval_operating_cost = Coin::new(40_000_000, "unym"); let epochs_in_interval = 720u32; - let rewarded_set_size = 240; - let active_set_size = 100; let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + // the import values here are active set being 100 and rewarded set being 240 + // since those are the values we were using in the past + let rewarded_set = RewardedSetParams { + entry_gateways: 20, + exit_gateways: 50, + mixnodes: 30, + standby: 140, + }; + let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; let epoch_reward_budget = interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); - let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + let stake_saturation_point = + Decimal::from_ratio(staking_supply, rewarded_set.rewarded_set_size()); let rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -258,8 +273,7 @@ mod tests { active_set_work_factor: Decimal::percent(1000), // value '10' interval_pool_emission, }, - rewarded_set_size, - active_set_size, + rewarded_set, }; let interval = Interval::init_interval( @@ -270,7 +284,7 @@ mod tests { let initial_pledge = Coin::new(initial_pledge, "unym"); let mut simulator = Simulator::new(rewarding_params, interval); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: profit_margin, interval_operating_cost, }; @@ -315,8 +329,10 @@ mod tests { fn simulator_returns_expected_values_for_base_case() { let mut simulator = base_simulator(10000_000000); - let epoch_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let epoch_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards = simulator.simulate_epoch_single_node(epoch_params).unwrap(); assert_eq!(rewards.delegates, Decimal::zero()); @@ -334,8 +350,10 @@ mod tests { .delegate("alice", Coin::new(18000_000000, "unym"), 0) .unwrap(); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards = simulator.simulate_epoch_single_node(node_params).unwrap(); compare_decimals( @@ -364,8 +382,10 @@ mod tests { #[test] fn delegation_and_undelegation() { let mut simulator = base_simulator(10000_000000); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap(); let expected_operator1 = "1128452.5416104363".parse().unwrap(); @@ -411,8 +431,10 @@ mod tests { // essentially all delegators' rewards (and the operator itself) are still correctly computed let original_pledge = coin(10000_000000, "unym"); let mut simulator = base_simulator(original_pledge.amount.u128()); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); // add 2 delegations at genesis (because it makes things easier and as shown with previous tests // delegating at different times still work) @@ -454,8 +476,10 @@ mod tests { fn withdrawing_delegator_reward() { // essentially all delegators' rewards (and the operator itself) are still correctly computed let mut simulator = base_simulator(10000_000000); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); // add 2 delegations at genesis (because it makes things easier and as shown with previous tests // delegating at different times still work) @@ -524,7 +548,7 @@ mod tests { fn simulating_multiple_epochs() { let mut simulator = base_simulator(10000_000000); - let mut is_active = true; + let mut work_factor = simulator.legacy_active_work_factor(); let mut performance = Percent::from_percentage_value(100).unwrap(); for epoch in 0..720 { if epoch == 0 { @@ -538,7 +562,7 @@ mod tests { .unwrap() } if epoch == 89 { - is_active = false; + work_factor = simulator.legacy_standby_work_factor(); } if epoch == 123 { simulator @@ -560,7 +584,7 @@ mod tests { // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be } if epoch == 345 { - is_active = true; + work_factor = simulator.legacy_active_work_factor(); } if epoch == 358 { performance = Percent::from_percentage_value(100).unwrap(); @@ -579,7 +603,7 @@ mod tests { // this has to always hold check_rewarding_invariant(&simulator); - let node_params = NodeRewardParams::new(performance, is_active); + let node_params = NodeRewardingParameters::new(performance, work_factor); simulator.simulate_epoch_single_node(node_params).unwrap(); } @@ -600,15 +624,23 @@ mod tests { // rather than just checking the final results let epochs_in_interval = 1u32; - let rewarded_set_size = 10; - let active_set_size = 6; let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + // the import values here are active set being 6 and rewarded set being 10 + // since those are the values we were using in the past + let rewarded_set = RewardedSetParams { + entry_gateways: 1, + exit_gateways: 2, + mixnodes: 3, + standby: 4, + }; + let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; let epoch_reward_budget = interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); - let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + let stake_saturation_point = + Decimal::from_ratio(staking_supply, rewarded_set.rewarded_set_size()); let rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -621,8 +653,7 @@ mod tests { active_set_work_factor: Decimal::percent(1000), // value '10' interval_pool_emission, }, - rewarded_set_size, - active_set_size, + rewarded_set, }; let interval = Interval::init_interval( @@ -636,7 +667,7 @@ mod tests { let n0 = simulator .bond( Coin::new(11_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -649,7 +680,7 @@ mod tests { let n1 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -662,7 +693,7 @@ mod tests { let n2 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -675,7 +706,7 @@ mod tests { let n3 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(0).unwrap(), interval_operating_cost: Coin::new(500_000_000, "unym"), }, @@ -688,7 +719,7 @@ mod tests { let n4 = simulator .bond( Coin::new(1000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -701,7 +732,7 @@ mod tests { let n5 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -714,7 +745,7 @@ mod tests { let n6 = simulator .bond( Coin::new(11_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -727,7 +758,7 @@ mod tests { let n7 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -740,7 +771,7 @@ mod tests { let n8 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(0).unwrap(), interval_operating_cost: Coin::new(500_000_000, "unym"), }, @@ -753,7 +784,7 @@ mod tests { let n9 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -767,17 +798,20 @@ mod tests { let uptime_09 = Percent::from_percentage_value(90).unwrap(); let uptime_0 = Percent::from_percentage_value(0).unwrap(); + let active_work = simulator.legacy_active_work_factor(); + let standby_work = simulator.legacy_standby_work_factor(); + let node_params = [ - (n0, NodeRewardParams::new(uptime_1, true)), - (n1, NodeRewardParams::new(uptime_1, true)), - (n2, NodeRewardParams::new(uptime_1, true)), - (n3, NodeRewardParams::new(uptime_09, true)), - (n4, NodeRewardParams::new(uptime_09, true)), - (n5, NodeRewardParams::new(uptime_0, true)), - (n6, NodeRewardParams::new(uptime_1, false)), - (n7, NodeRewardParams::new(uptime_1, false)), - (n8, NodeRewardParams::new(uptime_09, false)), - (n9, NodeRewardParams::new(uptime_0, false)), + (n0, NodeRewardingParameters::new(uptime_1, active_work)), + (n1, NodeRewardingParameters::new(uptime_1, active_work)), + (n2, NodeRewardingParameters::new(uptime_1, active_work)), + (n3, NodeRewardingParameters::new(uptime_09, active_work)), + (n4, NodeRewardingParameters::new(uptime_09, active_work)), + (n5, NodeRewardingParameters::new(uptime_0, active_work)), + (n6, NodeRewardingParameters::new(uptime_1, standby_work)), + (n7, NodeRewardingParameters::new(uptime_1, standby_work)), + (n8, NodeRewardingParameters::new(uptime_09, standby_work)), + (n9, NodeRewardingParameters::new(uptime_0, standby_work)), ] .into_iter() .collect::>(); diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs index 12a9e967bd..bc6d18a16e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{Delegation, EpochId, MixId, MixNodeCostParams, MixNodeRewarding}; +use crate::{Delegation, EpochId, NodeCostParams, NodeId, NodeRewarding}; use cosmwasm_std::{Addr, Coin}; use std::collections::HashMap; @@ -9,21 +9,21 @@ use crate::error::MixnetContractError; use crate::rewarding::helpers::truncate_reward; pub struct SimulatedNode { - pub mix_id: MixId, - pub rewarding_details: MixNodeRewarding, + pub mix_id: NodeId, + pub rewarding_details: NodeRewarding, pub delegations: HashMap, } impl SimulatedNode { pub fn new( - mix_id: MixId, - cost_params: MixNodeCostParams, + mix_id: NodeId, + cost_params: NodeCostParams, initial_pledge: &Coin, current_epoch: EpochId, ) -> Result { Ok(SimulatedNode { mix_id, - rewarding_details: MixNodeRewarding::initialise_new( + rewarding_details: NodeRewarding::initialise_new( cost_params, initial_pledge, current_epoch, @@ -59,8 +59,8 @@ impl SimulatedNode { ) -> Result<(Coin, Coin), MixnetContractError> { let delegator = delegator.into(); let delegation = self.delegations.remove(&delegator).ok_or( - MixnetContractError::NoMixnodeDelegationFound { - mix_id: MixId::MAX, + MixnetContractError::NodeDelegationNotFound { + node_id: NodeId::MAX, address: delegator, proxy: None, }, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs index 261fa99ff0..00130d52d8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::families::FamilyHead; -use crate::{Gateway, IdentityKey, MixNode, MixNodeCostParams}; +use crate::nym_node::NymNode; +use crate::{Gateway, MixNode, NodeCostParams}; use contracts_common::signing::{ ContractMessageContent, LegacyContractMessageContent, MessageType, Nonce, SignableMessage, SigningPurpose, @@ -12,20 +12,20 @@ use serde::Serialize; pub type SignableMixNodeBondingMsg = SignableMessage>; pub type SignableGatewayBondingMsg = SignableMessage>; +pub type SignableNymNodeBondingMsg = SignableMessage>; pub type SignableLegacyMixNodeBondingMsg = SignableMessage>; pub type SignableLegacyGatewayBondingMsg = SignableMessage>; -pub type SignableFamilyJoinPermitMsg = SignableMessage; #[derive(Serialize)] pub struct MixnodeBondingPayload { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, } impl MixnodeBondingPayload { - pub fn new(mix_node: MixNode, cost_params: MixNodeCostParams) -> Self { + pub fn new(mix_node: MixNode, cost_params: NodeCostParams) -> Self { Self { mix_node, cost_params, @@ -44,7 +44,7 @@ pub fn construct_mixnode_bonding_sign_payload( sender: Addr, pledge: Coin, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, ) -> SignableMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); let content = ContractMessageContent::new(sender, vec![pledge], payload); @@ -57,7 +57,7 @@ pub fn construct_legacy_mixnode_bonding_sign_payload( sender: Addr, pledge: Coin, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, ) -> SignableLegacyMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); let content: LegacyContractMessageContent<_> = @@ -109,39 +109,48 @@ pub fn construct_legacy_gateway_bonding_sign_payload( } #[derive(Serialize)] -pub struct FamilyJoinPermit { - // the granter of this permit - family_head: FamilyHead, - // the actual member we want to permit to join - member_node: IdentityKey, +pub struct NymNodeBondingPayload { + nym_node: NymNode, + cost_params: NodeCostParams, } -impl FamilyJoinPermit { - pub fn new(family_head: FamilyHead, member_node: IdentityKey) -> Self { - Self { - family_head, - member_node, +impl NymNodeBondingPayload { + pub fn new(nym_node: NymNode, cost_params: NodeCostParams) -> Self { + NymNodeBondingPayload { + nym_node, + cost_params, } } } -impl SigningPurpose for FamilyJoinPermit { +impl SigningPurpose for NymNodeBondingPayload { fn message_type() -> MessageType { - MessageType::new("family-join-permit") + MessageType::new("nym-node-bonding") } } -pub fn construct_family_join_permit( +pub fn construct_nym_node_bonding_sign_payload( nonce: Nonce, - family_head: FamilyHead, - member_node: IdentityKey, -) -> SignableFamilyJoinPermitMsg { - let payload = FamilyJoinPermit::new(family_head, member_node); + sender: Addr, + pledge: Coin, + nym_node: NymNode, + cost_params: NodeCostParams, +) -> SignableNymNodeBondingMsg { + let payload = NymNodeBondingPayload::new(nym_node, cost_params); + let content = ContractMessageContent::new(sender, vec![pledge], payload); - // note: we're NOT wrapping it in `ContractMessageContent` because the family head is not going to be the one - // sending the message to the contract - SignableMessage::new(nonce, payload) + SignableMessage::new(nonce, content) } -// TODO: depending on our threat model, we should perhaps extend it to include all _on_behalf methods -// (update: but we trust our vesting contract since its compromise would be even more devastating so there's no need) +pub fn construct_generic_node_bonding_payload( + nonce: Nonce, + sender: Addr, + pledge: Coin, + payload: T, +) -> SignableMessage> +where + T: SigningPurpose, +{ + let content = ContractMessageContent::new(sender, vec![pledge], payload); + SignableMessage::new(nonce, content) +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index f95c15eb34..35aac81f53 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -1,22 +1,127 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::MixnetContractError; -use crate::Layer; +use crate::nym_node::Role; use contracts_common::Percent; use cosmwasm_schema::cw_serde; use cosmwasm_std::Coin; use cosmwasm_std::{Addr, Uint128}; use std::fmt::{Display, Formatter}; -use std::ops::Index; // type aliases for better reasoning about available data pub type SphinxKey = String; pub type SphinxKeyRef<'a> = &'a str; -pub type MixId = u32; +pub type NodeId = u32; pub type BlockHeight = u64; +#[cw_serde] +pub struct RoleAssignment { + pub role: Role, + pub nodes: Vec, +} + +impl RoleAssignment { + pub fn is_final_assignment(&self) -> bool { + self.role.is_standby() + } +} + +#[cw_serde] +#[derive(Default)] +pub struct RewardedSet { + pub entry_gateways: Vec, + + pub exit_gateways: Vec, + + pub layer1: Vec, + + pub layer2: Vec, + + pub layer3: Vec, + + pub standby: Vec, +} + +impl RewardedSet { + pub fn is_empty(&self) -> bool { + self.entry_gateways.is_empty() + && self.exit_gateways.is_empty() + && self.layer1.is_empty() + && self.layer2.is_empty() + && self.layer3.is_empty() + && self.standby.is_empty() + } + + pub fn active_set_size(&self) -> usize { + self.entry_gateways.len() + + self.exit_gateways.len() + + self.layer1.len() + + self.layer2.len() + + self.layer3.len() + } + + pub fn rewarded_set_size(&self) -> usize { + self.active_set_size() + self.standby.len() + } + + pub fn try_get_mix_layer(&self, node_id: &NodeId) -> Option { + if self.layer1.contains(node_id) { + Some(1) + } else if self.layer2.contains(node_id) { + Some(2) + } else if self.layer3.contains(node_id) { + Some(3) + } else { + None + } + } + + pub fn is_entry(&self, node_id: &NodeId) -> bool { + self.entry_gateways.contains(node_id) + } + + pub fn is_exit(&self, node_id: &NodeId) -> bool { + self.exit_gateways.contains(node_id) + } + + pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { + self.layer1.contains(node_id) + || self.layer2.contains(node_id) + || self.layer3.contains(node_id) + } + + pub fn gateways(&self) -> Vec { + let mut gateways = Vec::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); + for entry in &self.entry_gateways { + gateways.push(*entry) + } + for exit in &self.exit_gateways { + gateways.push(*exit) + } + gateways + } + + pub fn active_mixnodes(&self) -> Vec { + let mut mixnodes = + Vec::with_capacity(self.layer1.len() + self.layer2.len() + self.layer3.len()); + for mix in &self.layer1 { + mixnodes.push(*mix) + } + for mix in &self.layer2 { + mixnodes.push(*mix) + } + for mix in &self.layer3 { + mixnodes.push(*mix) + } + mixnodes + } + + pub fn is_standby(&self, node_id: &NodeId) -> bool { + self.standby.contains(node_id) + } +} + #[cw_serde] pub struct RangedValue { pub minimum: T, @@ -76,113 +181,6 @@ where } } -/// Specifies layer assignment for the given mixnode. -#[cw_serde] -pub struct LayerAssignment { - /// The id of the mixnode. - mix_id: MixId, - - /// The layer to which it's going to be assigned - layer: Layer, -} - -impl LayerAssignment { - pub fn new(mix_id: MixId, layer: Layer) -> Self { - LayerAssignment { mix_id, layer } - } - - pub fn mix_id(&self) -> MixId { - self.mix_id - } - - pub fn layer(&self) -> Layer { - self.layer - } -} - -/// The current layer distribution of the mix network. -#[cw_serde] -#[derive(Copy, Default)] -pub struct LayerDistribution { - /// Number of nodes on the first layer. - pub layer1: u64, - - /// Number of nodes on the second layer. - pub layer2: u64, - - /// Number of nodes on the third layer. - pub layer3: u64, -} - -impl LayerDistribution { - pub fn choose_with_fewest(&self) -> Layer { - let layers = [ - (Layer::One, self.layer1), - (Layer::Two, self.layer2), - (Layer::Three, self.layer3), - ]; - - // we explicitly put 3 elements into the iterator, so the iterator is DEFINITELY - // not empty and thus the unwrap cannot fail - #[allow(clippy::unwrap_used)] - layers.iter().min_by_key(|x| x.1).unwrap().0 - } - - pub fn increment_layer_count(&mut self, layer: Layer) { - match layer { - Layer::One => self.layer1 += 1, - Layer::Two => self.layer2 += 1, - Layer::Three => self.layer3 += 1, - } - } - - pub fn decrement_layer_count(&mut self, layer: Layer) -> Result<(), MixnetContractError> { - match layer { - Layer::One => { - self.layer1 = - self.layer1 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer1, - subtrahend: 1, - })? - } - Layer::Two => { - self.layer2 = - self.layer2 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer2, - subtrahend: 1, - })? - } - Layer::Three => { - self.layer3 = - self.layer3 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer3, - subtrahend: 1, - })? - } - } - - Ok(()) - } -} - -impl Index for LayerDistribution { - type Output = u64; - - fn index(&self, index: Layer) -> &Self::Output { - match index { - Layer::One => &self.layer1, - Layer::Two => &self.layer2, - Layer::Three => &self.layer3, - } - } -} - /// The current state of the mixnet contract. #[cw_serde] pub struct ContractState { @@ -212,13 +210,10 @@ pub struct ContractState { #[cw_serde] pub struct ContractStateParams { /// Minimum amount a delegator must stake in orders for his delegation to get accepted. - pub minimum_mixnode_delegation: Option, + pub minimum_delegation: Option, - /// Minimum amount a mixnode must pledge to get into the system. - pub minimum_mixnode_pledge: Coin, - - /// Minimum amount a gateway must pledge to get into the system. - pub minimum_gateway_pledge: Coin, + /// Minimum amount a node must pledge to get into the system. + pub minimum_pledge: Coin, /// Defines the allowed profit margin range of operators. /// default: 0% - 100% diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs index 65825c6ad9..da31bdf30a 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs @@ -3,7 +3,7 @@ use crate::account::VestingAccountStorageKey; use cosmwasm_std::{Addr, Coin, OverflowError, StdError, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -50,7 +50,7 @@ pub enum VestingContractError { MultipleDenoms, #[error("VESTING ({}): No delegations found for account {0}, mix_identity {1}", line!())] - NoSuchDelegation(Addr, MixId), + NoSuchDelegation(Addr, NodeId), #[error("VESTING ({}): Only mixnet contract can perform this operation, got {0}", line!())] NotMixnetContract(Addr), @@ -95,7 +95,7 @@ pub enum VestingContractError { TooManyDelegations { address: Addr, acc_id: VestingAccountStorageKey, - mix_id: MixId, + mix_id: NodeId, num: u32, cap: u32, }, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 31fab7a7ef..741fde4f7b 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -6,7 +6,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; pub mod account; pub mod error; @@ -64,7 +64,7 @@ pub struct DelegationTimesResponse { pub account_id: u32, /// Id of the mixnode towards which the delegation was made - pub mix_id: MixId, + pub mix_id: NodeId, /// All timestamps where a delegation was made pub delegation_timestamps: Vec, @@ -77,7 +77,7 @@ pub struct AllDelegationsResponse { pub delegations: Vec, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option<(u32, MixId, u64)>, + pub start_next_after: Option<(u32, NodeId, u64)>, } /// Basic information regarding particular vesting account alongside the amount of vesting coins. diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index eefe07e9f1..303fe1e89f 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -5,11 +5,10 @@ use crate::{PledgeCap, VestingSpecification}; use contracts_common::signing::MessageSignature; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Timestamp}; -use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ gateway::GatewayConfigUpdate, - mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, - Gateway, IdentityKey, MixId, MixNode, + mixnode::{MixNodeConfigUpdate, NodeCostParams}, + Gateway, MixNode, NodeId, }; #[cfg(feature = "schema")] @@ -36,32 +35,16 @@ pub struct MigrateMsg {} #[cw_serde] pub enum ExecuteMsg { - // Families - /// Only owner of the node can crate the family with node as head - CreateFamily { - label: String, - }, - /// Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family - JoinFamily { - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamily { - family_head: FamilyHead, - }, - KickFamilyMember { - member: IdentityKey, - }, TrackReward { amount: Coin, address: String, }, ClaimOperatorReward {}, ClaimDelegatorReward { - mix_id: MixId, + mix_id: NodeId, }, UpdateMixnodeCostParams { - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, }, UpdateMixnodeConfig { new_config: MixNodeConfigUpdate, @@ -70,12 +53,12 @@ pub enum ExecuteMsg { address: String, }, DelegateToMixnode { - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, }, UndelegateFromMixnode { - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, }, CreateAccount { @@ -89,12 +72,12 @@ pub enum ExecuteMsg { }, TrackUndelegation { owner: String, - mix_id: MixId, + mix_id: NodeId, amount: Coin, }, BondMixnode { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, amount: Coin, }, @@ -142,17 +125,13 @@ pub enum ExecuteMsg { // no need to track migrated gateways as there are no vesting gateways on mainnet TrackMigratedDelegation { owner: String, - mix_id: MixId, + mix_id: NodeId, }, } impl ExecuteMsg { pub fn name(&self) -> &str { match self { - ExecuteMsg::CreateFamily { .. } => "VestingExecuteMsg::CreateFamily", - ExecuteMsg::JoinFamily { .. } => "VestingExecuteMsg::JoinFamily", - ExecuteMsg::LeaveFamily { .. } => "VestingExecuteMsg::LeaveFamily", - ExecuteMsg::KickFamilyMember { .. } => "VestingExecuteMsg::KickFamilyMember", ExecuteMsg::TrackReward { .. } => "VestingExecuteMsg::TrackReward", ExecuteMsg::ClaimOperatorReward { .. } => "VestingExecuteMsg::ClaimOperatorReward", ExecuteMsg::ClaimDelegatorReward { .. } => "VestingExecuteMsg::ClaimDelegatorReward", @@ -374,7 +353,7 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegation has been made. - mix_id: MixId, + mix_id: NodeId, /// Block timestamp of the delegation. block_timestamp_secs: u64, @@ -387,7 +366,7 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegations have been made. - mix_id: MixId, + mix_id: NodeId, }, /// Returns timestamps of delegations made towards particular mixnode by the provided vesting account address. @@ -397,14 +376,14 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegations have been made. - mix_id: MixId, + mix_id: NodeId, }, /// Returns all active delegations made with vesting tokens stored in this contract. #[cfg_attr(feature = "schema", returns(AllDelegationsResponse))] GetAllDelegations { /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option<(u32, MixId, u64)>, + start_after: Option<(u32, NodeId, u64)>, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs index e4c1644a94..da3adbbada 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs @@ -4,7 +4,7 @@ use contracts_common::Percent; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Timestamp, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use std::str::FromStr; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -155,7 +155,7 @@ pub struct VestingDelegation { pub account_id: u32, /// The id of the mixnode towards which the delegation has been made. - pub mix_id: MixId, + pub mix_id: NodeId, /// The block timestamp when the delegation has been made. pub block_timestamp: u64, @@ -165,7 +165,7 @@ pub struct VestingDelegation { } impl VestingDelegation { - pub fn storage_key(&self) -> (u32, MixId, u64) { + pub fn storage_key(&self) -> (u32, NodeId, u64) { (self.account_id, self.mix_id, self.block_timestamp) } } diff --git a/common/node-tester-utils/src/error.rs b/common/node-tester-utils/src/error.rs index 96337d0bc6..4c83ae7d3d 100644 --- a/common/node-tester-utils/src/error.rs +++ b/common/node-tester-utils/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::MixId; +use crate::NodeId; use nym_sphinx::chunking::ChunkingError; use nym_sphinx::receiver::MessageRecoveryError; use nym_topology::NymTopologyError; @@ -19,7 +19,7 @@ pub enum NetworkTestingError { InvalidTopology(#[from] NymTopologyError), #[error("The specified mixnode (id: {mix_id}) doesn't exist")] - NonExistentMixnode { mix_id: MixId }, + NonExistentMixnode { mix_id: NodeId }, #[error("The specified mixnode (identity: {mix_identity}) doesn't exist")] NonExistentMixnodeIdentity { mix_identity: String }, diff --git a/common/node-tester-utils/src/lib.rs b/common/node-tester-utils/src/lib.rs index 91d2c7c03f..444d1780f8 100644 --- a/common/node-tester-utils/src/lib.rs +++ b/common/node-tester-utils/src/lib.rs @@ -15,7 +15,7 @@ pub use nym_sphinx::{ pub use tester::NodeTester; // it feels wrong to redefine it, but I don't want to import the whole of contract commons just for this one type -pub(crate) type MixId = u32; +pub(crate) type NodeId = u32; #[macro_export] macro_rules! log_err { diff --git a/common/node-tester-utils/src/message.rs b/common/node-tester-utils/src/message.rs index 41dc517956..b17515d5b6 100644 --- a/common/node-tester-utils/src/message.rs +++ b/common/node-tester-utils/src/message.rs @@ -3,6 +3,7 @@ use crate::error::NetworkTestingError; use crate::node::TestableNode; +use crate::NodeId; use nym_sphinx::message::NymMessage; use nym_topology::{gateway, mix}; use serde::de::DeserializeOwned; @@ -34,13 +35,13 @@ impl TestMessage { } } - pub fn new_mix(node: &mix::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { + pub fn new_mix(node: &mix::LegacyNode, msg_id: u32, total_msgs: u32, ext: T) -> Self { Self::new(node, msg_id, total_msgs, ext) } - pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { - Self::new(node, msg_id, total_msgs, ext) - } + // pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { + // Self::new(node, msg_id, total_msgs, ext) + // } pub fn new_serialized( node: N, @@ -72,7 +73,7 @@ impl TestMessage { } pub fn mix_plaintexts( - node: &mix::Node, + node: &mix::LegacyNode, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> @@ -82,15 +83,16 @@ impl TestMessage { Self::new_plaintexts(node, total_msgs, ext) } - pub fn gateway_plaintexts( - node: &gateway::Node, + pub fn legacy_gateway_plaintexts( + node: &gateway::LegacyNode, + node_id: NodeId, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> where T: Serialize + Clone, { - Self::new_plaintexts(node, total_msgs, ext) + Self::new_plaintexts(&(node, node_id), total_msgs, ext) } pub fn as_json_string(&self) -> Result diff --git a/common/node-tester-utils/src/node.rs b/common/node-tester-utils/src/node.rs index ed4aa3b52e..d60623a6e2 100644 --- a/common/node-tester-utils/src/node.rs +++ b/common/node-tester-utils/src/node.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::MixId; +use crate::NodeId; use nym_topology::{gateway, mix}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; @@ -9,27 +9,27 @@ use std::fmt::{Display, Formatter}; #[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] pub struct TestableNode { pub encoded_identity: String, - pub owner: String, + pub node_id: NodeId, #[serde(rename = "type")] pub typ: NodeType, } impl TestableNode { - pub fn new(encoded_identity: String, owner: String, typ: NodeType) -> Self { + pub fn new(encoded_identity: String, typ: NodeType, node_id: NodeId) -> Self { TestableNode { encoded_identity, - owner, + node_id, typ, } } - pub fn new_mixnode(encoded_identity: String, owner: String, mix_id: MixId) -> Self { - TestableNode::new(encoded_identity, owner, NodeType::Mixnode { mix_id }) + pub fn new_mixnode(encoded_identity: String, node_id: NodeId) -> Self { + TestableNode::new(encoded_identity, NodeType::Mixnode, node_id) } - pub fn new_gateway(encoded_identity: String, owner: String) -> Self { - TestableNode::new(encoded_identity, owner, NodeType::Gateway) + pub fn new_gateway(encoded_identity: String, node_id: NodeId) -> Self { + TestableNode::new(encoded_identity, NodeType::Gateway, node_id) } pub fn is_mixnode(&self) -> bool { @@ -37,24 +37,34 @@ impl TestableNode { } } -impl<'a> From<&'a mix::Node> for TestableNode { - fn from(value: &'a mix::Node) -> Self { +impl<'a> From<&'a mix::LegacyNode> for TestableNode { + fn from(value: &'a mix::LegacyNode) -> Self { TestableNode { encoded_identity: value.identity_key.to_base58_string(), - owner: value.owner.as_ref().cloned().unwrap_or_default(), - typ: NodeType::Mixnode { - mix_id: value.mix_id, - }, + typ: NodeType::Mixnode, + node_id: value.mix_id, } } } -impl<'a> From<&'a gateway::Node> for TestableNode { - fn from(value: &'a gateway::Node) -> Self { +impl<'a> From<(&'a gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): (&'a gateway::LegacyNode, NodeId)) -> Self { + (&(gateway, node_id)).into() + } +} + +impl<'a> From<&'a (gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): &'a (gateway::LegacyNode, NodeId)) -> Self { + (gateway, *node_id).into() + } +} + +impl<'a, 'b> From<&'a (&'b gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): &'a (&'b gateway::LegacyNode, NodeId)) -> Self { TestableNode { - encoded_identity: value.identity_key.to_base58_string(), - owner: value.owner.as_ref().cloned().unwrap_or_default(), + encoded_identity: gateway.identity_key.to_base58_string(), typ: NodeType::Gateway, + node_id: *node_id, } } } @@ -63,8 +73,8 @@ impl Display for TestableNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{} {} owned by {}", - self.typ, self.encoded_identity, self.owner + "{}-{}: {}", + self.typ, self.node_id, self.encoded_identity ) } } @@ -72,7 +82,7 @@ impl Display for TestableNode { #[derive(Serialize, Deserialize, Hash, Clone, Copy, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum NodeType { - Mixnode { mix_id: MixId }, + Mixnode, Gateway, } @@ -85,7 +95,7 @@ impl NodeType { impl Display for NodeType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - NodeType::Mixnode { mix_id } => write!(f, "mixnode (mix_id {mix_id})"), + NodeType::Mixnode => write!(f, "mixnode"), NodeType::Gateway => write!(f, "gateway"), } } diff --git a/common/node-tester-utils/src/tester.rs b/common/node-tester-utils/src/tester.rs index e31d632cc7..2697c60831 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -3,7 +3,7 @@ use crate::error::NetworkTestingError; use crate::Empty; -use crate::MixId; +use crate::NodeId; use crate::TestMessage; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; @@ -76,13 +76,13 @@ where self } - pub fn testable_mix_topology(&self, node: &mix::Node) -> NymTopology { + pub fn testable_mix_topology(&self, node: &mix::LegacyNode) -> NymTopology { let mut topology = self.base_topology.clone(); topology.set_mixes_in_layer(node.layer as u8, vec![node.clone()]); topology } - pub fn testable_gateway_topology(&self, gateway: &gateway::Node) -> NymTopology { + pub fn testable_gateway_topology(&self, gateway: &gateway::LegacyNode) -> NymTopology { let mut topology = self.base_topology.clone(); topology.set_gateways(vec![gateway.clone()]); topology @@ -90,7 +90,7 @@ where pub fn simple_mixnode_test_packets( &mut self, - mix: &mix::Node, + mix: &mix::LegacyNode, test_packets: u32, ) -> Result, NetworkTestingError> { self.mixnode_test_packets(mix, Empty, test_packets, None) @@ -98,7 +98,7 @@ where pub fn mixnode_test_packets( &mut self, - mix: &mix::Node, + mix: &mix::LegacyNode, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -122,7 +122,7 @@ where pub fn mixnodes_test_packets( &mut self, - nodes: &[mix::Node], + nodes: &[mix::LegacyNode], msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -145,7 +145,7 @@ where pub fn existing_mixnode_test_packets( &mut self, - mix_id: MixId, + mix_id: NodeId, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -182,9 +182,10 @@ where self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) } - pub fn gateway_test_packets( + pub fn legacy_gateway_test_packets( &mut self, - gateway: &gateway::Node, + gateway: &gateway::LegacyNode, + node_id: NodeId, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -195,7 +196,9 @@ where let ephemeral_topology = self.testable_gateway_topology(gateway); let mut packets = Vec::with_capacity(test_packets as usize); - for plaintext in TestMessage::gateway_plaintexts(gateway, test_packets, msg_ext)? { + for plaintext in + TestMessage::legacy_gateway_plaintexts(gateway, node_id, test_packets, msg_ext)? + { packets.push(self.wrap_plaintext_data( plaintext, &ephemeral_topology, @@ -208,6 +211,7 @@ where pub fn existing_gateway_test_packets( &mut self, + node_id: NodeId, encoded_gateway_identity: String, msg_ext: T, test_packets: u32, @@ -222,7 +226,13 @@ where }); }; - self.gateway_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) + self.legacy_gateway_test_packets( + &node.clone(), + node_id, + msg_ext, + test_packets, + custom_recipient, + ) } pub fn wrap_plaintext_data( diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index ac1857fd82..9799d774fd 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -220,7 +220,7 @@ impl Default for SphinxMessageReceiver { mod message_receiver { use super::*; use nym_crypto::asymmetric::identity; - use nym_mixnet_contract_common::Layer; + use nym_mixnet_contract_common::LegacyMixLayer; use nym_topology::{gateway, mix, NymTopology}; use std::collections::BTreeMap; @@ -233,7 +233,7 @@ mod message_receiver { let mut mixes = BTreeMap::new(); mixes.insert( 1, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 123, owner: None, host: "10.20.30.40".parse().unwrap(), @@ -246,14 +246,14 @@ mod message_receiver { "B3GzG62aXAZNg14RoMCp3BhELNBrySLr2JqrwyfYFzRc", ) .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "0.8.0-dev".into(), }], ); mixes.insert( 2, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 234, owner: None, host: "11.21.31.41".parse().unwrap(), @@ -266,14 +266,14 @@ mod message_receiver { "5Z1VqYwM2xeKxd8H7fJpGWasNiDFijYBAee7MErkZ5QT", ) .unwrap(), - layer: Layer::Two, + layer: LegacyMixLayer::Two, version: "0.8.0-dev".into(), }], ); mixes.insert( 3, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 456, owner: None, host: "12.22.32.42".parse().unwrap(), @@ -286,7 +286,7 @@ mod message_receiver { "9EyjhCggr2QEA2nakR88YHmXgpy92DWxoe2draDRkYof", ) .unwrap(), - layer: Layer::Three, + layer: LegacyMixLayer::Three, version: "0.8.0-dev".into(), }], ); @@ -294,7 +294,7 @@ mod message_receiver { NymTopology::new( // currently coco_nodes don't really exist so this is still to be determined mixes, - vec![gateway::Node { + vec![gateway::LegacyNode { owner: None, host: "1.2.3.4".parse().unwrap(), mix_host: "1.2.3.4:1789".parse().unwrap(), diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index ae20786400..3596b7e38f 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -2,13 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{filter, NetworkAddress, NodeVersion}; -use nym_api_requests::models::DescribedGateway; +use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::GatewayBond; +use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::{NodeIdentity, NymNodeRoutingAddress}; use nym_sphinx_types::Node as SphinxNode; - -use nym_api_requests::nym_nodes::SkimmedNode; use rand::seq::SliceRandom; use rand::thread_rng; use std::fmt; @@ -49,7 +47,9 @@ pub enum GatewayConversionError { } #[derive(Clone)] -pub struct Node { +pub struct LegacyNode { + pub node_id: NodeId, + pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node @@ -65,15 +65,13 @@ pub struct Node { pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 // to be removed: - pub owner: Option, pub version: NodeVersion, } -impl std::fmt::Debug for Node { +impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("gateway::Node") .field("host", &self.host) - .field("owner", &self.owner) .field("mix_host", &self.mix_host) .field("clients_ws_port", &self.clients_ws_port) .field("clients_wss_port", &self.clients_wss_port) @@ -84,7 +82,7 @@ impl std::fmt::Debug for Node { } } -impl Node { +impl LegacyNode { pub fn parse_host(raw: &str) -> Result { // safety: this conversion is infallible // (but we retain result return type for legacy reasons) @@ -122,25 +120,21 @@ impl Node { } } -impl fmt::Display for Node { +impl fmt::Display for LegacyNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Node(id: {}, owner: {:?}, host: {})", - self.identity_key, self.owner, self.host, - ) + write!(f, "legacy gateway {} @ {}", self.node_id, self.host) } } -impl filter::Versioned for Node { +impl filter::Versioned for LegacyNode { fn version(&self) -> String { // TODO: return semver instead self.version.to_string() } } -impl<'a> From<&'a Node> for SphinxNode { - fn from(node: &'a Node) -> Self { +impl<'a> From<&'a LegacyNode> for SphinxNode { + fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) .try_into() .unwrap(); @@ -149,83 +143,7 @@ impl<'a> From<&'a Node> for SphinxNode { } } -impl<'a> TryFrom<&'a GatewayBond> for Node { - type Error = GatewayConversionError; - - fn try_from(bond: &'a GatewayBond) -> Result { - let host = Self::parse_host(&bond.gateway.host)?; - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = Self::extract_mix_host(&host, bond.gateway.mix_port)?; - - Ok(Node { - owner: Some(bond.owner.as_str().to_owned()), - host, - mix_host, - clients_ws_port: bond.gateway.clients_port, - clients_wss_port: None, - identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, - version: bond.gateway.version.as_str().into(), - }) - } -} - -impl TryFrom for Node { - type Error = GatewayConversionError; - - fn try_from(bond: GatewayBond) -> Result { - Node::try_from(&bond) - } -} - -impl<'a> TryFrom<&'a DescribedGateway> for Node { - type Error = GatewayConversionError; - - fn try_from(value: &'a DescribedGateway) -> Result { - let Some(ref self_described) = value.self_described else { - return (&value.bond).try_into(); - }; - - let ips = &self_described.host_information.ip_address; - if ips.is_empty() { - return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: value.bond.gateway.identity_key.clone(), - }); - } - - let host = match &self_described.host_information.hostname { - None => NetworkAddress::IpAddr(ips[0]), - Some(hostname) => NetworkAddress::Hostname(hostname.clone()), - }; - - // get ip from the self-reported values so we wouldn't need to do any hostname resolution - // (which doesn't really work in wasm) - let mix_host = SocketAddr::new(ips[0], value.bond.gateway.mix_port); - - Ok(Node { - owner: Some(value.bond.owner.as_str().to_owned()), - host, - mix_host, - clients_ws_port: self_described.mixnet_websockets.ws_port, - clients_wss_port: self_described.mixnet_websockets.wss_port, - identity_key: identity::PublicKey::from_base58_string( - &self_described.host_information.keys.ed25519, - )?, - sphinx_key: encryption::PublicKey::from_base58_string( - &self_described.host_information.keys.x25519, - )?, - version: self_described - .build_information - .build_version - .as_str() - .into(), - }) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for Node { +impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { type Error = GatewayConversionError; fn try_from(value: &'a SkimmedNode) -> Result { @@ -249,23 +167,15 @@ impl<'a> TryFrom<&'a SkimmedNode> for Node { NetworkAddress::IpAddr(*ip) }; - Ok(Node { + Ok(LegacyNode { + node_id: value.node_id, host, mix_host: SocketAddr::new(*ip, value.mix_port), clients_ws_port: entry_details.ws_port, clients_wss_port: entry_details.wss_port, identity_key: value.ed25519_identity_pubkey.parse()?, sphinx_key: value.x25519_sphinx_pubkey.parse()?, - owner: None, version: NodeVersion::Unknown, }) } } - -impl TryFrom for Node { - type Error = GatewayConversionError; - - fn try_from(value: DescribedGateway) -> Result { - Node::try_from(&value) - } -} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 5630122d0c..62c0c378bc 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -7,17 +7,15 @@ use crate::filter::VersionFilterable; pub use error::NymTopologyError; use log::{debug, info, warn}; -use mix::Node; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; use nym_config::defaults::var_names::NYM_API; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use nym_mixnet_contract_common::{IdentityKeyRef, NodeId}; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; use rand::prelude::SliceRandom; use rand::{CryptoRng, Rng}; use std::collections::BTreeMap; use std::convert::Infallible; - use std::fmt::{self, Display, Formatter}; use std::io; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; @@ -25,7 +23,6 @@ use std::str::FromStr; #[cfg(feature = "serializable")] use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; -use nym_api_requests::models::DescribedGateway; pub mod error; pub mod filter; @@ -118,45 +115,54 @@ impl Display for NetworkAddress { pub type MixLayer = u8; +// the reason for those having `Legacy` prefix is that eventually they should be using +// exactly the same types #[derive(Debug, Clone, Default)] pub struct NymTopology { - mixes: BTreeMap>, - gateways: Vec, + mixes: BTreeMap>, + gateways: Vec, } impl NymTopology { pub async fn new_from_env() -> Result { let api_url = std::env::var(NYM_API)?; - info!("Generating topology from {}", api_url); + info!("Generating topology from {api_url}"); - let mixnodes = reqwest::get(&format!("{}/v1/mixnodes", api_url)) + let mixnodes = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/mixnodes/skimmed",)) .await? - .json::>() + .json::>() .await? - .into_iter() - .map(|details| details.bond_information) - .map(mix::Node::try_from) + .nodes + .iter() + .map(mix::LegacyNode::try_from) .filter(Result::is_ok) .collect::, _>>()?; - let gateways = reqwest::get(&format!("{}/v1/gateways", api_url)) + let gateways = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/gateways/skimmed",)) .await? - .json::>() + .json::>() .await? - .into_iter() - .map(gateway::Node::try_from) + .nodes + .iter() + .map(gateway::LegacyNode::try_from) .filter(Result::is_ok) .collect::, _>>()?; let topology = NymTopology::new_unordered(mixnodes, gateways); Ok(topology) } - pub fn new(mixes: BTreeMap>, gateways: Vec) -> Self { + pub fn new( + mixes: BTreeMap>, + gateways: Vec, + ) -> Self { NymTopology { mixes, gateways } } - pub fn new_unordered(unordered_mixes: Vec, gateways: Vec) -> Self { + pub fn new_unordered( + unordered_mixes: Vec, + gateways: Vec, + ) -> Self { let mut mixes = BTreeMap::new(); for node in unordered_mixes.into_iter() { let layer = node.layer as MixLayer; @@ -171,10 +177,10 @@ impl NymTopology { where MI: Iterator, GI: Iterator, - G: TryInto, - M: TryInto, - >::Error: Display, - >::Error: Display, + G: TryInto, + M: TryInto, + >::Error: Display, + >::Error: Display, { let mut mixes = BTreeMap::new(); let mut gateways = Vec::new(); @@ -205,14 +211,11 @@ impl NymTopology { serde_json::from_reader(file).map_err(Into::into) } - pub fn from_detailed( - mix_details: Vec, - gateway_bonds: Vec, - ) -> Self { - nym_topology_from_detailed(mix_details, gateway_bonds) + pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { + nym_topology_from_basic_info(basic_mixes, basic_gateways) } - pub fn find_mix(&self, mix_id: MixId) -> Option<&mix::Node> { + pub fn find_mix(&self, mix_id: NodeId) -> Option<&mix::LegacyNode> { for nodes in self.mixes.values() { for node in nodes { if node.mix_id == mix_id { @@ -223,7 +226,10 @@ impl NymTopology { None } - pub fn find_mix_by_identity(&self, mixnode_identity: IdentityKeyRef) -> Option<&mix::Node> { + pub fn find_mix_by_identity( + &self, + mixnode_identity: IdentityKeyRef, + ) -> Option<&mix::LegacyNode> { for nodes in self.mixes.values() { for node in nodes { if node.identity_key.to_base58_string() == mixnode_identity { @@ -234,13 +240,13 @@ impl NymTopology { None } - pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::Node> { + pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::LegacyNode> { self.gateways .iter() .find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity) } - pub fn mixes(&self) -> &BTreeMap> { + pub fn mixes(&self) -> &BTreeMap> { &self.mixes } @@ -248,8 +254,8 @@ impl NymTopology { self.mixes.values().map(|m| m.len()).sum() } - pub fn mixes_as_vec(&self) -> Vec { - let mut mixes: Vec = vec![]; + pub fn mixes_as_vec(&self) -> Vec { + let mut mixes: Vec = vec![]; for layer in self.mixes().values() { mixes.extend(layer.to_owned()) @@ -257,20 +263,20 @@ impl NymTopology { mixes } - pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { + pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { assert!([1, 2, 3].contains(&layer)); self.mixes.get(&layer).unwrap().to_owned() } - pub fn gateways(&self) -> &[gateway::Node] { + pub fn gateways(&self) -> &[gateway::LegacyNode] { &self.gateways } - pub fn get_gateways(&self) -> Vec { + pub fn get_gateways(&self) -> Vec { self.gateways.clone() } - pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::Node> { + pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::LegacyNode> { self.gateways .iter() .find(|gateway| gateway.identity() == gateway_identity) @@ -280,11 +286,11 @@ impl NymTopology { self.get_gateway(gateway_identity).is_some() } - pub fn set_gateways(&mut self, gateways: Vec) { + pub fn set_gateways(&mut self, gateways: Vec) { self.gateways = gateways } - pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::Node, NymTopologyError> + pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::LegacyNode, NymTopologyError> where R: Rng + CryptoRng, { @@ -299,7 +305,7 @@ impl NymTopology { &self, rng: &mut R, num_mix_hops: u8, - ) -> Result, NymTopologyError> + ) -> Result, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -335,7 +341,7 @@ impl NymTopology { rng: &mut R, num_mix_hops: u8, gateway_identity: &NodeIdentity, - ) -> Result<(Vec, gateway::Node), NymTopologyError> + ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -376,7 +382,7 @@ impl NymTopology { } /// Overwrites the existing nodes in the specified layer - pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { + pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { self.mixes.insert(layer, mixes); } @@ -491,66 +497,33 @@ impl<'de> Deserialize<'de> for NymTopology { } } -pub trait IntoGatewayNode: TryInto -where - >::Error: Display, -{ - fn identity(&self) -> IdentityKeyRef; -} - -impl IntoGatewayNode for GatewayBond { - fn identity(&self) -> IdentityKeyRef { - &self.gateway.identity_key - } -} - -impl IntoGatewayNode for DescribedGateway { - fn identity(&self) -> IdentityKeyRef { - &self.bond.gateway.identity_key - } -} - -pub fn nym_topology_from_detailed( - mix_details: Vec, - gateway_bonds: Vec, -) -> NymTopology -where - G: IntoGatewayNode, - >::Error: Display, -{ +pub fn nym_topology_from_basic_info( + basic_mixes: &[SkimmedNode], + basic_gateways: &[SkimmedNode], +) -> NymTopology { let mut mixes = BTreeMap::new(); - for bond in mix_details - .into_iter() - .map(|details| details.bond_information) - { - let layer = bond.layer as MixLayer; - if layer == 0 || layer > 3 { - warn!( - "{} says it's on invalid layer {layer}!", - bond.mix_node.identity_key - ); + for mix in basic_mixes { + let Some(layer) = mix.get_mix_layer() else { + warn!("node {} doesn't have any assigned mix layer!", mix.node_id); continue; - } - let mix_id = bond.mix_id; - let mix_identity = bond.mix_node.identity_key.clone(); + }; let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); - match bond.try_into() { + match mix.try_into() { Ok(mix) => layer_entry.push(mix), Err(err) => { - warn!("Mix {mix_id} / {mix_identity} is malformed: {err}"); + warn!("node (mixnode) {} is malformed: {err}", mix.node_id); continue; } } } - let mut gateways = Vec::with_capacity(gateway_bonds.len()); - for bond in gateway_bonds.into_iter() { - let gate_id = bond.identity().to_owned(); - match bond.try_into() { + let mut gateways = Vec::with_capacity(basic_gateways.len()); + for gateway in basic_gateways { + match gateway.try_into() { Ok(gate) => gateways.push(gate), Err(err) => { - warn!("Gateway {gate_id} is malformed: {err}"); + warn!("node (gateway) {} is malformed: {err}", gateway.node_id); continue; } } @@ -568,13 +541,12 @@ mod converting_mixes_to_vec { use nym_crypto::asymmetric::{encryption, identity}; use super::*; - use nym_mixnet_contract_common::Layer; + use nym_mixnet_contract_common::LegacyMixLayer; #[test] fn returns_a_vec_with_hashmap_values() { - let node1 = mix::Node { + let node1 = mix::LegacyNode { mix_id: 42, - owner: Some("N/A".to_string()), host: "3.3.3.3".parse().unwrap(), mix_host: "3.3.3.3:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -585,21 +557,15 @@ mod converting_mixes_to_vec { "C7cown6dYCLZpLiMFC1PaBmhvLvmJmLDJGeRTbPD45bX", ) .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "0.2.0".into(), }; - let node2 = mix::Node { - owner: Some("Alice".to_string()), - ..node1.clone() - }; + let node2 = mix::LegacyNode { ..node1.clone() }; - let node3 = mix::Node { - owner: Some("Bob".to_string()), - ..node1.clone() - }; + let node3 = mix::LegacyNode { ..node1.clone() }; - let mut mixes: BTreeMap> = BTreeMap::new(); + let mut mixes = BTreeMap::new(); mixes.insert(1, vec![node1, node2]); mixes.insert(2, vec![node3]); @@ -607,7 +573,8 @@ mod converting_mixes_to_vec { let mixvec = topology.mixes_as_vec(); assert!(mixvec .iter() - .any(|node| node.owner.as_ref() == Some(&"N/A".to_string()))); + .any(|node| &node.identity_key.to_base58_string() + == "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7")); } } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index c1f6efeaa0..6a0ff57ec3 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{filter, NetworkAddress, NodeVersion}; +use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_crypto::asymmetric::{encryption, identity}; -pub use nym_mixnet_contract_common::Layer; -use nym_mixnet_contract_common::{MixId, MixNodeBond}; +pub use nym_mixnet_contract_common::LegacyMixLayer; +use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_types::Node as SphinxNode; - -use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use rand::seq::SliceRandom; use rand::thread_rng; use std::fmt::Formatter; @@ -42,26 +41,24 @@ pub enum MixnodeConversionError { } #[derive(Clone)] -pub struct Node { - pub mix_id: MixId, +pub struct LegacyNode { + pub mix_id: NodeId, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node pub mix_host: SocketAddr, pub identity_key: identity::PublicKey, pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 - pub layer: Layer, + pub layer: LegacyMixLayer, // to be removed: pub version: NodeVersion, - pub owner: Option, } -impl std::fmt::Debug for Node { +impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("mix::Node") .field("mix_id", &self.mix_id) - .field("owner", &self.owner) .field("host", &self.host) .field("mix_host", &self.mix_host) .field("identity_key", &self.identity_key.to_base58_string()) @@ -72,7 +69,7 @@ impl std::fmt::Debug for Node { } } -impl Node { +impl LegacyNode { pub fn parse_host(raw: &str) -> Result { // safety: this conversion is infallible // (but we retain result return type for legacy reasons) @@ -92,15 +89,15 @@ impl Node { } } -impl filter::Versioned for Node { +impl filter::Versioned for LegacyNode { fn version(&self) -> String { // TODO: return semver instead self.version.to_string() } } -impl<'a> From<&'a Node> for SphinxNode { - fn from(node: &'a Node) -> Self { +impl<'a> From<&'a LegacyNode> for SphinxNode { + fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) .try_into() .unwrap(); @@ -109,30 +106,7 @@ impl<'a> From<&'a Node> for SphinxNode { } } -impl<'a> TryFrom<&'a MixNodeBond> for Node { - type Error = MixnodeConversionError; - - fn try_from(bond: &'a MixNodeBond) -> Result { - let host = Self::parse_host(&bond.mix_node.host)?; - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = Self::extract_mix_host(&host, bond.mix_node.mix_port)?; - - Ok(Node { - mix_id: bond.mix_id, - owner: Some(bond.owner.as_str().to_owned()), - host, - mix_host, - identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key)?, - layer: bond.layer, - version: bond.mix_node.version.as_str().into(), - }) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for Node { +impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { type Error = MixnodeConversionError; fn try_from(value: &'a SkimmedNode) -> Result { @@ -155,23 +129,14 @@ impl<'a> TryFrom<&'a SkimmedNode> for Node { let host = NetworkAddress::IpAddr(*ip); - Ok(Node { + Ok(LegacyNode { mix_id: value.node_id, host, mix_host: SocketAddr::new(*ip, value.mix_port), identity_key: value.ed25519_identity_pubkey.parse()?, sphinx_key: value.x25519_sphinx_pubkey.parse()?, layer, - owner: None, version: NodeVersion::Unknown, }) } } - -impl TryFrom for Node { - type Error = MixnodeConversionError; - - fn try_from(bond: MixNodeBond) -> Result { - Node::try_from(&bond) - } -} diff --git a/common/topology/src/serde.rs b/common/topology/src/serde.rs index b68dd470bc..601b78dfd3 100644 --- a/common/topology/src/serde.rs +++ b/common/topology/src/serde.rs @@ -20,6 +20,7 @@ use thiserror::Error; #[cfg(feature = "wasm-serde-types")] use tsify::Tsify; +use nym_mixnet_contract_common::NodeId; #[cfg(feature = "wasm-serde-types")] use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; @@ -109,9 +110,6 @@ pub struct SerializableMixNode { #[serde(alias = "mix_id")] pub mix_id: u32, - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub owner: Option, - pub host: String, #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] @@ -131,40 +129,38 @@ pub struct SerializableMixNode { pub version: Option, } -impl TryFrom for mix::Node { +impl TryFrom for mix::LegacyNode { type Error = SerializableTopologyError; fn try_from(value: SerializableMixNode) -> Result { - let host = mix::Node::parse_host(&value.host)?; + let host = mix::LegacyNode::parse_host(&value.host)?; let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); let version = value.version.map(|v| v.as_str().into()).unwrap_or_default(); // try to completely resolve the host in the mix situation to avoid doing it every // single time we want to construct a path - let mix_host = mix::Node::extract_mix_host(&host, mix_port)?; + let mix_host = mix::LegacyNode::extract_mix_host(&host, mix_port)?; - Ok(mix::Node { + Ok(mix::LegacyNode { mix_id: value.mix_id, - owner: value.owner, host, mix_host, identity_key: identity::PublicKey::from_base58_string(&value.identity_key) .map_err(MixnodeConversionError::from)?, sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key) .map_err(MixnodeConversionError::from)?, - layer: mix::Layer::try_from(value.layer) + layer: mix::LegacyMixLayer::try_from(value.layer) .map_err(|_| SerializableTopologyError::InvalidMixLayer { value: value.layer })?, version, }) } } -impl<'a> From<&'a mix::Node> for SerializableMixNode { - fn from(value: &'a mix::Node) -> Self { +impl<'a> From<&'a mix::LegacyNode> for SerializableMixNode { + fn from(value: &'a mix::LegacyNode) -> Self { SerializableMixNode { mix_id: value.mix_id, - owner: value.owner.clone(), host: value.host.to_string(), mix_port: Some(value.mix_host.port()), identity_key: value.identity_key.to_base58_string(), @@ -181,11 +177,10 @@ impl<'a> From<&'a mix::Node> for SerializableMixNode { #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub struct SerializableGateway { - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub owner: Option, - pub host: String, + pub node_id: NodeId, + // optional ip address in the case of host being a hostname that can't be resolved // (thank you wasm) #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] @@ -215,11 +210,11 @@ pub struct SerializableGateway { pub version: Option, } -impl TryFrom for gateway::Node { +impl TryFrom for gateway::LegacyNode { type Error = SerializableTopologyError; fn try_from(value: SerializableGateway) -> Result { - let host = gateway::Node::parse_host(&value.host)?; + let host = gateway::LegacyNode::parse_host(&value.host)?; let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); let clients_ws_port = value @@ -232,11 +227,11 @@ impl TryFrom for gateway::Node { let mix_host = if let Some(explicit_ip) = value.explicit_ip { SocketAddr::new(explicit_ip, mix_port) } else { - gateway::Node::extract_mix_host(&host, mix_port)? + gateway::LegacyNode::extract_mix_host(&host, mix_port)? }; - Ok(gateway::Node { - owner: value.owner, + Ok(gateway::LegacyNode { + node_id: value.node_id, host, mix_host, clients_ws_port, @@ -250,11 +245,11 @@ impl TryFrom for gateway::Node { } } -impl<'a> From<&'a gateway::Node> for SerializableGateway { - fn from(value: &'a gateway::Node) -> Self { +impl<'a> From<&'a gateway::LegacyNode> for SerializableGateway { + fn from(value: &'a gateway::LegacyNode) -> Self { SerializableGateway { - owner: value.owner.clone(), host: value.host.to_string(), + node_id: value.node_id, explicit_ip: Some(value.mix_host.ip()), mix_port: Some(value.mix_host.port()), clients_ws_port: Some(value.clients_ws_port), diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index e98c56fd40..51ffa01e4e 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -1,9 +1,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::deprecated::DelegationEvent; use crate::error::TypesError; -use crate::mixnode::MixNodeCostParams; +use crate::mixnode::NodeCostParams; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, MixId}; +use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct Delegation { pub owner: String, - pub mix_id: MixId, + pub mix_id: NodeId, pub amount: DecCoin, pub height: u64, pub proxy: Option, // proxy address used to delegate the funds on behalf of another address @@ -28,7 +28,7 @@ impl Delegation { ) -> Result { Ok(Delegation { owner: delegation.owner.to_string(), - mix_id: delegation.mix_id, + mix_id: delegation.node_id, amount: reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?, height: delegation.height, proxy: delegation.proxy.map(|d| d.to_string()), @@ -44,14 +44,14 @@ impl Delegation { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct DelegationWithEverything { pub owner: String, - pub mix_id: MixId, + pub mix_id: NodeId, pub node_identity: String, pub amount: DecCoin, pub accumulated_by_delegates: Option, pub accumulated_by_operator: Option, pub block_height: u64, pub delegated_on_iso_datetime: Option, - pub cost_params: Option, + pub cost_params: Option, pub avg_uptime_percent: Option, #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] diff --git a/common/types/src/deprecated.rs b/common/types/src/deprecated.rs index 87f1f04e50..8d74026b3a 100644 --- a/common/types/src/deprecated.rs +++ b/common/types/src/deprecated.rs @@ -4,7 +4,7 @@ use crate::currency::DecCoin; use crate::error::TypesError; use crate::pending_events::{PendingEpochEvent, PendingEpochEventData}; -use nym_mixnet_contract_common::{IdentityKey, MixId}; +use nym_mixnet_contract_common::{IdentityKey, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -27,7 +27,7 @@ pub enum DelegationEventKind { #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub struct DelegationEvent { pub kind: DelegationEventKind, - pub mix_id: MixId, + pub mix_id: NodeId, pub address: String, pub amount: Option, pub proxy: Option, diff --git a/common/types/src/lib.rs b/common/types/src/lib.rs index 76aa6b2662..dbe40858d6 100644 --- a/common/types/src/lib.rs +++ b/common/types/src/lib.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![warn(clippy::todo)] + pub mod account; pub mod currency; pub mod delegation; @@ -12,6 +14,7 @@ pub mod gateway; pub mod helpers; pub mod mixnode; pub mod monitoring; +pub mod nym_node; pub mod pending_events; pub mod transaction; pub mod vesting; diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index 02246aad68..c06d0f9ece 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -5,10 +5,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use cosmwasm_std::Decimal; use nym_mixnet_contract_common::{ - EpochId, MixId, MixNode, MixNodeBond as MixnetContractMixNodeBond, - MixNodeCostParams as MixnetContractMixNodeCostParams, - MixNodeDetails as MixnetContractMixNodeDetails, - MixNodeRewarding as MixnetContractMixNodeRewarding, Percent, + EpochId, MixNode, MixNodeBond as MixnetContractMixNodeBond, + MixNodeDetails as MixnetContractMixNodeDetails, NodeCostParams as MixnetContractNodeCostParams, + NodeId, NodeRewarding as MixnetContractNodeRewarding, Percent, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -23,7 +22,7 @@ use std::net::IpAddr; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeDetails { pub bond_information: MixNodeBond, - pub rewarding_details: MixNodeRewarding, + pub rewarding_details: NodeRewarding, } impl MixNodeDetails { @@ -36,7 +35,7 @@ impl MixNodeDetails { details.bond_information, reg, )?, - rewarding_details: MixNodeRewarding::from_mixnet_contract_mixnode_rewarding( + rewarding_details: NodeRewarding::from_mixnet_contract_node_rewarding( details.rewarding_details, reg, )?, @@ -51,10 +50,9 @@ impl MixNodeDetails { )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { - pub mix_id: MixId, + pub mix_id: NodeId, pub owner: String, pub original_pledge: DecCoin, - pub layer: String, pub mix_node: MixNode, pub proxy: Option, pub bonding_height: u64, @@ -71,7 +69,6 @@ impl MixNodeBond { owner: bond.owner.into_string(), original_pledge: reg .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, - layer: bond.layer.into(), mix_node: bond.mix_node, proxy: bond.proxy.map(|p| p.into_string()), bonding_height: bond.bonding_height, @@ -83,11 +80,11 @@ impl MixNodeBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeRewarding.ts") + ts(export_to = "ts-packages/types/src/types/rust/NodeRewarding.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeRewarding { - pub cost_params: MixNodeCostParams, +pub struct NodeRewarding { + pub cost_params: NodeCostParams, #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub operator: Decimal, @@ -106,13 +103,13 @@ pub struct MixNodeRewarding { pub unique_delegations: u32, } -impl MixNodeRewarding { - pub fn from_mixnet_contract_mixnode_rewarding( - mix_rewarding: MixnetContractMixNodeRewarding, +impl NodeRewarding { + pub fn from_mixnet_contract_node_rewarding( + mix_rewarding: MixnetContractNodeRewarding, reg: &RegisteredCoins, - ) -> Result { - Ok(MixNodeRewarding { - cost_params: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + ) -> Result { + Ok(NodeRewarding { + cost_params: NodeCostParams::from_mixnet_contract_mixnode_cost_params( mix_rewarding.cost_params, reg, )?, @@ -132,19 +129,19 @@ impl MixNodeRewarding { ts(export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeCostParams { +pub struct NodeCostParams { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub profit_margin_percent: Percent, pub interval_operating_cost: DecCoin, } -impl MixNodeCostParams { +impl NodeCostParams { pub fn from_mixnet_contract_mixnode_cost_params( - cost_params: MixnetContractMixNodeCostParams, + cost_params: MixnetContractNodeCostParams, reg: &RegisteredCoins, - ) -> Result { - Ok(MixNodeCostParams { + ) -> Result { + Ok(NodeCostParams { profit_margin_percent: cost_params.profit_margin_percent, interval_operating_cost: reg .attempt_convert_to_display_dec_coin(cost_params.interval_operating_cost.into())?, @@ -154,8 +151,8 @@ impl MixNodeCostParams { pub fn try_convert_to_mixnet_contract_cost_params( self, reg: &RegisteredCoins, - ) -> Result { - Ok(MixnetContractMixNodeCostParams { + ) -> Result { + Ok(MixnetContractNodeCostParams { profit_margin_percent: self.profit_margin_percent, interval_operating_cost: reg .attempt_convert_to_base_coin(self.interval_operating_cost)? diff --git a/common/types/src/monitoring.rs b/common/types/src/monitoring.rs index 7769e61384..4768964fc1 100644 --- a/common/types/src/monitoring.rs +++ b/common/types/src/monitoring.rs @@ -1,9 +1,8 @@ -use std::{collections::HashSet, sync::LazyLock, time::SystemTime}; - use nym_crypto::asymmetric::identity::{PrivateKey, PublicKey, Signature}; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, sync::LazyLock, time::SystemTime}; static NETWORK_MONITORS: LazyLock> = LazyLock::new(|| { let mut nm = HashSet::new(); @@ -13,54 +12,26 @@ static NETWORK_MONITORS: LazyLock> = LazyLock::new(|| { #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] pub struct NodeResult { - pub node_id: MixId, + pub node_id: NodeId, pub identity: String, pub reliability: u8, } -#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] -pub struct MixnodeResult { - pub mix_id: MixId, - pub identity: String, - pub owner: String, - pub reliability: u8, -} - -impl MixnodeResult { - pub fn new(mix_id: MixId, identity: String, owner: String, reliability: u8) -> Self { - MixnodeResult { - mix_id, +impl NodeResult { + pub fn new(node_id: NodeId, identity: String, reliability: u8) -> Self { + NodeResult { + node_id, identity, - owner, reliability, } } } -#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] -pub struct GatewayResult { - pub identity: String, - pub owner: String, - pub reliability: u8, - pub mix_id: MixId, -} - -impl GatewayResult { - pub fn new(identity: String, owner: String, reliability: u8) -> Self { - GatewayResult { - identity, - owner, - reliability, - mix_id: 0, - } - } -} - #[derive(Serialize, Deserialize, JsonSchema)] #[serde(untagged)] pub enum MonitorResults { - Mixnode(Vec), - Gateway(Vec), + Mixnode(Vec), + Gateway(Vec), } #[derive(Serialize, Deserialize, JsonSchema)] @@ -105,7 +76,7 @@ impl MonitorMessage { } } - pub fn from_allowed(&self) -> bool { + pub fn is_in_allowed(&self) -> bool { NETWORK_MONITORS.contains(&self.signer) } diff --git a/common/types/src/nym_node.rs b/common/types/src/nym_node.rs new file mode 100644 index 0000000000..4b4e46212a --- /dev/null +++ b/common/types/src/nym_node.rs @@ -0,0 +1,94 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::currency::{DecCoin, RegisteredCoins}; +use crate::error::TypesError; +use crate::mixnode::NodeRewarding; +use nym_mixnet_contract_common::{NodeId, NymNode, PendingNodeChanges}; +use nym_mixnet_contract_common::{ + NymNodeBond as MixnetContractNymNodeBond, NymNodeDetails as MixnetContractNymNodeDetails, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Full details associated with given node. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NymNodeDetails.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct NymNodeDetails { + /// Basic bond information of this node, such as owner address, original pledge, etc. + pub bond_information: NymNodeBond, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the node that are scheduled to happen during future epoch/interval transitions. + pub pending_changes: PendingNodeChanges, +} + +impl NymNodeDetails { + pub fn from_mixnet_contract_nym_node_details( + details: MixnetContractNymNodeDetails, + reg: &RegisteredCoins, + ) -> Result { + Ok(NymNodeDetails { + bond_information: NymNodeBond::from_mixnet_contract_mixnode_bond( + details.bond_information, + reg, + )?, + rewarding_details: NodeRewarding::from_mixnet_contract_node_rewarding( + details.rewarding_details, + reg, + )?, + pending_changes: details.pending_changes, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NymNodeBond.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct NymNodeBond { + /// Unique id assigned to the bonded node. + pub node_id: NodeId, + + /// Address of the owner of this nym-node. + pub owner: String, + + /// Original amount pledged by the operator of this node. + pub original_pledge: DecCoin, + + /// Block height at which this nym-node has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, + + #[serde(flatten)] + /// Information provided by the operator for the purposes of bonding. + pub node: NymNode, +} + +impl NymNodeBond { + pub fn from_mixnet_contract_mixnode_bond( + bond: MixnetContractNymNodeBond, + reg: &RegisteredCoins, + ) -> Result { + Ok(NymNodeBond { + node_id: bond.node_id, + owner: bond.owner.into_string(), + original_pledge: reg + .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, + node: bond.node, + bonding_height: bond.bonding_height, + is_unbonding: bond.is_unbonding, + }) + } +} diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index d36e0b978c..a78ba568da 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -3,9 +3,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; -use crate::mixnode::MixNodeCostParams; +use crate::mixnode::NodeCostParams; use nym_mixnet_contract_common::{ - BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId, + BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, NodeId, PendingEpochEvent as MixnetContractPendingEpochEvent, PendingEpochEventKind as MixnetContractPendingEpochEventKind, PendingIntervalEvent as MixnetContractPendingIntervalEvent, @@ -48,25 +48,25 @@ impl PendingEpochEvent { pub enum PendingEpochEventData { Delegate { owner: String, - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, proxy: Option, }, Undelegate { owner: String, - mix_id: MixId, + mix_id: NodeId, proxy: Option, }, PledgeMore { - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, }, DecreasePledge { - mix_id: MixId, + mix_id: NodeId, decrease_by: DecCoin, }, UnbondMixnode { - mix_id: MixId, + mix_id: NodeId, }, UpdateActiveSetSize { new_size: u32, @@ -81,7 +81,7 @@ impl PendingEpochEventData { match pending_event { MixnetContractPendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, proxy, .. @@ -93,7 +93,7 @@ impl PendingEpochEventData { }), MixnetContractPendingEpochEventKind::Undelegate { owner, - mix_id, + node_id: mix_id, proxy, .. } => Ok(PendingEpochEventData::Undelegate { @@ -101,13 +101,13 @@ impl PendingEpochEventData { mix_id, proxy: proxy.map(|p| p.into_string()), }), - MixnetContractPendingEpochEventKind::PledgeMore { mix_id, amount } => { + MixnetContractPendingEpochEventKind::MixnodePledgeMore { mix_id, amount } => { Ok(PendingEpochEventData::PledgeMore { mix_id, amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, }) } - MixnetContractPendingEpochEventKind::DecreasePledge { + MixnetContractPendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, } => Ok(PendingEpochEventData::DecreasePledge { @@ -117,9 +117,7 @@ impl PendingEpochEventData { MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { Ok(PendingEpochEventData::UnbondMixnode { mix_id }) } - MixnetContractPendingEpochEventKind::UpdateActiveSetSize { new_size } => { - Ok(PendingEpochEventData::UpdateActiveSetSize { new_size }) - } + _ => todo!(), } } } @@ -160,8 +158,8 @@ impl PendingIntervalEvent { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub enum PendingIntervalEventData { ChangeMixCostParams { - mix_id: MixId, - new_costs: MixNodeCostParams, + mix_id: NodeId, + new_costs: NodeCostParams, }, UpdateRewardingParams { @@ -182,7 +180,7 @@ impl PendingIntervalEventData { MixnetContractPendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { Ok(PendingIntervalEventData::ChangeMixCostParams { mix_id, - new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + new_costs: NodeCostParams::from_mixnet_contract_mixnode_cost_params( new_costs, reg, )?, }) @@ -197,6 +195,7 @@ impl PendingIntervalEventData { epochs_in_interval, epoch_duration_secs, }), + _ => todo!(), } } } diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 05caa7d88b..8e001d94e3 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -67,10 +67,10 @@ pub async fn current_network_topology_async( }; let api_client = NymApiClient::new(url); - let mixnodes = api_client.get_cached_active_mixnodes().await?; - let gateways = api_client.get_cached_gateways().await?; + let mixnodes = api_client.get_basic_mixnodes().await?; + let gateways = api_client.get_basic_gateways().await?; - Ok(NymTopology::from_detailed(mixnodes, gateways).into()) + Ok(NymTopology::from_basic(&mixnodes, g & ateways).into()) } #[wasm_bindgen(js_name = "currentNetworkTopology")] @@ -88,7 +88,7 @@ pub async fn setup_gateway_wasm( client_store: &ClientStorage, force_tls: bool, chosen_gateway: Option, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], ) -> Result { // TODO: so much optimization and extra features could be added here, but that's for the future diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 6185826bbf..e15bd4a459 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -35,9 +35,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" [[package]] name = "arrayref" @@ -1271,6 +1271,7 @@ dependencies = [ name = "nym-mixnet-contract" version = "1.5.1" dependencies = [ + "anyhow", "bs58 0.4.0", "cosmwasm-derive", "cosmwasm-schema", @@ -1297,6 +1298,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "cw2", "humantime-serde", "log", diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a7c49c9099..104d1357e4 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = [ "coconut-bandwidth", "coconut-dkg", - "coconut-test", + "coconut-test", "ecash", "mixnet", "mixnet-vesting-integration-tests", @@ -32,6 +32,7 @@ incremental = false overflow-checks = true [workspace.dependencies] +anyhow = "1.0.86" bs58 = "0.4.0" cosmwasm-crypto = "=1.4.3" cosmwasm-derive = "=1.4.3" diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index 4ef1b04cba..e2fc5deb38 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -11,7 +11,7 @@ use nym_contracts_common::signing::{ContractMessageContent, MessageSignature, No use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, LayerAssignment, MixNodeCostParams, MixnodeBondingPayload, + CurrentIntervalResponse, LayerAssignment, MixnodeBondingPayload, NodeCostParams, PagedRewardedSetResponse, RewardingParams, SignableMixNodeBondingMsg, }; use nym_mixnet_contract_common::{ @@ -156,8 +156,8 @@ impl TestSetup { .execute_contract( rewarding_validator(), self.mixnet_contract(), - &MixnetExecuteMsg::RewardMixnode { - mix_id: *mix_id, + &MixnetExecuteMsg::RewardNode { + node_id: *mix_id, performance: Performance::hundred(), }, &[], @@ -206,7 +206,7 @@ impl TestSetup { pub fn valid_mixnode_with_sig( &mut self, owner: &str, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, stake: Coin, ) -> (MixNode, MessageSignature) { let signing_nonce: Nonce = self diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 957f981f2e..67f63d1007 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -44,6 +44,7 @@ thiserror = { workspace = true } time = { version = "0.3", features = ["macros"] } [dev-dependencies] +anyhow.workspace = true rand_chacha = "0.3" nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } diff --git a/contracts/mixnet/artifacts/checksums.txt b/contracts/mixnet/artifacts/checksums.txt deleted file mode 100644 index 770528b866..0000000000 --- a/contracts/mixnet/artifacts/checksums.txt +++ /dev/null @@ -1 +0,0 @@ -d8a3bddfd2d9f530ca373dfadf60a83eb1a199febec596a3ed37024a22012767 mixnode.wasm diff --git a/contracts/mixnet/artifacts/mixnode.wasm b/contracts/mixnet/artifacts/mixnode.wasm deleted file mode 100644 index 2d7f2adaa348a0ff1c2aed762f044ea7dcb3b323..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130585 zcmeFa3%upkRp_G>e(xH`6?5Ws9vBj3QnQ1h! zGvD7@d;ib>+*jQqBxbq|seAsf{aWv}_S$u+*he%P+{ zlU$ebufdgF@N7YOLraazWer@?zk~Yb=$R<@BiSPHzp0;Z+qYMANb%0ZhGJKx7_s6 z8?U|oh8u3b@k1X<61^H6`tbX&|GGjt) z>Eq_sIr-tVTwGjAdWVzpg@0H3ZNIpyVAEjK26 zn+C;XU+4N;Zu#K*t_M}uPTg>0b#vWW@B83~-v8au)wOr>{~K@r&`lrwz_k}A*H2x0 z?G4x8as9P7e&B{Y?Ol8Ap&PHi_1f>c{zEtBS@#DRU77Z->z}{lpQPsuiwD!u1>g0) z8*cn>{_df<|Lkl3b^0|oU+|5Wz5N{r&Mz+i!}QyC3@`ZG-!XT^w;cSR(*N@9^XGr? zg4=F=`}v|7H4Q`p?q~emMQ#(qBz~E&b#4|42`!zn=b4`bs+W+xNF$O_$nf^Y8^pc`CVi z)Vwgs_7~~yq;a6JUsrQkGV07_ol&dkl#K;$-`Ut-@FW?wr_)~1D$|8gyJ%g}&vbpq zs9UrP{#|+Qs9kmn{#|+Qu$vvGt`-HmWpc3JwDP0&fqu%zlq$M|OQ*A*zisF@9lWhK zYOAH9eMy#B+eO!|=*+OCruEg&r3rl~+6(1HmnM9snmcsqlC0A!(lWU;X`I<8(hZNI zvF<4HH#UmV@=+KDimb>79lJCtE;H^L!a=8KmU9dK5f3_}Y;b?dh?{fST$x@D0Gnm8 zz@Lj3c4f)lJSmz*b60lJ-h5UdY?gZ#b|=ZO!PP9E&1Dyj(jsddF0!(D$FN;A$}gt3 z9T>KFKetdeZXY&s5Lu?TMd}b?q!7la8%RSGGt4T|fGVy4;YJ#{#!+ODMh0o{57Njh z(#S;`KFCof(lCNz;6@tuw%sdoCyk7e@r4MYTjcto-9Q)m#OQ(w0$rr`I?{!KjFtvn zhV??Sz$iPSY*o`N8V0DV4TN+w7ri&j3k5{Jtv;jeSp#N%_1C80oOT4$y$oi>8UnknBEiQQhs4Vp=ZHWHu&C@0pxSpK__rt?8>g%YmIei ztb9H(TF(+|Nnc*2uH?KrnJkPl2p+Kzs-sfWLfM3f2MYswrg{zMsy9Da)tl|g6K(`T z0WhybBqfp&sDt-FYb7Fi;t+?>5s{4|lZfQ{4mhTP%xVB@2v5WZtXswhBI}lmNR=No z#0L}w-Yw??dy9yKp(G*&YVe`)sqLa=S7L?8f{_&(ffe9?UPo2{eFlql$ADxWj{ySQ z;20pMb;dxY8yG|QO6+Wu4tX#Tlalp;2yH;C{8izyCE+-hlML~aY)_HTr+f1`s35&0 zySRV=FUc-5nz$HcvCppNxOxY#UQ8DB8Umn9b`?*aJhDV32konUeDxktK=};c9s9l| zz7u5*{%PtulfGO8E%38vlzyu?2x08_&=pK1q}c`#6~#6U-Ra;`NkMU# zdVCQ-NF4f4twZA=F$1K57&{UeVs4ZkfS>u~BK@XRC{`;ZaEvVgQs4w}Og^hm4@QZT z!vLm8Mm~0t|LmwO!4!l}QnbeciHgMk@VW8A;2sos`804U_c}bv6M&{%`uI{Z_$6uS zG>YZiWmoT^hT9#*&7-!=4gsU$Dl-(A_U@DKx&N*50hU-J|V%Hm3C=5 zSE>9c7gtdfu4+10*;}}(>0FhAAciGasqLa^SCxX%SEgX7BXCti>bT-6H#{7eYmUK5 zJB%SwZ|;ypmKX;sdkbZ>oidsfz!(xe zsBJe+)tO;OP3tS84C)A!(JJ&h%G)|+==rK+06pU|$oAdX7(n7WW1!Lvj3InArVJa1 zQAT|rhIXmy6;^3CN)aPd;|k;xwz|-6!NKp%VlN^&6Q zziK7Nzxy5(z)BL4$QVji;Fx5C_i`b{BQa4(grFitG5lT#v$+y`)5OajiI-kt)+}}Z z*S!>%8R2;A2IS-Gh$8@;inuyG?q#;hrduK9QE6 zmupkbJ@{!)Ej)P%&`B^e_s2d)hX&9dV4E(Y;P+wlhW}rRQe3yoRgyixg z<{1=SDw&pTl9x}U3*`&4r8YZMt-aEg9dN7WN5EMzd zZ=u-yxRg|ahg|d}*Asl=V%o28t+)8w3m1w_{tT+MKtv*s`D}j+0iWep_-gV>=aUKy z<^L%_Ha7~$G8uNKr+OG3Basv6CXcE9 z&L!Cieia?+NW$oYIpg`x4qfjWrt`^g3;1pFyOn?2#!|&ZQL3;7hMP&3%%n?>bV&r9B16IzkwDtPk9;gu5U*pB1}>plHB?xm*8Xs49?q8e^xHwLiMZ60IuF3Da|zi32MY1os{T1F%0lAD}E zIBlOVjy?*~&w7+E2af~J?T^AQjo~Ko9^_=r04;}Q>a4l79Pj{XeXfLbE;$llH4Cha zn~?zJjG%W8Mf%+x0ji?tPQ}O8Ir!L!4&npd;^Wbnk14s5YnML}7uTPG+PfEKh`uLz zLu|$v6dn8rz-x`2YU> z)=t{3c7qsU>?=~x4_W5+U1>lC;v$)+l4FLV^ z+1zfMrdwV4(d9`aO6%ImlpsuGpBDmUKA(y*%O&sHizX7)AG-B4FC-D4(k~M6Nqxxu z6DAprd15c2`z;YymkXFC!wr3O*i;%StP4R_UD~RT=#oK83IXUuI|jH^7Ke3k%H=TF zHBv;9Y^KCUET2ZdKaI9>aTcW3W>?gzEl|Q{NrYUcuEen!Q@fMRg3Rt@Ahsu1ocT8@ z6^w=r_Cdna5z!mOQJY?(`Z@FuJjL$>LZo@7JeD~&`cNr`M}$f=)5hWuFq@ zVy@{{&dXzaU@3P#k<^jkZMDbq^Cf4MTXbhRpX z`}Z23%9=*FIHUDsw3;$7AVy+a$#8wCA3+lCmyK%IXGL20#Jy;w_C^gqq(CXn&^FD$ z4}!mes{eU`=XK{<>s+IhwttJrw~wQlDPpp9tfH9(nb0!97- zu_n(3W&53GVQDyS&xL^@+{{#i<;JF31F0`7K&2p)Qs26TRxed^H0W>?Yh&3#uUA4_ zfR4bWl!*gAghxzA_w@^LPCM=-(xF$$9cTJ!NVX5@ZYK1K#C3iRo!TIu$s2J&Xrg?z zo==_KI2WSLytK$PH*(aE*=**`* z70Js}siGFwr>SP?iFsL+Lh2JrcSkm7Ix$XJWP7u16ngCYHD!SaDEUy+2XX~2$qKvb za5Yz`ZYI5U@ndn3SF!d;R-uuTqyl72xn&|M3-Nx0>wOAVXXSI%EGY9?yRvue&G*O% zo6Gh~`OA0A|t@^9cRX38RA8e#t4#YHSY1}wuOFW3Jsjr-Qqx@al!f=f9ZVH*Bgu z>cNm6Tpe1u5&_4fg6=VMlFP_cVNio}G(R=?la!I>vsX*p*P5rn!NNq-Op3@%-IWiR z<*ORKn{agN-HarogVrA!>rS*!1A$Lq*7|sgriRUUPS>uWC>u(lbxbIShPtxH=P*TQ zpoBM?&O;NPIZd5BbDDaB#2=9@lROOw3iko&kTUH*>2fr7C-hph!Iis73}c4T$RrXr zG)K?HN4b~{rdy-jW>=_P@E{=%pU-8}s1D7#px@9yO`%NlqJm3^&Tys500j~trv)e? zEe~JSPjn&kg~#;LT@&WPiHQu}rr7~mmqq$6b0Ub!0MB*`r>)ixZX0!X$e}zj;?uxwB8=GawVz(fV zYzQwvX29rGN!4X#Z88fYrvVL4l2fIv8svc5XuvGc#BX)dhA_5BJk(_Xv56WV10qOO zQ3`h(b@w)C`6mxA%v_*Eul4?i(+2tF<##U3^qNWhpOfU6HKZ>DLn)9PZp_PzF6R%H z-R%cTNv+>a-g&O^V!2%~2_yn0V0B7CU-}V6N%sK@4N#ynV7nA4dwK@YG6no>fGV`7 zYWuBjxKai>bikWbnA6uElE9l&J1TS$g)U8Q(2U3>8Ht7TIV)uqXC;>I;IHYMLvC+n zX6j}N#X2{t21WEt{$H(XZxOpw;wJe7+k0w6M&L+^0HmNO*}%jd1$U8sVgTAMYs=eA zv0gQa{93(*UD+*rbF2N(D0L~FIPe4&;{|@KzKT=E`7S1QMWbOwN|w&S z3}r%8-%~UiST9wE2Bf+ZmCR2;h6cKyD>AQ8{gep_8E}vwpwVhd9e5zVEg=IPc1gCA zb{HrTBT849T4Ctmif6N~chNaPJpm1X2(e4jI=9fClybREE*4jfQ9>_}aF)KM%fxKZ zhAZq%Rf>hivsQ-1$4A+Y!}*5hw90gNR<>Wgn3&bF)J`7NX0`m$IRJWmw$=&3B|j%o;DhSH3zyW$%;tggFBl z?z%){&1KI7mvl)JCR+~>g4!ekVXDSz;BTDm(xBuJP+I_0X|AviwsM8>a#jWN@?K$$$pe55&>gmG zV~w(#L?Ue&TW(|;)hHJK&VG~mwYEDSNJ>0hBb-|y9E-R(bv4aNuT8QU&Dm3@IlNnn zS_I}K;YeZsxm46JmpvN@2to=};TAy(B{as>i}F|h&w?`9ui&s1AeVIUx6^(z%0J>az}k~3LO|65l$8Og=evZ5nso>S+=fCr;i$ zi>?9$k3h)~4CI8uMG)F--AyuvfSuC~W+GP}5Tz!}Jayo@(Pk&ZaVS*tErMRxMxl_X zgzNC^k`yE!Z{yz-|1$nH_}3M&xDXljU6}SUpEZ$l_OQV=7MbYcn=-90A%_XXOTj@bFu`;XW*KUYv)7cw%ClQ3@1f z(N-wPc)O-7T1=Bnk4bK`$ChCd5+D>T+4=bJeEL3X@HK{6SyT6bqHaOcgp6fLy)`~J z(xumw@WDr62jV2^Iy=g(Qk^zC90okcfIYO_3Zdm)3N1GyXv8ivm0%+-ZEYL-FleaX zG$e=~v&{}*vITe`_uvu7#^hhE2@zrgrgms@KTDK8klH9EuKbDw3Y$v?%w#6%#Eb}TQ%O)#_6=cOQKgUolD1E}wB#yN_7enr2Cdl=K z$<~2K|78C@7evmp`o&p)hF=+}OX)N-&E8|1B8{Y@IZ(Ref+MsT2*X>PBqz0qkfTKt za{v7UQ@1Bq}G3D1z6>=mh-~f?LLi z3iHb59&E=#2Rbr;Ebc8Ke=;?4wmOUyK@oMS%TIy;Ufa?UuOb~VTf-ZJgNEXiYoF}O zpG`;S*{pt}I2V_E^cK50r#Mej{BL=5IG9g|=Xt>0$71tskxoqKF(6~%+JKriA6NxB zX<8Ch(Iky4(!*vqKQM`03NjQj09<;`ia#|+NI6_~9JD}ikiklVvjJj&SU(U>23WA% zw4Q5LbmglG5bD-?l7)CWVe|vU%`Ir&7o(xXA1@k@&lmR}8NNlS5a$)Mz}BJH5cEu8 zF#(Z5N?9=_(Kn?`UDAuLKznk12N4dOQH!Smpn`uy)>wk{#V>yIZCW*<7)Jx!7W+cc zHK_71W^z(YojckhQ=VlNmzTL!Ln7}ERW^8=l!gpqTPv}J@>=*SterJVzrd&O4%P__TP7;_|7|#gxbay3V%;80?-JmMd~B4Z2O% z`8I076UYo0BS^@>A>7vHe-HF~^eMyyQ>LFSH0PpWdhp#&umHnm*SCKIHZjXy;Meoa zXIRBqkULvB3-V<(E^`*PX?4;za~7P`VR)z;6ctkjM(kjey?GIWs^7|&uxk|l)Z<0w zxIWL)0*r`*F|Mhk4H)Ax#Cw%&ivHII3mZ3S_<>h_0ENJ!QD7nIi~8V<0%5wg1jWi1 zGrTYnA)cQ0={I;o|PEov?(~$I@*Kdm?PW%!4<>uar{>fl%Y^x6>_eY3l)| z%XI7hD@$bnpyfw~sdRa7V?&(9oEYLR^nUz%ao!4Ufcbc?jKqvM6DH0w)4O^ATTRZU z0duCymNXZnNX%K*T9`2V;ml{uw63+9GLu$7a?+<5efn3(k$@@5B(u%CmnL7!7YZ_& zbIKGTmx_ND`8*SBtQ;y&{D3Cd_Ho50?^Zd*XyPiNujT4JdU#28HE)O+E5~o}DGgqx zE>8wOZ!K#{&?@V85?2>BHb4`iRVbSH z{+Wm2P_Y013d3ol1fT$bGYrF#G606(q%hnsr?x87ZCkUTvN*?-$)S-p7Hgspvf4xr zckjYGLq2F#CI*GAm|bI4rXrGrrukS4GMQhpiKJZfOIcho7pHl!MST(rsgMs*b4_%Z zR$dNMSO+5R@B@u2nkyEuJZ_&Nng*PZ8;Wxg9aY`j-ur(xWqn_zdEBRq;3b>*%_WSo zauMU^KNC=lor9U&}%a1ABXG>sNit==D2Or2iwddw< zPCI0GCs)e~1*{E^?=kUkO_h1%aa?)THiIK?)oWlHXLoXyVE~7ei1@)#lWMbJ8mk3Q z7P{oSk}$euA2sAq^PjDywX51SjHbNVmiwEn;BVSOYKvT2g1p_**#VtZ0K~e5T%d9F z$b4Q5a4z5GlXaL!vV|R@Pdxxcl~JCR4|v$C#iavIMy16y{nP(toliCFx$KC|md$|# z`a%YvmE*5~br-+1VABUw?*$f9=|%MhP6>0a1RJWldI$Ogk324`?M{7iMx{x?sAfb% za{fd_P*Fk@?zCkt8rXAlKm=FtAYYfHD#nHSwQ0TpUwQW))$>=olec=g8UNPt0r7$m zW>My{V_M?C?*lTW`8^td2JqTGDvuPkt-MIW7GHP-yj3=lEA&%&(!!nD6n159t(1IT z3R3_T+S6wNZ_%g4Hqj(aZrZ&)lVuGg2d1#et5By#94JGMrEXP=JkDBu=5acXoA|hn z5zC85(AdDnn1lc|=u4d<%bF%uZ;JKIsutg3#bfafT~ub5kSZ( zRAF+6GP#lgF@yAGuJ@SIV%+@mgZYndZ* zJpNArIlLXsl-(=)T|F9DgX|eV@$O^u zQ}>KE$u}%EJvy2y&;PJ+M>>=o@OCkC3mr-hvN^#NTO$-x2R{Uod-mQ)QF1ZhYBO!G z;NsqLm{Bo3SD0}*?mEtkhN)E`-l3#5i3tLbjWijI!aabCt6lE0XO+u-sGPBR6_ZJm zJb**ADi0W<7cH*9GC|Rv&q1;bVZcn8Iq;CK%?(#h@S)O_Rs6Cs*a?E?vO9)-{oFdV zyIWY?z#VZ@Vo{`q9=AF$Z=s+_&ge)82t2kb z0#?GmK(2KtYzhVsoEe%+G6ba9Lfipch&$jFxI_BwP|@mnKkx9`Zj~JMUaPzeP%r{j z(pgJ&&`$@%Aeul~&rEGu?`~?_jJ~h%w}b&ToOr|HM{CiAzH3~^LA0zhO;1&ZG;EY< zZCmvK>NgEiJ)m<;Mx&YCvetqq_4&9><^FlPA=CQrz?|p^hGdqtc4lzJpKH?g9f+l8 z&4}hJ8PV@GBO1M9HnRYV1+C`i*kWfz;PWb&~xoz19VAb85m zswY?&43kEy#0WX+gRe0YS|3e&GK0)&zD3jIo?&ET&x8H^nNHU6*;Sc5axAoUgfwAW zrVsH1{WRo;>fscXVEErFajbbIyNg>`+!`SRZg}$r-?;*dACbK)yJK&D%q$n~;EU*5 zyq*w|*sV=0uBz%Zi$(_0TlDFzrR`v0Gm@+)@YF+9nco_!G%y#4!kE8yf=#-#TNUQuU6w(RyPDUn!pB z8DBP#m8AeEq^@nZYj)}dxVhxM{fN6?eZ65nXzA#DPo2M-GR9_-1LfTY(V zD+VE=j%0P6Q3=tcKqn7Fen_V94RYlewTWJ?+e?+gg7<)@(9GZ-?&q?7GLmC2YBf<0 z5)Jx&kXQ*{L=L@d%%ORy7EOLJS*JE=4hf)U#U;UK@Ky(B^J`K76BJ?|g|t{?ShzB- zMxyKGw47i1t^_OH_k9Bkr+u+C@}OS{12|sKtSa}VB@!q-7e%8j&r~8 zg~MdulNN8+MN>H$irB(}|Qu78sU0Qj^2Ud}Ra<%?j z|Ee02aSZ`wJlGdYr`%lT<4e7MvogGP7Hb6D9=M%(lMwvDR4EiV^US9WG-g;g{2L_R z9Q9;2tiFb< z@)lKC(2h&_T#{;fjwp!Fo0jz+o+}wFeBZEC?eIJwKhK>BVok2i1II>fYo@r`L0q}@ z&ZQIo#>%DRauBH_6k^+0VRfmJL)Imtd-nUYxu?|+Jgc7Sl3^0S=zH>E@1^e&jVqcz z4RRL;ni!QC*tkN^x%}qa_~`aalNr04zOy`~JIUs>Y~5MC?w2{nGsviyAza8;Tkb4h zUA0VAWo)A#Jze_RVQMxIh@vdkdu6==n1);fgk-gqAd~tC`N>VJcrUW_MG-1(xwL#n zX{hy5MJ;(-i(1zPOFY1bw`3hxbk--}E7O~#H22wXA8;GNg4~CO8?M29z{MbiTKjAI zYr2i<(kBTZ$Jt8HTf@a9Ov4f~5oMv|^DKf|7I!`)R5Gz3Wu>I9%Fk zQ*+0SkbIo+n&e}in4aHc9Y8)J_?W)#F;SH(F?v7dy4n){2Sui2XljvTkJc@Tx1qeb zkjh0Vb0sx=A-|Uew^~KAi+JFg8-iACZFPNb(sCrnrF}^OqL7H z71%~jP$uL;gS{Zwq&ZU{b?Jyr)Q~{dJCPqy2YPhA%7xzM7*T}Z^_=cAJb7(f8Fz|P zSJp~nzAxA~fW^|qVatzv-#bNmQrnNw5)#yg7NOylb9~|>4n5&Oz?MZA-Ua28pGfAAh(9+;3Y-BfiGn-;bhwB z3MOESK@=-La~}C^N&^w-V`exI3&fFY_FN=rTHm~Rg7Lhded9x8or+8X3o~AG3?*)i znKeq$iGx845wRF;C8BlA$V#(FY-(#Tw{FEu2~oX#R9Fh?Q_aRYw#;Iu*NpRL0JiYq ztokgO5W7expy_cge+CcB6^t{liy7co2bq>)cJ)^RFS}vR;|hIavWSS3Ot*D_boL@a zs$rR#sS}6vUH}tvM7toHR;)dfwJzi$u0#Sd2pFhv=NC4WL?t26%anz%U9VMuHexff z6UJCsrEW3E3Mfbr!b0*7Dlh`=3-{*8D6OCnnkj>qBv34CB8X#w0Wc0K$6GyB5Obr9M!LR)K=RfzyKlbyVORg~*DHJU`!54%7+y|o7Xh2buvSvA$ z&CS&9#_A%c!0R#=v6XFxFT;`mCDEtZ*}y4?*1#zYR%%a>GY#|A0J#V1ko)Y5pZ@%B z{@U++Y65Z(Z5VQ=tBXkIn*_NBS{2Q;aI8fOkgCidveCo#Vu3+`(>c=82!wYDAW>WmvU`pR>}3gpA*iGlK+b=EB#Vk>5u z{Na5|nk*(h*n$fgNNcvT;mR*=zt z?dNAH+D0N|ZC0aYuYUrjEpj@cL{Ve4535ES_F=U>qiqV2qVUG*`>?jeOg>X!`(8^u z+Q!z-8`VCnQ2VO;Ub~%jW}^rkzJXBy5aDc%Q9#LIC)P6>#mY^uMHuCgBe3_>ihWo` zlT1k2WS6m?(fX+2#{*yx9gI^?`_LV6&vXx++vd=F#FCcMHtL{m3B!00elEKTYoNgq z0W8M6MH8&byEveTKU!Ny1lYnkrOVwuoFc>r5-~s=)KQL(dUUUnq{jV5Eyf#_Kw&x1 z`n=)M**L=_YN_*v&GS|+B2rc~0>9{I6~>T%=%+~3rKn(_$+dL(w)2+8H-$tEeqdfq z*_Fdh^CVYan!E$<2G;C9FUw!`XezV(DdCLCXzjNpOoUk&Fwv1*30PqB`8;5uD*2d7 zvYRoIal?i8m;p=&b|>$!sTo!K+WVs|c6Z!Ku#Ujev18qco0 z*qdD}bF+=>+BMhuajKI#j(9b9Aikv2|@Op)^OFfuV++eTtMk76USq!W5aA-yY7yTU=&b# zbNW#P0kwfq0FZS?0cPLGC`>@rM`3cO{DSO{&gue63pbh6TU9_EWsX)>NKV8{l=vX? zdm>X2EcEjL(UH4cMv=!5B$OqmNIFUE5WmzS2TcT;Eb_K=*+DG1*cuBad?xTT5?m_5 zWC5g1@h#j39b>|^EFF-x?Dz)XXkL+Bvvz`Dvb`eZc}$vk2&Q1-siSn)sHA4!>FfrN z-eK9xA%8PvVU6YQR$XhD zE9Ec4jzipLNYhlol>F*2MAB9a09%!x{9I zFxKoCuxV|aID!t?igrY7d?DH)Crv+3z8`SC8B%Fnt4JBkt$AnIbNSBWHt*j=^`O{(@7 zv71{Ysw9(?3>J3+e;IM~OR7<)CPXz(t-%OzVK+|AGKG88gdG9aS|yZb@^&ipX@jMa zg(^UAb|<$C4*QxHlfdO~sZUdbjg1nvqhnu|QL1VKyd|)l=_sxA@=+2Tjkx^Gnc!08 z;wU*YCgKt+D0C2m_l==aHM3zebGu+;kEbn(2hc#%K~l=`#ZHNW3vql!f4d8=RgGzW=)<0AFt zSnKp;JXr}{U@l|hOkky#lLwl^jf|Dt4D?rKcum(*)J#>gDJ75&s+zfknjOca)}_hC zE<@MFGYUGBrD1s9vJRe2B1`BQHqWYXjg6sF6G?V#bGd=;^Obdf)s!?j1ivOsburQ!vda15Y8PEbq=O ze>nz+Mw|EM?;+=EvqH%7yMkLF)fenQ;T1O(z8V}>YeeMj%Sw5;hjuXPw7Vu$T2|Cm zT9L70)0aa;^N;y~wX6or17OgQgAmj=3v(j#HJthE?be!BqkC1)7$M$Xp-gR5ryPh0 zwPqHgMfE%C77HiY7I_0UB$Lk|Vb$nh6o@}3JD}xufZ%S1fULE;A@Cm8l>tw6Lx6Pv zd|O#|jY?jb#r$R&S|HO50XkUq43ND?AX7rWgKRx(IznQFB|z0C3M>D9Qo$1+4L(Oy z@J7f%ggk&rASLK-HPpQty&YPM-s+HZdSmSXG#0E>rdKvVZ=2nUT7%vOHF~3ab>JNm z@GvP>(i>|>h5RwSP4&bHEA1`x(4^goRdzah`vuWkV;Q}P^EO6rL>lV!rVZ#d0wucQ z$Is~cw64phbq!BDonjq&Lxr3HB-W(2a>)U4G>ry!-LCYA%-9ll2^Vb~fT zS&!5>3on8ceUs&~C+ob$Nf%Kdt#Im=g#{*f%f@qB-Xd>__B?}-4u`Nnbn3Ll0imxJVjALqd%6Yk|_1)E;YxzSb|GthuJi{5{dyUGDO+*GX+94z~BM+}*vZD>X-Gy3R@)EEyyXfVprPFRov~ zax+<|^I?v%Dzwe&KuB6pw+wkUV^{7Y4`c*OD9fek2az3I8Y+VSf~iaNNW4f0Lt%`G zGy444XcOBx8f|9#JI-Jj ztPtGbFs9cX1_t064`WrkeLr^lD^9_6yPc7VBQmxpxlciPL>I7ME+e8vUd;uZ#I%u; z7P6(4AX2l)4^CL*;U!jku*hAh?7tcEfb2I$ye1)H!7=UPBu?m&HECDnndxvU6h7U% zCUm)i6pbVhzOo*gR}jeLT1%Ul30^kwxO!=5n6J8gj7`!g?3fxiY z*PKSpKG@3F^_)gg-x+lu_SsuY(Q=K< zXGmZ4=E8m#+sQs9P)k)!gBu#eS5dk{&=KKQ*;rQ7g8e>t#YxHC_bx%h2yDU z^@tKt9mC%JF1avlF=sR*4T()K!h(@i5`c=MrD50?_+)V*1y{Cqa@aN- zgu2<#$Y$=;L!z?aG;9xlplEBCPESWS*UC1Ivm`wb)`<-vEIg?-CZ7==o~>0RBadba zk=$s?EIVOP1}atPlv_q1`)<~k=NksYImxI!%g@yMzNiYpAnX2->(YBu6$S&&g+Zs? z(`j(YgP_5&C?IT)Fpaxr9jiJYgZY@Jvpxv?%6sJqL(W?2Urhr5Q4b)!iaZm%Bbpeb zlGhGdLq-!}r$4Z>Tf1^-hX$(+QHoz^J96xaQ(Io!zR0--G7=iRHna9bGamY6n*}ZH z=D{IkNthaS@hCFnNSEQ16WOdE4ZxQ>MP!cq9CO449=VBH`Hhs;@)z~4{M^T30iC?S zkhQ;p>`}{5tl`)Dy8mW9@|_m=#Ja9s77l?s(S#*r2$8_tHeU$aDL_cF-Do;bN=FL@ z*-&<$6$^%y`qW?%+=ekO`5Q1!3nt3)U>2ObAlrJlaSsX0uV!J-N3kMbX>c;7Q@he& zGrKw|A4*JWcI9=0aFn&6x+_1KvJEn&pbDmZSU*GqX zSM;nZdRC7)62unx(cw%h{Q38K8)Je{b(`_|);hio4e&>4BI12I%{DZl z3Z6}DP$Ph$>kZf=c>*SIU-Mn=2q2ZO8-VT5kVR`Bd516TmmC3E9?isD>}$^iuw$1< zm4-4zm5ZbpTvyH^OD-s3dyWX9s+uqD%qWEmSb@TOG<| z+n&&{+VY*&0JAouwwPG}t@?f!p8_rZzMu^PBZTq!Ig$!eb0p|XJ9y+IJJ!TEEZRJ- zjk^VB>|y_Hgs;l(8jq2%QQ+#RXz1Xa=hvGLGd+^Lq@ zvnQ$1%p+jNrLAPn%g4A3+bEylQtKiq_Xw9djE>7iF3)yd^aTKJyJNc4%!=*){1tk@ z`6B-PF}~L*>`p_FmmwVJ(4FIw?x>;Hb&n19vVQ&Rqr4Ik@E6I-Ul61xD44%qs3VUt z7B{PeXtsZjhLQw6PoA!xsHSHoV}y@{P8db8#h+yPNtz+<5_oSoGORQNN)=%v`=6Z$eM=h-FOf}+u$}0$GEvW@&x~I1cgujo znpqaDMYV3He2oh*fKOmXEB{1QvM`c(CvxSy*X**=*C7F|ee?hanXztj6Oc3QxY-dh_H8)56c)F+hmL=NCjGm-9mVYp(nU_2-@q5jWilU4RuI)Yx5^Ah}A zww1NT??`INS#~C5K1H)G*hRz=5@wQdG%#)aOxBdPGfmx1sTL9+U;uqd5nD7y+?Uti z#wpcgTDThanbOAK>q}@xL`#MF<^48i-|WiJtMZ;)>nduTF*(8Hb3!|&Je3+I7CGu0 z0)?pywHVX!OywL?|HH<2E?EIe`EQggka?06aFu#56sy zqWf;63Q~-C3@Jpy7*gJGH!<~<&HB*Sh0;xpPFwOKc3@>Y$D8x2iR{6orM(p$=8n%9$%YAW_`Bjo`X_muM=Ao*>t% z@}_3-EM(l>?Ky>~GRHnz7NK2XUwYGR%;TrWI?_9U)0@_&X0SimUJ=fr=V3}gbST*7!Q|=(9{LKCE z#%C#KPOjWMiF0=^g_%*XejM5uqvl|a+*aZ{p<2R!L;&bGj3w1FJO`0s0io25swCJ@ z#W^yP%BQ}MHvbxh)q$6E^V+>j$>5iW9ljxQ_Q}@e^h_Y}K+k;>q?_aif`)Z&&?(o@ zs+r7mVQ^EnzmTM93@)ToK8vV=ieEz}mOL!1&9coN_=`4xUfC2|HlC$30Z(<>0;%@i z=GKHBR{#p&Dw`+H0;8<09ULYau?S=Im%#F6K>;w?$yYTUKwPFa!;AlFEFL!llEJxb zGoY8hF5Bi2o#T0o=!h>pqC;(vrI5dvFN^5hHL~N;7SnM^2T7^_2&ld3p`DXRIE8i; zg`qd_@wz??HZ3AwU%oV9alV@;ZgF01^*pP3uH)gcys|jKuuM)Ozv8Fo2;-QL8Ao=W z#cc%Sq7aYlq$oI#i^|ALf!(Ew{pjFQG1hbHGsN|*sbuCbXb%${#6@Ytu=eVN6WMQc zEJSt`8ImKz-;I zK=>atGMwcSCeW~7%|_-bzn%?nPd&1ujdUzZ4y%Px zd+>GM4LH+-Cgv-ky>+UBRs`9{Z9jmn{&+umR37cvZ;gX_4yC9aazTr2YmEu4wnvgK zvsU&PwDyfMFK&@2+ph{~Zt(%t^wyy!B?&1t^e}gB;V(p^5K_}Ouxi@=Qu;E6knHTW z`hgaB2bpzaPE2XxnK|n)l*WGZwh@$xuQ*Z^o|!N4SczckSY!`z4Jm3A7Jp-bHfJH4 z=b{X%ToGn~BF@2opfRyd#YVNe`Z^N=%v2+ze z2>MyE2@54mg-`+xcL*gw`Pjl5Bf^ot@Ukki>*IEffjIatP{XH*C}6wRV+vxIRbvWH z5gu1?i|`c9G-J%5e8C#6#}yEIRJKZ7K~i0Dg@=&XlJb(=JTWQ0PFw+@9kjp(rF<=} z5TyK8Q8l}I=Pu$MU+A=TOMXOJ}4p zl+W~&MQWWSUg56t(xuF59=x{!`-*x+K(2Sa4SIAnJ>&El?OSt}oI>5bCbwg2yu+^? z_#xYao=(s-dMO`#0P`!eGoQhgWP^f6E5w9xE}{lC=klxJapbL~_61~*ymMnP7Xuk5is+Pfrc%#H%O#NFLX1T87da+WfXg@m!WoPdjKK!%3OCG zca@^YI=a(k=p*TnE-DhKr;pj;EMM)exVhaoznRnpJ}4j2W}eE{47Oq zZ0u&*qfE_fu;{%}8Q({2NquhsTN2Szt~lvn!0qbbbwAy^WJk3IP3fbhGh^v&62-f! zqih(h;?bS}Vgr683>ZJotQv=*Sr!gM4|Tf_X5>csN-GJXq7IAFLrLJ6wiTtP+A2zK zyOx@Xr9QiQ_`1>}(Q@1iW@AN?tHFuNBS{jOV|qe6Z<_8BSvZuG9HT>HKnMu1QP5q_ zIbM6X>IXk(15wVsVhi?R z5_C$jV;uTlCQ#|hx@NNpR2iTInMIc%QgK>gFPP28g~$-8eIk|cl*tu?wHjUA_7pfF zRq5;;V!bX^<%$MwD#hI^XGKZr5_wlr6_VPpR28_1+0Gm&ws_2T##p=u)%HAQ+kb;G z+i5~}9<$90oydxZ4lz4DKLsU>%rH1Eej#gSF&b(oN#n4ff5TYF!ek0$vr9EPQjrfZs8U4FY$L^ng}G&5SzOv?3S|Cx z+aXU>E66OZTVI8^G?Z+cGg*da$LLrSW7_fI?lT*O<|#Hd3XIToMuD;WMn++4c%q)RFO<5{ho-26g^_$Ue7`` zpI7f!<8LJbnFC&}Lz7LT+{}v4j`NB53|ACGz$YWr4;Fp+DwN(?>t|1u6vXYmOG8?g9RiJ?FEJxgoE&^ICyYccdnCc=FR z3Jw_=odPByb}my!G{)F1cFtEUk!57Dd40>dmbs69EV%F3nj6IJn) z_%h`=Sc76SgOs`4%W$~}NA$=e>x*1+ z17%~j+ZA7iV-bjm#jHuj_DrU{f3tfmYm&<)=QrqKm8?m8WuO(aCb6t8p$ZzqvtGhc zqXQTTjK7Ml^OCd6q0z_G4|;1oXN-6VH1-}3l=?!8Nw`4`lvo9BhjZqlz>*qQmc#;7V&kS zYe7I}OCPk4m%{Tr^k_Hasc5$rBoS0^=70C)g=0m;-F;p#A8&-COIGCh@RNgvQP zO8@b38}l0u?JQ?uP;(X{Ut-aVmM9bl7>=KilF>QLO|muvB_Kxvpkmf0?S-sOAFJAF zYxn-S>}kcT_s9#!Fj%14GP(8lJx|mdAr(8iohciQTjD75(A47Xs^&~;`q+}+7=?Q)% zP0L5`Ujl{P02o3)Kw>eZcACWD#Y1E1EQ!ODW3?wx6;F`kQOZlEY0ap$uu_84%vV%` z8_nRk)+D(6^|1seej7|rGoQVV%D8EKYbUoMTetd7&a;i|~mQjZS z7uQG}ekZ@L3ycwjniQ?lN?Bya-I0)1YWPHKW-??H=Hl@@VS&k*v(jN)&sOXJce zg-kavyTh57dj1^6nm%(xeTGQ-V~&qH!wMiFQE8||gzdQFXZ35nwuv2INN1{pryBM* z6sq_E$RVz@K^0ebk)$53o`9b65feYA#ynzD6o|^Xw%{YG= z-Nz@40BXmQP={C2A>3+nRdq|^_;O3aEDa{wYLkn_(XVEENZsmZQe&kkc1wm2RRWzFs93 zQ!T3^qmNEQvXa42?=C2C*q6}7po@uWChg6d171rCX=(m}reZJ){A)lK1d!3{cCvs{ z3uYOa8uwz+)UXicq2wSjD^?pD8ni5tZw0WRRFLxCI@khb zwP1-ETmcX|HuSDOphFYmF((@n+4d24qjW=L+7!*57WOSwyf*Bc&T9UR1(msc#&hHX zkI5_vJj@x!W`*E3n21CAmjC2?QRdGBvJ!ye*4JDv%g@na$a;&q=C8DdBL5>-6Ffng zkMQM_(I)r6Z2G)PDi)i2;NiUj#O^_#z+HIYoy^0{UmLeV3Jrc z@&g)sCrudXoSOvJ$He^%;R!RKI5sqOE;^y@s0yY~jci2SY$8#S(8=}E(Oi-(bC)$F zh5yb?vcseor&p+U>O0|a$-yTv>C^xZkr{)3m~uD&+MoUS@Bf=m{`8;yhopBNZ{U17 zg`t*|FFvpYJ{2C4v)cQcB=p*Q%f}a{H3~JM#rjMz5{!FW3}Iq_vS`nD7g}D^rXD9T z=xg=$JAJbRPM%CJq(Yt}*DQU29?*!wdzkMQq>_T*#L4Nmr!(qQIm0P?7XN$o2bQ!? zsDi6HA(12eRE?^?$WKc2>UoXDOMOwkrOY*jVSm5#Bp zkFWiYXE03jtH#3&V1N2ds<61u>ev*nYS=u!vf7Oda~*P7*1+p4$z=l#AP?hLAHdM{ME2MG>6@RUpTvvV$udX8YwQUpFZ?cF0x%7)!+SEyO^|L@r3k7ET z^`D|@32{+$KUb;c`k^pMNVUVnwHRC!3q$BcTR#(&k)S~kZbOl=&DtnD2dWh?#RyUI zUP|39YGt;^OeAgMWr0B1IA*n(QCS|oN_l>EVzNJwjfEADA|$zvc3-8p^oZid3RQ7x zmLeRzu*b(ipvWWX>KfJ}$j1{BU`kU0FMiIGOdchGtHlZO{(E@-AXvSRK3HBvuT z=Q|f>dd(#M55EU$HQr<=E)R}78*@5#z90F2(dGOhW%u?2cn5Yy@wqbc30#W<$hk1~ z=C73(p9N`PyQ}#zmzsn!)(S@^D`DOv>rCd@098mUBfM?fpu-x`f_s&A88WA@nfhS| zLA9eo7g6ZaV^qH8bP0=y6Jh)ol9EyQVrfPXhq!zhje9B#nxJoF+=liSAH4mpx%^)sn5G^W*4$I#OOJoe9iu0LN zX8I{WbT&eC=lT%+8((3Fe)pL`RKPkdL?zuN=l!^B(ao9=H4gv{P|>I+&74LxXEr8L z>U4lQ8A#8C&7wp?&{)uPkeI7E(Ij-EOmk-?v=>upI>KEcUQRzV?f>7TEeT9BQW*PRH=DLjRIA0TCq`l9bhe8svsTF5devNLYT%RW2Z+#YsVi zER#@RGCS{g)9T!EQ`?N&7sa7TXl9vRJby16`<`M7$RxT;jW>*YNdcnwe`^BX_Anp( zEgPmaw^pe!;6Ielr*}A>q$zTNQnhuhlEUxd9(y3yHqD<>-qrq_?aAQhQ?GhYm2h`) z6%r{DA0PoFa0BCCbWDoPfd){O+{)x=-OfZ=^LS)+b3$cG>VWyuo{C`Q>Xyh^@ z%9oKL(u96hZbF9WX!N8sF~b~cTHXhnvJ@*^l>-wqy8W>zpzO+C*!r4svGMlQ4Zrf| zs&8~vwpG9J?_x{eSRa1T8c<|MSLIe~fRAW{em|#Y&1FJNp^mx`)7*=g#H-F!wac>( zm(&C`K@ZEO5MFy&6fGqzUiea`5GHHdRG&h4@oU*xv7?<8brgKJLd`u9HIu*6d*v7T zUw)fSV3rvv+GYNM;U@o%Y#maISaq%OZNhVX+i1px{@}k&N4>}ATlb8%&lg7@#XQ;m z=xCaGaJEpJeiujZ^0g+~6kM8Sj~74)T(rlFylEnhLl zd~hSU%xYL6lydRMK+e~dPw+GNU-jVRKj6Wze3~EIAeKr)9}(dBkokleR@)qAwruck zsh>^M**w2jKL0CAvT=r*P;Rs7-b{;wL>tWv0#pUomJWb!^yX9c!(eKUC<@_m^PT&P?D6^H{v#A+ZFvIo+&+Ci zcmF&Zng1<)F}d#u4`#!IQ{Q|acOAcb_5Aye=w;`qzJ7g^Ud`(I3vDYj$Mt{6^>p9S zBS((xg@ii8mgvDS)2WR&V65yC>Jm=6wReWKK4*Z1cT=@@hA-ar!nDoKP5%2opUBC9uu*18V@NTB|&hWy!P2t_9+Pi>}h>8|OSz%*K*oJ(BERsf3>ULu470T{I8SuTbHclGYVRBm-Ev2G zx1;vX@z5>L4e!pay>mQt%bnrf&e}W2L$^FHygRS<&hgMK-xA)vrS{J8&@Imo@6NBi z6CSQ%Rn(Sp%U2uBiynIrqE32OvdKvwS6Tb94B3`rG*t}PmY5Ww4({GvoWAldj+JU$ zd3(Q)Ua~{%IA`DrO5u>Agecrn<|2AY69(*%g$TF;6M;8Zu+FO|Rn-q-FD-Pixm48z zv%zR=LstV_R8<817ls|#CHlfum6wdFs>9Rq7p|)Oi>RtPC90~b!&Q}k5mgnb8MQB5 zRrwcDRj~nM?F&~`{zX((9iT97#Z{Gm5mi-3s*3PvM`AHDBpWG6RehB)Bw`OMh-BO= zNylpu2W+r_p$UaFWRWC2_HQYV|KO6g?SJG)f>jnDJX1Ybq`&N&<`|J2u1|Ft%nG93@4d&SZ zo8OGzb41H&0nKjZM;20e+2Mm0|7dSG;Gc}Zt;N*M!)@GTUNMidqPH;G3|C;>Y`&tu zi3d=(=CH8=&k|!{waWSGmkD!V^bqExA={8qgg3wV=gE=j?Y&}inXv(O%7^5i^$!Zs z#pa@aE?^+@TrFr<44_NOATAIU@mj5w$+w&v>SQKuTqlPh56Y>_;rz5{k5wssX%w4n zzp^XX@n^z*rpQ!z_(N{6W7xMG$f3F7x zM;>-DyA^lf8FK|c3YE-vm~EIJ$V-?d20LE>jQKgRIKQVy7g8-BJG(gN{;L_Ma>JT9 zL;QnsQu;VMzi+r5w6UnNRF2GNN2bXIi>&#h%h>A49x%x1@>RY51#y=+??IJ*n!DJgqk!IhI8z=eJk*r>K)SpO z;B}-c8a#x5M8xuKII+?cMrV?wSeLDxtwi?h-*Sb`43D*le=IHX?Nq}DSF#OQ#(!UU zu`c-*{cpH(nP)kyK#*d-#XPb)ohq>v}*hYjwG1^17s% zysn1o;wZ%O=~`VZke#gSv1N6+4)eOC!@RDh>T2@ei?zCd-eg_REvw75o!2F8=XJGI zSBw9?P^+uCyslT4)#ZB8>ylpdy4tF%&3_Nefp^*v&L{izK;_*F@~%IcGM;*& zc0|}?u~12HseCTtwq*Lfe6hNVq+IEcDa;zgjks|n-GAoVcTI1~+B*4<|jkZ6kWo*i*Cu%dl zc+Y*qS)w~|gKu)uMdB1kzbU&2sV7tWzGC*sH<2RORSxRDLc%iNV-itmAv*N6Fk#8X z{+pw)+{3GP@MNWo^=uwwtO`n`S{2VAi!GhU<*ViM=&;C(!7D%eMjh0tuJ*v*2v%JY z;^i4~mIspYEakCi6|p^E5P^(9T@@V`dlJJzy>_(~+r|74mENOjup&L&^O9%qSnJDE zc)CJDS3A@Y_w#K_>HMzzpl`Ti!D9{r=dhWiu;{Sm*(ksqL+4DLvbbR9z_w zD3xy|Y}t>6q&M-nNVG$nLhnG_RMjbqF#q0Ugjos7njuuyac9&V{6HEs9kbI3km09+ z(kJ5usz*&~9Y#9%an@Jb&Nw>sl81xu7nBqX=lT@aA)zLx7xa;&WBn*@X_FfnL$I{s zCy8g4yUUmO!75pv-C`4Jgv~J+RD9q+tZGm#o$`lOm^;Dfgc7D6Q8m8=<$jw7mg5B3 z%G6P(pM>F4Y+Im! z8PB>Vb~hdKxD_s@3XGJ(YN8S0i9}`3>QNyJ8>)mZ-f3+VLW4q$o(s+uoq#qC5iG|+ zBmd)1GWzZ1Nqz*z@|5n+D;FQ}`Wcy=hRMvp#KNwopmyE{)-%(cRHymqH zvmpwhN$|(Hku+~;bi#A$60^{e*z@H*xF^Z>Q+Wf{a&Xw{cHr-{!H+5h#~`!@E=`IF zT>PkuJ~2iYQ~s$p-srF6-;a9zg7Z=BB4M*`QEv{^zN=d$^;@*$tS>`8? zk_?xU3@4&i(t=r1Efc#16j$J|#9r!Y!@SvacHMurf9&gKij(Fi#aU;u9p zxTz%q9#c$!=f{RB=4u+#s}-PSY%LTp2L{R%US^U>1+=!Tz$8=)X`nIX>j}I?Dl_#$ zDDerPB|)2(BPHrlBIM<9B@wOEfBqB;nj14YBusNfe20}J0~bIX?Zs{eF~0>0<{QQ% zsiGBHvtm{SN$?sX#CIqEEJJ>_&2VJ43tQnppB*Lx0P_gIeEO^iz-;heRRdsGff?qnw&9jl zL#BiJVuQnF|HoB_J5>!gUwybtI^IbXWCHx#%#G7(pGK$MN?0MB7t0r*8 zJsTC-p~Y|=<;7c`B&sRd&E|Ok7`BE40!~=kexJ~SMkY|V=8~lRqu==SCtC_oA&-cl zH}xF&5^MBrW1A(V6k1d%?PEGwiWcPEFt+1}xvo?F@q9$;sbOKP}S{ZH*uhEjz*^0^6#D zk5LtX$W)w8#xuM~t&@yh{+nE_-{qAZ&eh?kyu)y>_3YkTNp~kRKq1*V#lLJ? zqV!7xfUE>Vxmg&K_Dc!*2O1is=ZXVl!m#xdH}p4HF^1rvCIF|2U^G+*`)5j&1s*uS z7*k=&NExFHS4k-0C8Vu;OcCAFmx+&ilP^&v`j~Y}CLLEZQ_O@hSw1-u395?nd>BUS z(GIIyV6J}PXVe<~v!o=TAWlBemm35W{?P10y5ibiJGBID|C079YG|)6QOl{+Hk6{o z3o7rb@&A%=V4~=@VapZ0!)(-bCEpE7z9r2clss^?+o0sR)hGB(Rcw{Hpj_uXwYre>oNZ(F+MaO~ed7Tp|Y*Rd?0f9jjiAgTpXP z^^#uKDA}zbc6Q0@G#lujN#{ZmJ|d|a?HdK~R#CEDf#21vbQ{#tH7?g_pVFBcbXV3M zlpC})D^QRVH19P>xyDzf9Z#<^*gR*s%Bbp;#%&Lx%7}HYt1_yxY}Fuez{lnrd^)I% zrvw_!=g3B6JhCg5d?s6UnwfJ1q~pdZk^LK>OXDnkEhK#^I4@?A^2lkFO>QEMK4wjo z$9V#jPNdNymDW)O#~nMs5c4*L?``|DK&-Bs+yq7O+9qhdRY;aO);h^@>(FVs zhrAs`+wSB?X-ED4QtVP{+op_%LI2yI01fr-)p3 z9&_2+1jQ(+{266~ZrnCy_-3V#+G7T38-wJ=AZ9jcQ;%q}tH&}4T?&SMwwkc26 zvrYMa!ZtN?ZETy8kZ83ARHjf5g9i6)S(>s>H9E0}S(=}H_7gwU4)!SosGZK;KJB}G zN+;buRjJB8tx84f)Q!l zp#dO*xOyyiKwz#LASw&fh=VmaB9167?iF7`6^nsTAWZg;@8}#an8*}lkQp5O0AyHK zR=H_+tq>U>srj&##9+Nqlo)rYF$HAsS9I07~b!p-!6T=BM8c>T;(29{%kLg$2EQ(1ro2f`|BW(Ff+!& zNDFXCO}`QxZNS%pgp{8UP_n{Vu4!!MwIb}VW(E*HJ&ZRB~zpu7zeX%lU59F_P%j675M+gg<=DYS;ajh}rQ ze_MDl$qdJO?^R!GviH|$)q;z~&@}#UZ2I)Z482%x%m@J${m;0lHC;hc4&1(~mk;%H zP2x_1AR0hxfB;MCG$1Hs9Yq}rZ1KrLP^AhvYz?{sDA;hjW1%`zSz-ndIkW3I?;5x% zh|`7~q9%$0W}|8Zu5DL(BvQ}@3;VFhC;|*sT;U3720yvZbgzl_(*#UJR)Dl8TiLXO zpNSE)1!)I$Wnr-2f3swl8rm<38;nc2%Da>CVRgAi=C&mZ%!B)LnJEg^UxA!`QBSCx zmSt;>jLWxH8sbd!130M1Ik|O_GgpbIxp5P_R#gvQkeF<5R7^@exGEh}ozWU^tM z8ib3Qb|~n!YX?8@j*O_NRqEPdvlk~SIeuN!4g-HZaB~%(!u>dD#W$cG!0Z~K)Rvj6 zq6N()2F0xP%uzYW#HpRSWj4>3q#RaX9J88g$dgPfR!^@PL;f50NVHL95LHVDP7@j>4|KtP8or@cx{r|@7bx;>am zdKYu8jbgeUR7E%D7Pp;7Z*_)E?lYZ~S92wupy@qikp-OyCCr&f<3OsO}P@DeI+Ij#7A;|FQ8|!t!Yu2IA-N~Hk!??HOf;NU;cmPy$hV3Rdx6OT+W$uX3k7< z0s(R(dFBvEfLtcG$z-_Y2@(jmpn!NmlSy(yCYPCHE&*DE0ivafimh+OJ6aUHP_V^9 zEiKWa#fplp7ra!_(*K|LeXWfZEv=Wl-{0EL^PI~}LWuVB*8hLtl;_S$Q$ zz4qQ*k%h^Yl;+N8J&_(UO-I}wR!lMCUeHh;jeNQ->}-o<6-fJ2pYRGIHu)OiQ9w^K zZslfVq}+H_G^Z$6+UyJ~b7Z<*4Qt~bSDy1}Zf8Mxc?CIBW&7ckMM66)n%8@Aaq`TI zLI@g@wbNmk_R1xL^jb4fiF#!fV9jkhm|%)qbxYS6=?mdu0t`cpb}MTxo||EqOHCUw zY(4N`SdU@#Y3wkJKBImy3_qaH^iYan)RRMD*tONrX;%FNf6g;YgGUsCcN+L}hj`cU zM{{n2X0h_yP~uWUtWFiz{Tg!=>O3gisRD^p6|8%zbFY#MmJr5PnC+2Wx&p`sl2sJh(697H@pfylId#?CM`3^-*Wl6avPt~9#f~=Y zch}6gp&@S|n2~ea%3k8c1(u~QP}_Kl-B~*C7VkxNe3(09Cr2TgM_jX09xX0L(2{cs zGzT89v<{a}=npktUOPCvv8wq3>!g(7GUKlnnBStj21}3SoOdY44sd!#%7SW715SP& z^{2Sf!*|HXptWqr<73c;^5lE^PJJ!#(!M=@h9mIGpYfpwq1MIUrTDgncssR~0zdX4 zMKu@rX=T96u&zL*A$is=$9z)>2wvii3$jhU%5!8&+3y2IsEP*hXdsWq54D3{P0S4H-yX!}3HBBm?uK z%G%xx!JohQ0)emmc{-P_((wziepJ0?voPdVn;smH-<*o0>- zm>;*x{UeA!e7_nrKdzWdSp4QBI9;oaLHy%Caj~=Hd=9+ zJ7-G9;=uPLxXMjX&<0M_^P!%0(I^-FkY-jgPX=+9HeWEH>?mf*CE^nww<2H^yt~6Y z^``9>(;)BwYOgy>xGYlTG^5myyL5s+IKmYh&b}{7rvYq{wnfU<)+}SyMSNfOKd@i> zKJWTd_>%TFZSX^84R=y{j(r?3|6;|{VHTPObY@_D47qLpQ+zGg*~ceb6f;|m#D6$8 zmfM5q7|hUcR9z`F+IYxfil#%IHQI7h?04ogxqu{`OstB4P0FqcJ5xbKDjQUMjHSfb zYTJoJs+=^}^9lw2Z~m+Q6q?b%{IRXo8qoMQuo^~RYa;zet$>46z=Zj;;S8V=n>6fo zcAwGZxHx>hFX;dUQJBCJ2n7X)f_LB(vglm)3|nn{H61T%(E&1f?br9Qxgt)sP%LG)>|hrVqpHYPcm6K%^8>7Z zYuTjB+tkj0az)(?N=<6oRRA7%VQ^&iFoYME}Ke+0obYC&?>{{*TV2AF7!N{Y9cOp;7iD)pu&HX{~_5p zoC$XzOPkuDUr=Olw9ltTK3=w6DtoM5@SLoI(i=a@S*xdb_SbXBza(D6g6*R?$OxUB ziPGBZ!T36Dv$;01GKS_+5BTSRYbt{Ur`7Q}RAu(TupY+mPlI%C`&r^r>$oCQ6G|9= z191xcR?ZFsmV|awXzCipP_^{ffzFhs+-5g9`n}n@!-L8F46_0V+RhN zW6-W0>gdr%=vYC2s252~p=0*qs7i>zLx|(+)95z)&|dq%KFBD)BhJ}C)kzm&`#h}) zP%RV9x{ej%(IXWmpokv_QgLYrMd}nU-(7d2GrYPvC9LBV)N>LezS_l_XMvGGg)vMo z?Kru81#%5lIY&}!5ivq!6spuQ6C`++ZT3{l*J~`0XdLiQPuRBB*cPbCLBnwxMUbk` z-J0UUj`HVZ5D{;E0v7@|hDewE(1?t4rAory*pTbD&joAlI0dk_++odd7I9Dny`7zX zW>^b_wH3lzCBd5P4+Kw&La;_*mSTB(r!8TN0L5(Tc^ce>!7pRv7x-#?Pn+D&8#c0`^d^ON}b+N-V8BENb`IgGivQk2*1{a6Svp; z@VJKZj&D!##|@Ji{>G8S#N{nldvU>3PB)S|j%hz5fsg%AS$WI!uH*F%kH2LqucM*7 zN$tJH%J3KG9ILRsJ1LzPzQVQ;7EnFtDtdDl4`*wWpUDU~scT~HSr zYeJQdn8m7`G;iFYSgSQD`IP$DHpQX^W_eGlk8M{hcAhLYs*f!yRw8i<^NDq_HqDFW z>ZA|pii~t80wG&Eu}ELcVrOl{5u1E~WT*nq5B4b$qQ!5*3Qw2oWCh6V7Hz zZ+4Rs@+P)F)zX zQbr=y3TnlhjkS>MgD(#lO*_QxLLo(fYEY>%WfYZ4oF-GIr0iW115K2wdpa$jN~{g= zT@6UnpkN3I_M(eD|caMXqxAZwBxiPcFVrB3AtTt=oxuk?iP zcBi{6P^MiSq_uM8wDT(6OzmJWrIr%Ja%_@R;nDheUK? zqc&cn?yFA(f+zgoo{GGuCr!qfglw$MmcT6_pGKY+Xi+ohqZ9nRP`KMG`h<7Tw@hp@-g(l$+&He)o0%dzjz znsdPIWF?oKUIU5ROvldZaFufKMZCn826%Bd^2o3~hATW8f4Z!)@cJ7F~P*Oa^1!f?OV4MX+Wa~wd zw%ipu^Gp|_U2FzWV;Hwx#6mK>XM4PmTuzZ?2BRb6(t4us*kE3DDVRog*ky=D!t!CV z6H5|NgF#B&OJq#$3@_A2nop`YUWYud(G;mxb9%wJOU;h#)MthHu&p`|s?cvnO^2%^v# zu!yG-c^sOMwalfD*=bkTxGtl?)Yw95&fm`Ui%BQ8)vQjA!L%BKsT3jZ zYznZAMXj2E@=>dFGTukfSJ85Sex~-=(HJR$Z1JP9t@Ke#9gER+Jra`(VZ~UC#nMIS z%VIHkV=*?r8t-T66M7SiX;t4Ul@TLlY>z9(VQ4=Pv;d~Tr3@HgAb#pMD;M0TvsK6@ z-kJUuc$|D}hr9qS`aXztQWaJ-EHntg5x%Igky8+}@1Cw)=t=3s5rZsq_HcTN^N}mX z-}AL#&tw$0;FOV-MCDcz0!4w+40AeinL_x$R0j@4n_6g8Mjc{x!0scJcQIy(T2VI% zWdaHy9Z)710*Nb6H$jm6Q|I8ZdrtKs7m$A}@sVPb7ifH(kc3-!B z(o7D^^9x1z;kZBD5yX*V+GoSltP~wc&3SBbI8O70d!ZGSQZH&TrM@-dbgfLOpJXAl zZd2;pEmUW)=s>AQR8^NlhYbW8wx6W3L9SUU=Iec8>4Xjhqd5`hso7N0NvDy+=D$iZ z|IKfY4prBj`nu*FQC;)1x=yUCYj#rCy!Kg#s%vI_U8fvTUGuWKrq|UqBdO~Y=~@%x zkX;Bh9G-kcb)AycbzEIt>{l=hpDb-|;<{MctJO94i0V2yt7}SKU1)e#*Iem~6W4{B zSF7u!BdTj|Ru`w;Du>aAZ4-rZPLg^waa|}iwYn-tRM$yaU9EL>nR`BURiyJ;U11~5 z=d{!qisq5P*I{VkeWU^xfw$yf${ZF4X2g`?JJ39~b4qh{A(7`vNDj6m_Dc7%*iLEH z^&_$)9fM{lYmd!PR-a(9B}h$U`gZU|;vOQeYMCWsh(pqHc?GDnb~L2M$) zcT5JZO-_g`&52#%Yy`lxs|E@^=t$`Hw zgcyYGp4$t%=o*4wtVC*$@AB8p{B6NDZJ4Yk0Db77Z)*Zx1K7?X$`nHwBOjwgsWk>5 zL5-so#DKRW#Wc2Ff@wN{^VtA4#k!tt?(2X1oH#ivZOW^-K&BYeMlo zg8T@)N8eN|5=YprSUL{VRDd{oP-&|ScC9)=!AbABEWiZ7+WcX{w9Ox=^s=fL;Mg2! zz3zL$goly9U6LiMislcj6Pe)_laNnha+z73`NLwtF#@z?U9E&*j{+E`I$QS0raDWK zSZ$f`tfvywXMKlZ6mZruXD5|lX_y-cO#`1S|)>sSoA0I9ALmm1pm0lO?gm^leN zXVaz*YEnEc3Y^Byk}}4wL-iWSNE`0E9{Vp?Px!VCOSvp1H}NG->&~ca^=kb}3k)E+0twqWW}JVwPxRf8e|#d?g#){YU4|#DfW7W3t6t@lU0fb@xeI2&2)hb8M)S zAECZt^lQNy%_m$7IV+S^LhOEWw-u?-TwU;UDi+xe?oz8vTGM;mJz5JDgyM-LnG|(2 z-LOWo<+t5~y|9*2yKyXK{Ym>B4Og#KCa+o4lq*!O$!q2}eP$TxbEWAsD-~S#Qg?q_ zN8SBxz?j_MMni7fx34nk9-Os~ms?d0%7RLO6dS+dq#)QQ?^`pgsYa~@U>I)MekcUt zn|Xu?YD-hTCaXT;nN--ER{i=tTScB2YA*f@T|-6Hw3`MKqzVz^Y=AMbHXYXW}G*kOo_f!`+n< z_oRw@={C()c|wwsRR?2m+_%EnLAuebO-baAjBg~%le*Ci@6C(we}egKuW(eAV{lrj zVRSjQ#sYEcwrj>KJij0w8jxxGWR?8fgXaU?!-4=re^#c|wZ z)JA()pu>Nsl`R31%Vx!6f1WIGrXTLgKHQ-co$>zcLz3mm<_`zb4`S`{9SWH*kXE!F z$UpB~Tfx-=vC!ze+v|~NE0I*To}OT7 zP+o~-VY>%jE_E1WLC}!wvA~9(Zylzx22!QFVMEjTX@H*u!ZhD(H?hbgz@t=>~}fh$itPf45(54dvhWnWb$@vPMN54r^?9 z5@(PjSPb<#DF_$d3dfN!HV|x(fa0?>dx!L>R!->{?Ml}gHwSnk;Be(FSYy&d*$K2J zL(&I731D_bjrQ}7{Y3(1{21Tk)8Ye){u>mH&6!Y|XZ0PJ22hXORmFIfSardO+xkPz z;m#fB0c$RCai?UtP9v@jY3Y^xCTUT>O{78KAt*U7j6`@O z!1{2nr^Q^hV3`1B&KWCJY_KMD+piv=S>}Ne_qg3z0Tm|KlxQoL93~2Uh|~#4sHuxc zB!`a|SJmyGdhu1MRRZ7SVQ#)MXmKS;caJ@nfS&=U^yW5l3J3#8IG!JQa&Ety>f>ec z$Nw_vHucorQf(ikZ=Hv9BTXx58CkO(mT#r2oAzTiZn69!^ojHdjk+zCi(HrCjp9+X z7w><Q0{yIpZ+r;9!!YX_1663LwCRX_dfKcKe`fMO54jWN^<8XBy^pfpip|1Bhd5$ zqokYU!-a&Vn^_Xt4@^&GRHWgEt?uSeB(JUbNywNy@P{rIN9p{FAskMAaRcT+VKPLUbCG39>|*n|QGrJinNLcgVpykNDA zSg-~>Q{4;w9G35OdymS0>|>>3Q;pk>7?wZ8)k7i#<`$p-a4f~QnX$`ef4Qgyt9k!p z-=W&)UfP8oHYYoUQmKCIvv%T_Uh4)HJVAH?p!Q0@bPzueI^xsg{ebJ{Nx{m>L@nyc zED9tW4uwj!))~>t?*bop2Ji#$PM7B9GBe=VcH03;);~})WCV=`tMy;}v%9tCZMSqo8`^ROgnEw-^A-->#DXDW74*;3Q4CcB}2dlfca{P;bwZfSQ*+}PE z5Ym8(HNGtv?{hHW!ijsId#3>LUoo%p6kn9j=@rG5As5Sw1i6E#ozH3E`6!Ttrcr}d zehGkiB}a3dxIj-a@YrB%1m~3=4BRZuElY|Odo=VyfZ9#uc&JDpq()Hys|aCWgmqQd zT%J9MZ}mf>OX0d9k+UHIJBa6ILt?&$#0)-h^Ce}^4mO!WY09 zWxqI_f#d{dMoGdIttmzI?K87v;M(`j)37P&%Chu7QvTF!6_3Ao@OLG|a}ovOc6=Qk zdGQ?y$riQQj(*yaIE~7)9qziz_m zpS`^#W)bS?4vh^XJF7x7ECFf<%+L0Gl2_8pe4jq4G#&q>|W&^ znuv0h7RDNzCuEn^2`nmBeXMQ`;uH7AxjozI53B8bR(hkHZI1&WiOmv(eRXT6CmUVJ z^hIG|teu{7_Kbi8s@X9V75Au}AvGwxXIo<{bYWizw6MuKh#|N2)Cas6I@j%~1YAl? znDak$S1-J&T!GReKcB)~su(=7N|xgHI962wNW&#wNCuih&`Q@Way+o@FSoC+7L(xV9y_=$ihjhg&h(|*aGBE zQI@clGf|@yppi3vc^E9SGQ^lnr$q*Kh2Ryd{UA}e7Au5}HJ$$CtgJ3bABxaYYKw0a zu(Ph+10_2cu`^H8x3bZUFAi4*CFI5zNR4*DLKT8t(R7rH1~#k>vK^oy#o>^Kyw%6STsJ zEU&XC#aQ^3(tD)aXy^@*rDcJI=7E(qxkUzOoVJVVTWkO^#M}U)ObsAkoe46s5m3<; zO`>9bh5D&oBcP#1+XEyju}a#(pfS#z`kF~kXq7F6nu{`gV>lK>a3JdjrY;w+Od>ec z_Px*&BIeua-0hth_2b(Uexl~HG-2m%YU0-ftT@^iSUWwWxfFl|oJIhkUAj>QY=jQ$ zMh2kF1Qdr!UtdHT#qE=!(qMI6%}@>;4z8l-I{8Zjr)Qwr_WOJ~`Zxn3@)%D=-NDSe>kZ_V8g>JQmS^KiBOkt za*YD`AX!FWg@nWfUEiwL5Y65Q8PkNkp!(WF(!8C8OZLlBMRDkWvZ9v1HOt$dXO{2$?V-qOiu2VSx24 z86-`>lEsjLLU0_O9S8O7*lo>ItHD4*@+sXzGb+rn8sjdACtm{CA{U#?tW)hx zb!F5j30^`V1Jf}Uu_`s)(W0`G9-4I%2>snet~A)Pi@>#W!DZyDSX2Bs`K?=HP!Z)W zv2BL>Xw0<~Kf!B!aE={W}3Oty^=$R#ZT!BZD7(KIOPb%?sAFNMiKNl0B`!SCbe$ zvt-|?k9k%BeNp3{sSg}bKwlK|!}`GU3h0Yso~sXx{g@biQOqwClLl^6jJ_ylUwz;X z1@uKRx7P>mR6t)8^Y;3{T?*)nV(wMoJxM^%Eam|P?oML#%#uBbtmBu39H*;5KUk;LekC3{+duO=~iX33sa;5$i-o>{U33Otj< z=$R#ZUV$GbF?wdn#!wLA=aLvbvt+j^<`+qfo>{Ux6tjQx2*%E3pZG$99`RiHBRF#c<40Uj)&h3Gt>oVFiMV2o zXDYohU5CzDaCA)@vAM8-379nJpP5F8rh@d%e`5GIqJ>CfpyjBZ2_>Xbc%m+ZPF)DG z+ndIK7*^Aab2W{D-D!u>7-;g5X$*x-W2lom?FJyfL1QQwwE#Fxma2dtkFMRUDg>Yb zsk=^9KwC>x1tv37lskPx5Z@)@x+B9J(OBtH6(ncUW785NG_$W0`6N$CTVQb#FeH-8 z)T&NEWpqiDRih~P8&DVwj0C7g7*H78Xy+m7b;FW@kxHIg4eQi+&FzpBpVue>FUgRV z%`g!yqd@eLXls&!_qeMUrCMoBm+yvlRG3=?)j1X>kEO&UVK)^Lv;Ze$um#UFPtTE( zbR~U+j&u_61Qr(mnu?!USftW93k#T?1sIv;8K@QdFO}vHmVwS>;ZSshRMVh&>g>FT zW47rKr+Ff^8yzu55;}4=8|ri_CxuO{a+2`CbWP^$FK~}ta?Zbh}=`1wx@+-w5-$u!ke>zq;Q&@#dIqMWr|NE z4hPvd-707FlpCj8F{D!6N^_Aqk^#q~PPdY|V4Q9xVHzwlZkkkldi^!gBkoQ4VslEDbC07znWjYOnkr?ISIb>{?EI5Vw}V44CMdw0k7E7ZHWdOxV~MIvi>0j3L;E!Z)UH9gNj;-l8tk1rVR(3em;^*=y&6@rD1|AAxP-!(oWx@+3^KM=!3kkd zTigU@`HZ|ZuuMZXfhif#2AE)RfI-gKRx~}P=(5x8$f?deG{%xz5(QlHSb{bR#BZfJ z_qfJDbIw88*LtN6Z=gzGif@Rov&^6~+%uEmbKM>phAF+a*2$R2l^$)8v7Rgl(T>iH z))Grz1<9QIKag#-{OODGWsM^-?~)rc&MDqu%|K>gdE0lE1oc`2`V2USVTW7|Ojc^| zwxc0a$s>eXRUX7k)xtZhA?q3eo1#5XszVAgWQN`Y^$VHslY)?4Z38c2)*5xG zfrg3Mo?{oI;F=1ov9N z5(C2}{b;f^*w^d|mRM$@abmuR-!?5+uSvRb932#4Nj-%01jvVyo+M9D%?n6RrSBFw z*O=O2%1{~n+lGkfj+KFINC`Lf1Id;}Xq@D7kS`9Kkm1AwNo{N#NUE%kmX$y>(=e7Q zOqh&UH=00pGL$CB$`l?P=};oOhSIp|L3Hy_a!|RUq*2p2loFFDKqqCT`6I3T*^t|RZP2ze3#snWakRZ?pB&ADi%-ID?5N2qlcl$)$K+_1NHH;BsP+6_Ws zX-OA)Htdzob-a4Upgx!8BdL%<=oka*csA7vLYvCkBdEjG4I7du`G~96R?6z^ z-NZ~xQkKR@DxoX2?SBXG7y1hy1^||5US?yLwmz3?GFUIEgzdf!YP89)uj_`cQ~*OHB9Z9 zV2V9N?3gqXDARR7m{uanIp8pMQMu@#h+@LTgV&vagZ#=r@=Hrypi{xGn+D^TzhO^( z`M!L}^zzT?%lG`!)oA85zaq&M$+{Wu#^A z?rnO1hJVVRCF+~q>xzuPKZ2ZpoZ-6Fq)K_?aKQ40n3_wLPx48JH>f)bH~eMDupZ*wX)=J8Dd=bQYr6j-l!v+Pe+bv$kBzVpKk^UzNu)03`&_c9 zUuCZ`d;EO+ppz{lBJfQO%AcZz7r5H8^A@M3X8!m9pvoI7l&`rU07bKS@X?BdvYepgkiofvO2^vrdQzGLGM>1|AFLSkL25_XoDu*d;72=KCw!Udk4a>zNEgm5&I#O`5| zvKt!0g`Q>@S*WxLDyO5F?O;PTwz1itGXx3hOrS|R;E+p1RAO32dsc=WXQ-}aUZ9M8 z#8sSjoFSGbiqY)~lXR@1D*)4vlqb8nSPTU2BYR_ZjA2{t7(**%2nQ)0Ac*3xNy7kA zB^1C`EX~2K6vxSlI}mHw^#%?EBsjz9sPyjiK@S8L&V%9$YjvVCNWe^EcDV@a_IB_T zuV&Ld16<`2!|el%Jm^U;e5c3OcF4G;p0EO7xWGYra1P}yq=^0OEgSRKiWyrk2%kVG zW=x4J?9kKgl~%U20l+iNH})b|O8Md`t!nu@+b*p2fQuQvATT_p^r2muVV?>J_@*zeomQ9(JU- zCXNnY5$8odOM3Q^7seT21$7h|Y6Z_)7s0-14V&ELrvv#FY<0%EElesg-{e|;ilQC) zckT0<2f|OFHZ3=)ZKoO9!x(c}IJUptzJl!F~5CasB z`d{%o)d5(qw@WT=NjRv~Ef}&!ITkv2G5Zl=1>a+1F#H2HrPEz)T?QO0N>g8+>(`KM zS=jSB+ak$e6m8PBSP6W&EdnOnAKC^xQ4#c}73>a)Wjm}L5~~@3GN3T{9(H1&Qhuru zhs;ie#RS2ru(Zf=#FVJ!s@(}N6h8)|_F@c)dS^WW^*e^gkO@}~@kU792t=d;Di@Gd zq>?Nn|FEt*o4fY!>qzJy-20lm>(FP@4%Pgv9^)W=y6cbxwJqyj*U=IoE)W_X_8Omh`8Cg`E#4(UtY{XJOze@IAkpo4vPrfI5`h9l+wQ2NmKP2jjl6KwwY}$PYLgnh zf>Gs{pYcl@K~MZ+cvsCD8ZZt-No;r3J>faC;EV~sr<5NS_vhl!uMj#h4gHluE-rot z@8Vl9EBjRXkG$U|%_fx?8GPz%56b4o0;H!kI5a_I?$m5LLy{B^q&Pjwj*S8n`VQKu z*dKZdgN8@0qCPYqD6~;sD1lqGO!8MW*Vr>tC&yIl#yxAFT#JP!HC^pT+RHM@ZB4!dvHH{P z8NJr+K}35&KhvmU$bH(Gs+_zJvjxxEzgydR?Gzinr8}?hH5P@ePBPPpL8|5(X~j+$ zquo|=^09P(;3^)&CYV#r{D&}N-BStUvRN6E?hE5c7uW^&89UJQ=prCAw}ED5UCJX$1rj^G2u+JoD!%c{E1sIZT_uqaz6$b*AVwX5(d_v1bU^IA9TeZ z6VzsVUY_W@9AgbN_j<_IQ9<|4=H14@*NnF}Xz78OLE7tFv>BUAG+5X+9)HIMi*}?* zZe2jiFuCkjZ|*h>MnGy`n8J{>{?R`<=Cjh6?<#bJKsza4I)+tf8i(Y6_-Gjj({1$m z_{{C`*GNM1WbcqZ@T@@srVJ82w5vB1 zBC{jsm>j3AK%eD*!wpd&qeqe;w+;=xWgV9Qt7EbA#`831 zTSgca`)9ZZ<8dT!Iun-91DZm@x4BPc0GBa1_KC~;fMV-%EhU(!^e>~uNVlP!|fK zkabaAd;z`aC^lg*k8ctwgn2 zem70MQDK+`otuWi!ogqu{Mzk%-1Oc2btoPq|8>c(H+E|wxoKCqbPR4l#)p;qo{|8i zLt$cPk!7-wZcZz>nGb@3d&uZbNUL71MZS%Jv`wbMi#29#63I9hQ_56r+|&q@r01P| zRmx`RI;h-OKYbzAPhGBb<}d~xJ)qsjA`X_kQmzXm;^CbPThPh&UomJe{xiqt0*oRe zeJ}n~eV_Clk>_l+IS; zIN)neXUV7F$tYuY0V{X>ILUZ2n+PcFFi%VQq=R={MkSXHNWq#;g8J!%K1ik$EtR?H zgfB%#5RbA1JsHVtI-zScozQn;JC@U?6Xi5U2q0jG8KVwJH6CF-n>Bb(W{r|=H1&iM z8Md4rw|WPQai3{IrS%(#WICtoG@p+{B=zPvAfXHKjpf( zF)PUbzktIUkcYuxCuE+^yrM|ZZ#Y~k&CpjYcuTg7(|QTN)8Z#wBOC12rP6=;4J)jH zEpR4t2FsicH@Zr1vw@55rxU*<_@oy^WJ{FE9WaV(T=f)Z(d>@a%o9ef%W*vIFxJazgIcA!3q1ohBP2lHm-arEp5qGzJMcH`9gl)9zTYP zM4pCa#B5#JM6A*;8&NExA_tYV9}ds3B^h#leLO=c(h=(MHzA&p?!;T_(;YRQ>#_VB zTCbf5(N{UzO*$}P?V?cV-E}1H9O)9Kb);0^WJ#0$PM0v7JMe3AEau1=S>KEaR9f+c zsN^{@l^K@_3F!?b+olXAICHmPp&?>3M8vr=OtIK;#qYC42HAD74>Em8$>k^tW=@VO zlm zC?qgDq$ zmeNqPC(iFJb-@x-aB5;4;0W*14um`|NQErSTxoV>%M$;koBNI^*V~FgiF0r0j8;*u zI&WHB0l-8uGbDLL$ShhmE zOH?jl%l$|7*KVIQC+~-$7f$e()3+Svx_^^#Bv*=mq$h>v+(mS}l)r6Ow_N!7I?a#K$9|58kJ;iOO)e@$ z(WS5@My04NFOou?Zn;$G4h7*eB7MfNv*4)?M&g~)d89guLv@Oix6+2rke56_-I$?- zC@t;gy}*DqfG4eqI6zv;8FBeA@Vyq=GieR3s0ZJ=@m6bSSJKY-r1TDhx*0A|*UeBDeNoqu^jCdHl1mk8WM>s( zbZq74S>Lh?DDB%nrr*qr!t(KCc+K7KqDd7WUWz8H2Z(;`f9|)ls^ZcJDN48;p}H&1 z94E&mNn;uI6BLlfGN2C;8#dKRx?nAE2kS8O%9cADc_99Tv?kt7`&N#}Qz(zyi#kmw zl0(x5XqWsD#&dLrZ)4#Qqqm1Ok6<8$|E_Umy#HgIzw-v3WwcxR;&d7_rDHJBGiQK z_wxS!Y5M%#AMiS9-H<;2N*IXWcc5)8fe-%aC>L*7@-MuNS8@(sW@;Otfo{fM5w~TbXDjsjLJfC;Q$B)edxRD)GibU%ygA{oUUIjm}JL!rP57f|CL2A^fSzg~!NK{K*7 zN8az(Mas87WbV4iro28`>l!-Asj(eM%B zyZ|Vog#;Ak5)s%8%6J+FPl*Aj_REm|yw+4C`8c6z9#GZEJQ4|+>fsIb>(0Sy$$VbL zJMtOdrVnO(^O@t64`En0%iIXU$ItSShbW+lg<&LNKxUZ3T9y6gXC13f;}z2MYchy zrEUst$);d@PG>Y#WK*!F+`5V&D~2=#-)Y8XiNrBoRX>R}LThs@*y+?7zmscfspTJ5 zD!tjSEkDXhR5EyHKs8=S|1zP}78H~F2DPA=JCX%O7;k`VP`8j*x4uZhb?b{!s1UZm z4;BLE+oZElM&trUDc7@1p(#jLqXL@^V#nwsNS@h4gP64|8^kEu2;GL$n36yFJL$RhZ?&_-Q1(yRdBsDHOVgmg6l1sNHMb`28+9U}m2Hj04H&0SYO(N%~M3 z_cvz@<41i~J{rsV6$SlB(@p*73>sXlv+ve;DHBL3aJu!}laRDJwQS%fU7Q$0%s5RL zf>m%#DCo|6K@j**V;CR62T2Xm25W=a_svZ4Q=GM-R3sxs_B?Bb5v|l-*`)# zpsv1^c4uAE!lZj^z$XnozoVdj2Nmg7ZyghB1dyR3v<8elO}eZp*mf8`TGcTJLXHx_ z>5nnx=rvh_(V|znz|tY+jZ{fW#Ay+w@E#^vwDe>biAlB#BSi)u)nSil7|@p_w9>X_sMVDv-R7OT#}Y}lp~iU1-SIc z1@4z6Kc__z40Aa-GnhR=RrGqAeZ`>ps5v)2T?#Ru7~anNgULt0!oualla4Oh4;!%A z&xkfl<)YGcZCF3TZIJ$xfthGQ7Y~{ zlwk?RDg~vx@pV%8Bcmu5N`6KavV)fX**z?YyDz>U}eheey8E@wfm#!&=Hm`vW zZ_U9^e`E2>qe7==^jElt>i)^Q(D*nEPL$WWod)uqQnMPvLzb+*W&!)s<`Tjt7&UD> zDoCo3PY!6(dYifG3Sou-=DR=+DTM8$_uF6e^o6g=f7$GHrym5aJLSz|tAdapg2jBzcv}##U#L?Uw+Op zP7UX28qON{Z7RYshf_^0ZgB%gC#N%BFlD9V1(RyycxlmikyrU4$4fd;*jD^9V1&dO zKR6I~V!`e7XR4Q4?WUl=SfeZmRwgrm9Z*4{qnPZX%`EDJSq88_o}{O|$zHM zexEX_OW02LysSl^fsh~;Z3R>OM_i#6{~+%c&jNZU$?Ba+peW~*AlrZ=t_%*+YUx8f z$`KqKo~}%sNqh{)Zj2_*B>Ip#lQ>ez2&**YOB0Sllu@KWUynTJ>PF#>H>~k);Jd8D zlW%1r_S0AY#)^`YD1YM(m4Me9BG@6hk`y;oRL%`@AXLe&1eN`UeMl#zWej4w83!>1 zp6bLb2`GuA#7r&L<|b#83Ou{lBl2k_L`xp)0x+bMKqh9ys6>ACO6Q_m2Bct2l2G_j zyY$FwAwq+_Gpj18LqsoeRh~ldw$jUe-9!LJ_j~52WBZ+`Q?m6rd+vJ!%?*sz*J-cp9%_V8L;V?MYR{R zqiZ$~xd7p}Pj?m9_-{b0KIgW55}y?aEPx5)^-l7+@Ansa`S&)%)C zLv%L5^wvu275BYPw)?2{brc(@3}0mkFAB7&#|mrwTPp>vuZwhW`l0*BCoPb)31`4> zGGOWM!6MSo*nY3{<5t~^+~&DIpGS#bJHHNo_1`BEp3JY@^qqB3@7%%ufi2a6!Cig3 z21f@*HtihTykqm=z{pVF=8^Mu?H;*)Tebh1Z6hQ7yQ=za80Z@q9NxUGuYX`e|CZq@ zd58FSWW(0cfi1&*Teb{UhldxB4i7D+puU}pH}?%)Gq`xDdQJcE$k6qRhle&VzNUX< z+vui6n+JC-TvFYk-i=M1J`UG9Ud9nwW)7l#~OZ@3|}Mg zw+;>N+K|9KGMK(yMJ^Kb50G_Wq`z;Zx_NL*l@{+BzGi3Nrs~e>b=A$I`sy%c`7U?W zkZ-8Eg(TOIjRtSn+qbiSOW(-g(3atmTHspHuw|(4+AMacy19RMKez;wN$&Jn9U2-O z8r-pNpnv3L)#2TP1H;u_;JNRb>Tuu4$WZ?#KpDQEZ(z&LY8p?4eQ_GMqk28nj8?(P z@<3GJd417K`iFP#?7Ked-?e)u#SM(~jr0!=L_^h)(V>CrmZ)zavU;P@0nlIFJW}1V z^ZLa@AaV2JB|V$Ac5hm~wQEUV_tt9P)|E?FtlHGKa`TqH>WZGOWlQ?DF4?ql)3Q~I zclK`@>Z9ZPc0v!$Y_RTL&BCj}m}Q{R5!4w|9*_C$96nfxW+x zx-t;A8Kw31Hd5T##&dG?qkhNmJC@&6e)YWQIKnW7oe04QZa zln#Q3bQ_cOdw7cD_7B)Fj;`z<7+JEkCz>}eTBvk)BsUZR#7Yb}#RZ9_G8o zqxfPc3EMM*dvHk-xcZCKH|>X0{${KBkYo6b)hwf@^o!oLx2J~#xks^{oCs{_}J zY}>GVsJgZPy6P4hy%SisQJ<*g6xJEvOizC(cetJ&9N2k%G(5W7=7wmX51zQYZ*z4x z8r?0*a8*QI*RAC*E&U`)7e-I!m$v*A-skbVy71qJH@+KK>UrZKM)2_U!;FLtZfL?W zE$GJ8p;%Q)XIZAha=0c2bGGm%YSB;Bc`Cot`02a;G{Q8-Pv@txuXGFe9qq)id2py| z^Ty!r;l+%`k!?10cn=rL-X)a#$v}0+cl`m>@|D9<3q!LyG|;!x&1$_7GhUU6n(1w* z+J{85X=gRkEZ-Z!4_eFCs%BoYo9`NzH}O0aFS&y_@sc#n3-OW%lk}hEsqtKoVbq6T zWBfTT<3?SVA#sbzF%E;zQQwu6|MxUx4$n0FR8#odge6g?@!xNXf2Jw^e>KJbOH=$$ zn&N-f6#t8+_=8RHd3wAa-Xh_8_}KE=5RVet5I-QC^3z`(ciF}4-oG1$(lD7Y;nN7$$1ALPF^yl; z6tA$xW*WbcusC}fUPidS{Ft!huQdK8gvAf*!HTO>)x96&IiJ zl9%;HFPDnpI%;5$Ua3Zs(NHi3qRY{K5CD+ut3=RceA_js&O#6xj;sY&m_HeLE20eIaGy`k#+^mrYa=eFBkx&zD0|oD+Z(8Lxbp< zBbM1(I5IRis<{gCQ{N6i>Qyy+ceD*4)u^GN>!YoMLsob{v@)=_I)Gxix!M_BRgJa* zkl0*xCn|W=%4IB>RA7pvM%KCL;nIN@M7vCNrYI1)i@7)&9z>g^+G=zy%41T?Anh1h zbo4ZyN;l1VKe}n6_upN1djAE`p~fd&GdO@4zZ23;wRShNSGQ;eFHb3Zvq`sYbXVWN zA=31aMtt!%5BDx;cj$UX=*zHt(9OHCvZ%L2m7_a%Qs>pw{c4`8_~rQ}3DJD9bBIor zF3ne${+uNJm9o4zb!T7{JBFK-;7waDucEUEEof20P}Sz@1}UGY&zJRKtKlnkNI}ro zO9w~Rq7!VvKC&g@CRdJZ?MYr=f)U|zN=j89>aLeg#3wKljrQ$K(yuYK=KLzEMgL|h zI@gV#Y{GOy)cfQyj&FRHXA5PJ5;_`A;MTYnXpy?n`wnSN;Q14tOyo)0-LypM!0Mn& zTs*dHnoF9_M*mhdttQuZkw&~Y0m}(P(UmaIYzm<`XP{8!7n5GPLDITNHkC-c65%ni zh@2HU1e|pPX~lEz3wQ4joB|KlVmrb)<#1Em7g*}VbZ`v+SEKZ+HFOY*X zXQfRYg(pexwtsBYFG{0C)9L%b=&nsL%rsaX-rNVl53A2%(~HhasLQe4tbu~W7z9l+ zcwH-NshKS_#G@)|63Fd~HjWI9R6)n;itlm7uX*9(Ra&n~TfbJl z&2*Wvj7HQ=5S{es1UUzyL8E{POWlbau*g*D!JRdlWFCeEiNKO#kAAL!KW?cm+&-*f zAQO4lqNR(LC0gBLY_vv0rrJGzLfPBI_!F;|T&=%4{biiREKRTREV*5Ob?Gzq-Jls= z3LM+2*G2K==dN4V++fd3%X}wgNxuFsYWJ=|nT{xb|Fp9A8lKX#B(JC8*YRGd3*XGU z>=9Qc7QS@WamH_Bx87yb%ic`VtZ~vYV|}Yl+9Q1buWAc$EKVD9JN3Ml`XA<}wxr>& zBw-ET7wjLlWj=`Jdf?=`l;e8-=zHZbS_x2IJ;P}tf55vm$x+Rkhtos4muH$&npc#g z**XpXIq#y-`fyYieu($prm#jpn*QH;mu)i*|AKc(F!f>0;A!}mymvQ+JL|$FG(%}c zX?lg#TWPq1ux9i$+};#Fxhbp@nCtT&OSry#h3n&|CgDtRG2%VET`QX9>{>O=8ck#u zi+Ko>R}eR~4&KztkivgPQeQf0*|a@;{jN>$J0_N`Tcz6p@Eu7xck>jy_wgjFcV0T3 zRMzU0!&Uiz}juekE%uXyFFUcIqz)8;MJZT;JK?A*1RbLkt>txpnOMM_Jz^%|FQ zV34%KBcpq-z3%$osa13Of<=pey%H8Ju9bE)B^^zkT8#@g{CfFMUo01p!~S{fnKxlWiiea6gLC(fP|Sx+8Jy7J&c5Tg{XM-LvPPkYZe z^Wed=&OXOw{O$YKAa~?RQ_e~n(14#BcEF2EP>-FI&K=SG_F>?l=WB4P^;tL8QAb1T zXUuf%tIzA+it!#g-xe55~O|7oA*Cl9f^*99n7NOzng9l$E{hxF2|2ykppu zBHMrPzkBvcUej9;3-)reVX39u@-)Wcv-yC_@fs>oHn>*5l&J|osZO( zYN9}3o5H~XfoeCF&&}R*~|xrX1l{8sbRpWuH9ztl@({ciaM zHG6498&LqOiI+y|<%27YDWq1hT&|>>{qZTzGWJah*1BC9{uW_bXwTy(dy!yT%kO-C zvhr#zRNvO|yD)iISgYa}^Sgv!{r5`=|Jtj}TX&75YS@B#ZUufLa2}h~#l!F;t-4P( z_FZUKX0XH>#j3^&xKgEfs(C|EpK$aA>XL2}LqyU+#wWy6Y{TB_W)fp6>Z5nDy(ddj zq<@b5$~T?-veK<(DMWT$v#i?kSR=01lg8<~jo&ZhJJul?r=n{INAaJ*67)OOp+Q?u zkOj^HGG!&HMh3A-?V>?4PHhzqSky54ve{+pQti~&iBt6_>sVX*aSm)YnIr}|XW5dn zqa`g{32|#BiD6~YDW|$I_#)`m+dbb|DrN0W-?t>?yc}HUPvhY#eo~UI(`ugv|0bDr zhI^g;lEolw1h%dvd!|ds7tgzmvUqs+^3>Q%`!(}w5WQ_0?ca(1CGSSnnbQ3$cz-3o zSMiJZdm<^Ti*P5uG^~|Ul^y+V+k&!R1qZ=1j1)V5cvJdGLsUd`_{69Pad zQTHXMIje_ON2LdLUDwsswWMol*RrnVT`Rh}yH_%X^luTG6#)$%>^bmaSO6V#SK?6)RWttXS3E)xD&9Y4@`3<=rc~ zySrC*_jIpX*|l=X%B3rptz5ox#merLD_8ccT-DRnv!rKf&$6E7Ju7;;dsg=J^sHJ1 zh^wf66-BQi+bTYJUOgVJ79RM?!ZIN`5;M%zr#pPS669$$G!1ViJhv|V942$|u{8eY zgpZ%7>|wYvJi5AT^R^-I{V?T6qZ7niOt1mMbC;5{UyFl&r*i^ z{y%wEvNBi%N{Dd50%ze*<6VANdTpu6C|7GpT6gj;CXe9X#8bNVI2)-U|4|ZMkiZ|VU$E*|oanj_AF8=v17A-#K>ep=ekq>_AFF*HpU;6Sh|IC@1 zlPtFPjPus6yYRJJs{7vd?)QHFOJDAoa_V{OuG&(4^V>c`;?I8md(ZsyfsQHXt=m!^ z+xMrB{Pm+>`^JGE-}?GD-TmRee)O|{`=u{`>)RLH`Bz{3+?T$*?$Y&Fz4Emie*Z0R z{luR?^ysHQ_qX4ea{LLezWQhX{;Pvy9lQ2?_j{8j&lwn;b>fEC-S{W>|K%h9dHe~p z=d3;d()F)+)zz=P@up9H_N!m}&Ve8QbZGdkBcp$`c*7}+7T5&@bYU$AOHN5UwP`A|L?C3 zdeMf;E#J%C@{;1rT+5W3KR#*fV=LyD#-0wR7yaDg-11x@^b0M8DQ%ZcKBjPGAInC}Ib7L#^VL(w9xjf3;k2Hxt)-{9w%Fb>QkosU zD)-9Pv0JCjYCFF5;@sF9TRwJo`w6)vAIRPOty2o^`TW?2rrf-%eBJ!hT8MpfZtSna znc?IPuZ7LCelCDagSLDrn3OBCJL#DGu~Vk{#|0+@(>rG6Pb|*$w}(4|M}n^eUv2+d z>(_&C1mE(X&VN66HuzC=AoqOm9|45wx6eD{>`T|b^?mPqe@mgY`>eBH{*x!alAAiM zd*#coy6K~Ta{phgc>0*%`~A1PFC#xu-=*ufRImQ?hi1+y6x&KuPv~CNd+$e{`ey6O zJKlD0q3w*bxAwpF4+b~<=wDy8>0R%hH~-4_-t~b$zWbhg@B7pvkF}KAk2|sVobz7# z;d`F=;$4O5Gb$&aea>e;my1q1`IPy~dU`Lo@Z!rZ$94#fZQNYlx?}jd*WL8SyFYsW z1N)!+ll!lKVqowOUt9Uzd`OpU4gJN7#%?(=Trzo9Zf@)B{OS3ZxVhNcb$q@voSFAm_v9|hFV3|US_`YAc^tFT9rosD6mso_ z%hs)2*0HRxsMvP%DKEQpe(|)I&zgEd>!rEblh2(rz0lTjL2+K|Xz85zw3ajSZ7nZt z@$=;{KlY|gvo9#NjeYpFmGeq%Egi?MDztSk$d$+TpSk7o_6u6u)}A->g5u>TuDZFf zw(Z35{Bbi(|NY#Kd`H+?DF5Nc zv7faK7Y9yQJNB-r?N_x@#O_0s58snahKlCoRZrX@7a!*q`*A*s&nj3Q4z&z3bMeiXCA`IMV*g z0z)R(1N`%gl}m5FqW!oq%okdx7YeOmN8!}5FPz-AujP>JIAO~hDJZE{?Jo}^r z&F7bfC7){kNyCyUq^rK1pXL_*Ht>_flk_&z3o-?PW#s-X8%B3;7#TFBU<1-cW8RJA z^+M0PH5Ykr%Ww2vbL?H-F(=$LyFGe(_P;KOPCu>duG0rUym3Kr@6!ur|NQBN-mmUj z@xFskulS|k*X_4`pnGmd-^!0o`hL&iX`QROX7#Q8$?OkYcwl?qrRxVjaOry=>0Dp^ z%JKUFvOZ^{3_hRz`XL^zhorezcP0xgzYT(8k=;tIW9~3EF}gY!F`s zGsv+IE%avxAqO=O&Xd5O8XOO+AT=cy{X*Cl%=XV9Z#y~XQ~0Vd4<9K6B`Z_lP+_3( ztY8(erFER>U*PMK8s9JaFZF{$dvTK=w3b>f3}#ZA@2{NXQ(wO1&u#U$=KK~w3Z@6S zu$|-FBCaRq}5*-UK>>Wy__o8>bHd7rujfxPLbzq+!EegA6TM*3i&jsrN2aXo~#XegqLymQz-KPab|3^Za zj{sE0S&LGB;Qx=j7t+ey{4nQ#i1NMQouvo-?#Vsj63RL+oSXNHXR`}(c`GB!-w+Cc zE&g48SUk?K>HEj~lL}$}Z;EQd3Bn}3qf~zIzk#QP=gi>BqP}bw7Kp8Kz7s8L^@E?% z*U*4}2X*EAsBM0W^-fC=E&|D30W|n8J3c6=wBKo=0`LrVsVX1%nVvlVoZL(GPCIk? nVlgPp&b^(TlDVZtf0BQE-k(e=tM6q@r2y9(aMu>=1HhdXGg diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index c754e65fdb..95f69ca095 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -86,21 +86,15 @@ "InitialRewardingParams": { "type": "object", "required": [ - "active_set_size", "active_set_work_factor", "initial_reward_pool", "initial_staking_supply", "interval_pool_emission", - "rewarded_set_size", + "rewarded_set_params", "staking_supply_scale_factor", "sybil_resistance" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "active_set_work_factor": { "$ref": "#/definitions/Decimal" }, @@ -113,10 +107,8 @@ "interval_pool_emission": { "$ref": "#/definitions/Percent" }, - "rewarded_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "$ref": "#/definitions/RewardedSetParams" }, "staking_supply_scale_factor": { "$ref": "#/definitions/Percent" @@ -167,6 +159,42 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -199,228 +227,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "assign_node_layer" - ], - "properties": { - "assign_node_layer": { - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "$ref": "#/definitions/Layer" - }, - "mix_id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "create_family_on_behalf" - ], - "properties": { - "create_family_on_behalf": { - "type": "object", - "required": [ - "label", - "owner_address" - ], - "properties": { - "label": { - "type": "string" - }, - "owner_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family", - "type": "object", - "required": [ - "join_family_on_behalf" - ], - "properties": { - "join_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "join_permit", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family_on_behalf" - ], - "properties": { - "leave_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member_on_behalf" - ], - "properties": { - "kick_family_member_on_behalf": { - "type": "object", - "required": [ - "head_address", - "member" - ], - "properties": { - "head_address": { - "type": "string" - }, - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -466,23 +272,21 @@ { "type": "object", "required": [ - "update_active_set_size" + "update_active_set_distribution" ], "properties": { - "update_active_set_size": { + "update_active_set_distribution": { "type": "object", "required": [ - "active_set_size", - "force_immediately" + "force_immediately", + "update" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "force_immediately": { "type": "boolean" + }, + "update": { + "$ref": "#/definitions/ActiveSetUpdate" } }, "additionalProperties": false @@ -561,36 +365,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "advance_current_epoch" - ], - "properties": { - "advance_current_epoch": { - "type": "object", - "required": [ - "expected_active_set_size", - "new_rewarded_set" - ], - "properties": { - "expected_active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "new_rewarded_set": { - "type": "array", - "items": { - "$ref": "#/definitions/LayerAssignment" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -614,6 +388,27 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "assign_roles" + ], + "properties": { + "assign_roles": { + "type": "object", + "required": [ + "assignment" + ], + "properties": { + "assignment": { + "$ref": "#/definitions/RoleAssignment" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -629,7 +424,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -659,7 +454,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -793,17 +588,17 @@ { "type": "object", "required": [ - "update_mixnode_cost_params" + "update_cost_params" ], "properties": { - "update_mixnode_cost_params": { + "update_cost_params": { "type": "object", "required": [ "new_costs" ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -825,7 +620,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "owner": { "type": "string" @@ -882,6 +677,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "migrate_mixnode" + ], + "properties": { + "migrate_mixnode": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1019,16 +827,104 @@ { "type": "object", "required": [ - "delegate_to_mixnode" + "migrate_gateway" ], "properties": { - "delegate_to_mixnode": { + "migrate_gateway": { + "type": "object", + "properties": { + "cost_params": { + "anyOf": [ + { + "$ref": "#/definitions/NodeCostParams" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "bond_nym_node" + ], + "properties": { + "bond_nym_node": { "type": "object", "required": [ - "mix_id" + "cost_params", + "node", + "owner_signature" ], "properties": { - "mix_id": { + "cost_params": { + "$ref": "#/definitions/NodeCostParams" + }, + "node": { + "$ref": "#/definitions/NymNode" + }, + "owner_signature": { + "$ref": "#/definitions/MessageSignature" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unbond_nym_node" + ], + "properties": { + "unbond_nym_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_node_config" + ], + "properties": { + "update_node_config": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/NodeConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1069,16 +965,16 @@ { "type": "object", "required": [ - "undelegate_from_mixnode" + "undelegate" ], "properties": { - "undelegate_from_mixnode": { + "undelegate": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1119,23 +1015,23 @@ { "type": "object", "required": [ - "reward_mixnode" + "reward_node" ], "properties": { - "reward_mixnode": { + "reward_node": { "type": "object", "required": [ - "mix_id", - "performance" + "node_id", + "params" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "performance": { - "$ref": "#/definitions/Percent" + "params": { + "$ref": "#/definitions/NodeRewardingParameters" } }, "additionalProperties": false @@ -1186,10 +1082,10 @@ "withdraw_delegator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1262,9 +1158,81 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_mixnode" + ], + "properties": { + "testing_unchecked_bond_legacy_mixnode": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/MixNode" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_gateway" + ], + "properties": { + "testing_unchecked_bond_legacy_gateway": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/Gateway" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -1284,8 +1252,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -1300,15 +1267,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -1319,8 +1278,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1346,10 +1305,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -1467,14 +1422,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -1512,39 +1469,6 @@ }, "additionalProperties": false }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "LayerAssignment": { - "description": "Specifies layer assignment for the given mixnode.", - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "description": "The layer to which it's going to be assigned", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, - "mix_id": { - "description": "The id of the mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "MessageSignature": { "type": "array", "items": { @@ -1637,7 +1561,31 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeConfigUpdate": { + "type": "object", + "properties": { + "custom_http_port": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "type": [ + "string", + "null" + ] + }, + "restore_default_http_port": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -1646,7 +1594,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1654,7 +1602,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -1664,6 +1612,61 @@ }, "additionalProperties": false }, + "NodeRewardingParameters": { + "description": "Parameters used for rewarding particular node.", + "type": "object", + "required": [ + "performance", + "work_factor" + ], + "properties": { + "performance": { + "description": "Performance of the particular node in the current epoch.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + }, + "work_factor": { + "description": "Amount of work performed by this node in the current epoch also known as 'omega' in the paper", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", "allOf": [ @@ -1704,6 +1707,74 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + }, + "RoleAssignment": { + "type": "object", + "required": [ + "nodes", + "role" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -1727,158 +1798,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the list of families registered in this contract.", - "type": "object", - "required": [ - "get_all_families_paged" - ], - "properties": { - "get_all_families_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the list of all family members registered in this contract.", - "type": "object", - "required": [ - "get_all_members_paged" - ], - "properties": { - "get_all_members_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family head.", - "type": "object", - "required": [ - "get_family_by_head" - ], - "properties": { - "get_family_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family label.", - "type": "object", - "required": [ - "get_family_by_label" - ], - "properties": { - "get_family_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family head.", - "type": "object", - "required": [ - "get_family_members_by_head" - ], - "properties": { - "get_family_members_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family label.", - "type": "object", - "required": [ - "get_family_members_by_label" - ], - "properties": { - "get_family_members_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", "type": "object", @@ -1991,40 +1910,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current list of mixnodes in the rewarded set.", - "type": "object", - "required": [ - "get_rewarded_set" - ], - "properties": { - "get_rewarded_set": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded mixnodes.", "type": "object", @@ -2150,7 +2035,7 @@ "minimum": 0.0 }, "owner": { - "description": "The address of the owner of the the mixnodes used for the query.", + "description": "The address of the owner of the mixnodes used for the query.", "type": "string" }, "start_after": { @@ -2355,20 +2240,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current layer configuration of the mix network.", - "type": "object", - "required": [ - "get_layer_distribution" - ], - "properties": { - "get_layer_distribution": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded gateways.", "type": "object", @@ -2448,16 +2319,175 @@ "additionalProperties": false }, { - "description": "Gets all delegations associated with particular mixnode", + "description": "Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes", "type": "object", "required": [ - "get_mixnode_delegations" + "get_preassigned_gateway_ids" ], "properties": { - "get_mixnode_delegations": { + "get_preassigned_gateway_ids": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_node_bonds_paged" + ], + "properties": { + "get_nym_node_bonds_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_nodes_detailed_paged" + ], + "properties": { + "get_nym_nodes_detailed_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "get_unbonded_nym_node" + ], + "properties": { + "get_unbonded_nym_node": { "type": "object", "required": [ - "mix_id" + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_paged" + ], + "properties": { + "get_unbonded_nym_nodes_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that belonged to a particular owner.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_owner_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_owner_paged": { + "type": "object", + "required": [ + "owner" ], "properties": { "limit": { @@ -2469,7 +2499,244 @@ "format": "uint32", "minimum": 0.0 }, - "mix_id": { + "owner": { + "description": "The address of the owner of the nym-node used for the query", + "type": "string" + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that used the particular identity key.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_identity_key_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_identity_key_paged": { + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node used for the query.", + "type": "string" + }, + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information belonging to the particular owner.", + "type": "object", + "required": [ + "get_owned_nym_node" + ], + "properties": { + "get_owned_nym_node": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of the node owner to use for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information of a node with the provided id.", + "type": "object", + "required": [ + "get_nym_node_details" + ], + "properties": { + "get_nym_node_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nym-node information given its current identity key.", + "type": "object", + "required": [ + "get_nym_node_details_by_identity_key" + ], + "properties": { + "get_nym_node_details_by_identity_key": { + "type": "object", + "required": [ + "node_identity" + ], + "properties": { + "node_identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the nym-node used for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the rewarding information of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_rewarding_details" + ], + "properties": { + "get_node_rewarding_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the stake saturation of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_stake_saturation" + ], + "properties": { + "get_node_stake_saturation": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_role_assignment" + ], + "properties": { + "get_role_assignment": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_rewarded_set_metadata" + ], + "properties": { + "get_rewarded_set_metadata": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets all delegations associated with particular node", + "type": "object", + "required": [ + "get_node_delegations" + ], + "properties": { + "get_node_delegations": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2550,14 +2817,14 @@ "type": "object", "required": [ "delegator", - "mix_id" + "node_id" ], "properties": { "delegator": { "description": "The address of the owner of the delegation.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2647,16 +2914,16 @@ "description": "Gets the reward amount accrued by the particular mixnode that has not yet been claimed.", "type": "object", "required": [ - "get_pending_mix_node_operator_reward" + "get_pending_node_operator_reward" ], "properties": { - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2679,14 +2946,14 @@ "type": "object", "required": [ "address", - "mix_id" + "node_id" ], "properties": { "address": { "description": "Address of the delegator to use for the query.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2716,7 +2983,7 @@ "type": "object", "required": [ "estimated_performance", - "mix_id" + "node_id" ], "properties": { "estimated_performance": { @@ -2727,7 +2994,18 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2751,7 +3029,7 @@ "required": [ "address", "estimated_performance", - "mix_id" + "node_id" ], "properties": { "address": { @@ -2766,18 +3044,22 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -2953,6 +3235,17 @@ "$ref": "#/definitions/Decimal" } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } }, @@ -3055,7 +3348,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -3081,8 +3374,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -3115,110 +3408,6 @@ } } }, - "get_all_families_paged": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedFamiliesResponse", - "description": "Response containing paged list of all families registered in the contract.", - "type": "object", - "required": [ - "families" - ], - "properties": { - "families": { - "description": "The families registered in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/Family" - } - }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", - "type": "object", - "required": [ - "head", - "label" - ], - "properties": { - "head": { - "description": "Owner of this family.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "label": { - "description": "Human readable label for this family.", - "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_all_members_paged": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMembersResponse", - "description": "Response containing paged list of all family members (of ALL families) registered in the contract.", - "type": "object", - "required": [ - "members" - ], - "properties": { - "members": { - "description": "The members alongside their family heads.", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/FamilyHead" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, "get_bonded_mixnode_details_by_identity": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MixnodeDetailsByIdentityResponse", @@ -3269,14 +3458,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -3333,7 +3514,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -3350,14 +3530,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -3399,35 +3571,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -3448,6 +3592,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -3460,14 +3605,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -3483,7 +3655,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -3523,7 +3695,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -3536,6 +3708,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -3735,11 +3916,12 @@ }, "get_delegation_details": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixNodeDelegationResponse", + "title": "NodeDelegationResponse", "description": "Response containing delegation details.", "type": "object", "required": [ - "mixnode_still_bonded" + "mixnode_still_bonded", + "node_still_bonded" ], "properties": { "delegation": { @@ -3755,6 +3937,10 @@ }, "mixnode_still_bonded": { "description": "Flag indicating whether the node towards which the delegation was made is still bonded in the network.", + "deprecated": true, + "type": "boolean" + }, + "node_still_bonded": { "type": "boolean" } }, @@ -3790,7 +3976,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -3816,8 +4002,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -3918,7 +4104,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -3944,8 +4130,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -4061,13 +4247,39 @@ ] }, { - "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set.", - "type": "string", - "enum": [ - "advancing_epoch" - ] + "description": "Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, all issued actions got resolved and node roles should now be assigned before advancing into the next epoch.", + "type": "object", + "required": [ + "role_assignment" + ], + "properties": { + "role_assignment": { + "type": "object", + "required": [ + "next" + ], + "properties": { + "next": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } }, @@ -4249,194 +4461,6 @@ } } }, - "get_family_by_head": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyByHeadResponse", - "description": "Response containing family information.", - "type": "object", - "required": [ - "head" - ], - "properties": { - "family": { - "description": "If applicable, the family associated with the provided head.", - "anyOf": [ - { - "$ref": "#/definitions/Family" - }, - { - "type": "null" - } - ] - }, - "head": { - "description": "The family head used for the query.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", - "type": "object", - "required": [ - "head", - "label" - ], - "properties": { - "head": { - "description": "Owner of this family.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "label": { - "description": "Human readable label for this family.", - "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_by_label": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyByLabelResponse", - "description": "Response containing family information.", - "type": "object", - "required": [ - "label" - ], - "properties": { - "family": { - "description": "If applicable, the family associated with the provided label.", - "anyOf": [ - { - "$ref": "#/definitions/Family" - }, - { - "type": "null" - } - ] - }, - "label": { - "description": "The family label used for the query.", - "type": "string" - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", - "type": "object", - "required": [ - "head", - "label" - ], - "properties": { - "head": { - "description": "Owner of this family.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "label": { - "description": "Human readable label for this family.", - "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_members_by_head": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyMembersByHeadResponse", - "description": "Response containing family members information.", - "type": "object", - "required": [ - "head", - "members" - ], - "properties": { - "head": { - "description": "The family head used for the query.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "members": { - "description": "All members belonging to the specified family.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "definitions": { - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_members_by_label": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyMembersByLabelResponse", - "description": "Response containing family members information.", - "type": "object", - "required": [ - "label", - "members" - ], - "properties": { - "label": { - "description": "The family label used for the query.", - "type": "string" - }, - "members": { - "description": "All members belonging to the specified family.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, "get_gateway_bond": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GatewayBondResponse", @@ -4751,38 +4775,6 @@ } } }, - "get_layer_distribution": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "LayerDistribution", - "description": "The current layer distribution of the mix network.", - "type": "object", - "required": [ - "layer1", - "layer2", - "layer3" - ], - "properties": { - "layer1": { - "description": "Number of nodes on the first layer.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "layer2": { - "description": "Number of nodes on the second layer.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "layer3": { - "description": "Number of nodes on the third layer.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "get_mix_node_bonds": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PagedMixnodeBondsResponse", @@ -4837,14 +4829,6 @@ } } }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -4901,7 +4885,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -4918,14 +4901,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -4967,8 +4942,7 @@ } ] } - }, - "additionalProperties": false + } }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", @@ -5034,14 +5008,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -5098,7 +5064,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -5115,14 +5080,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -5164,35 +5121,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -5213,6 +5142,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -5225,14 +5155,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -5248,7 +5205,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -5288,7 +5245,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -5301,6 +5258,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -5326,122 +5292,6 @@ } } }, - "get_mixnode_delegations": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMixNodeDelegationsResponse", - "description": "Response containing paged list of all delegations made towards particular mixnode.", - "type": "object", - "required": [ - "delegations" - ], - "properties": { - "delegations": { - "description": "Each individual delegation made.", - "type": "array", - "items": { - "$ref": "#/definitions/Delegation" - } - }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Delegation": { - "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", - "type": "object", - "required": [ - "amount", - "cumulative_reward_ratio", - "height", - "mix_id", - "owner" - ], - "properties": { - "amount": { - "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "cumulative_reward_ratio": { - "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "height": { - "description": "Block height where this delegation occurred.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "description": "Address of the owner of this delegation.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "proxy": { - "description": "Proxy address used to delegate the funds on behalf of another address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, "get_mixnode_details": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MixnodeDetailsResponse", @@ -5494,14 +5344,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -5558,7 +5400,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -5575,14 +5416,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -5624,35 +5457,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -5673,6 +5478,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -5685,14 +5491,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -5708,7 +5541,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -5748,7 +5581,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -5761,6 +5594,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -5805,7 +5647,7 @@ "description": "If there exists a mixnode with the provided id, this field contains its rewarding information.", "anyOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" }, { "type": "null" @@ -5834,7 +5676,7 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -5843,7 +5685,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -5851,7 +5693,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -5861,7 +5703,7 @@ }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -5877,7 +5719,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -5917,7 +5759,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -5941,6 +5783,323 @@ } } }, + "get_node_delegations": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNodeDelegationsResponse", + "description": "Response containing paged list of all delegations made towards particular node.", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "description": "Each individual delegation made.", + "type": "array", + "items": { + "$ref": "#/definitions/Delegation" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "type": "object", + "required": [ + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" + ], + "properties": { + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Proxy address used to delegate the funds on behalf of another address", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_node_rewarding_details": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeRewardingDetailsResponse", + "description": "Response containing rewarding information of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "rewarding_details": { + "description": "If there exists a node with the provided id, this field contains its rewarding information.", + "anyOf": [ + { + "$ref": "#/definitions/NodeRewarding" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_node_stake_saturation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StakeSaturationResponse", + "description": "Response containing the current state of the stake saturation of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "current_saturation": { + "description": "The current stake saturation of this node that is indirectly used in reward calculation formulas. Note that it can't be larger than 1.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "uncapped_saturation": { + "description": "The current, absolute, stake saturation of this node. Note that as the name suggests it can be larger than 1. However, anything beyond that value has no effect on the total node reward.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } + }, "get_number_of_pending_events": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "NumberOfPendingEventsResponse", @@ -5966,6 +6125,1033 @@ }, "additionalProperties": false }, + "get_nym_node_bonds_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeBondsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "The nym node bond information present in the contract.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeBond" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_nym_node_details": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsResponse", + "description": "Response containing details of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there exists a node with the provided id, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_nym_node_details_by_identity_key": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsByIdentityResponse", + "description": "Response containing details of a bonded node with the provided identity key.", + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "details": { + "description": "If there exists a bonded node with the provided identity key, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node.", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_nym_nodes_detailed_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeDetailsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "All nym-node details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeDetails" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "get_owned_gateway": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GatewayOwnershipResponse", @@ -6178,14 +7364,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -6242,7 +7420,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -6259,14 +7436,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -6308,35 +7477,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -6357,6 +7498,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -6369,14 +7511,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -6392,7 +7561,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -6432,7 +7601,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -6445,6 +7614,316 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_owned_nym_node": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeOwnershipResponse", + "description": "Response containing details of a node belonging to the particular owner.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Validated address of the node owner.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "details": { + "description": "If the provided address owns a nym-node, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -6476,7 +7955,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -6514,6 +7994,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -6570,6 +8054,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -6618,7 +8132,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -6628,7 +8142,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -6640,8 +8154,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6672,7 +8186,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -6681,12 +8195,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6720,10 +8234,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -6731,7 +8279,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -6754,10 +8302,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -6810,20 +8392,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6833,6 +8415,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -6876,6 +8480,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -6949,7 +8583,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -6959,7 +8593,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -6971,8 +8605,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7003,7 +8637,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -7012,12 +8646,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7051,10 +8685,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -7062,7 +8730,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7085,10 +8753,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -7141,20 +8843,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7164,6 +8866,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -7255,14 +8979,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -7300,7 +9026,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -7309,7 +9035,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7317,7 +9043,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -7379,7 +9105,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -7389,6 +9115,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -7458,6 +9218,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -7554,14 +9350,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -7599,7 +9397,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -7608,7 +9406,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7616,7 +9414,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -7703,7 +9501,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -7713,6 +9511,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -7782,19 +9614,56 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" } } }, - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PendingRewardResponse", "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -7832,6 +9701,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -7868,7 +9741,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -7906,6 +9780,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -7936,64 +9814,163 @@ } } }, - "get_rewarded_set": { + "get_preassigned_gateway_ids": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedRewardedSetResponse", - "description": "Response containing paged list of all mixnodes in the rewarded set.", + "title": "PreassignedGatewayIdsResponse", "type": "object", "required": [ - "nodes" + "ids" ], "properties": { - "nodes": { - "description": "Nodes in the current rewarded set.", + "ids": { "type": "array", "items": { - "type": "array", - "items": [ - { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - { - "$ref": "#/definitions/RewardedSetNodeStatus" - } - ], - "maxItems": 2, - "minItems": 2 + "$ref": "#/definitions/PreassignedId" } }, "start_next_after": { "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", "type": [ - "integer", + "string", "null" - ], - "format": "uint32", - "minimum": 0.0 + ] } }, "additionalProperties": false, "definitions": { - "RewardedSetNodeStatus": { - "description": "Current state of given node in the rewarded set.", - "oneOf": [ - { - "description": "Node that is currently active, i.e. is expected to be used by clients for mixing packets.", - "type": "string", - "enum": [ - "active" + "PreassignedId": { + "type": "object", + "required": [ + "identity", + "node_id" + ], + "properties": { + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", + "type": "string" + }, + "node_id": { + "description": "The id pre-assigned to this gateway", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_rewarded_set_metadata": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RolesMetadataResponse", + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/definitions/RewardedSetMetadata" + } + }, + "additionalProperties": false, + "definitions": { + "RewardedSetMetadata": { + "description": "Metadata associated with the rewarded set.", + "type": "object", + "required": [ + "entry_gateway_metadata", + "epoch_id", + "exit_gateway_metadata", + "fully_assigned", + "layer1_metadata", + "layer2_metadata", + "layer3_metadata", + "standby_metadata" + ], + "properties": { + "entry_gateway_metadata": { + "description": "Metadata for the 'EntryGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } ] }, - { - "description": "Node that is currently in standby, i.e. it's present in the rewarded set but is not active.", - "type": "string", - "enum": [ - "standby" + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateway_metadata": { + "description": "Metadata for the 'ExitGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "fully_assigned": { + "description": "Indicates whether all roles got assigned to the set for this epoch.", + "type": "boolean" + }, + "layer1_metadata": { + "description": "Metadata for the 'Layer1' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer2_metadata": { + "description": "Metadata for the 'Layer2' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer3_metadata": { + "description": "Metadata for the 'Layer3' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "standby_metadata": { + "description": "Metadata for the 'Standby' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } ] } - ] + }, + "additionalProperties": false + }, + "RoleMetadata": { + "description": "Metadata associated with particular node role.", + "type": "object", + "required": [ + "highest_id", + "num_nodes" + ], + "properties": { + "highest_id": { + "description": "Highest, also latest, node-id of a node assigned this role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_nodes": { + "description": "Number of nodes assigned this particular role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -8003,17 +9980,10 @@ "description": "Parameters used for reward calculation.", "type": "object", "required": [ - "active_set_size", "interval", - "rewarded_set_size" + "rewarded_set" ], "properties": { - "active_set_size": { - "description": "The expected number of mixnodes in the active set.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "interval": { "description": "Parameters that should remain unchanged throughout an interval.", "allOf": [ @@ -8022,11 +9992,8 @@ } ] }, - "rewarded_set_size": { - "description": "The expected number of mixnodes in the rewarded set (i.e. active + standby).", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set": { + "$ref": "#/definitions/RewardedSetParams" } }, "additionalProperties": false, @@ -8123,6 +10090,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -8131,6 +10134,32 @@ "title": "String", "type": "string" }, + "get_role_assignment": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochAssignmentResponse", + "type": "object", + "required": [ + "epoch_id", + "nodes" + ], + "properties": { + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + }, "get_signing_nonce": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "uint32", @@ -8140,7 +10169,7 @@ }, "get_stake_saturation": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "StakeSaturationResponse", + "title": "MixStakeSaturationResponse", "description": "Response containing the current state of the stake saturation of a mixnode with the provided id.", "type": "object", "required": [ @@ -8263,8 +10292,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -8279,15 +10307,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -8298,8 +10318,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -8377,8 +10397,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -8393,15 +10412,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -8412,8 +10423,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -8857,6 +10868,291 @@ "additionalProperties": false } } + }, + "get_unbonded_nym_node": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnbondedNodeResponse", + "description": "Response containing basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there existed a nym-node with the provided id, this field contains its basic information.", + "anyOf": [ + { + "$ref": "#/definitions/UnbondedNymNode" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested nym-node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_by_identity_key_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_by_owner_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } } } } diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index c5c773e76d..a71339ac52 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -24,228 +24,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "assign_node_layer" - ], - "properties": { - "assign_node_layer": { - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "$ref": "#/definitions/Layer" - }, - "mix_id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "create_family_on_behalf" - ], - "properties": { - "create_family_on_behalf": { - "type": "object", - "required": [ - "label", - "owner_address" - ], - "properties": { - "label": { - "type": "string" - }, - "owner_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family", - "type": "object", - "required": [ - "join_family_on_behalf" - ], - "properties": { - "join_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "join_permit", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family_on_behalf" - ], - "properties": { - "leave_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member_on_behalf" - ], - "properties": { - "kick_family_member_on_behalf": { - "type": "object", - "required": [ - "head_address", - "member" - ], - "properties": { - "head_address": { - "type": "string" - }, - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -291,23 +69,21 @@ { "type": "object", "required": [ - "update_active_set_size" + "update_active_set_distribution" ], "properties": { - "update_active_set_size": { + "update_active_set_distribution": { "type": "object", "required": [ - "active_set_size", - "force_immediately" + "force_immediately", + "update" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "force_immediately": { "type": "boolean" + }, + "update": { + "$ref": "#/definitions/ActiveSetUpdate" } }, "additionalProperties": false @@ -386,36 +162,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "advance_current_epoch" - ], - "properties": { - "advance_current_epoch": { - "type": "object", - "required": [ - "expected_active_set_size", - "new_rewarded_set" - ], - "properties": { - "expected_active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "new_rewarded_set": { - "type": "array", - "items": { - "$ref": "#/definitions/LayerAssignment" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -439,6 +185,27 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "assign_roles" + ], + "properties": { + "assign_roles": { + "type": "object", + "required": [ + "assignment" + ], + "properties": { + "assignment": { + "$ref": "#/definitions/RoleAssignment" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -454,7 +221,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -484,7 +251,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -618,17 +385,17 @@ { "type": "object", "required": [ - "update_mixnode_cost_params" + "update_cost_params" ], "properties": { - "update_mixnode_cost_params": { + "update_cost_params": { "type": "object", "required": [ "new_costs" ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -650,7 +417,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "owner": { "type": "string" @@ -707,6 +474,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "migrate_mixnode" + ], + "properties": { + "migrate_mixnode": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -844,16 +624,104 @@ { "type": "object", "required": [ - "delegate_to_mixnode" + "migrate_gateway" ], "properties": { - "delegate_to_mixnode": { + "migrate_gateway": { + "type": "object", + "properties": { + "cost_params": { + "anyOf": [ + { + "$ref": "#/definitions/NodeCostParams" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "bond_nym_node" + ], + "properties": { + "bond_nym_node": { "type": "object", "required": [ - "mix_id" + "cost_params", + "node", + "owner_signature" ], "properties": { - "mix_id": { + "cost_params": { + "$ref": "#/definitions/NodeCostParams" + }, + "node": { + "$ref": "#/definitions/NymNode" + }, + "owner_signature": { + "$ref": "#/definitions/MessageSignature" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unbond_nym_node" + ], + "properties": { + "unbond_nym_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_node_config" + ], + "properties": { + "update_node_config": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/NodeConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -894,16 +762,16 @@ { "type": "object", "required": [ - "undelegate_from_mixnode" + "undelegate" ], "properties": { - "undelegate_from_mixnode": { + "undelegate": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -944,23 +812,23 @@ { "type": "object", "required": [ - "reward_mixnode" + "reward_node" ], "properties": { - "reward_mixnode": { + "reward_node": { "type": "object", "required": [ - "mix_id", - "performance" + "node_id", + "params" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "performance": { - "$ref": "#/definitions/Percent" + "params": { + "$ref": "#/definitions/NodeRewardingParameters" } }, "additionalProperties": false @@ -1011,10 +879,10 @@ "withdraw_delegator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1087,9 +955,81 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_mixnode" + ], + "properties": { + "testing_unchecked_bond_legacy_mixnode": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/MixNode" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_gateway" + ], + "properties": { + "testing_unchecked_bond_legacy_gateway": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/Gateway" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -1109,8 +1049,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -1125,15 +1064,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -1144,8 +1075,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1171,10 +1102,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -1292,14 +1219,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -1337,39 +1266,6 @@ }, "additionalProperties": false }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "LayerAssignment": { - "description": "Specifies layer assignment for the given mixnode.", - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "description": "The layer to which it's going to be assigned", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, - "mix_id": { - "description": "The id of the mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "MessageSignature": { "type": "array", "items": { @@ -1462,7 +1358,31 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeConfigUpdate": { + "type": "object", + "properties": { + "custom_http_port": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "type": [ + "string", + "null" + ] + }, + "restore_default_http_port": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -1471,7 +1391,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1479,7 +1399,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -1489,6 +1409,61 @@ }, "additionalProperties": false }, + "NodeRewardingParameters": { + "description": "Parameters used for rewarding particular node.", + "type": "object", + "required": [ + "performance", + "work_factor" + ], + "properties": { + "performance": { + "description": "Performance of the particular node in the current epoch.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + }, + "work_factor": { + "description": "Amount of work performed by this node in the current epoch also known as 'omega' in the paper", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", "allOf": [ @@ -1529,6 +1504,74 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + }, + "RoleAssignment": { + "type": "object", + "required": [ + "nodes", + "role" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index e025d9f11f..bb2dd70702 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -82,21 +82,15 @@ "InitialRewardingParams": { "type": "object", "required": [ - "active_set_size", "active_set_work_factor", "initial_reward_pool", "initial_staking_supply", "interval_pool_emission", - "rewarded_set_size", + "rewarded_set_params", "staking_supply_scale_factor", "sybil_resistance" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "active_set_work_factor": { "$ref": "#/definitions/Decimal" }, @@ -109,10 +103,8 @@ "interval_pool_emission": { "$ref": "#/definitions/Percent" }, - "rewarded_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "$ref": "#/definitions/RewardedSetParams" }, "staking_supply_scale_factor": { "$ref": "#/definitions/Percent" @@ -163,6 +155,42 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/query.json b/contracts/mixnet/schema/raw/query.json index c938a56f64..90d0477a30 100644 --- a/contracts/mixnet/schema/raw/query.json +++ b/contracts/mixnet/schema/raw/query.json @@ -15,158 +15,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the list of families registered in this contract.", - "type": "object", - "required": [ - "get_all_families_paged" - ], - "properties": { - "get_all_families_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the list of all family members registered in this contract.", - "type": "object", - "required": [ - "get_all_members_paged" - ], - "properties": { - "get_all_members_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family head.", - "type": "object", - "required": [ - "get_family_by_head" - ], - "properties": { - "get_family_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family label.", - "type": "object", - "required": [ - "get_family_by_label" - ], - "properties": { - "get_family_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family head.", - "type": "object", - "required": [ - "get_family_members_by_head" - ], - "properties": { - "get_family_members_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family label.", - "type": "object", - "required": [ - "get_family_members_by_label" - ], - "properties": { - "get_family_members_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", "type": "object", @@ -279,40 +127,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current list of mixnodes in the rewarded set.", - "type": "object", - "required": [ - "get_rewarded_set" - ], - "properties": { - "get_rewarded_set": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded mixnodes.", "type": "object", @@ -438,7 +252,7 @@ "minimum": 0.0 }, "owner": { - "description": "The address of the owner of the the mixnodes used for the query.", + "description": "The address of the owner of the mixnodes used for the query.", "type": "string" }, "start_after": { @@ -643,20 +457,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current layer configuration of the mix network.", - "type": "object", - "required": [ - "get_layer_distribution" - ], - "properties": { - "get_layer_distribution": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded gateways.", "type": "object", @@ -736,16 +536,175 @@ "additionalProperties": false }, { - "description": "Gets all delegations associated with particular mixnode", + "description": "Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes", "type": "object", "required": [ - "get_mixnode_delegations" + "get_preassigned_gateway_ids" ], "properties": { - "get_mixnode_delegations": { + "get_preassigned_gateway_ids": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_node_bonds_paged" + ], + "properties": { + "get_nym_node_bonds_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_nodes_detailed_paged" + ], + "properties": { + "get_nym_nodes_detailed_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "get_unbonded_nym_node" + ], + "properties": { + "get_unbonded_nym_node": { "type": "object", "required": [ - "mix_id" + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_paged" + ], + "properties": { + "get_unbonded_nym_nodes_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that belonged to a particular owner.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_owner_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_owner_paged": { + "type": "object", + "required": [ + "owner" ], "properties": { "limit": { @@ -757,7 +716,244 @@ "format": "uint32", "minimum": 0.0 }, - "mix_id": { + "owner": { + "description": "The address of the owner of the nym-node used for the query", + "type": "string" + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that used the particular identity key.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_identity_key_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_identity_key_paged": { + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node used for the query.", + "type": "string" + }, + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information belonging to the particular owner.", + "type": "object", + "required": [ + "get_owned_nym_node" + ], + "properties": { + "get_owned_nym_node": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of the node owner to use for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information of a node with the provided id.", + "type": "object", + "required": [ + "get_nym_node_details" + ], + "properties": { + "get_nym_node_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nym-node information given its current identity key.", + "type": "object", + "required": [ + "get_nym_node_details_by_identity_key" + ], + "properties": { + "get_nym_node_details_by_identity_key": { + "type": "object", + "required": [ + "node_identity" + ], + "properties": { + "node_identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the nym-node used for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the rewarding information of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_rewarding_details" + ], + "properties": { + "get_node_rewarding_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the stake saturation of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_stake_saturation" + ], + "properties": { + "get_node_stake_saturation": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_role_assignment" + ], + "properties": { + "get_role_assignment": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_rewarded_set_metadata" + ], + "properties": { + "get_rewarded_set_metadata": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets all delegations associated with particular node", + "type": "object", + "required": [ + "get_node_delegations" + ], + "properties": { + "get_node_delegations": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -838,14 +1034,14 @@ "type": "object", "required": [ "delegator", - "mix_id" + "node_id" ], "properties": { "delegator": { "description": "The address of the owner of the delegation.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -935,16 +1131,16 @@ "description": "Gets the reward amount accrued by the particular mixnode that has not yet been claimed.", "type": "object", "required": [ - "get_pending_mix_node_operator_reward" + "get_pending_node_operator_reward" ], "properties": { - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -967,14 +1163,14 @@ "type": "object", "required": [ "address", - "mix_id" + "node_id" ], "properties": { "address": { "description": "Address of the delegator to use for the query.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -1004,7 +1200,7 @@ "type": "object", "required": [ "estimated_performance", - "mix_id" + "node_id" ], "properties": { "estimated_performance": { @@ -1015,7 +1211,18 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -1039,7 +1246,7 @@ "required": [ "address", "estimated_performance", - "mix_id" + "node_id" ], "properties": { "address": { @@ -1054,18 +1261,22 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -1241,6 +1452,17 @@ "$ref": "#/definitions/Decimal" } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_all_delegations.json b/contracts/mixnet/schema/raw/response_to_get_all_delegations.json index a919220c46..71878b1827 100644 --- a/contracts/mixnet/schema/raw/response_to_get_all_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_all_delegations.json @@ -66,7 +66,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -92,8 +92,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json index 59fc029331..8bde1a5732 100644 --- a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json +++ b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json @@ -48,14 +48,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -112,7 +104,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -129,14 +120,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -178,35 +161,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -227,6 +182,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -239,14 +195,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -262,7 +245,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -302,7 +285,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -315,6 +298,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_delegation_details.json b/contracts/mixnet/schema/raw/response_to_get_delegation_details.json index 8371493b30..f8807370a5 100644 --- a/contracts/mixnet/schema/raw/response_to_get_delegation_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_delegation_details.json @@ -1,10 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixNodeDelegationResponse", + "title": "NodeDelegationResponse", "description": "Response containing delegation details.", "type": "object", "required": [ - "mixnode_still_bonded" + "mixnode_still_bonded", + "node_still_bonded" ], "properties": { "delegation": { @@ -20,6 +21,10 @@ }, "mixnode_still_bonded": { "description": "Flag indicating whether the node towards which the delegation was made is still bonded in the network.", + "deprecated": true, + "type": "boolean" + }, + "node_still_bonded": { "type": "boolean" } }, @@ -55,7 +60,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -81,8 +86,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json b/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json index b6e7e7a6ee..e3556f1db2 100644 --- a/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json @@ -66,7 +66,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -92,8 +92,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_epoch_status.json b/contracts/mixnet/schema/raw/response_to_get_epoch_status.json index 1a85e633bf..cd4a2b0016 100644 --- a/contracts/mixnet/schema/raw/response_to_get_epoch_status.json +++ b/contracts/mixnet/schema/raw/response_to_get_epoch_status.json @@ -81,13 +81,39 @@ ] }, { - "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set.", - "type": "string", - "enum": [ - "advancing_epoch" - ] + "description": "Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, all issued actions got resolved and node roles should now be assigned before advancing into the next epoch.", + "type": "object", + "required": [ + "role_assignment" + ], + "properties": { + "role_assignment": { + "type": "object", + "required": [ + "next" + ], + "properties": { + "next": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json b/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json index 61bdf6b3f1..ceb99ca8cb 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json +++ b/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json @@ -52,14 +52,6 @@ } } }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -116,7 +108,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -133,14 +124,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -182,8 +165,7 @@ } ] } - }, - "additionalProperties": false + } }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", diff --git a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json index bc95bb0839..4bcb8772ac 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json +++ b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json @@ -56,14 +56,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -120,7 +112,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -137,14 +128,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -186,35 +169,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -235,6 +190,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -247,14 +203,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -270,7 +253,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -310,7 +293,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -323,6 +306,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json index 82d8c56b90..dccb346034 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json @@ -54,7 +54,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -80,8 +80,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json index 4d7bd42da4..68d3856e11 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json @@ -50,14 +50,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -114,7 +106,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -131,14 +122,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -180,35 +163,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -229,6 +184,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -241,14 +197,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -264,7 +247,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -304,7 +287,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -317,6 +300,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json index 9b30dc4a75..dcb9cb67cd 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json @@ -17,7 +17,7 @@ "description": "If there exists a mixnode with the provided id, this field contains its rewarding information.", "anyOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" }, { "type": "null" @@ -46,7 +46,7 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -55,7 +55,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -63,7 +63,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -73,7 +73,7 @@ }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -89,7 +89,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -129,7 +129,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" diff --git a/contracts/mixnet/schema/raw/response_to_get_node_delegations.json b/contracts/mixnet/schema/raw/response_to_get_node_delegations.json new file mode 100644 index 0000000000..c8ff4df221 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_delegations.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNodeDelegationsResponse", + "description": "Response containing paged list of all delegations made towards particular node.", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "description": "Each individual delegation made.", + "type": "array", + "items": { + "$ref": "#/definitions/Delegation" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "type": "object", + "required": [ + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" + ], + "properties": { + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Proxy address used to delegate the funds on behalf of another address", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json b/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json new file mode 100644 index 0000000000..e9b3a6e545 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeRewardingDetailsResponse", + "description": "Response containing rewarding information of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "rewarding_details": { + "description": "If there exists a node with the provided id, this field contains its rewarding information.", + "anyOf": [ + { + "$ref": "#/definitions/NodeRewarding" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json b/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json new file mode 100644 index 0000000000..3aa1cddc13 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StakeSaturationResponse", + "description": "Response containing the current state of the stake saturation of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "current_saturation": { + "description": "The current stake saturation of this node that is indirectly used in reward calculation formulas. Note that it can't be larger than 1.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "uncapped_saturation": { + "description": "The current, absolute, stake saturation of this node. Note that as the name suggests it can be larger than 1. However, anything beyond that value has no effect on the total node reward.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json new file mode 100644 index 0000000000..6be0563f7a --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeBondsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "The nym node bond information present in the contract.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeBond" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json new file mode 100644 index 0000000000..4707dce857 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json @@ -0,0 +1,299 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsResponse", + "description": "Response containing details of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there exists a node with the provided id, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json new file mode 100644 index 0000000000..aea64e0746 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json @@ -0,0 +1,297 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsByIdentityResponse", + "description": "Response containing details of a bonded node with the provided identity key.", + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "details": { + "description": "If there exists a bonded node with the provided identity key, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node.", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json b/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json new file mode 100644 index 0000000000..0b96dd856c --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json @@ -0,0 +1,297 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeDetailsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "All nym-node details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeDetails" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json index da895c4bf4..94867ce877 100644 --- a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json +++ b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json @@ -52,14 +52,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -116,7 +108,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -133,14 +124,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -182,35 +165,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -231,6 +186,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -243,14 +199,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -266,7 +249,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -306,7 +289,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -319,6 +302,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json b/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json new file mode 100644 index 0000000000..ed1ccc194d --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json @@ -0,0 +1,301 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeOwnershipResponse", + "description": "Response containing details of a node belonging to the particular owner.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Validated address of the node owner.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "details": { + "description": "If the provided address owns a nym-node, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json index e885d5976c..4ae2297c32 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json @@ -4,7 +4,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -42,6 +43,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json index 7588cad3e9..67b89db672 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json @@ -24,6 +24,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -72,7 +102,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -82,7 +112,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -94,8 +124,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -126,7 +156,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -135,12 +165,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -174,10 +204,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -185,7 +249,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -208,10 +272,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -264,20 +362,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -287,6 +385,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json index 09a4d03787..54a6a8e52f 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json @@ -32,6 +32,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -105,7 +135,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -115,7 +145,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -127,8 +157,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -159,7 +189,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -168,12 +198,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -207,10 +237,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -218,7 +282,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -241,10 +305,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -297,20 +395,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -320,6 +418,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json b/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json index a0877027a9..f6926bf188 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json @@ -80,14 +80,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -125,7 +127,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -134,7 +136,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -142,7 +144,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -204,7 +206,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -214,6 +216,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -283,6 +319,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json b/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json index ccbdc22178..188e99023a 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json @@ -88,14 +88,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -133,7 +135,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -142,7 +144,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -150,7 +152,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -237,7 +239,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -247,6 +249,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -316,6 +352,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json new file mode 100644 index 0000000000..4ae2297c32 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PendingRewardResponse", + "description": "Response containing information about accrued rewards.", + "type": "object", + "required": [ + "mixnode_still_fully_bonded", + "node_still_fully_bonded" + ], + "properties": { + "amount_earned": { + "description": "The amount of tokens that could be claimed.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "amount_earned_detailed": { + "description": "The full pending rewards. Note that it's nearly identical to `amount_earned`, however, it contains few additional decimal points for more accurate reward calculation.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "amount_staked": { + "description": "The amount of tokens initially staked.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "mixnode_still_fully_bonded": { + "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json index e885d5976c..4ae2297c32 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json @@ -4,7 +4,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -42,6 +43,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, diff --git a/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json b/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json new file mode 100644 index 0000000000..bfd6be0f84 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PreassignedGatewayIdsResponse", + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "$ref": "#/definitions/PreassignedId" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "PreassignedId": { + "type": "object", + "required": [ + "identity", + "node_id" + ], + "properties": { + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", + "type": "string" + }, + "node_id": { + "description": "The id pre-assigned to this gateway", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json b/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json new file mode 100644 index 0000000000..54bdbf9f6c --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RolesMetadataResponse", + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/definitions/RewardedSetMetadata" + } + }, + "additionalProperties": false, + "definitions": { + "RewardedSetMetadata": { + "description": "Metadata associated with the rewarded set.", + "type": "object", + "required": [ + "entry_gateway_metadata", + "epoch_id", + "exit_gateway_metadata", + "fully_assigned", + "layer1_metadata", + "layer2_metadata", + "layer3_metadata", + "standby_metadata" + ], + "properties": { + "entry_gateway_metadata": { + "description": "Metadata for the 'EntryGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateway_metadata": { + "description": "Metadata for the 'ExitGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "fully_assigned": { + "description": "Indicates whether all roles got assigned to the set for this epoch.", + "type": "boolean" + }, + "layer1_metadata": { + "description": "Metadata for the 'Layer1' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer2_metadata": { + "description": "Metadata for the 'Layer2' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer3_metadata": { + "description": "Metadata for the 'Layer3' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "standby_metadata": { + "description": "Metadata for the 'Standby' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + } + }, + "additionalProperties": false + }, + "RoleMetadata": { + "description": "Metadata associated with particular node role.", + "type": "object", + "required": [ + "highest_id", + "num_nodes" + ], + "properties": { + "highest_id": { + "description": "Highest, also latest, node-id of a node assigned this role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_nodes": { + "description": "Number of nodes assigned this particular role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json b/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json index 8840bc31f2..d99be0119b 100644 --- a/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json @@ -4,17 +4,10 @@ "description": "Parameters used for reward calculation.", "type": "object", "required": [ - "active_set_size", "interval", - "rewarded_set_size" + "rewarded_set" ], "properties": { - "active_set_size": { - "description": "The expected number of mixnodes in the active set.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "interval": { "description": "Parameters that should remain unchanged throughout an interval.", "allOf": [ @@ -23,11 +16,8 @@ } ] }, - "rewarded_set_size": { - "description": "The expected number of mixnodes in the rewarded set (i.e. active + standby).", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set": { + "$ref": "#/definitions/RewardedSetParams" } }, "additionalProperties": false, @@ -124,6 +114,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_role_assignment.json b/contracts/mixnet/schema/raw/response_to_get_role_assignment.json new file mode 100644 index 0000000000..9d7ff6c4e3 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_role_assignment.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochAssignmentResponse", + "type": "object", + "required": [ + "epoch_id", + "nodes" + ], + "properties": { + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "additionalProperties": false +} diff --git a/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json b/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json index 4a86c03934..c6a38d46a2 100644 --- a/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json +++ b/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "StakeSaturationResponse", + "title": "MixStakeSaturationResponse", "description": "Response containing the current state of the stake saturation of a mixnode with the provided id.", "type": "object", "required": [ diff --git a/contracts/mixnet/schema/raw/response_to_get_state.json b/contracts/mixnet/schema/raw/response_to_get_state.json index 9103e87b72..df4584dde6 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state.json +++ b/contracts/mixnet/schema/raw/response_to_get_state.json @@ -77,8 +77,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -93,15 +92,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -112,8 +103,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" diff --git a/contracts/mixnet/schema/raw/response_to_get_state_params.json b/contracts/mixnet/schema/raw/response_to_get_state_params.json index 52d0191167..7ef87647d8 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_state_params.json @@ -4,8 +4,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -20,15 +19,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -39,8 +30,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json new file mode 100644 index 0000000000..dde90a69e3 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnbondedNodeResponse", + "description": "Response containing basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there existed a nym-node with the provided id, this field contains its basic information.", + "anyOf": [ + { + "$ref": "#/definitions/UnbondedNymNode" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested nym-node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/src/compat/helpers.rs b/contracts/mixnet/src/compat/helpers.rs new file mode 100644 index 0000000000..0f57650eaf --- /dev/null +++ b/contracts/mixnet/src/compat/helpers.rs @@ -0,0 +1,144 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::storage as mixnode_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::support::helpers::ensure_epoch_in_progress_state; +use cosmwasm_std::{Coin, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::helpers::{NodeBond, NodeDetails, PendingChanges}; +use mixnet_contract_common::NodeId; + +pub fn ensure_can_withdraw_rewards(node_details: &D) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // we can only withdraw rewards for a bonded node (i.e. not in the process of unbonding) + // otherwise we know there are no rewards to withdraw + node_details.bond_info().ensure_bonded()?; + + Ok(()) +} + +pub fn ensure_can_modify_cost_params( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // changing cost params is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(storage)?; + + // we can only change cost params for a bonded node (i.e. not in the process of unbonding) + node_details.bond_info().ensure_bonded()?; + + Ok(()) +} + +fn ensure_can_modify_pledge( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // changing pledge is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(storage)?; + + // we can only change pledge for a bonded node (i.e. not in the process of unbonding) + node_details.bond_info().ensure_bonded()?; + + // the node can't have any pending pledge changes + node_details + .pending_changes() + .ensure_no_pending_pledge_changes()?; + + Ok(()) +} + +// remove duplicate code and make sure the same checks are performed everywhere +// (so nothing is accidentally missing) +pub fn ensure_can_increase_pledge( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + ensure_can_modify_pledge(storage, node_details) +} + +// remove duplicate code and make sure the same checks are performed everywhere +// (so nothing is accidentally missing) +pub fn ensure_can_decrease_pledge( + storage: &dyn Storage, + node_details: &D, + decrease_by: &Coin, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + ensure_can_modify_pledge(storage, node_details)?; + + let minimum_pledge = mixnet_params_storage::minimum_node_pledge(storage)?; + + // check that the denomination is correct + if decrease_by.denom != minimum_pledge.denom { + return Err(MixnetContractError::WrongDenom { + received: decrease_by.denom.clone(), + expected: minimum_pledge.denom, + }); + } + + // also check if the request contains non-zero amount + // (otherwise it's a no-op and we should we waste gas when resolving events?) + if decrease_by.amount.is_zero() { + return Err(MixnetContractError::ZeroCoinAmount); + } + + // decreasing pledge can't result in the new pledge being lower than the minimum amount + let new_pledge_amount = node_details + .bond_info() + .original_pledge() + .amount + .saturating_sub(decrease_by.amount); + if new_pledge_amount < minimum_pledge.amount { + return Err(MixnetContractError::InvalidPledgeReduction { + current: node_details.bond_info().original_pledge().amount, + decrease_by: decrease_by.amount, + minimum: minimum_pledge.amount, + denom: minimum_pledge.denom, + }); + } + + Ok(()) +} + +pub fn get_bond( + storage: &dyn Storage, + node_id: NodeId, +) -> Result, MixnetContractError> { + if let Ok(mix_bond) = mixnode_storage::mixnode_bonds().load(storage, node_id) { + Ok(Box::new(mix_bond)) + } else { + let node_bond = nymnodes_storage::nym_nodes() + .load(storage, node_id) + .map_err(|_| MixnetContractError::NymNodeBondNotFound { node_id })?; + Ok(Box::new(node_bond)) + } +} + +pub fn may_get_bond( + storage: &dyn Storage, + node_id: NodeId, +) -> StdResult>> { + if let Some(mix_bond) = mixnode_storage::mixnode_bonds().may_load(storage, node_id)? { + Ok(Some(Box::new(mix_bond))) + } else if let Some(node_bond) = nymnodes_storage::nym_nodes().may_load(storage, node_id)? { + Ok(Some(Box::new(node_bond))) + } else { + Ok(None) + } +} diff --git a/contracts/mixnet/src/compat/mod.rs b/contracts/mixnet/src/compat/mod.rs new file mode 100644 index 0000000000..94d181c9dc --- /dev/null +++ b/contracts/mixnet/src/compat/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod helpers; +pub(crate) mod queries; +mod storage_traits; +pub(crate) mod transactions; diff --git a/contracts/mixnet/src/compat/queries.rs b/contracts/mixnet/src/compat/queries.rs new file mode 100644 index 0000000000..908c7122bd --- /dev/null +++ b/contracts/mixnet/src/compat/queries.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// // will return details of either nym-node, legacy mixnode or legacy gateway +// pub fn query_owned_node() { +// todo!() +// } + +pub(crate) mod rewards { + use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_owner}; + use crate::nodes::helpers::{get_node_details_by_id, get_node_details_by_owner}; + use cosmwasm_std::{Addr, Deps, StdResult}; + use mixnet_contract_common::{NodeId, PendingRewardResponse}; + + #[allow(deprecated)] + pub(crate) fn pending_operator_reward( + deps: Deps<'_>, + operator: Addr, + ) -> StdResult { + // check if owns mixnode or nymnode and query accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, operator.clone())? { + Ok(PendingRewardResponse { + amount_staked: Some(nym_node_details.original_pledge().clone()), + amount_earned: Some(nym_node_details.pending_operator_reward()), + amount_earned_detailed: Some(nym_node_details.pending_detailed_operator_reward()?), + mixnode_still_fully_bonded: !nym_node_details.is_unbonding(), + node_still_fully_bonded: !nym_node_details.is_unbonding(), + }) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, operator)? + { + Ok(PendingRewardResponse { + amount_staked: Some(legacy_mixnode_details.original_pledge().clone()), + amount_earned: Some(legacy_mixnode_details.pending_operator_reward()), + amount_earned_detailed: Some( + legacy_mixnode_details.pending_detailed_operator_reward()?, + ), + mixnode_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + node_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + }) + } else { + Ok(PendingRewardResponse::default()) + } + } + + #[allow(deprecated)] + pub(crate) fn pending_operator_reward_by_id( + deps: Deps<'_>, + node_id: NodeId, + ) -> StdResult { + // check if owns mixnode or nymnode and query accordingly + if let Some(nym_node_details) = get_node_details_by_id(deps.storage, node_id)? { + Ok(PendingRewardResponse { + amount_staked: Some(nym_node_details.original_pledge().clone()), + amount_earned: Some(nym_node_details.pending_operator_reward()), + amount_earned_detailed: Some(nym_node_details.pending_detailed_operator_reward()?), + mixnode_still_fully_bonded: !nym_node_details.is_unbonding(), + node_still_fully_bonded: !nym_node_details.is_unbonding(), + }) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_id(deps.storage, node_id)? + { + Ok(PendingRewardResponse { + amount_staked: Some(legacy_mixnode_details.original_pledge().clone()), + amount_earned: Some(legacy_mixnode_details.pending_operator_reward()), + amount_earned_detailed: Some( + legacy_mixnode_details.pending_detailed_operator_reward()?, + ), + mixnode_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + node_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + }) + } else { + Ok(PendingRewardResponse::default()) + } + } +} diff --git a/contracts/mixnet/src/compat/storage_traits.rs b/contracts/mixnet/src/compat/storage_traits.rs new file mode 100644 index 0000000000..5cb369ff51 --- /dev/null +++ b/contracts/mixnet/src/compat/storage_traits.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// use crate::mixnodes::storage as mixnodes_storage; +// use crate::nodes::storage as nymnodes_storage; +// use crate::rewards::storage as rewards_storage; +// use cosmwasm_std::Storage; +// use cw_storage_plus::Map; +// use mixnet_contract_common::error::MixnetContractError; +// use mixnet_contract_common::mixnode::PendingMixNodeChanges; +// use mixnet_contract_common::{NodeId, PendingNodeChanges}; +// use serde::de::DeserializeOwned; +// use serde::{Deserialize, Serialize}; +// +// // I've created this trait to ensure everything is always stored in the right storage bucket, +// // because I fear I might have accidentally missed something during the transition period +// // of having BOTH mixnodes and nym-nodes +// pub(crate) trait NodeDetailsStorage { +// // +// } +// +// pub(crate) trait NodeBondStorage { +// // +// } +// +// pub(crate) trait PendingChangesStorage: Sized + Serialize + DeserializeOwned { +// const STORAGE_MAP: Map<'static, NodeId, Self>; +// +// fn save(&self, storage: &mut dyn Storage, node_id: NodeId) -> Result<(), MixnetContractError> { +// Ok(Self::STORAGE_MAP.save(storage, node_id, self)?) +// } +// +// fn load(storage: &dyn Storage, node_id: NodeId) -> Result { +// Ok(Self::STORAGE_MAP.load(storage, node_id)?) +// } +// } +// +// pub(crate) trait RewardingStorage { +// // +// } +// +// impl PendingChangesStorage for PendingNodeChanges { +// const STORAGE_MAP: Map<'static, NodeId, Self> = nymnodes_storage::PENDING_NYMNODE_CHANGES; +// } +// +// impl PendingChangesStorage for PendingMixNodeChanges { +// const STORAGE_MAP: Map<'static, NodeId, Self> = mixnodes_storage::PENDING_MIXNODE_CHANGES; +// } diff --git a/contracts/mixnet/src/compat/transactions.rs b/contracts/mixnet/src/compat/transactions.rs new file mode 100644 index 0000000000..dbedab1325 --- /dev/null +++ b/contracts/mixnet/src/compat/transactions.rs @@ -0,0 +1,95 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::transactions::{ + try_decrease_mixnode_pledge, try_increase_mixnode_pledge, try_update_mixnode_cost_params, +}; +use crate::nodes::helpers::get_node_details_by_owner; +use crate::nodes::transactions::{ + try_decrease_nym_node_pledge, try_increase_nym_node_pledge, try_update_nym_node_cost_params, +}; +use crate::rewards::transactions::{ + try_withdraw_mixnode_operator_reward, try_withdraw_nym_node_operator_reward, +}; +use crate::support::helpers::{ + ensure_operating_cost_within_range, ensure_profit_margin_within_range, +}; +use cosmwasm_std::{Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::NodeCostParams; + +pub(crate) fn try_increase_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_increase_nym_node_pledge(deps, env, info.funds, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_increase_mixnode_pledge(deps, env, info.funds, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub fn try_decrease_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + decrease_by: Coin, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_decrease_nym_node_pledge(deps, env, decrease_by, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_decrease_mixnode_pledge(deps, env, decrease_by, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub(crate) fn try_update_cost_params( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + new_costs: NodeCostParams, +) -> Result { + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; + + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_update_nym_node_cost_params(deps, env, new_costs, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_update_mixnode_cost_params(deps, env, new_costs, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub(crate) fn try_withdraw_operator_reward( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_withdraw_nym_node_operator_reward(deps, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_withdraw_mixnode_operator_reward(deps, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 0ea867c00c..3d2d165113 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -1,42 +1,54 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::Uint128; +use cosmwasm_std::{Coin, Uint128}; +use mixnet_contract_common::{ + NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_contracts_common::Percent; -/// Constant specifying minimum of coin amount required to bond a gateway -pub const INITIAL_GATEWAY_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); +/// Constant specifying minimum of coin amount required to bond a node +pub const INITIAL_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); -/// Constant specifying minimum of coin amount required to bond a mixnode -pub const INITIAL_MIXNODE_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); +pub fn default_node_costs>(rewarding_denom: S) -> NodeCostParams { + // safety: our hardcoded PM value is a valid percent + #[allow(clippy::unwrap_used)] + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: Coin::new(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, rewarding_denom), + } +} // retrieval limits // TODO: those would need to be empirically verified whether they're not way too small or way too high -pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; -pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; +pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; +pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 75; -pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 100; +pub const NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; -pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 300; +pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 75; +pub const NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 75; -pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; -pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 300; +pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 200; +pub const UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT: u32 = 200; -pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; -pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; +pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 500; -pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; -pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; +pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT: u32 = 500; -pub const REWARDED_SET_MAX_RETRIEVAL_LIMIT: u32 = 1000; - -pub const FAMILIES_DEFAULT_RETRIEVAL_LIMIT: u32 = 10; -pub const FAMILIES_MAX_RETRIEVAL_LIMIT: u32 = 20; +pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 100; // storage keys pub const DELEGATION_PK_NAMESPACE: &str = "dl"; @@ -46,7 +58,6 @@ pub const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; pub const GATEWAYS_PK_NAMESPACE: &str = "gt"; pub const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; -pub const REWARDED_SET_KEY: &str = "rs"; pub const CURRENT_EPOCH_STATUS_KEY: &str = "ces"; pub const CURRENT_INTERVAL_KEY: &str = "ci"; pub const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic"; @@ -60,7 +71,10 @@ pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; pub const ADMIN_STORAGE_KEY: &str = "admin"; pub const CONTRACT_STATE_KEY: &str = "state"; -pub const LAYER_DISTRIBUTION_KEY: &str = "layers"; +pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles"; +pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata"; +pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles"; + pub const NODE_ID_COUNTER_KEY: &str = "nic"; pub const PENDING_MIXNODE_CHANGES_NAMESPACE: &str = "pmc"; pub const MIXNODES_PK_NAMESPACE: &str = "mnn"; @@ -68,16 +82,26 @@ pub const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; pub const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; pub const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; +pub const PENDING_NYMNODE_CHANGES_NAMESPACE: &str = "pnc"; +pub const NYMNODE_PK_NAMESPACE: &str = "nn"; +pub const NYMNODE_OWNER_IDX_NAMESPACE: &str = "nno"; +pub const NYMNODE_IDENTITY_IDX_NAMESPACE: &str = "nni"; + pub const UNBONDED_MIXNODES_PK_NAMESPACE: &str = "ubm"; pub const UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE: &str = "umo"; pub const UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "umi"; +pub const UNBONDED_NYMNODE_PK_NAMESPACE: &str = "ubnn"; +pub const UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE: &str = "ubno"; +pub const UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE: &str = "ubni"; + +pub const CUMULATIVE_EPOCH_WORK_KEY: &str = "cumulative_epoch_work"; pub const REWARDING_PARAMS_KEY: &str = "rparams"; pub const PENDING_REWARD_POOL_KEY: &str = "prp"; pub const MIXNODES_REWARDING_PK_NAMESPACE: &str = "mnr"; - -pub const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; -pub const FAMILIES_MAP_NAMESPACE: &str = "fam2"; -pub const MEMBERS_MAP_NAMESPACE: &str = "memb2"; +pub const NYMNODE_REWARDING_PK_NAMESPACE: &str = MIXNODES_REWARDING_PK_NAMESPACE; pub const SIGNING_NONCES_NAMESPACE: &str = "sn"; + +// temporary storage keys created for the transition period: +pub const LEGACY_GATEWAY_ID_NAMESPACE: &str = "lgidr"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index cda8264afa..36ffe22623 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,20 +1,23 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::constants::INITIAL_PLEDGE_AMOUNT; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::storage as mixnode_storage; -use crate::rewards::storage as rewards_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::queued_migrations::migrate_to_nym_nodes_usage; +use crate::rewards::storage::RewardingStorage; +use crate::support::tests::legacy; use cosmwasm_std::{ - entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, + coin, entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, + Response, }; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, - OperatingCostRange, ProfitMarginRange, QueryMsg, + NodeCostParams, OperatingCostRange, ProfitMarginRange, QueryMsg, }; -use nym_contracts_common::set_build_information; +use nym_contracts_common::{set_build_information, Percent}; // version info for migration info const CONTRACT_NAME: &str = "crate:nym-mixnet-contract"; @@ -36,14 +39,10 @@ fn default_initial_state( vesting_contract_address, rewarding_denom: rewarding_denom.clone(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: rewarding_denom.clone(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom: rewarding_denom, - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }, profit_margin, interval_operating_cost, @@ -93,8 +92,8 @@ pub fn instantiate( rewarding_validator_address, )?; mixnet_params_storage::initialise_storage(deps.branch(), state, info.sender)?; - mixnode_storage::initialise_storage(deps.storage)?; - rewards_storage::initialise_storage(deps.storage, reward_params)?; + RewardingStorage::new().initialise(deps.storage, reward_params)?; + nymnodes_storage::initialise_storage(deps.storage)?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; set_build_information!(deps.storage)?; @@ -110,29 +109,12 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { + // state/sys-params-related ExecuteMsg::UpdateAdmin { admin } => { crate::mixnet_contract_settings::transactions::try_update_contract_admin( deps, info, admin, ) } - ExecuteMsg::AssignNodeLayer { mix_id, layer } => { - crate::mixnodes::transactions::assign_mixnode_layer(deps, info, mix_id, layer) - } - // families - ExecuteMsg::CreateFamily { label } => { - crate::families::transactions::try_create_family(deps, info, label) - } - ExecuteMsg::JoinFamily { - join_permit, - family_head, - } => crate::families::transactions::try_join_family(deps, info, join_permit, family_head), - ExecuteMsg::LeaveFamily { family_head } => { - crate::families::transactions::try_leave_family(deps, info, family_head) - } - ExecuteMsg::KickFamilyMember { member } => { - crate::families::transactions::try_head_kick_member(deps, info, member) - } - // state/sys-params-related ExecuteMsg::UpdateRewardingValidatorAddress { address } => { crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address( deps, info, address, @@ -145,14 +127,14 @@ pub fn execute( updated_parameters, ) } - ExecuteMsg::UpdateActiveSetSize { - active_set_size, + ExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, - } => crate::rewards::transactions::try_update_active_set_size( + } => crate::rewards::transactions::try_update_active_set_distribution( deps, env, info, - active_set_size, + update, force_immediately, ), ExecuteMsg::UpdateRewardingParams { @@ -180,17 +162,9 @@ pub fn execute( ExecuteMsg::BeginEpochTransition {} => { crate::interval::transactions::try_begin_epoch_transition(deps, env, info) } - ExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - // families_in_layer, - expected_active_set_size, - } => crate::interval::transactions::try_advance_epoch( - deps, - env, - info, - new_rewarded_set, - expected_active_set_size, - ), + ExecuteMsg::AssignRoles { assignment } => { + crate::interval::transactions::try_assign_roles(deps, env, info, assignment) + } ExecuteMsg::ReconcileEpochEvents { limit } => { crate::interval::transactions::try_reconcile_epoch_events(deps, env, info, limit) } @@ -208,23 +182,15 @@ pub fn execute( cost_params, owner_signature, ), - ExecuteMsg::PledgeMore {} => { - crate::mixnodes::transactions::try_increase_pledge(deps, env, info) - } - ExecuteMsg::DecreasePledge { decrease_by } => { - crate::mixnodes::transactions::try_decrease_pledge(deps, env, info, decrease_by) - } ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } - ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - crate::mixnodes::transactions::try_update_mixnode_cost_params( - deps, env, info, new_costs, - ) - } ExecuteMsg::UpdateMixnodeConfig { new_config } => { crate::mixnodes::transactions::try_update_mixnode_config(deps, info, new_config) } + ExecuteMsg::MigrateMixnode {} => { + crate::mixnodes::transactions::try_migrate_to_nymnode(deps, info) + } // gateway-related: ExecuteMsg::BondGateway { @@ -243,27 +209,60 @@ pub fn execute( ExecuteMsg::UpdateGatewayConfig { new_config } => { crate::gateways::transactions::try_update_gateway_config(deps, info, new_config) } + ExecuteMsg::MigrateGateway { cost_params } => { + crate::gateways::transactions::try_migrate_to_nymnode(deps, info, cost_params) + } + + // nym-node related: + ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + } => crate::nodes::transactions::try_add_nym_node( + deps, + env, + info, + node, + cost_params, + owner_signature, + ), + ExecuteMsg::UnbondNymNode {} => { + crate::nodes::transactions::try_remove_nym_node(deps, env, info) + } + ExecuteMsg::UpdateNodeConfig { update } => { + crate::nodes::transactions::try_update_node_config(deps, info, update) + } + + // nym-node/mixnode-related: + ExecuteMsg::PledgeMore {} => { + crate::compat::transactions::try_increase_pledge(deps, env, info) + } + ExecuteMsg::DecreasePledge { decrease_by } => { + crate::compat::transactions::try_decrease_pledge(deps, env, info, decrease_by) + } + ExecuteMsg::UpdateCostParams { new_costs } => { + crate::compat::transactions::try_update_cost_params(deps, env, info, new_costs) + } // delegation-related: - ExecuteMsg::DelegateToMixnode { mix_id } => { - crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id) + ExecuteMsg::Delegate { node_id } => { + crate::delegations::transactions::try_delegate_to_node(deps, env, info, node_id) } - ExecuteMsg::UndelegateFromMixnode { mix_id } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode( - deps, env, info, mix_id, + ExecuteMsg::Undelegate { node_id } => { + crate::delegations::transactions::try_remove_delegation_from_node( + deps, env, info, node_id, ) } // reward-related - ExecuteMsg::RewardMixnode { - mix_id, - performance, - } => crate::rewards::transactions::try_reward_mixnode(deps, env, info, mix_id, performance), + ExecuteMsg::RewardNode { node_id, params } => { + crate::rewards::transactions::try_reward_node(deps, env, info, node_id, params) + } ExecuteMsg::WithdrawOperatorReward {} => { - crate::rewards::transactions::try_withdraw_operator_reward(deps, info) + crate::compat::transactions::try_withdraw_operator_reward(deps, info) } - ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + ExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { crate::rewards::transactions::try_withdraw_delegator_reward(deps, info, mix_id) } @@ -276,11 +275,7 @@ pub fn execute( } // legacy vesting - ExecuteMsg::CreateFamilyOnBehalf { .. } - | ExecuteMsg::JoinFamilyOnBehalf { .. } - | ExecuteMsg::LeaveFamilyOnBehalf { .. } - | ExecuteMsg::KickFamilyMemberOnBehalf { .. } - | ExecuteMsg::BondMixnodeOnBehalf { .. } + ExecuteMsg::BondMixnodeOnBehalf { .. } | ExecuteMsg::PledgeMoreOnBehalf { .. } | ExecuteMsg::DecreasePledgeOnBehalf { .. } | ExecuteMsg::UnbondMixnodeOnBehalf { .. } @@ -301,6 +296,24 @@ pub fn execute( ExecuteMsg::TestingResolveAllPendingEvents { limit } => { crate::testing::transactions::try_resolve_all_pending_events(deps, env, limit) } + ExecuteMsg::TestingUncheckedBondLegacyMixnode { node } => { + legacy::save_new_mixnode( + deps.storage, + env, + node, + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(20).unwrap(), + interval_operating_cost: coin(40_000_000, "unym"), + }, + info.sender, + info.funds[0].clone(), + )?; + Ok(Response::default()) + } + ExecuteMsg::TestingUncheckedBondLegacyGateway { node } => { + legacy::save_new_gateway(deps.storage, env, node, info.sender, info.funds[0].clone())?; + Ok(Response::default()) + } } } @@ -311,24 +324,6 @@ pub fn query( msg: QueryMsg, ) -> Result { let query_res = match msg { - QueryMsg::GetAllFamiliesPaged { limit, start_after } => to_binary( - &crate::families::queries::get_all_families_paged(deps.storage, start_after, limit)?, - ), - QueryMsg::GetAllMembersPaged { limit, start_after } => to_binary( - &crate::families::queries::get_all_members_paged(deps.storage, start_after, limit)?, - ), - QueryMsg::GetFamilyByHead { head } => to_binary( - &crate::families::queries::get_family_by_head(&head, deps.storage)?, - ), - QueryMsg::GetFamilyByLabel { label } => to_binary( - &crate::families::queries::get_family_by_label(label, deps.storage)?, - ), - QueryMsg::GetFamilyMembersByHead { head } => to_binary( - &crate::families::queries::get_family_members_by_head(&head, deps.storage)?, - ), - QueryMsg::GetFamilyMembersByLabel { label } => to_binary( - &crate::families::queries::get_family_members_by_label(label, deps.storage)?, - ), QueryMsg::GetContractVersion {} => { to_binary(&crate::mixnet_contract_settings::queries::query_contract_version()) } @@ -354,9 +349,6 @@ pub fn query( QueryMsg::GetCurrentIntervalDetails {} => to_binary( &crate::interval::queries::query_current_interval_details(deps, env)?, ), - QueryMsg::GetRewardedSet { limit, start_after } => to_binary( - &crate::interval::queries::query_rewarded_set_paged(deps, start_after, limit)?, - ), // mixnode-related: QueryMsg::GetMixNodeBonds { start_after, limit } => to_binary( @@ -410,9 +402,6 @@ pub fn query( QueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => to_binary( &crate::mixnodes::queries::query_mixnode_details_by_identity(deps, mix_identity)?, ), - QueryMsg::GetLayerDistribution {} => { - to_binary(&crate::mixnodes::queries::query_layer_distribution(deps)?) - } // gateway-related: QueryMsg::GetGateways { limit, start_after } => to_binary( @@ -424,20 +413,80 @@ pub fn query( QueryMsg::GetOwnedGateway { address } => to_binary( &crate::gateways::queries::query_owned_gateway(deps, address)?, ), + QueryMsg::GetPreassignedGatewayIds { limit, start_after } => to_binary( + &crate::gateways::queries::query_preassigned_ids_paged(deps, start_after, limit)?, + ), - // delegation-related: - QueryMsg::GetMixnodeDelegations { - mix_id, - start_after, + // nym-node-related: + QueryMsg::GetNymNodeBondsPaged { start_after, limit } => to_binary( + &crate::nodes::queries::query_nymnode_bonds_paged(deps, start_after, limit)?, + ), + QueryMsg::GetNymNodesDetailedPaged { limit, start_after } => to_binary( + &crate::nodes::queries::query_nymnodes_details_paged(deps, start_after, limit)?, + ), + QueryMsg::GetUnbondedNymNode { node_id } => to_binary( + &crate::nodes::queries::query_unbonded_nymnode(deps, node_id)?, + ), + QueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => to_binary( + &crate::nodes::queries::query_unbonded_nymnodes_paged(deps, limit, start_after)?, + ), + QueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner, limit, + start_after, } => to_binary( - &crate::delegations::queries::query_mixnode_delegations_paged( + &crate::nodes::queries::query_unbonded_nymnodes_by_owner_paged( deps, - mix_id, - start_after, + owner, limit, + start_after, )?, ), + QueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key, + limit, + start_after, + } => to_binary( + &crate::nodes::queries::query_unbonded_nymnodes_by_identity_paged( + deps, + identity_key, + limit, + start_after, + )?, + ), + QueryMsg::GetOwnedNymNode { address } => { + to_binary(&crate::nodes::queries::query_owned_nymnode(deps, address)?) + } + QueryMsg::GetNymNodeDetails { node_id } => to_binary( + &crate::nodes::queries::query_nymnode_details(deps, node_id)?, + ), + QueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => to_binary( + &crate::nodes::queries::query_nymnode_details_by_identity(deps, node_identity)?, + ), + QueryMsg::GetNodeRewardingDetails { node_id } => to_binary( + &crate::nodes::queries::query_nymnode_rewarding_details(deps, node_id)?, + ), + QueryMsg::GetNodeStakeSaturation { node_id } => to_binary( + &crate::nodes::queries::query_stake_saturation(deps, node_id)?, + ), + QueryMsg::GetRoleAssignment { role } => { + to_binary(&crate::nodes::queries::query_epoch_assignment(deps, role)?) + } + QueryMsg::GetRewardedSetMetadata {} => { + to_binary(&crate::nodes::queries::query_rewarded_set_metadata(deps)?) + } + + // delegation-related: + QueryMsg::GetNodeDelegations { + node_id, + start_after, + limit, + } => to_binary(&crate::delegations::queries::query_node_delegations_paged( + deps, + node_id, + start_after, + limit, + )?), QueryMsg::GetDelegatorDelegations { delegator, start_after, @@ -451,11 +500,11 @@ pub fn query( )?, ), QueryMsg::GetDelegationDetails { - mix_id, + node_id, delegator, proxy, - } => to_binary(&crate::delegations::queries::query_mixnode_delegation( - deps, mix_id, delegator, proxy, + } => to_binary(&crate::delegations::queries::query_node_delegation( + deps, node_id, delegator, proxy, )?), QueryMsg::GetAllDelegations { start_after, limit } => to_binary( &crate::delegations::queries::query_all_delegations_paged(deps, start_after, limit)?, @@ -465,38 +514,40 @@ pub fn query( QueryMsg::GetPendingOperatorReward { address } => to_binary( &crate::rewards::queries::query_pending_operator_reward(deps, address)?, ), - QueryMsg::GetPendingMixNodeOperatorReward { mix_id } => to_binary( - &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, mix_id)?, + QueryMsg::GetPendingNodeOperatorReward { node_id } => to_binary( + &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, node_id)?, ), QueryMsg::GetPendingDelegatorReward { address, - mix_id, + node_id, proxy, } => to_binary(&crate::rewards::queries::query_pending_delegator_reward( - deps, address, mix_id, proxy, + deps, address, node_id, proxy, )?), QueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, } => to_binary( &crate::rewards::queries::query_estimated_current_epoch_operator_reward( deps, - mix_id, + node_id, estimated_performance, + estimated_work, )?, ), QueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, } => to_binary( &crate::rewards::queries::query_estimated_current_epoch_delegator_reward( deps, address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, )?, ), @@ -538,13 +589,19 @@ pub fn query( #[entry_point] pub fn migrate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, _env: Env, msg: MigrateMsg, ) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // remove all family-related things + crate::queued_migrations::families_purge(deps.branch())?; + + // prepare the ground for using nym-nodes rather than standalone mixnodes/gateways + migrate_to_nym_nodes_usage(deps.branch(), &msg)?; + // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh // environment, one of the contracts will HAVE TO go through a migration @@ -561,9 +618,12 @@ pub fn migrate( #[cfg(test)] mod tests { use super::*; + use crate::rewards::storage as rewards_storage; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::reward_params::{IntervalRewardParams, RewardingParams}; + use mixnet_contract_common::reward_params::{ + IntervalRewardParams, RewardedSetParams, RewardingParams, + }; use mixnet_contract_common::{InitialRewardingParams, Percent}; use std::time::Duration; @@ -585,8 +645,12 @@ mod tests { sybil_resistance: Percent::from_percentage_value(23).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), - rewarded_set_size: 543, - active_set_size: 123, + rewarded_set_params: RewardedSetParams { + entry_gateways: 123, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, }, profit_margin: ProfitMarginRange { minimum: "0.05".parse().unwrap(), @@ -609,14 +673,10 @@ mod tests { vesting_contract_address: Addr::unchecked("bar456"), rewarding_denom: "uatom".into(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: "uatom".into(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom: "uatom".into(), - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }, profit_margin: ProfitMarginRange { minimum: Percent::from_percentage_value(5).unwrap(), @@ -631,7 +691,7 @@ mod tests { let expected_epoch_reward_budget = Decimal::from_ratio(100_000_000_000_000u128, 1234u32) * Decimal::percent(1); - let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 543u32); + let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 313u32); let expected_rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -644,8 +704,12 @@ mod tests { active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), }, - rewarded_set_size: 543, - active_set_size: 123, + rewarded_set: RewardedSetParams { + entry_gateways: 123, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, }; let state = mixnet_params_storage::CONTRACT_STATE diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs index 3991711a06..e82496f1a9 100644 --- a/contracts/mixnet/src/delegations/helpers.rs +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -5,17 +5,17 @@ use crate::delegations::storage; use crate::rewards::storage as rewards_storage; use cosmwasm_std::{Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::MixNodeRewarding; +use mixnet_contract_common::mixnode::NodeRewarding; use mixnet_contract_common::Delegation; pub(crate) fn undelegate( store: &mut dyn Storage, delegation: Delegation, - mut mix_rewarding: MixNodeRewarding, + mut mix_rewarding: NodeRewarding, ) -> Result { let tokens = mix_rewarding.undelegate(&delegation)?; - rewards_storage::MIXNODE_REWARDING.save(store, delegation.mix_id, &mix_rewarding)?; + rewards_storage::MIXNODE_REWARDING.save(store, delegation.node_id, &mix_rewarding)?; storage::delegations().replace(store, delegation.storage_key(), None, Some(&delegation))?; Ok(tokens) @@ -32,16 +32,19 @@ mod tests { fn undelegation_updates_mix_rewarding_storage_and_deletes_delegation() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(100_000_000_000))); + let mix_id = + test.add_rewarded_set_nymnode_id("mix-owner", Some(Uint128::new(100_000_000_000))); let delegator = "delegator"; let og_amount = Uint128::new(200_000_000); test.add_immediate_delegation(delegator, og_amount, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); let mix_rewarding = test.mix_rewarding(mix_id); let delegation = test.delegation(mix_id, delegator, &None); diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 01e7b65648..fb46e8056f 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -2,38 +2,40 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::compat; use crate::constants::{ DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, }; -use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::Deps; use cosmwasm_std::Order; use cosmwasm_std::StdResult; use cw_storage_plus::Bound; -use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; +use mixnet_contract_common::delegation::{NodeDelegationResponse, OwnerProxySubKey}; use mixnet_contract_common::{ - delegation, Delegation, MixId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, + delegation, Delegation, NodeId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedNodeDelegationsResponse, }; -pub(crate) fn query_mixnode_delegations_paged( +pub(crate) fn query_node_delegations_paged( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, start_after: Option, limit: Option, -) -> StdResult { +) -> StdResult { let limit = limit .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; let start = start_after.map(|subkey| { - Bound::exclusive(Delegation::generate_storage_key_with_subkey(mix_id, subkey)) + Bound::exclusive(Delegation::generate_storage_key_with_subkey( + node_id, subkey, + )) }); let delegations = storage::delegations() .idx .mixnode - .prefix(mix_id) + .prefix(node_id) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| record.map(|r| r.1)) @@ -41,7 +43,7 @@ pub(crate) fn query_mixnode_delegations_paged( let start_next_after = delegations.last().map(|del| del.proxy_storage_key()); - Ok(PagedMixNodeDelegationsResponse::new( + Ok(PagedNodeDelegationsResponse::new( delegations, start_next_after, )) @@ -50,7 +52,7 @@ pub(crate) fn query_mixnode_delegations_paged( pub(crate) fn query_delegator_delegations_paged( deps: Deps<'_>, delegation_owner: String, - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, ) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; @@ -74,7 +76,7 @@ pub(crate) fn query_delegator_delegations_paged( let start_next_after = delegations .last() - .map(|del| (del.mix_id, del.proxy_storage_key())); + .map(|del| (del.node_id, del.proxy_storage_key())); Ok(PagedDelegatorDelegationsResponse::new( delegations, @@ -83,30 +85,26 @@ pub(crate) fn query_delegator_delegations_paged( } // queries for delegation value of given address for particular node -pub(crate) fn query_mixnode_delegation( +pub(crate) fn query_node_delegation( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, delegation_owner: String, proxy: Option, -) -> StdResult { +) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; let validated_proxy = proxy .map(|proxy| deps.api.addr_validate(&proxy)) .transpose()?; let storage_key = - Delegation::generate_storage_key(mix_id, &validated_owner, validated_proxy.as_ref()); + Delegation::generate_storage_key(node_id, &validated_owner, validated_proxy.as_ref()); let delegation = storage::delegations().may_load(deps.storage, storage_key)?; - let mixnode_still_bonded = mixnodes_storage::mixnode_bonds() - .may_load(deps.storage, mix_id)? - .map(|bond| !bond.is_unbonding) + let node_still_bonded = compat::helpers::may_get_bond(deps.storage, node_id)? + .map(|bond| !bond.is_unbonding()) .unwrap_or_default(); - Ok(MixNodeDelegationResponse::new( - delegation, - mixnode_still_bonded, - )) + Ok(NodeDelegationResponse::new(delegation, node_still_bonded)) } pub(crate) fn query_all_delegations_paged( @@ -141,7 +139,7 @@ mod tests { fn add_dummy_mixes_with_delegations(test: &mut TestSetup, delegators: usize, mixes: usize) { for i in 0..mixes { - let mix_id = test.add_dummy_mixnode(&format!("mix-owner{}", i), None); + let mix_id = test.add_legacy_mixnode(&format!("mix-owner{}", i), None); for delegator in 0..delegators { let name = &format!("delegator{}", delegator); test.add_immediate_delegation(name, 100_000_000u32, mix_id) @@ -157,7 +155,7 @@ mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 200); @@ -165,20 +163,20 @@ mod tests { let limit = 2; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); assert_eq!(limit, page1.delegations.len() as u32); } #[test] fn has_default_limit() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 500); // query without explicitly setting a limit - let page1 = query_mixnode_delegations_paged(test.deps(), mix_id, None, None).unwrap(); + let page1 = query_node_delegations_paged(test.deps(), mix_id, None, None).unwrap(); assert_eq!( DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, @@ -189,7 +187,7 @@ mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 5000); @@ -197,8 +195,7 @@ mod tests { // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 10000; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)) - .unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)).unwrap(); assert_eq!( DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, @@ -210,12 +207,12 @@ mod tests { fn pagination_works() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); test.add_immediate_delegation("addr1", 1000u32, mix_id); let per_page = 2; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.delegations.len()); @@ -225,20 +222,20 @@ mod tests { // page1 should have 2 results on it let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); assert_eq!(2, page1.delegations.len()); test.add_immediate_delegation("addr3", 1000u32, mix_id); // page1 still has the same 2 results let another_page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); assert_eq!(2, another_page1.delegations.len()); assert_eq!(page1, another_page1); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); - let page2 = query_mixnode_delegations_paged( + let page2 = query_node_delegations_paged( test.deps(), mix_id, Some(start_after.clone()), @@ -251,7 +248,7 @@ mod tests { // save another one test.add_immediate_delegation("addr4", 1000u32, mix_id); - let page2 = query_mixnode_delegations_paged( + let page2 = query_node_delegations_paged( test.deps(), mix_id, Some(start_after), @@ -266,10 +263,10 @@ mod tests { #[test] fn all_retrieved_delegations_are_towards_specified_mixnode() { let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); - let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); + let mix_id3 = test.add_legacy_mixnode("mix-owner3", None); + let mix_id4 = test.add_legacy_mixnode("mix-owner4", None); let env = test.env(); // add other "out of order" delegations manually @@ -282,21 +279,21 @@ mod tests { test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id4, 10); test.add_immediate_delegation("random-delegator4", 1000u32, mix_id2); - let res1 = query_mixnode_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); + let res1 = query_node_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); assert_eq!(res1.delegations.len(), 10); - assert!(res1.delegations.into_iter().all(|d| d.mix_id == mix_id1)); + assert!(res1.delegations.into_iter().all(|d| d.node_id == mix_id1)); - let res2 = query_mixnode_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); + let res2 = query_node_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); assert_eq!(res2.delegations.len(), 14); - assert!(res2.delegations.into_iter().all(|d| d.mix_id == mix_id2)); + assert!(res2.delegations.into_iter().all(|d| d.node_id == mix_id2)); - let res3 = query_mixnode_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); + let res3 = query_node_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); assert_eq!(res3.delegations.len(), 10); - assert!(res3.delegations.into_iter().all(|d| d.mix_id == mix_id3)); + assert!(res3.delegations.into_iter().all(|d| d.node_id == mix_id3)); - let res4 = query_mixnode_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); + let res4 = query_node_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); assert_eq!(res4.delegations.len(), 10); - assert!(res4.delegations.into_iter().all(|d| d.mix_id == mix_id4)); + assert!(res4.delegations.into_iter().all(|d| d.node_id == mix_id4)); } } @@ -365,11 +362,11 @@ mod tests { let mut test = TestSetup::new(); // note that mix_ids are monotonically increasing - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); - let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); - let mix_id5 = test.add_dummy_mixnode("mix-owner5", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); + let mix_id3 = test.add_legacy_mixnode("mix-owner3", None); + let mix_id4 = test.add_legacy_mixnode("mix-owner4", None); + let mix_id5 = test.add_legacy_mixnode("mix-owner5", None); // add few delegations from unrelated delegators for mix_id in [mix_id1, mix_id2, mix_id3, mix_id4, mix_id5] { @@ -526,8 +523,8 @@ mod tests { // note that mix_ids are monotonically increasing and are the first chunk of all // delegation storage keys, - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -541,7 +538,7 @@ mod tests { assert_eq!(1, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].mix_id == mix_id1 + && page1.delegations[0].node_id == mix_id1 ); test.add_immediate_delegation(delegator1, 1000u32, mix_id2); @@ -552,11 +549,11 @@ mod tests { assert_eq!(2, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].mix_id == mix_id1 + && page1.delegations[0].node_id == mix_id1 ); assert!( page1.delegations[1].owner.as_str() == delegator1 - && page1.delegations[1].mix_id == mix_id2 + && page1.delegations[1].node_id == mix_id2 ); test.add_immediate_delegation(delegator2, 1000u32, mix_id1); @@ -567,11 +564,11 @@ mod tests { assert_eq!(2, another_page1.delegations.len()); assert!( another_page1.delegations[0].owner.as_str() == delegator1 - && another_page1.delegations[0].mix_id == mix_id1 + && another_page1.delegations[0].node_id == mix_id1 ); assert!( another_page1.delegations[1].owner.as_str() == delegator2 - && another_page1.delegations[1].mix_id == mix_id1 + && another_page1.delegations[1].node_id == mix_id1 ); // retrieving the next page should start after the last key on this page @@ -583,7 +580,7 @@ mod tests { assert_eq!(1, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].mix_id == mix_id2 + && page2.delegations[0].node_id == mix_id2 ); // save another one @@ -596,72 +593,145 @@ mod tests { assert_eq!(2, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].mix_id == mix_id2 + && page2.delegations[0].node_id == mix_id2 ); assert!( page2.delegations[1].owner.as_str() == delegator2 - && page2.delegations[1].mix_id == mix_id2 + && page2.delegations[1].node_id == mix_id2 ); } } #[cfg(test)] - mod querying_for_specific_mixnode_delegation { + mod querying_for_specific_node_delegation { use super::*; - #[test] - fn when_delegation_doesnt_exist() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[cfg(test)] + mod legacy_mixnodes { + use super::*; - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert!(res.delegation.is_none()); - assert!(res.mixnode_still_bonded); + #[allow(deprecated)] + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.mixnode_still_bonded); + assert!(res.node_still_bonded); + } + + #[allow(deprecated)] + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.immediately_unbond_mixnode(mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + assert!(!res.node_still_bonded); + } + + #[allow(deprecated)] + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.start_unbonding_mixnode(mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + assert!(!res.node_still_bonded); + } + + #[allow(deprecated)] + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.mixnode_still_bonded); + assert!(res.node_still_bonded); + } } - #[test] - fn when_delegation_exists_but_mixnode_has_unbonded() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[cfg(test)] + mod nym_nodes { + use super::*; - test.add_immediate_delegation(owner, 1000u32, mix_id); - test.immediately_unbond_mixnode(mix_id); + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(!res.mixnode_still_bonded); - } + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.node_still_bonded); + } - #[test] - fn when_delegation_exists_but_mixnode_is_unbonding() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - test.add_immediate_delegation(owner, 1000u32, mix_id); - test.start_unbonding_mixnode(mix_id); + test.add_immediate_delegation(owner, 1000u32, node_id); + test.immediately_unbond_nymnode(node_id); - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(!res.mixnode_still_bonded); - } + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.node_still_bonded); + } - #[test] - fn when_delegation_exists_with_fully_bonded_node() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - test.add_immediate_delegation(owner, 1000u32, mix_id); + test.add_immediate_delegation(owner, 1000u32, node_id); + test.start_unbonding_nymnode(node_id); - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(res.mixnode_still_bonded); + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.node_still_bonded); + } + + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, node_id); + + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.node_still_bonded); + } } } } diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 5d3e6b45f2..3fa2931f64 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -6,15 +6,15 @@ use crate::constants::{ }; use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; use mixnet_contract_common::delegation::OwnerProxySubKey; -use mixnet_contract_common::{Addr, Delegation, MixId}; +use mixnet_contract_common::{Addr, Delegation, NodeId}; // It's a composite key on node's id and delegator address -type PrimaryKey = (MixId, OwnerProxySubKey); +type PrimaryKey = (NodeId, OwnerProxySubKey); pub(crate) struct DelegationIndex<'a> { pub(crate) owner: MultiIndex<'a, Addr, Delegation, PrimaryKey>, - pub(crate) mixnode: MultiIndex<'a, MixId, Delegation, PrimaryKey>, + pub(crate) mixnode: MultiIndex<'a, NodeId, Delegation, PrimaryKey>, } impl<'a> IndexList for DelegationIndex<'a> { @@ -32,7 +32,7 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega DELEGATION_OWNER_IDX_NAMESPACE, ), mixnode: MultiIndex::new( - |_pk, d| d.mix_id, + |_pk, d| d.node_id, DELEGATION_PK_NAMESPACE, DELEGATION_MIXNODE_IDX_NAMESPACE, ), diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index b91c6c8582..714afe33f0 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -4,21 +4,22 @@ use super::storage; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::storage as mixnodes_storage; -use crate::support::helpers::{ensure_epoch_in_progress_state, validate_delegation_stake}; +use crate::support::helpers::{ + ensure_any_node_bonded, ensure_epoch_in_progress_state, validate_delegation_stake, +}; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_pending_delegation_event, new_pending_undelegation_event, }; use mixnet_contract_common::pending_events::PendingEpochEventKind; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; -pub(crate) fn try_delegate_to_mixnode( +pub(crate) fn try_delegate_to_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // delegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; @@ -27,18 +28,12 @@ pub(crate) fn try_delegate_to_mixnode( let contract_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; let delegation = validate_delegation_stake( info.funds, - contract_state.params.minimum_mixnode_delegation, + contract_state.params.minimum_delegation, contract_state.rewarding_denom, )?; // check if the target node actually exists and is still bonded - match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { - None => return Err(MixnetContractError::MixNodeBondNotFound { mix_id }), - Some(bond) if bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }) - } - _ => (), - } + ensure_any_node_bonded(deps.storage, mix_id)?; // push the event onto the queue and wait for it to be picked up at the end of the epoch let cosmos_event = new_pending_delegation_event(&info.sender, &delegation, mix_id); @@ -49,33 +44,33 @@ pub(crate) fn try_delegate_to_mixnode( Ok(Response::new().add_event(cosmos_event)) } -pub(crate) fn try_remove_delegation_from_mixnode( +pub(crate) fn try_remove_delegation_from_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, + node_id: NodeId, ) -> Result { // undelegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + let storage_key = Delegation::generate_storage_key(node_id, &info.sender, None); if storage::delegations() .may_load(deps.storage, storage_key)? .is_none() { - return Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + return Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: info.sender.into_string(), proxy: None, }); } // push the event onto the queue and wait for it to be picked up at the end of the epoch - let cosmos_event = new_pending_undelegation_event(&info.sender, mix_id); + let cosmos_event = new_pending_undelegation_event(&info.sender, node_id); - let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, mix_id); + let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, node_id); interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) @@ -94,6 +89,7 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; use cosmwasm_std::{coin, Addr, Decimal}; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -104,7 +100,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -118,10 +116,10 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender, mix_id); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -136,10 +134,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender, 42); + let res = try_delegate_to_node(test.deps_mut(), env, sender, 42); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { mix_id: 42 }) + Err(MixnetContractError::NymNodeBondNotFound { node_id: 42 }) ) } @@ -149,16 +147,16 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(0, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); let sender3 = mock_info(owner, &[coin(1000, "some-weird-coin")]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender2, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender3, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender3, mix_id); assert_eq!( res, Err(MixnetContractError::WrongDenom { @@ -174,7 +172,7 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[coin(150_000_000, TEST_COIN_DENOM)]); @@ -182,12 +180,12 @@ mod tests { let mut contract_state = mixnet_params_storage::CONTRACT_STATE .load(test.deps().storage) .unwrap(); - contract_state.params.minimum_mixnode_delegation = Some(min_delegation.clone()); + contract_state.params.minimum_delegation = Some(min_delegation.clone()); mixnet_params_storage::CONTRACT_STATE .save(test.deps_mut().storage, &contract_state) .unwrap(); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!( res, Err(MixnetContractError::InsufficientDelegation { @@ -196,7 +194,7 @@ mod tests { }) ); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } @@ -207,10 +205,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); - let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); + let mix_id_unbonding = test.add_legacy_mixnode("mix-owner-unbonding", None); + let mix_id_unbonded = test.add_legacy_mixnode("mix-owner-unbonded", None); let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.add_legacy_mixnode("mix-owner-unbonded-leftover", None); // manually adjust delegation info as to indicate the rewarding information shouldnt get removed let mut rewarding_details = rewards_storage::MIXNODE_REWARDING @@ -247,7 +245,7 @@ mod tests { ) .unwrap(); - let res = try_delegate_to_mixnode( + let res = try_delegate_to_node( test.deps_mut(), env.clone(), sender.clone(), @@ -260,7 +258,7 @@ mod tests { }) ); - let res = try_delegate_to_mixnode( + let res = try_delegate_to_node( test.deps_mut(), env.clone(), sender.clone(), @@ -268,17 +266,16 @@ mod tests { ); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { - mix_id: mix_id_unbonded + Err(MixnetContractError::NymNodeBondNotFound { + node_id: mix_id_unbonded }) ); - let res = - try_delegate_to_mixnode(test.deps_mut(), env, sender, mix_id_unbonded_leftover); + let res = try_delegate_to_node(test.deps_mut(), env, sender, mix_id_unbonded_leftover); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { - mix_id: mix_id_unbonded_leftover + Err(MixnetContractError::NymNodeBondNotFound { + node_id: mix_id_unbonded_leftover }) ); } @@ -289,15 +286,15 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert!(res.is_ok()); // still OK - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } @@ -307,13 +304,13 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let amount1 = coin(100_000_000, TEST_COIN_DENOM); let sender1 = mock_info(owner, &[amount1.clone()]); - try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); + try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); let events = test.pending_epoch_events(); @@ -332,6 +329,7 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::coin; use cosmwasm_std::testing::mock_info; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -342,12 +340,14 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("owner", None); + let mix_id = test.add_legacy_mixnode("owner", None); test.add_immediate_delegation("foomp", 1000u32, mix_id); let mut status = EpochStatus::new(test.rewarding_validator().sender); @@ -356,7 +356,7 @@ mod tests { .unwrap(); let env = test.env(); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), mock_info("sender", &[]), @@ -375,13 +375,13 @@ mod tests { let env = test.env(); let owner = "delegator"; let sender = mock_info(owner, &[]); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_legacy_mixnode("mix-owner", None); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender, mix_id); + let res = try_remove_delegation_from_node(test.deps_mut(), env, sender, node_id); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: owner.to_string(), proxy: None }) @@ -394,17 +394,17 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); - try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); + try_delegate_to_node(test.deps_mut(), env.clone(), sender1, node_id).unwrap(); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_remove_delegation_from_node(test.deps_mut(), env, sender2, node_id); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: owner.to_string(), proxy: None }) @@ -419,10 +419,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[]); - let normal_mix_id = test.add_dummy_mixnode("mix-owner", None); - let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); + let normal_mix_id = test.add_legacy_mixnode("mix-owner", None); + let mix_id_unbonding = test.add_legacy_mixnode("mix-owner-unbonding", None); let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.add_legacy_mixnode("mix-owner-unbonded-leftover", None); test.add_immediate_delegation(owner, 10000u32, normal_mix_id); test.add_immediate_delegation(owner, 10000u32, mix_id_unbonding); @@ -443,7 +443,7 @@ mod tests { ) .unwrap(); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), sender.clone(), @@ -451,7 +451,7 @@ mod tests { ); assert!(res.is_ok()); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), sender.clone(), @@ -459,7 +459,7 @@ mod tests { ); assert!(res.is_ok()); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env, sender, diff --git a/contracts/mixnet/src/families/mod.rs b/contracts/mixnet/src/families/mod.rs deleted file mode 100644 index f3c80cce37..0000000000 --- a/contracts/mixnet/src/families/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -pub mod queries; -pub mod signature_helpers; -pub mod storage; -pub mod transactions; diff --git a/contracts/mixnet/src/families/queries.rs b/contracts/mixnet/src/families/queries.rs deleted file mode 100644 index 96c7f79e23..0000000000 --- a/contracts/mixnet/src/families/queries.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{families, get_members, must_get_family, MEMBERS}; -use crate::constants::{FAMILIES_DEFAULT_RETRIEVAL_LIMIT, FAMILIES_MAX_RETRIEVAL_LIMIT}; -use crate::families::storage::must_get_family_by_label; -use cosmwasm_std::{Order, Storage}; -use cw_storage_plus::Bound; -use mixnet_contract_common::families::{ - Family, FamilyByHeadResponse, FamilyByLabelResponse, FamilyHead, FamilyMembersByHeadResponse, - PagedFamiliesResponse, PagedMembersResponse, -}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKeyRef}; -use mixnet_contract_common::{FamilyMembersByLabelResponse, IdentityKey}; - -pub fn get_family_by_label( - label: String, - storage: &dyn Storage, -) -> Result { - let family = families() - .idx - .label - .item(storage, label.clone())? - .map(|o| o.1); - Ok(FamilyByLabelResponse { label, family }) -} - -pub fn get_family_by_head( - head: IdentityKeyRef<'_>, - storage: &dyn Storage, -) -> Result { - let family = families().may_load(storage, head.to_string())?; - Ok(FamilyByHeadResponse { - head: FamilyHead::new(head), - family, - }) -} - -// TODO: this should be returning a paged response! -pub fn get_family_members_by_head( - head: IdentityKeyRef<'_>, - storage: &dyn Storage, -) -> Result { - let family_head = FamilyHead::new(head); - let family = must_get_family(&family_head, storage)?; - let members = get_members(&family, storage)?; - - Ok(FamilyMembersByHeadResponse { - head: family.head().to_owned(), - members, - }) -} - -// TODO: this should be returning a paged response! -pub fn get_family_members_by_label( - label: String, - storage: &dyn Storage, -) -> Result { - let family = must_get_family_by_label(label.clone(), storage)?; - let members = get_members(&family, storage)?; - - Ok(FamilyMembersByLabelResponse { label, members }) -} - -pub fn get_all_families_paged( - storage: &dyn Storage, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit - .unwrap_or(FAMILIES_DEFAULT_RETRIEVAL_LIMIT) - .min(FAMILIES_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let response = families() - .range(storage, start, None, Order::Ascending) - .take(limit) - .filter_map(|r| r.ok()) - .map(|(_key, family)| family) - .collect::>(); - - let start_next_after = response - .last() - .map(|response| response.head_identity().to_string()); - - Ok(PagedFamiliesResponse { - families: response, - start_next_after, - }) -} - -pub fn get_all_members_paged( - storage: &dyn Storage, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit - .unwrap_or(FAMILIES_DEFAULT_RETRIEVAL_LIMIT) - .min(FAMILIES_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let response = MEMBERS - .range(storage, start, None, Order::Ascending) - .take(limit) - .filter_map(|r| r.ok()) - .collect::>(); - - let start_next_after = response.last().map(|r| r.0.clone()); - - Ok(PagedMembersResponse { - members: response, - start_next_after, - }) -} diff --git a/contracts/mixnet/src/families/signature_helpers.rs b/contracts/mixnet/src/families/signature_helpers.rs deleted file mode 100644 index 3c6bb79626..0000000000 --- a/contracts/mixnet/src/families/signature_helpers.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::mixnodes::storage as mixnodes_storage; -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::Deps; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::{construct_family_join_permit, IdentityKeyRef}; -use nym_contracts_common::signing::{MessageSignature, Verifier}; - -pub(crate) fn verify_family_join_permit( - deps: Deps<'_>, - granter: FamilyHead, - member: IdentityKeyRef, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(granter.identity())?; - - // that's kinda a backwards way of getting the granter's nonce, but it works, so ¯\_(ツ)_/¯ - let Some(head_mixnode) = mixnodes_storage::mixnode_bonds() - .idx - .identity_key - .item(deps.storage, granter.identity().to_owned())? - .map(|record| record.1) - else { - return Err(MixnetContractError::FamilyDoesNotExist { - head: granter.identity().to_string(), - }); - }; - let nonce = signing_storage::get_signing_nonce(deps.storage, head_mixnode.owner)?; - let msg = construct_family_join_permit(nonce, granter, member.to_owned()); - - if deps.api.verify_message(msg, signature, &public_key)? { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } -} diff --git a/contracts/mixnet/src/families/storage.rs b/contracts/mixnet/src/families/storage.rs deleted file mode 100644 index 05b9566b7d..0000000000 --- a/contracts/mixnet/src/families/storage.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{Order, Storage}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; -use mixnet_contract_common::families::{Family, FamilyHead}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey, IdentityKeyRef}; - -use crate::constants::{FAMILIES_INDEX_NAMESPACE, FAMILIES_MAP_NAMESPACE, MEMBERS_MAP_NAMESPACE}; - -type FamilyHeadKey = IdentityKey; - -pub struct FamilyIndex<'a> { - pub label: UniqueIndex<'a, FamilyHeadKey, Family>, -} - -impl<'a> IndexList for FamilyIndex<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.label]; - Box::new(v.into_iter()) - } -} - -// storage access function. -pub fn families<'a>() -> IndexedMap<'a, FamilyHeadKey, Family, FamilyIndex<'a>> { - let indexes = FamilyIndex { - label: UniqueIndex::new(|d| d.label().to_string(), FAMILIES_INDEX_NAMESPACE), - }; - IndexedMap::new(FAMILIES_MAP_NAMESPACE, indexes) -} - -pub const MEMBERS: Map = Map::new(MEMBERS_MAP_NAMESPACE); - -// TODO: this introduces an unbounded query. We should redesign it. -pub fn get_members( - family: &Family, - store: &dyn Storage, -) -> Result, MixnetContractError> { - Ok(MEMBERS - .range(store, None, None, Order::Ascending) - .filter_map(|res| res.ok()) - .filter(|(_member, head)| head == family.head()) - .map(|(member, _storage_key)| member) - .collect()) -} - -pub fn must_get_family( - head: &FamilyHead, - store: &dyn Storage, -) -> Result { - let key = head.identity(); - - families() - .may_load(store, key.to_string())? - .ok_or(MixnetContractError::FamilyDoesNotExist { - head: head.identity().to_string(), - }) -} - -pub fn must_get_family_by_label( - label: String, - store: &dyn Storage, -) -> Result { - families() - .idx - .label - .item(store, label.clone())? - .map(|record| record.1) - .ok_or(MixnetContractError::FamilyLabelDoesNotExist { label }) -} - -pub fn save_family(f: &Family, store: &mut dyn Storage) -> Result<(), MixnetContractError> { - Ok(families().save(store, f.head_identity().to_string(), f)?) -} - -pub fn add_family_member( - f: &Family, - store: &mut dyn Storage, - member: IdentityKeyRef<'_>, -) -> Result<(), MixnetContractError> { - Ok(MEMBERS.save(store, member.to_string(), f.head())?) -} - -pub fn remove_family_member(store: &mut dyn Storage, member: IdentityKeyRef<'_>) { - MEMBERS.remove(store, member.to_string()) -} - -pub fn is_family_member( - store: &dyn Storage, - f: &Family, - member: IdentityKeyRef<'_>, -) -> Result { - let existing_head = MEMBERS.may_load(store, member.to_owned())?; - Ok(existing_head.as_ref() == Some(f.head())) -} - -pub fn is_any_member( - store: &dyn Storage, - member: IdentityKeyRef<'_>, -) -> Result, MixnetContractError> { - Ok(MEMBERS.may_load(store, member.to_string())?) -} diff --git a/contracts/mixnet/src/families/transactions.rs b/contracts/mixnet/src/families/transactions.rs deleted file mode 100644 index ee88f98e63..0000000000 --- a/contracts/mixnet/src/families/transactions.rs +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{ - add_family_member, is_any_member, is_family_member, must_get_family, remove_family_member, - save_family, -}; -use crate::families::queries::get_family_by_label; -use crate::families::signature_helpers::verify_family_join_permit; -use crate::support::helpers::ensure_bonded; -use cosmwasm_std::{DepsMut, MessageInfo, Response}; -use mixnet_contract_common::families::{Family, FamilyHead}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; -use nym_contracts_common::signing::MessageSignature; - -/// Creates a new MixNode family with senders node as head -pub(crate) fn try_create_family( - deps: DepsMut, - info: MessageInfo, - label: String, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - let family_head = FamilyHead::new(existing_bond.identity()); - - // can't overwrite existing family - if must_get_family(&family_head, deps.storage).is_ok() { - return Err(MixnetContractError::FamilyCanHaveOnlyOne); - } - - // the label must be unique - if get_family_by_label(label.clone(), deps.storage)? - .family - .is_some() - { - return Err(MixnetContractError::FamilyWithLabelExists(label)); - } - - let family = Family::new(family_head, label); - save_family(&family, deps.storage)?; - Ok(Response::default()) -} - -pub(crate) fn try_join_family( - deps: DepsMut, - info: MessageInfo, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - if family_head.identity() == existing_bond.identity() { - return Err(MixnetContractError::CantJoinOwnFamily { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - if let Some(family) = is_any_member(deps.storage, existing_bond.identity())? { - return Err(MixnetContractError::AlreadyMemberOfFamily( - family.identity().to_string(), - )); - } - - verify_family_join_permit( - deps.as_ref(), - family_head.clone(), - existing_bond.identity(), - join_permit, - )?; - - let family = must_get_family(&family_head, deps.storage)?; - - add_family_member(&family, deps.storage, existing_bond.identity())?; - - Ok(Response::default()) -} - -pub(crate) fn try_leave_family( - deps: DepsMut, - info: MessageInfo, - family_head: FamilyHead, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - if family_head.identity() == existing_bond.identity() { - return Err(MixnetContractError::CantLeaveOwnFamily { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - let family = must_get_family(&family_head, deps.storage)?; - if !is_family_member(deps.storage, &family, existing_bond.identity())? { - return Err(MixnetContractError::NotAMember { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - remove_family_member(deps.storage, existing_bond.identity()); - - Ok(Response::default()) -} - -pub(crate) fn try_head_kick_member( - deps: DepsMut, - info: MessageInfo, - member: IdentityKey, -) -> Result { - let head_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - // make sure we're still in the mixnet - ensure_bonded(&head_bond)?; - - // make sure we're not trying to kick ourselves... - if member == head_bond.identity() { - return Err(MixnetContractError::CantLeaveOwnFamily { - head: head_bond.identity().to_string(), - member, - }); - } - - // get the family details - let family_head = FamilyHead::new(head_bond.identity()); - let family = must_get_family(&family_head, deps.storage)?; - - // make sure the member we're trying to kick is an actual member - if !is_family_member(deps.storage, &family, &member)? { - return Err(MixnetContractError::NotAMember { - head: family_head.identity().to_string(), - member, - }); - } - - // finally get rid of the member - remove_family_member(deps.storage, &member); - Ok(Response::default()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::families::queries::get_family_by_head; - use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; - use crate::support::tests::fixtures; - use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::mock_info; - - #[test] - fn test_family_crud() { - let mut test = TestSetup::new(); - let env = test.env(); - - let head = "alice"; - let malicious_head = "timmy"; - let member = "bob"; - - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); - let cost_params = fixtures::mix_node_cost_params_fixture(); - - let (head_mixnode, head_bond_sig, head_keypair) = test.mixnode_with_signature(head, None); - let (malicious_mixnode, malicious_bond_sig, _malicious_keypair) = - test.mixnode_with_signature(malicious_head, None); - let (member_mixnode, member_bond_sig, _member_keypair) = - test.mixnode_with_signature(member, None); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env.clone(), - mock_info(head, &[minimum_pledge.clone()]), - head_mixnode.clone(), - cost_params.clone(), - head_bond_sig, - ) - .unwrap(); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env.clone(), - mock_info(malicious_head, &[minimum_pledge.clone()]), - malicious_mixnode, - cost_params.clone(), - malicious_bond_sig, - ) - .unwrap(); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env, - mock_info(member, &[minimum_pledge]), - member_mixnode.clone(), - cost_params, - member_bond_sig, - ) - .unwrap(); - - try_create_family(test.deps_mut(), mock_info(head, &[]), "test".to_string()).unwrap(); - let family_head = FamilyHead::new(&head_mixnode.identity_key); - assert!(must_get_family(&family_head, test.deps().storage).is_ok()); - - let nope = try_create_family( - test.deps_mut(), - mock_info(malicious_head, &[]), - "test".to_string(), - ); - - match nope { - Ok(_) => panic!("This should fail, since family with label already exists"), - Err(e) => match e { - MixnetContractError::FamilyWithLabelExists(label) => assert_eq!(label, "test"), - _ => panic!("This should return FamilyWithLabelExists"), - }, - } - - let family = get_family_by_label("test".to_string(), test.deps().storage) - .unwrap() - .family; - assert!(family.is_some()); - assert_eq!(family.unwrap().head_identity(), family_head.identity()); - - let family = get_family_by_head(family_head.identity(), test.deps().storage) - .unwrap() - .family - .unwrap(); - assert_eq!(family.head_identity(), family_head.identity()); - - let join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); - - try_join_family( - test.deps_mut(), - mock_info(member, &[]), - join_permit, - family_head.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - - assert!( - is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - try_leave_family(test.deps_mut(), mock_info(member, &[]), family_head.clone()).unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - assert!( - !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - let new_join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); - - try_join_family( - test.deps_mut(), - mock_info(member, &[]), - new_join_permit, - family_head.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - - assert!( - is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - try_head_kick_member( - test.deps_mut(), - mock_info(head, &[]), - member_mixnode.identity_key.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - assert!( - !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/gateways/mod.rs b/contracts/mixnet/src/gateways/mod.rs index ebff4a6ace..f3a3c9941f 100644 --- a/contracts/mixnet/src/gateways/mod.rs +++ b/contracts/mixnet/src/gateways/mod.rs @@ -3,6 +3,5 @@ pub mod helpers; pub mod queries; -pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index bfd7eb0c4d..1cb19655e4 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -5,6 +5,7 @@ use super::storage; use crate::constants::{GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, GATEWAY_BOND_MAX_RETRIEVAL_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; +use mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; use mixnet_contract_common::{ GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse, }; @@ -56,6 +57,29 @@ pub fn query_gateway_bond(deps: Deps<'_>, identity: IdentityKey) -> StdResult, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(50).min(100) as usize; + + let start = start_after.as_deref().map(Bound::exclusive); + + let ids = storage::PREASSIGNED_LEGACY_IDS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(identity, node_id)| PreassignedId { identity, node_id })) + .collect::>>()?; + + let start_next_after = ids.last().map(|id| id.identity.clone()); + + Ok(PreassignedGatewayIdsResponse { + ids, + start_next_after, + }) +} + #[cfg(test)] pub(crate) mod tests { use super::*; @@ -113,61 +137,44 @@ pub(crate) mod tests { #[test] fn gateway_pagination_works() { - let mut deps = test_helpers::init_contract(); - let mut rng = test_helpers::test_rng(); let stake = tests::fixtures::good_gateway_pledge(); + let mut test = TestSetup::new(); - // prepare 4 messages and identities that are sorted by the generated identities + // prepare 4 gateways that are sorted by the generated identities // (because we query them in an ascended manner) - let mut exec_data = (0..4) - .map(|i| { - let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_gateway_msg( - &mut rng, - deps.as_ref(), - stake.clone(), - &sender, - ); - (msg, (sender, identity)) - }) + let mut gateways = (0..4) + .map(|i| test.gateway_with_signature(format!("sender{}", i), None).0) .collect::>(); - exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); - let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); + gateways.sort_by(|g1, g2| g1.identity_key.cmp(&g2.identity_key)); - let info = mock_info(&sender_identities[0].0.clone(), &stake); - execute(deps.as_mut(), mock_env(), info, messages[0].clone()).unwrap(); + let info = mock_info("sender0", &stake); + test.save_legacy_gateway(gateways[0].clone(), &info); let per_page = 2; - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.nodes.len()); // save another - let info = mock_info( - &sender_identities[1].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[1].clone()).unwrap(); + let info = mock_info("sender1", &stake); + test.save_legacy_gateway(gateways[1].clone(), &info); // page1 should have 2 results on it - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - let info = mock_info( - &sender_identities[2].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[2].clone()).unwrap(); + let info = mock_info("sender2", &stake); + test.save_legacy_gateway(gateways[2].clone(), &info); // page1 still has 2 results - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); let page2 = query_gateways_paged( - deps.as_ref(), + test.deps(), Option::from(start_after.clone()), Option::from(per_page), ) @@ -176,14 +183,11 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - let info = mock_info( - &sender_identities[3].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[3].clone()).unwrap(); + let info = mock_info("sender3", &stake); + test.save_legacy_gateway(gateways[3].clone(), &info); let page2 = query_gateways_paged( - deps.as_ref(), + test.deps(), Option::from(start_after), Option::from(per_page), ) @@ -202,13 +206,13 @@ pub(crate) mod tests { assert!(res.gateway.is_none()); // gateway was added to "bob", "fred" still does not own one - test.add_dummy_gateway("bob", None); + test.add_legacy_gateway("bob", None); let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // "fred" now owns a gateway! - test.add_dummy_gateway("fred", None); + test.add_legacy_gateway("fred", None); let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_some()); diff --git a/contracts/mixnet/src/gateways/signature_helpers.rs b/contracts/mixnet/src/gateways/signature_helpers.rs deleted file mode 100644 index 6f9b712554..0000000000 --- a/contracts/mixnet/src/gateways/signature_helpers.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Coin, Deps}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ - construct_gateway_bonding_sign_payload, construct_legacy_gateway_bonding_sign_payload, Gateway, -}; -use nym_contracts_common::signing::MessageSignature; -use nym_contracts_common::signing::Verifier; - -pub(crate) fn verify_gateway_bonding_signature( - deps: Deps<'_>, - sender: Addr, - pledge: Coin, - gateway: Gateway, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(&gateway.identity_key)?; - - // reconstruct the payload, first try the current format, then attempt legacy - let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_gateway_bonding_sign_payload( - nonce, - sender.clone(), - pledge.clone(), - gateway.clone(), - ); - - if deps - .api - .verify_message(msg, signature.clone(), &public_key)? - { - Ok(()) - } else { - // attempt to use legacy - let msg_legacy = - construct_legacy_gateway_bonding_sign_payload(nonce, sender, pledge, gateway); - if deps - .api - .verify_message(msg_legacy, signature, &public_key)? - { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } - } -} diff --git a/contracts/mixnet/src/gateways/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 9b5892844a..95bf11dcc9 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -1,10 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE}; +use crate::constants::{ + GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE, LEGACY_GATEWAY_ID_NAMESPACE, +}; use cosmwasm_std::Addr; -use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex}; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; +use nym_contracts_common::IdentityKey; + +pub(crate) const PREASSIGNED_LEGACY_IDS: Map = + Map::new(LEGACY_GATEWAY_ID_NAMESPACE); pub(crate) struct GatewayBondIndex<'a> { pub(crate) owner: UniqueIndex<'a, Addr, GatewayBond>, diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 6075c25894..5720c48455 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -3,21 +3,21 @@ use super::helpers::must_get_gateway_bond_by_owner; use super::storage; -use crate::gateways::signature_helpers::verify_gateway_bonding_signature; +use crate::constants::default_node_costs; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::signing::storage as signing_storage; -use crate::support::helpers::{ensure_no_existing_bond, validate_pledge}; -use cosmwasm_std::{BankMsg, DepsMut, Env, MessageInfo, Response}; +use crate::nodes::helpers::save_new_nymnode_with_id; +use crate::nodes::transactions::add_nym_node_inner; +use crate::support::helpers::ensure_epoch_in_progress_state; +use crate::support::helpers::AttachSendTokens; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_gateway_bonding_event, new_gateway_config_update_event, new_gateway_unbonding_event, + new_gateway_config_update_event, new_gateway_unbonding_event, new_migrated_gateway_event, }; use mixnet_contract_common::gateway::GatewayConfigUpdate; -use mixnet_contract_common::{Gateway, GatewayBond}; +use mixnet_contract_common::{Gateway, GatewayBondingPayload, NodeCostParams}; use nym_contracts_common::signing::MessageSignature; -// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce -// so that we could return a better error message if it doesn't match? pub(crate) fn try_add_gateway( deps: DepsMut<'_>, env: Env, @@ -25,51 +25,19 @@ pub(crate) fn try_add_gateway( gateway: Gateway, owner_signature: MessageSignature, ) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::minimum_gateway_pledge(deps.storage)?; - let pledge = validate_pledge(info.funds, minimum_pledge)?; + let signed_payload = GatewayBondingPayload::new(gateway.clone()); + let denom = mixnet_params_storage::rewarding_denom(deps.storage)?; + let cost_params = default_node_costs(denom); - // if the client has an active bonded mixnode or gateway, don't allow bonding - ensure_no_existing_bond(&info.sender, deps.storage)?; - - // check if somebody else has already bonded a gateway with this identity - if let Some(existing_bond) = - storage::gateways().may_load(deps.storage, &gateway.identity_key)? - { - if existing_bond.owner != info.sender { - return Err(MixnetContractError::DuplicateGateway { - owner: existing_bond.owner, - }); - } - } - - // check if this sender actually owns the gateway by checking the signature - verify_gateway_bonding_signature( - deps.as_ref(), - info.sender.clone(), - pledge.clone(), - gateway.clone(), + add_nym_node_inner( + deps, + env, + info, + gateway.into(), + cost_params, owner_signature, - )?; - - // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; - - let gateway_identity = gateway.identity_key.clone(); - let bond = GatewayBond::new( - pledge.clone(), - info.sender.clone(), - env.block.height, - gateway, - ); - - storage::gateways().save(deps.storage, bond.identity(), &bond)?; - - Ok(Response::new().add_event(new_gateway_bonding_event( - &info.sender, - &pledge, - &gateway_identity, - ))) + signed_payload, + ) } pub(crate) fn try_remove_gateway( @@ -77,31 +45,18 @@ pub(crate) fn try_remove_gateway( info: MessageInfo, ) -> Result { // try to find the node of the sender - let gateway_bond = match storage::gateways() - .idx - .owner - .item(deps.storage, info.sender.clone())? - { - Some(record) => record.1, - None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner: info.sender }), - }; - - // send bonded funds back to the bond owner - let return_tokens = BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![gateway_bond.pledge_amount()], - }; + let gateway_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; // remove the bond storage::gateways().remove(deps.storage, gateway_bond.identity())?; Ok(Response::new() - .add_message(return_tokens) .add_event(new_gateway_unbonding_event( &info.sender, &gateway_bond.pledge_amount, gateway_bond.identity(), - ))) + )) + .send_tokens(&info.sender, gateway_bond.pledge_amount)) } pub(crate) fn try_update_gateway_config( @@ -129,19 +84,72 @@ pub(crate) fn try_update_gateway_config( Ok(Response::new().add_event(cfg_update_event)) } +pub fn try_migrate_to_nymnode( + deps: DepsMut, + info: MessageInfo, + cost_params: Option, +) -> Result { + let gateway_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; + + // currently on mainnet there are no gateways bonded with vesting tokens + // if somebody decides to make one between now and when this is deployed, + // it's on them. they have to unbond and rebond. simple as that. + if gateway_bond.proxy.is_some() { + return Err(MixnetContractError::VestingNodeMigration); + } + + ensure_epoch_in_progress_state(deps.storage)?; + + // remove the bond + storage::gateways().remove(deps.storage, gateway_bond.identity())?; + + let cost_params = + cost_params.unwrap_or_else(|| default_node_costs(&gateway_bond.pledge_amount.denom)); + + let gateway_identity = gateway_bond.gateway.identity_key.clone(); + + // this should have been added during migration + let node_id = storage::PREASSIGNED_LEGACY_IDS + .may_load(deps.storage, gateway_identity.clone())? + .ok_or_else(|| MixnetContractError::InconsistentState { + comment: "legacy gateway did not have a pre-assigned node id".to_string(), + })?; + + // create nym-node entry + // for gateways it's quite straightforward as there are no delegations or rewards to worry about + save_new_nymnode_with_id( + deps.storage, + node_id, + gateway_bond.block_height, + gateway_bond.gateway.into(), + cost_params, + info.sender.clone(), + gateway_bond.pledge_amount, + )?; + + storage::PREASSIGNED_LEGACY_IDS.remove(deps.storage, gateway_identity.clone()); + + Ok(Response::new().add_event(new_migrated_gateway_event( + &info.sender, + &gateway_identity, + node_id, + ))) +} + #[cfg(test)] pub mod tests { use super::*; use crate::contract::execute; use crate::gateways::queries; use crate::interval::pending_events; - use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; + use crate::mixnet_contract_settings::storage::minimum_node_pledge; + use crate::nodes::helpers::get_node_details_by_identity; + use crate::signing::storage as signing_storage; use crate::support::tests; - use crate::support::tests::fixtures; - use crate::support::tests::fixtures::good_mixnode_pledge; + use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Addr, Uint128}; + use cosmwasm_std::{Addr, BankMsg, Uint128}; use mixnet_contract_common::ExecuteMsg; #[test] @@ -150,7 +158,7 @@ pub mod tests { // if we fail validation (by say not sending enough funds let sender = "alice"; - let minimum_pledge = minimum_gateway_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); @@ -180,7 +188,7 @@ pub mod tests { let info = mock_info(sender, &[minimum_pledge]); // if there was already a gateway bonded by particular user - test.add_dummy_gateway(sender, None); + test.add_legacy_gateway(sender, None); // it fails let result = try_add_gateway(test.deps_mut(), env.clone(), info, gateway, sig); @@ -189,9 +197,9 @@ pub mod tests { // the same holds if the user already owns a mixnode let sender2 = "mixnode-owner"; - let mix_id = test.add_dummy_mixnode(sender2, None); + let mix_id = test.add_legacy_mixnode(sender2, None); - let info = mock_info(sender2, &fixtures::good_gateway_pledge()); + let info = mock_info(sender2, &good_gateway_pledge()); let (gateway, sig) = test.gateway_with_signature(sender2, None); let result = try_add_gateway( @@ -206,8 +214,14 @@ pub mod tests { // but after he unbonds it, it's all fine again pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - let result = try_add_gateway(test.deps_mut(), env, info, gateway, sig); + let result = try_add_gateway(test.deps_mut(), env, info.clone(), gateway.clone(), sig); assert!(result.is_ok()); + + // and the node has been added as a nym-node + let nym_node = get_node_details_by_identity(test.deps().storage, gateway.identity_key) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender) } #[test] @@ -276,7 +290,8 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - try_remove_gateway(test.deps_mut(), info.clone()).unwrap(); + // the moment gateway got bonded, it got added as a nymnode thus we have to remove nym-node + test.immediately_unbond_node(gateway.identity_key.clone()); let res = try_add_gateway(test.deps_mut(), env, info, gateway, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); @@ -301,7 +316,7 @@ pub mod tests { ); // let's add a node owned by bob - test.add_dummy_gateway("bob", None); + test.add_legacy_gateway("bob", None); // attempt to unbond fred's node, which doesn't exist let info = mock_info("fred", &[]); @@ -324,7 +339,7 @@ pub mod tests { assert_eq!(&Addr::unchecked("bob"), first_node.owner()); // add a node owned by fred - let fred_identity = test.add_dummy_gateway("fred", None); + let fred_identity = test.add_legacy_gateway("fred", None); // let's make sure we now have 2 nodes: let nodes = queries::query_gateways_paged(test.deps(), None, None) @@ -340,7 +355,7 @@ pub mod tests { // we should see a funds transfer from the contract back to fred let expected_message = BankMsg::Send { to_address: String::from(info.sender), - amount: tests::fixtures::good_gateway_pledge(), + amount: good_gateway_pledge(), }; // run the executor and check that we got back the correct results @@ -386,7 +401,7 @@ pub mod tests { }) ); - test.add_dummy_gateway(owner, None); + test.add_legacy_gateway(owner, None); // "normal" update succeeds let res = try_update_gateway_config(test.deps_mut(), info, update.clone()); diff --git a/contracts/mixnet/src/interval/helpers.rs b/contracts/mixnet/src/interval/helpers.rs index dd45c7511a..f1802973ac 100644 --- a/contracts/mixnet/src/interval/helpers.rs +++ b/contracts/mixnet/src/interval/helpers.rs @@ -1,12 +1,13 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::interval::storage; +use crate::rewards; use crate::rewards::storage as rewards_storage; -use cosmwasm_std::{Response, Storage}; +use cosmwasm_std::{Env, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::new_interval_config_update_event; -use mixnet_contract_common::{BlockHeight, Interval}; +use mixnet_contract_common::{BlockHeight, EpochId, Interval}; use std::time::Duration; pub(crate) fn change_interval_config( @@ -33,6 +34,26 @@ pub(crate) fn change_interval_config( ))) } +/// Update the storage to advance into the next epoch. +/// It's responsibility of the caller to ensure the epoch is actually over. +pub(crate) fn advance_epoch( + storage: &mut dyn Storage, + env: Env, +) -> Result { + let current_interval = storage::current_interval(storage)?; + + // if the current **INTERVAL** is over, apply reward pool changes + if current_interval.is_current_interval_over(&env) { + // this one is a very important one! + rewards::helpers::apply_reward_pool_changes(storage)?; + } + + let updated_interval = current_interval.advance_epoch(); + storage::save_interval(storage, &updated_interval)?; + + Ok(updated_interval.current_epoch_absolute_id()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index fa44e36ec3..59e39e434e 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -1,21 +1,22 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Addr, Coin, DepsMut, Env, Response}; +use cosmwasm_std::{Addr, BankMsg, Coin, DepsMut, Env, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event, - new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, new_pledge_decrease_event, - new_pledge_increase_event, new_rewarding_params_update_event, new_undelegation_event, + new_active_set_update_event, new_active_set_update_failure, new_cost_params_update_event, + new_delegation_event, new_delegation_on_unbonded_node_event, new_mixnode_unbonding_event, + new_nym_node_unbonding_event, new_pledge_decrease_event, new_pledge_increase_event, + new_rewarding_params_update_event, new_undelegation_event, }; -use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::pending_events::{ PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, PendingIntervalEventKind, }; -use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; -use mixnet_contract_common::{BlockHeight, Delegation, MixId}; +use mixnet_contract_common::reward_params::{ActiveSetUpdate, IntervalRewardingParamsUpdate}; +use mixnet_contract_common::{BlockHeight, Delegation, NodeId}; use crate::delegations; use crate::delegations::storage as delegations_storage; @@ -23,8 +24,11 @@ use crate::interval::helpers::change_interval_config; use crate::interval::storage; use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::helpers::{cleanup_post_unbond_nym_node_storage, get_node_details_by_id}; +use crate::nodes::storage as nymnodes_storage; use crate::rewards::storage as rewards_storage; -use crate::support::helpers::AttachSendTokens; +use crate::rewards::storage::RewardingStorage; +use crate::support::helpers::{ensure_any_node_bonded, AttachSendTokens}; pub(crate) trait ContractExecutableEvent { // note: the error only means a HARD error like we failed to read from storage. @@ -38,33 +42,37 @@ pub(crate) fn delegate( env: &Env, created_at: BlockHeight, owner: Addr, - mix_id: MixId, + node_id: NodeId, amount: Coin, ) -> Result { - // check if the target node still exists (it might have unbonded between this event getting created - // and being executed). Do note that it's absolutely possible for a mixnode to get immediately - // unbonded at this very block (if the event was pending), but that's tough luck, then it's up - // to the delegator to click the undelegate button - let mixnode_details = match get_mixnode_details_by_id(deps.storage, mix_id)? { - Some(details) - if details.rewarding_details.still_bonded() - && !details.bond_information.is_unbonding => - { - details - } - _ => { - // if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the - // delegator; - let response = Response::new() - .send_tokens(&owner, amount.clone()) - .add_event(new_delegation_on_unbonded_node_event(&owner, mix_id)); - - return Ok(response); - } + // first check - see if we have any rewarding information in the storage, if not, the node has fully unbonded + // (and also didn't have any delegations) + let Some(mut node_rewarding) = + rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? + else { + return Ok(Response::new() + .send_tokens(&owner, amount) + .add_event(new_delegation_on_unbonded_node_event(&owner, node_id))); }; + // more extensive check to see if node is still bonded, however, this time we have to unfortunately check + // BOTH nym-node and legacy nym-node. + // the underlying target node might have unbonded between this event getting created + // and being executed. Do note that it's absolutely possible for a node to get immediately + // unbonded at this very block (if the event was pending), but that's tough luck, then it's up + // to the delegator to click the undelegate button + match ensure_any_node_bonded(deps.storage, node_id) { + // cosmwasm-std errors are critical and recoverable because they imply issues with the underlying storage + Err(MixnetContractError::StdErr { source }) => return Err(source.into()), + Err(_unbonded) => { + return Ok(Response::new() + .send_tokens(&owner, amount) + .add_event(new_delegation_on_unbonded_node_event(&owner, node_id))); + } + Ok(_) => {} + } + let new_delegation_amount = amount.clone(); - let mut mix_rewarding = mixnode_details.rewarding_details; // the delegation_amount might get increased if there's already a pre-existing delegation on this mixnode // (in that case we just create a fresh delegation with the sum of both) @@ -72,12 +80,12 @@ pub(crate) fn delegate( // if there's an existing delegation, then withdraw the full reward and create a new delegation // with the sum of both - let storage_key = Delegation::generate_storage_key(mix_id, &owner, None); + let storage_key = Delegation::generate_storage_key(node_id, &owner, None); let old_delegation = if let Some(existing_delegation) = delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? { // completely remove the delegation from the node - let og_with_reward = mix_rewarding.undelegate(&existing_delegation)?; + let og_with_reward = node_rewarding.undelegate(&existing_delegation)?; // and adjust the new value by the amount removed (which contains the original delegation // alongside any earned rewards) @@ -89,20 +97,20 @@ pub(crate) fn delegate( }; // add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one) - mix_rewarding.add_base_delegation(stored_delegation_amount.amount)?; + node_rewarding.add_base_delegation(stored_delegation_amount.amount)?; let cosmos_event = new_delegation_event( created_at, &owner, &new_delegation_amount, - mix_id, - mix_rewarding.total_unit_reward, + node_id, + node_rewarding.total_unit_reward, ); let delegation = Delegation::new( owner, - mix_id, - mix_rewarding.total_unit_reward, + node_id, + node_rewarding.total_unit_reward, stored_delegation_amount, env.block.height, ); @@ -114,7 +122,7 @@ pub(crate) fn delegate( Some(&delegation), old_delegation.as_ref(), )?; - rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &node_rewarding)?; Ok(Response::new().add_event(cosmos_event)) } @@ -123,7 +131,7 @@ pub(crate) fn undelegate( deps: DepsMut<'_>, created_at: BlockHeight, owner: Addr, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // see if the delegation still exists (in case of impatient user who decided to send multiple // undelegation requests in an epoch) @@ -132,13 +140,17 @@ pub(crate) fn undelegate( None => return Ok(Response::default()), Some(delegation) => delegation, }; - let mix_rewarding = - rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( - "mixnode rewarding got removed from the storage whilst there's still an existing delegation", + + // note: rewarding information for nym-nodes and mixnodes are stored under the same storage structure + // and in the same underlying map so it doesn't matter which one we load + + let rewarding_info = + rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( + "node rewarding got removed from the storage whilst there's still an existing delegation", ))?; // this also appropriately adjusts the storage let tokens_to_return = - delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; + delegations::helpers::undelegate(deps.storage, delegation, rewarding_info)?; let response = Response::new() .send_tokens(&owner, tokens_to_return.clone()) @@ -147,11 +159,55 @@ pub(crate) fn undelegate( Ok(response) } +pub(crate) fn unbond_nym_node( + deps: DepsMut<'_>, + env: &Env, + created_at: BlockHeight, + node_id: NodeId, +) -> Result { + // if we're here it means user executed `try_remove_nym_node` and as a result node was set to be + // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) + // thus the node with all its associated information MUST exist in the storage. + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to get unbonded doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_some() { + return Err(MixnetContractError::inconsistent_state( + "attempted to unbond nym node while there are associated pending pledge changes", + )); + } + + // the denom on the original pledge was validated at the time of bonding, so we can safely reuse it here + let rewarding_denom = &node_details.bond_information.original_pledge.denom; + let tokens = node_details + .rewarding_details + .operator_pledge_with_reward(rewarding_denom); + + let owner = &node_details.bond_information.owner; + + // send bonded funds (alongside all earned rewards) to the bond owner + let return_tokens = BankMsg::Send { + to_address: owner.to_string(), + amount: vec![tokens], + }; + + // remove the bond and if there are no delegations left, also the rewarding information + cleanup_post_unbond_nym_node_storage(deps.storage, env, &node_details)?; + + let response = Response::new() + .add_message(return_tokens) + .add_event(new_nym_node_unbonding_event(created_at, node_id)); + + Ok(response) +} + pub(crate) fn unbond_mixnode( deps: DepsMut<'_>, env: &Env, created_at: BlockHeight, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // if we're here it means user executed `_try_remove_mixnode` and as a result node was set to be // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) @@ -186,28 +242,76 @@ pub(crate) fn unbond_mixnode( Ok(response) } -pub(crate) fn update_active_set_size( +pub(crate) fn update_active_set( deps: DepsMut<'_>, created_at: BlockHeight, - active_set_size: u32, + update: ActiveSetUpdate, ) -> Result { // We don't have to check for authorization as this event can only be pushed // by the authorized entity. // Furthermore, we don't need to check whether the epoch is finished as the // queue is only emptied upon the epoch finishing. - // Also, we know the update is valid as we checked for that before pushing the event onto the queue. + let global_rewarding_params_storage = RewardingStorage::load().global_rewarding_params; + let mut rewarding_params = global_rewarding_params_storage.load(deps.storage)?; - let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; - rewarding_params.try_change_active_set_size(active_set_size)?; - rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + // unfortunately this will be a silent error, + // but for this to happen an admin must have been screwing around by pushing active set update onto the queue + // but updating rewarded set immediately, so it's on them + if let Err(err) = rewarding_params.try_change_active_set(update) { + return Ok(Response::new().add_event(new_active_set_update_failure(err))); + } + global_rewarding_params_storage.save(deps.storage, &rewarding_params)?; - Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size))) + Ok(Response::new().add_event(new_active_set_update_event(created_at, update))) } -pub(crate) fn increase_pledge( +pub(crate) fn increase_nym_node_pledge( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, + node_id: NodeId, + increase: Coin, +) -> Result { + // note: we have already validated the amount to know it has the correct denomination + + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to increase its pledge doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_none() { + return Err(MixnetContractError::inconsistent_state( + "attempted to increase nym node pledge while there are no associated pending changes", + )); + } + + let mut updated_bond = node_details.bond_information.clone(); + let mut updated_rewarding = node_details.rewarding_details; + + updated_bond.original_pledge.amount += increase.amount; + updated_rewarding.increase_operator_uint128(increase.amount)?; + + let mut pending_changes = node_details.pending_changes; + pending_changes.pledge_change = None; + + // update all: bond information, rewarding details and pending pledge changes + nymnodes_storage::nym_nodes().replace( + deps.storage, + node_id, + Some(&updated_bond), + Some(&node_details.bond_information), + )?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &updated_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(new_pledge_increase_event(created_at, node_id, &increase))) +} + +pub(crate) fn increase_mixnode_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + mix_id: NodeId, increase: Coin, ) -> Result { // note: we have already validated the amount to know it has the correct denomination @@ -247,10 +351,10 @@ pub(crate) fn increase_pledge( Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase))) } -pub(crate) fn decrease_pledge( +pub(crate) fn decrease_mixnode_pledge( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, + mix_id: NodeId, decrease_by: Coin, ) -> Result { // the target node MUST exist - we have checked it at the time of putting this event onto the queue @@ -296,6 +400,61 @@ pub(crate) fn decrease_pledge( Ok(response) } +pub(crate) fn decrease_nym_node_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + node_id: NodeId, + decrease_by: Coin, +) -> Result { + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to increase its pledge doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_none() { + return Err(MixnetContractError::inconsistent_state( + "attempted to increase nym node pledge while there are no associated pending changes", + )); + } + + let mut updated_bond = node_details.bond_information.clone(); + let mut updated_rewarding = node_details.rewarding_details; + + let mut pending_changes = node_details.pending_changes; + pending_changes.pledge_change = None; + + // SAFETY: the subtraction here can't overflow as before the event was pushed into the queue, + // we checked that the new value will be higher than minimum pledge (which is also strictly positive) + updated_bond.original_pledge.amount -= decrease_by.amount; + updated_rewarding.decrease_operator_uint128(decrease_by.amount)?; + + let owner = &node_details.bond_information.owner; + + // send the removed tokens back to the operator + let return_tokens = BankMsg::Send { + to_address: owner.to_string(), + amount: vec![decrease_by.clone()], + }; + + // update all: bond information, rewarding details and pending pledge changes + nymnodes_storage::nym_nodes().replace( + deps.storage, + node_id, + Some(&updated_bond), + Some(&node_details.bond_information), + )?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &updated_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + let response = Response::new() + .add_message(return_tokens) + .add_event(new_pledge_decrease_event(created_at, node_id, &decrease_by)); + + Ok(response) +} + impl ContractExecutableEvent for PendingEpochEventData { fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result { // note that the basic validation on all those events was already performed before @@ -303,25 +462,37 @@ impl ContractExecutableEvent for PendingEpochEventData { match self.kind { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, .. } => delegate(deps, env, self.created_at, owner, mix_id, amount), - PendingEpochEventKind::Undelegate { owner, mix_id, .. } => { - undelegate(deps, self.created_at, owner, mix_id) + PendingEpochEventKind::Undelegate { + owner, + node_id: mix_id, + .. + } => undelegate(deps, self.created_at, owner, mix_id), + PendingEpochEventKind::NymNodePledgeMore { node_id, amount } => { + increase_nym_node_pledge(deps, self.created_at, node_id, amount) } - PendingEpochEventKind::PledgeMore { mix_id, amount } => { - increase_pledge(deps, self.created_at, mix_id, amount) + PendingEpochEventKind::MixnodePledgeMore { mix_id, amount } => { + increase_mixnode_pledge(deps, self.created_at, mix_id, amount) } - PendingEpochEventKind::DecreasePledge { + PendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + } => decrease_nym_node_pledge(deps, self.created_at, node_id, decrease_by), + PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, - } => decrease_pledge(deps, self.created_at, mix_id, decrease_by), + } => decrease_mixnode_pledge(deps, self.created_at, mix_id, decrease_by), PendingEpochEventKind::UnbondMixnode { mix_id } => { unbond_mixnode(deps, env, self.created_at, mix_id) } - PendingEpochEventKind::UpdateActiveSetSize { new_size } => { - update_active_set_size(deps, self.created_at, new_size) + PendingEpochEventKind::UnbondNymNode { node_id } => { + unbond_nym_node(deps, env, self.created_at, node_id) + } + PendingEpochEventKind::UpdateActiveSet { update } => { + update_active_set(deps, self.created_at, update) } } } @@ -330,8 +501,8 @@ impl ContractExecutableEvent for PendingEpochEventData { pub(crate) fn change_mix_cost_params( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, - new_costs: MixNodeCostParams, + mix_id: NodeId, + new_costs: NodeCostParams, ) -> Result { // almost an entire interval might have passed since the request was issued -> check if the // node still exists @@ -345,12 +516,48 @@ pub(crate) fn change_mix_cost_params( _ => return Ok(Response::default()), }; - let cosmos_event = new_mixnode_cost_params_update_event(created_at, mix_id, &new_costs); + let mut pending_changes = + mixnodes_storage::PENDING_MIXNODE_CHANGES.load(deps.storage, mix_id)?; + pending_changes.cost_params_change = None; + + let cosmos_event = new_cost_params_update_event(created_at, mix_id, &new_costs); // TODO: can we just change cost_params without breaking rewarding calculation? // (I'm almost certain we can, but well, it has to be tested) mix_rewarding.cost_params = new_costs; rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + mixnodes_storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn change_nym_node_cost_params( + deps: DepsMut<'_>, + created_at: BlockHeight, + node_id: NodeId, + new_costs: NodeCostParams, +) -> Result { + // almost an entire interval might have passed since the request was issued -> check if the + // node still exists + // + // note: there's no check if the bond is in "unbonding" state, as epoch actions would get + // cleared before touching interval actions + let mut node_rewarding = + match rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(node_rewarding) if node_rewarding.still_bonded() => node_rewarding, + // if node doesn't exist anymore, don't do anything, simple as that. + _ => return Ok(Response::default()), + }; + + let mut pending_changes = + nymnodes_storage::PENDING_NYMNODE_CHANGES.load(deps.storage, node_id)?; + pending_changes.cost_params_change = None; + + let cosmos_event = new_cost_params_update_event(created_at, node_id, &new_costs); + + node_rewarding.cost_params = new_costs; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &node_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; Ok(Response::new().add_event(cosmos_event)) } @@ -404,10 +611,12 @@ impl ContractExecutableEvent for PendingIntervalEventData { // note that the basic validation on all those events was already performed before // they were pushed onto the queue match self.kind { - PendingIntervalEventKind::ChangeMixCostParams { - mix_id: mix, - new_costs, - } => change_mix_cost_params(deps, self.created_at, mix, new_costs), + PendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { + change_mix_cost_params(deps, self.created_at, mix_id, new_costs) + } + PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs } => { + change_nym_node_cost_params(deps, self.created_at, node_id, new_costs) + } PendingIntervalEventKind::UpdateRewardingParams { update } => { update_rewarding_params(deps, self.created_at, update) } @@ -449,7 +658,7 @@ mod tests { #[test] fn returns_the_tokens_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let delegation = 120_000_000u128; let delegation_coin = coin(delegation, TEST_COIN_DENOM); @@ -512,7 +721,7 @@ mod tests { #[test] fn returns_the_tokens_is_mixnode_is_unbonding() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let delegation = 120_000_000u128; let delegation_coin = coin(delegation, TEST_COIN_DENOM); @@ -575,7 +784,8 @@ mod tests { #[test] fn if_delegation_already_exists_a_fresh_one_with_sum_of_both_is_created() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let delegation_og = 120_000_000u128; let delegation_new = 543_000_000u128; @@ -619,7 +829,8 @@ mod tests { #[test] fn if_delegation_already_exists_with_unclaimed_rewards_fresh_one_is_created() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let delegation_og = 120_000_000u128; let delegation_new = 543_000_000u128; @@ -628,12 +839,12 @@ mod tests { // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -642,12 +853,12 @@ mod tests { test.add_immediate_delegation(owner, delegation_og, mix_id); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -701,7 +912,8 @@ mod tests { #[test] fn appropriately_updates_state_for_fresh_delegation() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let owner = "delegator"; let delegation = 120_000_000u128; @@ -710,12 +922,12 @@ mod tests { // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -766,7 +978,7 @@ mod tests { #[test] fn doesnt_return_any_tokens_if_it_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let owner = Addr::unchecked("delegator"); @@ -777,7 +989,7 @@ mod tests { #[test] fn errors_out_if_mix_rewarding_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let owner = Addr::unchecked("delegator"); test.add_immediate_delegation(owner.as_str(), 100_000_000u32, mix_id); @@ -795,36 +1007,27 @@ mod tests { #[test] fn returns_all_delegated_tokens_with_earned_rewards() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let owner = "delegator"; let delegation = 120_000_000u128; + let active_params = test.active_node_params(100.0); + // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.add_immediate_delegation(owner, delegation, mix_id); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let expected_reward = dist1.delegates + dist2.delegates; let truncated_reward = truncate_reward_amount(expected_reward); @@ -853,12 +1056,19 @@ mod tests { #[cfg(test)] mod mixnode_unbonding { - use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::interval::pending_events::unbond_mixnode; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::{try_decrease_pledge, try_increase_pledge}; - use crate::support::tests::test_helpers::get_bank_send_msg; + use crate::mixnodes::transactions::{ + try_decrease_mixnode_pledge, try_increase_mixnode_pledge, + }; + use crate::rewards::storage as rewards_storage; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::{get_bank_send_msg, TestSetup}; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Uint128; + use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Uint128, WasmMsg}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -884,14 +1094,9 @@ mod tests { // increase let owner = "mix-owner1"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - try_increase_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner, &change.clone()), - ) - .unwrap(); + try_increase_pledge(test.deps_mut(), env.clone(), mock_info(owner, &change)).unwrap(); let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id); assert!(matches!( @@ -902,7 +1107,7 @@ mod tests { // decrease let owner = "mix-owner2"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); try_decrease_pledge( test.deps_mut(), @@ -921,10 +1126,11 @@ mod tests { // artificial let owner = "mix-owner3"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); let changes = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; mixnodes_storage::PENDING_MIXNODE_CHANGES @@ -943,20 +1149,19 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); let mix_details = mixnodes_storage::mixnode_bonds() .load(test.deps().storage, mix_id) .unwrap(); - let layer = mix_details.layer; test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -991,10 +1196,6 @@ mod tests { .load(test.deps().storage, mix_id) .unwrap() ); - assert_eq!( - mixnodes_storage::LAYERS.load(test.deps().storage).unwrap()[layer], - 0 - ) } } @@ -1012,7 +1213,7 @@ mod tests { let mut test = TestSetup::new(); let amount = test.coin(123); - let res = increase_pledge(test.deps_mut(), 123, 1, amount); + let res = increase_mixnode_pledge(test.deps_mut(), 123, 1, amount); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1026,9 +1227,9 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - let res = increase_pledge(test.deps_mut(), 123, mix_id, change); + let res = increase_mixnode_pledge(test.deps_mut(), 123, mix_id, change); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1038,7 +1239,7 @@ mod tests { #[test] fn updates_stored_bond_information_and_rewarding_details() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -1046,7 +1247,7 @@ mod tests { .unwrap(); let amount = test.coin(12345); - increase_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -1071,13 +1272,13 @@ mod tests { let pledge2 = Uint128::new(50_000_000); let pledge3 = Uint128::new(200_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); @@ -1090,11 +1291,11 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1108,7 +1309,7 @@ mod tests { let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1118,16 +1319,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge]); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(dist.operator); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1162,11 +1363,11 @@ mod tests { // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1181,7 +1382,7 @@ mod tests { let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1197,7 +1398,7 @@ mod tests { // go few epochs of rewarding before adding more pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); @@ -1206,11 +1407,11 @@ mod tests { } let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(cumulative_op_reward); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1245,11 +1446,11 @@ mod tests { // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1261,11 +1462,11 @@ mod tests { #[test] fn updates_the_pending_pledge_changes_field() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - increase_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(test.deps().storage, mix_id) .unwrap(); @@ -1285,7 +1486,7 @@ mod tests { let mut test = TestSetup::new(); let amount = test.coin(123); - let res = decrease_pledge(test.deps_mut(), 123, 1, amount); + let res = decrease_mixnode_pledge(test.deps_mut(), 123, 1, amount); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1299,9 +1500,9 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, change); + let res = decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, change); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1311,7 +1512,7 @@ mod tests { #[test] fn updates_stored_bond_information_and_rewarding_details() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -1319,7 +1520,7 @@ mod tests { .unwrap(); let amount = test.coin(12345); - decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -1341,11 +1542,12 @@ mod tests { fn returns_tokens_back_to_the_owner() { let mut test = TestSetup::new(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_rewarded_legacy_mixnode(owner, None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + let res = + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); assert_eq!(res.messages.len(), 1); assert_eq!( @@ -1364,13 +1566,13 @@ mod tests { let pledge_change = Uint128::new(50_000_000); let pledge3 = Uint128::new(150_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); @@ -1383,11 +1585,11 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1401,7 +1603,7 @@ mod tests { let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1411,16 +1613,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge]); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); let pledge3 = Uint128::new(150_000_000_000) + truncate_reward_amount(dist.operator); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1455,11 +1657,11 @@ mod tests { // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1474,7 +1676,7 @@ mod tests { let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1490,7 +1692,7 @@ mod tests { // go few epochs of rewarding before decreasing pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); @@ -1499,11 +1701,11 @@ mod tests { } let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); let pledge3 = Uint128::new(150_000_000_000) + truncate_reward_amount(cumulative_op_reward); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1538,11 +1740,11 @@ mod tests { // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1554,11 +1756,11 @@ mod tests { #[test] fn updates_the_pending_pledge_changes_field() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - decrease_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(test.deps().storage, mix_id) .unwrap(); @@ -1573,12 +1775,23 @@ mod tests { .load(test.deps().storage) .unwrap(); - update_active_set_size(test.deps_mut(), 123, 50).unwrap(); + update_active_set( + test.deps_mut(), + 123, + ActiveSetUpdate { + entry_gateways: 50, + exit_gateways: 50, + mixnodes: 100, + }, + ) + .unwrap(); let updated = rewards_storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap(); - assert_ne!(current.active_set_size, updated.active_set_size); - assert_eq!(updated.active_set_size, 50) + assert_ne!(updated.rewarded_set, current.rewarded_set); + assert_eq!(updated.rewarded_set.mixnodes, 100); + assert_eq!(updated.rewarded_set.entry_gateways, 50); + assert_eq!(updated.rewarded_set.exit_gateways, 50); } #[cfg(test)] @@ -1591,12 +1804,12 @@ mod tests { #[test] fn doesnt_do_anything_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let env = test.env(); unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - let new_params = MixNodeCostParams { + let new_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; @@ -1608,11 +1821,16 @@ mod tests { #[test] fn for_bonded_mixnode_updates_saved_value() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let before = test.mix_rewarding(mix_id).cost_params; - let new_params = MixNodeCostParams { + // this would have been normally populated when creating the event itself + mixnodes_storage::PENDING_MIXNODE_CHANGES + .save(test.deps_mut().storage, mix_id, &Default::default()) + .unwrap(); + + let new_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; @@ -1620,13 +1838,11 @@ mod tests { let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params.clone()); assert_eq!( res, - Ok( - Response::new().add_event(new_mixnode_cost_params_update_event( - 123, - mix_id, - &new_params, - )) - ) + Ok(Response::new().add_event(new_cost_params_update_event( + 123, + mix_id, + &new_params, + ))) ); let after = test.mix_rewarding(mix_id).cost_params; @@ -1655,7 +1871,7 @@ mod tests { sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()), active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let res = update_rewarding_params(test.deps_mut(), 123, update); diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index 423d9a90ae..5649d5bb83 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -4,7 +4,6 @@ use crate::constants::{ EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT, EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT, INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT, INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT, - REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, REWARDED_SET_MAX_RETRIEVAL_LIMIT, }; use crate::interval::storage; use cosmwasm_std::{Deps, Env, Order, StdResult}; @@ -12,9 +11,9 @@ use cw_storage_plus::Bound; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use mixnet_contract_common::{ - CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, MixId, - NumberOfPendingEventsResponse, PagedRewardedSetResponse, PendingEpochEventResponse, - PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse, + CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, + NumberOfPendingEventsResponse, PendingEpochEventResponse, PendingEpochEventsResponse, + PendingIntervalEventResponse, PendingIntervalEventsResponse, }; pub fn query_epoch_status(deps: Deps<'_>) -> StdResult { @@ -30,30 +29,6 @@ pub fn query_current_interval_details( Ok(CurrentIntervalResponse::new(interval, env)) } -pub fn query_rewarded_set_paged( - deps: Deps<'_>, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit - .unwrap_or(REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT) - .min(REWARDED_SET_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let nodes = storage::REWARDED_SET - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>>()?; - - let start_next_after = nodes.last().map(|node| node.0); - - Ok(PagedRewardedSetResponse { - nodes, - start_next_after, - }) -} - pub fn query_pending_epoch_events_paged( deps: Deps<'_>, env: Env, @@ -174,7 +149,7 @@ mod tests { fn push_dummy_interval_action(test: &mut TestSetup) { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let env = test.env(); storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); @@ -214,95 +189,6 @@ mod tests { assert_eq!(res.current_blocktime, env.block.time.seconds()); } - #[cfg(test)] - mod rewarded_set { - use super::*; - - fn set_rewarded_set_to_n_nodes(test: &mut TestSetup, n: usize) { - let set = (1u32..).take(n).collect::>(); - test.force_change_rewarded_set(set) - } - - #[test] - fn obeys_limits() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 200); - - let limit = 2; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(limit)).unwrap(); - assert_eq!(limit, page1.nodes.len() as u32); - } - - #[test] - fn has_default_limit() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 2000); - - // query without explicitly setting a limit - let page1 = query_rewarded_set_paged(test.deps(), None, None).unwrap(); - - assert_eq!( - REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, - page1.nodes.len() as u32 - ); - } - - #[test] - fn has_max_limit() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 2000); - - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 10000; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(crazy_limit)).unwrap(); - - assert_eq!(REWARDED_SET_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); - } - - #[test] - fn pagination_works() { - let mut test = TestSetup::new(); - - set_rewarded_set_to_n_nodes(&mut test, 1); - - let per_page = 2; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - - // page should have 1 result on it - assert_eq!(1, page1.nodes.len()); - - set_rewarded_set_to_n_nodes(&mut test, 2); - - // page1 should have 2 results on it - let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - assert_eq!(2, page1.nodes.len()); - - set_rewarded_set_to_n_nodes(&mut test, 3); - - // page1 still has the same 2 results - let another_page1 = - query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - assert_eq!(2, another_page1.nodes.len()); - assert_eq!(page1, another_page1); - - // retrieving the next page should start after the last key on this page - let start_after = page1.start_next_after.unwrap(); - let page2 = - query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); - - assert_eq!(1, page2.nodes.len()); - - // save another one - set_rewarded_set_to_n_nodes(&mut test, 4); - - let page2 = - query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.nodes.len()); - } - } - #[cfg(test)] mod pending_epoch_events { use super::*; @@ -605,7 +491,7 @@ mod tests { // it exists let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let env = test.env(); storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action.clone()) diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 33eddd4d4e..2a359cd43f 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -4,22 +4,19 @@ use crate::constants::{ CURRENT_EPOCH_STATUS_KEY, CURRENT_INTERVAL_KEY, EPOCH_EVENT_ID_COUNTER_KEY, INTERVAL_EVENT_ID_COUNTER_KEY, LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, - PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY, + PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, }; -use cosmwasm_std::{Addr, Env, Order, StdResult, Storage}; +use cosmwasm_std::{Addr, Env, StdResult, Storage}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::pending_events::{ PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, }; use mixnet_contract_common::{ - EpochEventId, EpochStatus, Interval, IntervalEventId, MixId, PendingIntervalEventKind, - RewardedSetNodeStatus, + EpochEventId, EpochStatus, Interval, IntervalEventId, PendingIntervalEventKind, }; -use std::collections::HashMap; pub(crate) const CURRENT_EPOCH_STATUS: Item<'_, EpochStatus> = Item::new(CURRENT_EPOCH_STATUS_KEY); pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new(CURRENT_INTERVAL_KEY); -pub(crate) const REWARDED_SET: Map = Map::new(REWARDED_SET_KEY); pub(crate) const EPOCH_EVENT_ID_COUNTER: Item = Item::new(EPOCH_EVENT_ID_COUNTER_KEY); pub(crate) const INTERVAL_EVENT_ID_COUNTER: Item = @@ -111,49 +108,6 @@ pub(crate) fn push_new_interval_event( Ok(event_id) } -pub(crate) fn update_rewarded_set( - storage: &mut dyn Storage, - active_set_size: u32, - new_set: Vec, -) -> StdResult<()> { - // our goal is to reduce the number of reads and writes to the underlying storage, - // whilst completely overwriting the current rewarded set. - // the naive implementation would be to read the entire current rewarded set, - // remove all of those entries - // and write the new one in its place. - // However, very often it might turn out that a node hasn't changed its status in the updated epoch, - // and in those cases we can save on having to remove the entry and writing a new one. - - // Note: so far it seems the contract compiles (and stores) fine with a `HashMap`, but if we ever - // run into any issues due to any randomness? we can switch it up for a BTreeMap - let mut old_nodes = REWARDED_SET - .range(storage, None, None, Order::Ascending) - .collect::, _>>()?; - - for (i, node_id) in new_set.into_iter().enumerate() { - // first k nodes are active - let set_status = if i < active_set_size as usize { - RewardedSetNodeStatus::Active - } else { - RewardedSetNodeStatus::Standby - }; - - if !matches!(old_nodes.get(&node_id), Some(status) if status == &set_status) { - // if the status changed, or didn't exist, write it down: - REWARDED_SET.save(storage, node_id, &set_status)?; - } - - old_nodes.remove(&node_id); - } - - // finally remove the entries for nodes that no longer exist [in the rewarded set] - for old_node_id in old_nodes.keys() { - REWARDED_SET.remove(storage, *old_node_id) - } - - Ok(()) -} - pub(crate) fn initialise_storage( storage: &mut dyn Storage, starting_interval: Interval, @@ -172,50 +126,8 @@ mod tests { use super::*; use crate::support::tests::fixtures; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::mock_dependencies; use rand_chacha::rand_core::RngCore; - fn read_entire_set(storage: &dyn Storage) -> HashMap { - REWARDED_SET - .range(storage, None, None, Order::Ascending) - .map(|r| r.unwrap()) - .collect() - } - - #[test] - fn updating_rewarded_set() { - // just some variables to keep test assertions more concise - let active = &RewardedSetNodeStatus::Active; - let standby = &RewardedSetNodeStatus::Standby; - - let mut deps = mock_dependencies(); - let store = deps.as_mut().storage; - assert!(read_entire_set(store).is_empty()); - - // writing initial rewarded set shouldn't do anything fancy - update_rewarded_set(store, 2, vec![6, 2, 7, 4, 1]).unwrap(); - let current_set = read_entire_set(store); - assert_eq!(current_set.len(), 5); - assert_eq!(active, current_set.get(&6).unwrap()); - assert_eq!(active, current_set.get(&2).unwrap()); - assert_eq!(standby, current_set.get(&7).unwrap()); - assert_eq!(standby, current_set.get(&4).unwrap()); - assert_eq!(standby, current_set.get(&1).unwrap()); - assert!(!current_set.contains_key(&42)); - - update_rewarded_set(store, 2, vec![2, 5, 6, 3, 4]).unwrap(); - let current_set = read_entire_set(store); - assert_eq!(current_set.len(), 5); - assert_eq!(active, current_set.get(&2).unwrap()); - assert_eq!(active, current_set.get(&5).unwrap()); - assert_eq!(standby, current_set.get(&6).unwrap()); - assert_eq!(standby, current_set.get(&3).unwrap()); - assert_eq!(standby, current_set.get(&4).unwrap()); - // those no longer are in the rewarded set - assert!(!current_set.contains_key(&7)); - assert!(!current_set.contains_key(&1)); - } - #[test] fn pushing_new_epoch_event_returns_its_id() { let mut test = TestSetup::new(); @@ -252,7 +164,7 @@ mod tests { for _ in 0..500 { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); @@ -264,7 +176,7 @@ mod tests { for _ in 0..10 { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index adc8faa9cc..ca4c7c0563 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -2,26 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::interval::helpers::change_interval_config; +use crate::interval::helpers::{advance_epoch, change_interval_config}; use crate::interval::pending_events::ContractExecutableEvent; use crate::interval::storage::push_new_interval_event; use crate::mixnet_contract_settings::storage::ADMIN; -use crate::mixnodes::transactions::update_mixnode_layer; -use crate::rewards; -use crate::rewards::storage as rewards_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::nodes::storage::{read_rewarded_set_metadata, reset_inactive_metadata}; +use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_authorized, }; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Order, Response, Storage}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_advance_epoch_event, new_epoch_transition_start_event, + new_advance_epoch_event, new_assigned_role_event, new_epoch_transition_start_event, new_pending_epoch_events_execution_event, new_pending_interval_config_update_event, new_pending_interval_events_execution_event, new_reconcile_pending_events, }; +use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::pending_events::PendingIntervalEventKind; -use mixnet_contract_common::{EpochState, EpochStatus, LayerAssignment, MixId}; -use std::collections::BTreeSet; +use mixnet_contract_common::{EpochState, EpochStatus, RoleAssignment}; // those two should be called in separate tx (from advancing epoch), // since there might be a lot of events to execute. @@ -176,51 +176,15 @@ pub fn try_reconcile_epoch_events( }; if progress { - current_epoch_status.state = EpochState::AdvancingEpoch; + current_epoch_status.state = EpochState::RoleAssignment { + next: Role::first(), + }; storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?; } Ok(response) } -fn update_rewarded_set( - storage: &mut dyn Storage, - new_rewarded_set: Vec, - expected_active_set_size: u32, -) -> Result<(), MixnetContractError> { - let reward_params = rewards_storage::REWARDING_PARAMS.load(storage)?; - - // the rewarded set has been determined based off active set size taken from the contract, - // thus the expected value HAS TO match - if expected_active_set_size != reward_params.active_set_size { - return Err(MixnetContractError::UnexpectedActiveSetSize { - received: expected_active_set_size, - expected: reward_params.active_set_size, - }); - } - - if new_rewarded_set.len() as u32 > reward_params.rewarded_set_size { - return Err(MixnetContractError::UnexpectedRewardedSetSize { - received: new_rewarded_set.len() as u32, - expected: reward_params.rewarded_set_size, - }); - } - - // check for duplicates - let mut tmp_set = BTreeSet::new(); - for node_id in &new_rewarded_set { - if !tmp_set.insert(node_id) { - return Err(MixnetContractError::DuplicateRewardedSetNode { mix_id: *node_id }); - } - } - - Ok(storage::update_rewarded_set( - storage, - expected_active_set_size, - new_rewarded_set, - )?) -} - pub fn try_begin_epoch_transition( deps: DepsMut<'_>, env: Env, @@ -231,31 +195,29 @@ pub fn try_begin_epoch_transition( // can't do pre-mature epoch transition... let current_interval = storage::current_interval(deps.storage)?; - if !current_interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_interval.current_epoch_start_unix_timestamp(), - epoch_end: current_interval.current_epoch_end_unix_timestamp(), - }); - } + current_interval.ensure_current_epoch_is_over(&env)?; // ensure some other validator (currently not a problem), hasn't already committed to epoch progression ensure_epoch_in_progress_state(deps.storage)?; - // Note: if at any point we decide to change our rewarded set to be few thousand nodes - // and the below call fails, we'll have to pass `last_node_in_rewarded_set` as an argument to this function - // and then verify whether the provided value is valid (by using range iterator on `REWARDED_SET` - // and checking if there are any other entries following the provided value) - let rewarded_set = storage::REWARDED_SET - .range(deps.storage, None, None, Order::Ascending) - .map(|kv| kv.map(|kv| kv.0)) - .collect::, _>>()?; + let metadata = read_rewarded_set_metadata(deps.storage)?; + + // TODO: with pre-announcing rewarded set, this will have to happen elsewhere + reset_inactive_metadata( + deps.storage, + current_interval.current_epoch_absolute_id() + 1, + )?; + + // make sure to reset the submitted work for this epoch (since it's 0 now) + RewardingStorage::load().reset_cumulative_epoch_work(deps.storage)?; + + let final_node_id = metadata.highest_rewarded_id(); // if there are no nodes to reward (i.e. empty rewarded set), we go straight into event reconciliation - let new_epoch_state = if let Some(last) = rewarded_set.last() { + let new_epoch_state = if final_node_id != 0 { EpochState::Rewarding { last_rewarded: 0, - final_node_id: *last, + final_node_id, } } else { EpochState::ReconcilingEvents @@ -271,51 +233,51 @@ pub fn try_begin_epoch_transition( Ok(Response::new().add_event(new_epoch_transition_start_event(current_interval))) } -pub fn try_advance_epoch( +pub fn try_assign_roles( deps: DepsMut<'_>, env: Env, info: MessageInfo, - layer_assignments: Vec, - expected_active_set_size: u32, + assignment: RoleAssignment, ) -> Result { // Only rewarding validator can attempt to advance epoch let mut current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?; - current_epoch_status.ensure_is_in_advancement_state()?; + current_epoch_status.ensure_is_in_expected_role_assignment_state(assignment.role)?; - // we must make sure that we roll into new epoch / interval with up to date state - // with no pending actions (like somebody wanting to update their profit margin) - let current_interval = storage::current_interval(deps.storage)?; - if !current_interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_interval.current_epoch_start_unix_timestamp(), - epoch_end: current_interval.current_epoch_end_unix_timestamp(), - }); - } + let role = assignment.role; + let assigned = assignment.nodes.len() as u32; - // if the current interval is over, apply reward pool changes - if current_interval.is_current_interval_over(&env) { - // this one is a very important one! - rewards::helpers::apply_reward_pool_changes(deps.storage)?; - } + let rewarded_set_params = RewardingStorage::load() + .global_rewarding_params + .load(deps.storage)? + .rewarded_set; - let updated_interval = current_interval.advance_epoch(); - let num_nodes = layer_assignments.len(); + // make sure we're not attempting to assign too many nodes to particular role + rewarded_set_params.ensure_role_count(role, assigned)?; - let new_rewarded_set = layer_assignments.iter().map(|l| l.mix_id()).collect(); + let next = assignment.role.next(); - // finally save updated interval and the rewarded set - storage::save_interval(deps.storage, &updated_interval)?; - update_rewarded_set(deps.storage, new_rewarded_set, expected_active_set_size)?; + // save the nodes for this layer + nymnodes_storage::save_assignment(deps.storage, assignment)?; - for a in layer_assignments { - update_mixnode_layer(a.mix_id(), a.layer(), deps.storage)?; - } + // TODO: optimise: if next is standby and standby set is empty, immediately advance + let event = match next { + Some(next_roles) => { + // update the state for the next assignment + current_epoch_status.state = EpochState::RoleAssignment { next: next_roles }; + new_assigned_role_event(role, assigned) + } + None => { + // the last role has been assigned => we're ready to progress into the next epoch + nymnodes_storage::swap_active_role_bucket(deps.storage)?; + let epoch_id = advance_epoch(deps.storage, env)?; + current_epoch_status.state = EpochState::InProgress; + new_advance_epoch_event(epoch_id) + } + }; - current_epoch_status.state = EpochState::InProgress; storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?; - Ok(Response::new().add_event(new_advance_epoch_event(updated_interval, num_nodes as u32))) + Ok(Response::new().add_event(event)) } pub(crate) fn try_update_interval_config( @@ -370,10 +332,13 @@ pub(crate) fn try_update_interval_config( #[cfg(test)] mod tests { use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::rewards::storage as rewards_storage; use crate::support::tests::fixtures; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::Addr; use mixnet_contract_common::pending_events::PendingEpochEventKind; + use mixnet_contract_common::NodeId; fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { // if you attempt to undelegate non-existent delegation, @@ -381,7 +346,7 @@ mod tests { let env = test.env(); for i in 0..n { let dummy_action = - PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as MixId); + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as NodeId); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } } @@ -392,8 +357,8 @@ mod tests { let env = test.env(); for i in 0..n { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { - mix_id: i as MixId, - new_costs: fixtures::mix_node_cost_params_fixture(), + mix_id: i as NodeId, + new_costs: fixtures::node_cost_params_fixture(), }; storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } @@ -402,7 +367,7 @@ mod tests { #[cfg(test)] mod performing_pending_epoch_actions { use super::*; - use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::fixtures::{active_set_update_fixture, TEST_COIN_DENOM}; use cosmwasm_std::{coin, coins, BankMsg, Empty, SubMsg}; use mixnet_contract_common::events::{ new_active_set_update_event, new_delegation_on_unbonded_node_event, @@ -470,7 +435,9 @@ mod tests { ); push_n_dummy_epoch_actions(&mut test, 10); - let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + let action_with_event = PendingEpochEventKind::UpdateActiveSet { + update: active_set_update_fixture(), + }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) .unwrap(); push_n_dummy_epoch_actions(&mut test, 10); @@ -478,7 +445,10 @@ mod tests { perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); assert_eq!( res, - Response::new().add_event(new_active_set_update_event(env.block.height, 50)) + Response::new().add_event(new_active_set_update_event( + env.block.height, + active_set_update_fixture() + )) ); assert_eq!(executed, 21); assert_eq!( @@ -494,7 +464,7 @@ mod tests { let mut test = TestSetup::new(); let env = test.env(); - let legit_mix = test.add_dummy_mixnode("mix-owner", None); + let legit_mix = test.add_legacy_mixnode("mix-owner", None); let delegator = Addr::unchecked("delegator"); let amount = 123_456_789u128; test.add_immediate_delegation(delegator.as_str(), amount, legit_mix); @@ -522,10 +492,15 @@ mod tests { })); // updating active set should only emit events and no cosmos messages - let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + let action_with_event = PendingEpochEventKind::UpdateActiveSet { + update: active_set_update_fixture(), + }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) .unwrap(); - expected_events.push(new_active_set_update_event(env.block.height, 50)); + expected_events.push(new_active_set_update_event( + env.block.height, + active_set_update_fixture(), + )); // undelegation just returns tokens and emits event let legit_undelegate = @@ -613,10 +588,10 @@ mod tests { use crate::support::tests::fixtures::TEST_COIN_DENOM; use cosmwasm_std::{coin, Empty, SubMsg}; use mixnet_contract_common::events::{ - new_interval_config_update_event, new_mixnode_cost_params_update_event, + new_cost_params_update_event, new_interval_config_update_event, new_rewarding_params_update_event, }; - use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; use mixnet_contract_common::Percent; @@ -682,7 +657,7 @@ mod tests { push_n_dummy_interval_actions(&mut test, 10); let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let action_with_event = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -717,25 +692,30 @@ mod tests { let mut expected_events = Vec::new(); let expected_messages: Vec> = Vec::new(); - let legit_mix = test.add_dummy_mixnode("mix-owner", None); - let new_costs = MixNodeCostParams { + let legit_mix = test.add_legacy_mixnode("mix-owner", None); + let new_costs = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(12).unwrap(), interval_operating_cost: coin(123_000, TEST_COIN_DENOM), }; + // this would have been normally populated when creating the event itself + mixnodes_storage::PENDING_MIXNODE_CHANGES + .save(test.deps_mut().storage, legit_mix, &Default::default()) + .unwrap(); + let cost_change = PendingIntervalEventKind::ChangeMixCostParams { mix_id: legit_mix, new_costs: new_costs.clone(), }; storage::push_new_interval_event(test.deps_mut().storage, &env, cost_change).unwrap(); - expected_events.push(new_mixnode_cost_params_update_event( + expected_events.push(new_cost_params_update_event( env.block.height, legit_mix, &new_costs, )); let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -858,7 +838,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -972,6 +954,7 @@ mod tests { new_delegation_on_unbonded_node_event, new_rewarding_params_update_event, }; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + use nym_contracts_common::Percent; #[test] fn can_only_be_performed_if_in_reconciling_state() { @@ -981,7 +964,9 @@ mod tests { last_rewarded: 0, final_node_id: 0, }, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1019,7 +1004,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1068,7 +1055,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1092,7 +1081,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1312,7 +1303,7 @@ mod tests { // interval event let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -1354,89 +1345,24 @@ mod tests { } } - #[test] - fn updating_rewarded_set() { - // the actual logic behind writing stuff to the storage has been tested in - // different unit test - let mut test = TestSetup::new(); - let current_active_set = test.rewarding_params().active_set_size; - let current_rewarded_set = test.rewarding_params().rewarded_set_size; - - // active set size has to match the expectation - let err = update_rewarded_set( - test.deps_mut().storage, - vec![1, 2, 3], - current_active_set - 10, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::UnexpectedActiveSetSize { - received: current_active_set - 10, - expected: current_active_set, - } - ); - - // number of nodes provided has to be equal or smaller than the current rewarded set size - - // fewer nodes - let res = update_rewarded_set(test.deps_mut().storage, vec![1, 2, 3], current_active_set); - assert!(res.is_ok()); - - let exact_num = (1u32..) - .take(current_rewarded_set as usize) - .collect::>(); - let res = update_rewarded_set(test.deps_mut().storage, exact_num, current_active_set); - assert!(res.is_ok()); - - // one more - let too_many = (1u32..) - .take((current_rewarded_set + 1) as usize) - .collect::>(); - let err = - update_rewarded_set(test.deps_mut().storage, too_many, current_active_set).unwrap_err(); - assert_eq!( - err, - MixnetContractError::UnexpectedRewardedSetSize { - received: current_rewarded_set + 1, - expected: current_rewarded_set, - } - ); - - // doesn't allow for duplicates - let nodes_with_duplicate = vec![1, 2, 3, 4, 5, 1]; - let err = update_rewarded_set( - test.deps_mut().storage, - nodes_with_duplicate, - current_active_set, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::DuplicateRewardedSetNode { mix_id: 1 } - ); - let nodes_with_duplicate = vec![1, 2, 3, 5, 4, 5]; - let err = update_rewarded_set( - test.deps_mut().storage, - nodes_with_duplicate, - current_active_set, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::DuplicateRewardedSetNode { mix_id: 5 } - ); - } - #[cfg(test)] - mod advancing_epoch { + mod assigning_roles { use super::*; - use crate::mixnodes::queries::query_mixnode_details; - use crate::rewards::models::RewardPoolChange; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; - use mixnet_contract_common::{Layer, RewardedSetNodeStatus}; + use cosmwasm_std::Uint128; + + fn setup_test() -> TestSetup { + let mut test = TestSetup::new(); + + for i in 0..10 { + test.add_dummy_nymnode(&format!("node-owner-{i}"), None); + } + + test.skip_to_current_epoch_end(); + test.set_epoch_role_assignment_state(); + + test + } #[test] fn can_only_be_performed_if_in_advancing_epoch_state() { @@ -1451,10 +1377,9 @@ mod tests { for bad_state in bad_states { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; + test.add_legacy_mixnode("1", Some(Uint128::new(100000000))); + test.add_legacy_mixnode("2", Some(Uint128::new(100000000))); + test.add_legacy_mixnode("3", Some(Uint128::new(100000000))); test.skip_to_current_epoch_end(); @@ -1462,24 +1387,17 @@ mod tests { status.state = bad_state; storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let role_assignment = RoleAssignment { + role: Role::Layer1, + nodes: vec![1, 2, 3], + }; let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, sender, role_assignment); assert_eq!( res, - Err(MixnetContractError::EpochNotInAdvancementState { + Err(MixnetContractError::EpochNotInRoleAssignmentState { current_state: bad_state }) ); @@ -1489,276 +1407,381 @@ mod tests { #[test] fn epoch_state_is_correctly_updated() { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; - test.skip_to_current_epoch_end(); - test.set_epoch_advancement_state(); + test.set_epoch_role_assignment_state(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let cases = vec![ + ( + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![1, 2, 3], + }, + EpochState::RoleAssignment { + next: Role::EntryGateway, + }, + ), + ( + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![4, 5, 6], + }, + EpochState::RoleAssignment { next: Role::Layer1 }, + ), + ( + RoleAssignment { + role: Role::Layer1, + nodes: vec![7, 8, 9], + }, + EpochState::RoleAssignment { next: Role::Layer2 }, + ), + ( + RoleAssignment { + role: Role::Layer2, + nodes: vec![9, 10, 11], + }, + EpochState::RoleAssignment { next: Role::Layer3 }, + ), + ( + RoleAssignment { + role: Role::Layer3, + nodes: vec![12], + }, + EpochState::RoleAssignment { + next: Role::Standby, + }, + ), + ( + RoleAssignment { + role: Role::Standby, + nodes: vec![42], + }, + EpochState::InProgress, + ), ]; - let env = test.env(); - let sender = test.rewarding_validator(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); + for (assignment, expected) in cases { + let env = test.env(); + let sender = test.rewarding_validator(); + try_assign_roles(test.deps_mut(), env, sender, assignment).unwrap(); - let expected = EpochStatus { - being_advanced_by: test.rewarding_validator().sender, - state: EpochState::InProgress, - }; - assert_eq!( - expected, - storage::current_epoch_status(test.deps().storage).unwrap() - ) + let expected = EpochStatus { + being_advanced_by: test.rewarding_validator().sender, + state: expected, + }; + assert_eq!( + expected, + storage::current_epoch_status(test.deps().storage).unwrap() + ); + } } #[test] fn can_only_be_performed_by_specified_rewarding_validator() { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; + test.add_dummy_nymnode("1", Some(Uint128::new(100000000))); + test.add_dummy_nymnode("2", Some(Uint128::new(100000000))); + test.add_dummy_nymnode("3", Some(Uint128::new(100000000))); let some_sender = mock_info("foomper", &[]); test.skip_to_current_epoch_end(); - test.set_epoch_advancement_state(); + test.set_epoch_role_assignment_state(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let role_assignment = RoleAssignment { + role: Role::first(), + nodes: vec![1, 2, 3], + }; let env = test.env(); - let res = try_advance_epoch( - test.deps_mut(), - env, - some_sender, - layer_assignments.clone(), - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, some_sender, role_assignment.clone()); assert_eq!(res, Err(MixnetContractError::Unauthorized)); // good address (sanity check) let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, sender, role_assignment); assert!(res.is_ok()) } #[test] - fn can_only_be_performed_if_epoch_is_over() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); + fn has_maximum_nodes_per_role() -> anyhow::Result<()> { + fn nodes_vec(start: NodeId, count: u32) -> Vec { + (start..start + count).collect() + } - let current_active_set = test.rewarding_params().active_set_size; + let mut test = setup_test(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, ]; let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender.clone(), - layer_assignments.clone(), - current_active_set, - ); - assert!(matches!( - res, - Err(MixnetContractError::EpochInProgress { .. }) - )); - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 1).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::One - ); + for role in roles { + let max_count = test.max_role_count(role); - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 2).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::Two - ); - - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 3).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::Three - ); - - // sanity check - test.skip_to_current_epoch_end(); - - let env = test.env(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); - assert!(res.is_ok()) - } - - #[test] - fn if_interval_is_over_applies_reward_pool_changes() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); - - let current_active_set = test.rewarding_params().active_set_size; - - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - - let start_params = test.rewarding_params(); - - let pool_update = Decimal::from_atomics(100_000_000u32, 0).unwrap(); - // push some changes - rewards_storage::PENDING_REWARD_POOL_CHANGE - .save( - test.deps_mut().storage, - &RewardPoolChange { - removed: pool_update, - added: Default::default(), + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: nodes_vec(1, max_count + 1), }, - ) - .unwrap(); + ); + assert_eq!( + res.unwrap_err(), + MixnetContractError::IllegalRoleCount { + role, + assigned: max_count + 1, + allowed: max_count, + } + ); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: nodes_vec(1, max_count), + }, + ); + assert!(res.is_ok()); + } - // end of epoch - nothing has happened - let sender = test.rewarding_validator(); - test.skip_to_current_epoch_end(); - - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments.clone(), - current_active_set, - ) - .unwrap(); - - let params = test.rewarding_params(); - let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE - .load(test.deps().storage) - .unwrap(); - assert_eq!(params, start_params); - assert_eq!(pool_change.removed, pool_update); - - let sender = test.rewarding_validator(); - test.skip_to_current_interval_end(); - test.set_epoch_advancement_state(); - - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); - - let epochs_in_interval = test.current_interval().epochs_in_interval(); - let update = IntervalRewardingParamsUpdate { - reward_pool: Some(start_params.interval.reward_pool - pool_update), - staking_supply: Some(start_params.interval.staking_supply + pool_update), - ..Default::default() - }; - let mut expected = start_params; - expected - .try_apply_updates(update, epochs_in_interval) - .unwrap(); - - let params = test.rewarding_params(); - let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE - .load(test.deps().storage) - .unwrap(); - assert_eq!(params, expected); - assert_eq!(pool_change.removed, Decimal::zero()); + Ok(()) } #[test] - fn updates_rewarded_set_and_interval_data() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); + fn cant_be_performed_out_of_order() -> anyhow::Result<()> { + let mut test = setup_test(); - let current_active_set = test.rewarding_params().active_set_size; + let env = test.env(); + let sender = test.rewarding_validator(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - - let interval_pre = test.current_interval(); - let rewarded_set_pre = test.rewarded_set(); - assert!(rewarded_set_pre.is_empty()); - - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let expected_order = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, ]; - let sender = test.rewarding_validator(); - test.skip_to_current_interval_end(); - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); + for (i, role) in expected_order.iter().enumerate() { + let wrong_role = if role == &Role::Layer1 { + Role::Layer2 + } else { + Role::Layer1 + }; - let interval_post = test.current_interval(); - let rewarded_set = test.rewarded_set(); + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: wrong_role, + nodes: vec![i as u32], + }, + ); + assert_eq!( + res.unwrap_err(), + MixnetContractError::UnexpectedRoleAssignment { + expected: *role, + got: wrong_role + } + ); - let expected_id = interval_pre.current_epoch_absolute_id() + 1; - assert_eq!(interval_post.current_epoch_absolute_id(), expected_id); - assert_eq!( - rewarded_set, - vec![ - (1, RewardedSetNodeStatus::Active), - (2, RewardedSetNodeStatus::Active), - (3, RewardedSetNodeStatus::Active) - ] - ); + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: *role, + nodes: vec![i as u32], + }, + ); + assert!(res.is_ok()); + } + + Ok(()) + } + + #[cfg(test)] + mod correctly_updates_storage { + use super::*; + use mixnet_contract_common::nym_node::RoleMetadata; + + fn perform_partial_assignment(test: &mut TestSetup) -> anyhow::Result<()> { + let env = test.env(); + let sender = test.rewarding_validator(); + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![1, 2, 3], + }, + )?; + + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![4, 5, 6], + }, + )?; + + try_assign_roles( + test.deps_mut(), + env, + sender, + RoleAssignment { + role: Role::Layer1, + nodes: vec![7, 8], + }, + )?; + + Ok(()) + } + + #[test] + fn updates_metadata() -> anyhow::Result<()> { + let mut test = setup_test(); + + let initial = test.inactive_roles_metadata(); + + // initial state + let empty = RoleMetadata::default(); + assert_eq!(empty, initial.entry_gateway_metadata); + assert_eq!(empty, initial.layer1_metadata); + assert_eq!(empty, initial.layer2_metadata); + assert_eq!(empty, initial.layer3_metadata); + assert_eq!(empty, initial.exit_gateway_metadata); + assert_eq!(empty, initial.standby_metadata); + + perform_partial_assignment(&mut test)?; + + let updated = test.inactive_roles_metadata(); + assert_eq!(3, updated.exit_gateway_metadata.highest_id); + assert_eq!(3, updated.exit_gateway_metadata.num_nodes); + assert_eq!(6, updated.entry_gateway_metadata.highest_id); + assert_eq!(3, updated.entry_gateway_metadata.num_nodes); + assert_eq!(8, updated.layer1_metadata.highest_id); + assert_eq!(2, updated.layer1_metadata.num_nodes); + + assert_eq!(empty, updated.layer2_metadata); + assert_eq!(empty, updated.layer3_metadata); + assert_eq!(empty, updated.standby_metadata); + + Ok(()) + } + + #[test] + fn updates_role_data() -> anyhow::Result<()> { + let mut test = setup_test(); + + assert!(test.inactive_roles(Role::ExitGateway).is_empty()); + assert!(test.inactive_roles(Role::EntryGateway).is_empty()); + assert!(test.inactive_roles(Role::Layer1).is_empty()); + assert!(test.inactive_roles(Role::Layer2).is_empty()); + assert!(test.inactive_roles(Role::Layer3).is_empty()); + assert!(test.inactive_roles(Role::Standby).is_empty()); + + perform_partial_assignment(&mut test)?; + + assert_eq!(3, test.inactive_roles(Role::ExitGateway).len()); + assert_eq!(3, test.inactive_roles(Role::EntryGateway).len()); + assert_eq!(2, test.inactive_roles(Role::Layer1).len()); + assert!(test.inactive_roles(Role::Layer2).is_empty()); + assert!(test.inactive_roles(Role::Layer3).is_empty()); + assert!(test.inactive_roles(Role::Standby).is_empty()); + + Ok(()) + } + + #[test] + fn updates_epoch_status() -> anyhow::Result<()> { + let mut test = setup_test(); + + let env = test.env(); + let sender = test.rewarding_validator(); + + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ]; + + for (i, role) in roles.into_iter().enumerate() { + let expected_next = role.next(); + + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: vec![i as u32], + }, + )?; + + let state = test.epoch_state(); + match expected_next { + None => assert_eq!(state, EpochState::InProgress), + Some(next) => assert_eq!(state, EpochState::RoleAssignment { next }), + } + } + + Ok(()) + } + + #[test] + fn swaps_roles_buckets_after_final_role() -> anyhow::Result<()> { + let mut test = setup_test(); + + let env = test.env(); + let sender = test.rewarding_validator(); + + let active = test.active_roles_bucket(); + + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ]; + + for (i, role) in roles.into_iter().enumerate() { + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: vec![i as u32], + }, + )?; + } + + assert_eq!(test.active_roles_bucket(), active.other()); + + Ok(()) + } } } @@ -1778,7 +1801,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 985cffda57..8e1450421d 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -1,17 +1,19 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] -mod constants; +pub(crate) mod compat; +pub mod constants; pub mod contract; mod delegations; -mod families; mod gateways; mod interval; mod mixnet_contract_settings; mod mixnodes; +mod nodes; mod queued_migrations; mod rewards; pub mod signing; diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 5d80fec78a..415704d04a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -49,9 +49,8 @@ pub(crate) mod tests { vesting_contract_address: Addr::unchecked("foomp"), rewarding_denom: "unym".to_string(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: coin(123u128, "unym"), - minimum_gateway_pledge: coin(456u128, "unym"), + minimum_delegation: None, + minimum_pledge: coin(123u128, "unym"), profit_margin: Default::default(), interval_operating_cost: Default::default(), }, diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 0c8502089e..3897585856 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -7,7 +7,9 @@ use cosmwasm_std::{Coin, StdResult}; use cw_controllers::Admin; use cw_storage_plus::Item; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ContractState, OperatingCostRange, ProfitMarginRange}; +use mixnet_contract_common::{ + ContractState, ContractStateParams, OperatingCostRange, ProfitMarginRange, +}; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); pub(crate) const ADMIN: Admin = Admin::new(ADMIN_STORAGE_KEY); @@ -18,16 +20,10 @@ pub fn rewarding_validator_address(storage: &dyn Storage) -> Result Result { +pub(crate) fn minimum_node_pledge(storage: &dyn Storage) -> Result { Ok(CONTRACT_STATE .load(storage) - .map(|state| state.params.minimum_mixnode_pledge)?) -} - -pub(crate) fn minimum_gateway_pledge(storage: &dyn Storage) -> Result { - Ok(CONTRACT_STATE - .load(storage) - .map(|state| state.params.minimum_gateway_pledge)?) + .map(|state| state.params.minimum_pledge)?) } pub(crate) fn profit_margin_range( @@ -52,7 +48,7 @@ pub(crate) fn minimum_delegation_stake( ) -> Result, MixnetContractError> { Ok(CONTRACT_STATE .load(storage) - .map(|state| state.params.minimum_mixnode_delegation)?) + .map(|state| state.params.minimum_delegation)?) } pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result { @@ -67,6 +63,12 @@ pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result Result { + Ok(CONTRACT_STATE.load(storage).map(|state| state.params)?) +} + pub(crate) fn initialise_storage( deps: DepsMut<'_>, initial_state: ContractState, diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index b458f6fbbc..16750eb44c 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -75,7 +75,7 @@ pub(crate) fn try_update_contract_settings( #[cfg(test)] pub mod tests { use super::*; - use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; + use crate::constants::INITIAL_PLEDGE_AMOUNT; use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::support::tests::test_helpers; @@ -129,14 +129,10 @@ pub mod tests { let denom = rewarding_denom(deps.as_ref().storage).unwrap(); let new_params = ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: denom.clone(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom, - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT + Uint128::new(1234), + amount: Uint128::new(12345), }, profit_margin: Default::default(), interval_operating_cost: Default::default(), diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index b8dd15ae6e..fc2d56254f 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -2,15 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::interval::storage as interval_storage; -use crate::mixnodes::storage::{assign_layer, next_mixnode_id_counter}; use crate::rewards::storage as rewards_storage; -use cosmwasm_std::{Addr, Coin, Decimal, Env, StdResult, Storage}; +use cosmwasm_std::{Addr, Decimal, Env, StdResult, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::{ - MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode, -}; -use mixnet_contract_common::{IdentityKey, Layer, MixId, MixNode, MixNodeBond}; +use mixnet_contract_common::mixnode::{MixNodeDetails, UnbondedMixnode}; +use mixnet_contract_common::{IdentityKey, MixNodeBond, NodeId}; pub(crate) fn must_get_mixnode_bond_by_owner( store: &dyn Storage, @@ -50,7 +46,7 @@ pub(crate) fn attach_mix_details( pub(crate) fn get_mixnode_details_by_id( store: &dyn Storage, - mix_id: MixId, + mix_id: NodeId, ) -> StdResult> { if let Some(bond_information) = storage::mixnode_bonds().may_load(store, mix_id)? { attach_mix_details(store, bond_information).map(Some) @@ -91,38 +87,13 @@ pub(crate) fn get_mixnode_details_by_identity( } } -pub(crate) fn save_new_mixnode( - storage: &mut dyn Storage, - env: Env, - mixnode: MixNode, - cost_params: MixNodeCostParams, - owner: Addr, - pledge: Coin, -) -> Result<(MixId, Layer), MixnetContractError> { - let layer = assign_layer(storage)?; - let mix_id = next_mixnode_id_counter(storage)?; - let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); - - let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; - let mixnode_bond = MixNodeBond::new(mix_id, owner, pledge, layer, mixnode, env.block.height); - - // save mixnode bond data - // note that this implicitly checks for uniqueness on identity key, sphinx key and owner - storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; - - // save rewarding data - rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; - - Ok((mix_id, layer)) -} - pub(crate) fn cleanup_post_unbond_mixnode_storage( storage: &mut dyn Storage, env: &Env, current_details: &MixNodeDetails, ) -> Result<(), MixnetContractError> { let mix_id = current_details.bond_information.mix_id; - // remove all bond information (we don't need it anymore + // remove all bond information since we don't need it anymore // note that "normal" remove is `may_load` followed by `replace` with a `None` // and we have already loaded the data from the storage storage::mixnode_bonds().replace( @@ -160,20 +131,17 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( unbonding_height: env.block.height, }, )?; - storage::decrement_layer_count(storage, current_details.bond_information.layer) + Ok(()) } #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::support::tests::fixtures::{ - mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, - }; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::{coin, Uint128}; + use cosmwasm_std::Uint128; pub(crate) struct DummyMixnode { - pub mix_id: MixId, + pub mix_id: NodeId, pub owner: Addr, pub identity: IdentityKey, } @@ -375,101 +343,12 @@ pub(crate) mod tests { assert!(res.is_none()); } - #[test] - fn saving_new_mixnode() { - let mut test = TestSetup::new(); - - // get some mixnodes in - test.add_dummy_mixnode("owner1", None); - test.add_dummy_mixnode("owner2", None); - test.add_dummy_mixnode("owner3", None); - test.add_dummy_mixnode("owner4", None); - test.add_dummy_mixnode("owner5", None); - - let env = test.env(); - let id_key = "identity-key"; - let sphinx_key = "sphinx-key"; - let mut mixnode = mix_node_fixture(); - mixnode.identity_key = id_key.into(); - mixnode.sphinx_key = sphinx_key.into(); - let cost_params = mix_node_cost_params_fixture(); - let owner = Addr::unchecked("mix-owner"); - let pledge = coin(100_000_000, TEST_COIN_DENOM); - - let (id, layer) = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - owner.clone(), - pledge.clone(), - ) - .unwrap(); - assert_eq!(id, 6); - assert_eq!(layer, Layer::Three); - - assert_eq!( - storage::MIXNODE_ID_COUNTER - .load(test.deps().storage) - .unwrap(), - 6 - ); - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer3, 2); - let mix_details = get_mixnode_details_by_id(test.deps().storage, id) - .unwrap() - .unwrap(); - assert_eq!(mix_details.mix_id(), id); - assert_eq!(mix_details.original_pledge(), &pledge); - assert_eq!( - mix_details.bond_information.bonding_height, - env.block.height - ); - - // try to add node with duplicate identity... - let mut mixnode = mix_node_fixture(); - mixnode.identity_key = id_key.into(); - let res = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - Addr::unchecked("different-owner"), - pledge.clone(), - ); - assert!(res.is_err()); - - // and duplicate owner... - let mixnode = mix_node_fixture(); - let res = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - owner, - pledge.clone(), - ); - assert!(res.is_err()); - - // and duplicate sphinx key... - let mut mixnode = mix_node_fixture(); - mixnode.sphinx_key = sphinx_key.into(); - let res = save_new_mixnode( - test.deps_mut().storage, - env, - mixnode, - cost_params, - Addr::unchecked("different-owner"), - pledge, - ); - assert!(res.is_err()); - } - #[test] fn cleaning_post_unbond_storage() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let mix_id_leftover = test.add_dummy_mixnode("mix-owner-leftover", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let mix_id_leftover = test.add_legacy_mixnode("mix-owner-leftover", None); // manually adjust delegation info as to indicate the rewarding information shouldnt get removed let mut rewarding_details = test.mix_rewarding(mix_id_leftover); @@ -516,9 +395,6 @@ pub(crate) mod tests { }; assert_eq!(unbonded_details, expected); - // layers are decremented - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer1, 0); - let details2 = get_mixnode_details_by_id(test.deps().storage, mix_id_leftover) .unwrap() .unwrap(); @@ -549,8 +425,5 @@ pub(crate) mod tests { unbonding_height: env.block.height, }; assert_eq!(unbonded_details, expected); - - // layers are decremented - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer2, 0); } } diff --git a/contracts/mixnet/src/mixnodes/mod.rs b/contracts/mixnet/src/mixnodes/mod.rs index ebff4a6ace..f3a3c9941f 100644 --- a/contracts/mixnet/src/mixnodes/mod.rs +++ b/contracts/mixnet/src/mixnodes/mod.rs @@ -3,6 +3,5 @@ pub mod helpers; pub mod queries; -pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index 5d7b61054d..8300edf5f1 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -15,17 +15,17 @@ use crate::rewards::storage as rewards_storage; use cosmwasm_std::{Deps, Order, StdResult, Storage}; use cw_storage_plus::Bound; use mixnet_contract_common::mixnode::{ - MixNodeBond, MixNodeDetails, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixNodeBond, MixNodeDetails, MixStakeSaturationResponse, MixnodeRewardingDetailsResponse, + PagedMixnodesDetailsResponse, PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, }; use mixnet_contract_common::{ - IdentityKey, LayerDistribution, MixId, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, PagedMixnodeBondsResponse, + IdentityKey, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, + NodeId, PagedMixnodeBondsResponse, }; pub fn query_mixnode_bonds_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -51,7 +51,7 @@ pub fn query_mixnode_bonds_paged( fn attach_node_details( storage: &dyn Storage, - read_bond: StdResult<(MixId, MixNodeBond)>, + read_bond: StdResult<(NodeId, MixNodeBond)>, ) -> StdResult { match read_bond { Ok((_, bond)) => attach_mix_details(storage, bond), @@ -61,7 +61,7 @@ fn attach_node_details( pub fn query_mixnodes_details_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -87,7 +87,7 @@ pub fn query_mixnodes_details_paged( pub fn query_unbonded_mixnodes_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -113,7 +113,7 @@ pub fn query_unbonded_mixnodes_paged( pub fn query_unbonded_mixnodes_by_owner_paged( deps: Deps<'_>, owner: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let owner = deps.api.addr_validate(&owner)?; @@ -144,7 +144,7 @@ pub fn query_unbonded_mixnodes_by_owner_paged( pub fn query_unbonded_mixnodes_by_identity_paged( deps: Deps<'_>, identity_key: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -180,7 +180,7 @@ pub fn query_owned_mixnode(deps: Deps<'_>, address: String) -> StdResult, mix_id: MixId) -> StdResult { +pub fn query_mixnode_details(deps: Deps<'_>, mix_id: NodeId) -> StdResult { let mixnode_details = get_mixnode_details_by_id(deps.storage, mix_id)?; Ok(MixnodeDetailsResponse { @@ -203,7 +203,7 @@ pub fn query_mixnode_details_by_identity( pub fn query_mixnode_rewarding_details( deps: Deps<'_>, - mix_id: MixId, + mix_id: NodeId, ) -> StdResult { let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?; @@ -213,7 +213,10 @@ pub fn query_mixnode_rewarding_details( }) } -pub fn query_unbonded_mixnode(deps: Deps<'_>, mix_id: MixId) -> StdResult { +pub fn query_unbonded_mixnode( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { let unbonded_info = storage::unbonded_mixnodes().may_load(deps.storage, mix_id)?; Ok(UnbondedMixnodeResponse { @@ -222,11 +225,14 @@ pub fn query_unbonded_mixnode(deps: Deps<'_>, mix_id: MixId) -> StdResult, mix_id: MixId) -> StdResult { +pub fn query_stake_saturation( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { let mix_rewarding = match rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { Some(mix_rewarding) => mix_rewarding, None => { - return Ok(StakeSaturationResponse { + return Ok(MixStakeSaturationResponse { mix_id, current_saturation: None, uncapped_saturation: None, @@ -236,17 +242,13 @@ pub fn query_stake_saturation(deps: Deps<'_>, mix_id: MixId) -> StdResult) -> StdResult { - storage::LAYERS.load(deps.storage) -} - #[cfg(test)] pub(crate) mod tests { use super::*; @@ -303,7 +305,7 @@ pub(crate) mod tests { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id let mut test = TestSetup::new(); - test.add_dummy_mixnode("addr1", None); + test.add_legacy_mixnode("addr1", None); let per_page = 2; let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); @@ -312,13 +314,13 @@ pub(crate) mod tests { assert_eq!(1, page1.nodes.len()); // save another - test.add_dummy_mixnode("addr2", None); + test.add_legacy_mixnode("addr2", None); // page1 should have 2 results on it let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test.add_dummy_mixnode("addr3", None); + test.add_legacy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = @@ -334,7 +336,7 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - test.add_dummy_mixnode("addr4", None); + test.add_legacy_mixnode("addr4", None); let page2 = query_mixnode_bonds_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); @@ -393,7 +395,7 @@ pub(crate) mod tests { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id let mut test = TestSetup::new(); - test.add_dummy_mixnode("addr1", None); + test.add_legacy_mixnode("addr1", None); let per_page = 2; let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); @@ -402,13 +404,13 @@ pub(crate) mod tests { assert_eq!(1, page1.nodes.len()); // save another - test.add_dummy_mixnode("addr2", None); + test.add_legacy_mixnode("addr2", None); // page1 should have 2 results on it let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test.add_dummy_mixnode("addr3", None); + test.add_legacy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = @@ -425,7 +427,7 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - test.add_dummy_mixnode("addr4", None); + test.add_legacy_mixnode("addr4", None); let page2 = query_mixnodes_details_paged(test.deps(), Some(start_after), Some(per_page)) @@ -491,7 +493,7 @@ pub(crate) mod tests { #[test] fn pagination_works() { - fn add_unbonded(storage: &mut dyn Storage, id: MixId) { + fn add_unbonded(storage: &mut dyn Storage, id: NodeId) { storage::unbonded_mixnodes() .save( storage, @@ -557,7 +559,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_owner(storage: &mut dyn Storage, id: MixId, owner: &str) { + fn add_unbonded_with_owner(storage: &mut dyn Storage, id: NodeId, owner: &str) { storage::unbonded_mixnodes() .save( storage, @@ -789,7 +791,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_identity(storage: &mut dyn Storage, id: MixId, identity: &str) { + fn add_unbonded_with_identity(storage: &mut dyn Storage, id: NodeId, identity: &str) { storage::unbonded_mixnodes() .save( storage, @@ -1061,7 +1063,7 @@ pub(crate) mod tests { assert_eq!(address, res.address); // when it [fully] exists - let id = test.add_dummy_mixnode(&address, None); + let id = test.add_legacy_mixnode(&address, None); let res = query_owned_mixnode(test.deps(), address.clone()).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(address, details.bond_information.owner); @@ -1098,7 +1100,7 @@ pub(crate) mod tests { assert_eq!(42, res.mix_id); // it exists - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); let res = query_mixnode_details(test.deps(), mix_id).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(mix_id, details.bond_information.mix_id); @@ -1120,7 +1122,7 @@ pub(crate) mod tests { assert!(res.is_none()); // it exists - let mix_id = test.add_dummy_mixnode("owner", None); + let mix_id = test.add_legacy_mixnode("owner", None); // this was already tested to be working : ) let expected = query_mixnode_details(test.deps(), mix_id) .unwrap() @@ -1143,13 +1145,10 @@ pub(crate) mod tests { assert!(res.rewarding_details.is_none()); assert_eq!(42, res.mix_id); - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); let res = query_mixnode_rewarding_details(test.deps(), mix_id).unwrap(); let details = res.rewarding_details.unwrap(); - assert_eq!( - fixtures::mix_node_cost_params_fixture(), - details.cost_params - ); + assert_eq!(fixtures::node_cost_params_fixture(), details.cost_params); assert_eq!(mix_id, res.mix_id); } @@ -1165,7 +1164,7 @@ pub(crate) mod tests { assert_eq!(42, res.mix_id); // add and unbond the mixnode - let mix_id = test.add_dummy_mixnode(sender, None); + let mix_id = test.add_legacy_mixnode(sender, None); pending_events::unbond_mixnode(test.deps_mut(), &mock_env(), 123, mix_id).unwrap(); let res = query_unbonded_mixnode(test.deps(), mix_id).unwrap(); @@ -1188,7 +1187,7 @@ pub(crate) mod tests { .unwrap(); let saturation_point = rewarding_params.interval.stake_saturation_point; - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); // below saturation point // there's only the base pledge without any delegation diff --git a/contracts/mixnet/src/mixnodes/signature_helpers.rs b/contracts/mixnet/src/mixnodes/signature_helpers.rs deleted file mode 100644 index 04942aaff5..0000000000 --- a/contracts/mixnet/src/mixnodes/signature_helpers.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Coin, Deps}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ - construct_legacy_mixnode_bonding_sign_payload, construct_mixnode_bonding_sign_payload, MixNode, - MixNodeCostParams, -}; -use nym_contracts_common::signing::MessageSignature; -use nym_contracts_common::signing::Verifier; - -pub(crate) fn verify_mixnode_bonding_signature( - deps: Deps<'_>, - sender: Addr, - pledge: Coin, - mixnode: MixNode, - cost_params: MixNodeCostParams, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(&mixnode.identity_key)?; - - // reconstruct the payload, first try the current format, then attempt legacy - let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_mixnode_bonding_sign_payload( - nonce, - sender.clone(), - pledge.clone(), - mixnode.clone(), - cost_params.clone(), - ); - - if deps - .api - .verify_message(msg, signature.clone(), &public_key)? - { - Ok(()) - } else { - // attempt to use legacy - let msg_legacy = construct_legacy_mixnode_bonding_sign_payload( - nonce, - sender, - pledge, - mixnode, - cost_params, - ); - if deps - .api - .verify_message(msg_legacy, signature, &public_key)? - { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } - } -} diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index 2007456463..1a68bbebfb 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -2,30 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use crate::constants::{ - LAYER_DISTRIBUTION_KEY, MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, - MIXNODES_PK_NAMESPACE, MIXNODES_SPHINX_IDX_NAMESPACE, NODE_ID_COUNTER_KEY, - PENDING_MIXNODE_CHANGES_NAMESPACE, UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, - UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, UNBONDED_MIXNODES_PK_NAMESPACE, + MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, MIXNODES_PK_NAMESPACE, + MIXNODES_SPHINX_IDX_NAMESPACE, PENDING_MIXNODE_CHANGES_NAMESPACE, + UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, + UNBONDED_MIXNODES_PK_NAMESPACE, }; -use cosmwasm_std::{StdResult, Storage}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; -use mixnet_contract_common::error::MixnetContractError; +use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex, UniqueIndex}; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::SphinxKey; -use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixId, MixNodeBond}; +use mixnet_contract_common::{Addr, IdentityKey, MixNodeBond, NodeId}; -pub const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY); -pub const MIXNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); -pub const PENDING_MIXNODE_CHANGES: Map = +pub const PENDING_MIXNODE_CHANGES: Map = Map::new(PENDING_MIXNODE_CHANGES_NAMESPACE); // keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past mixnodes // if we ever decide it's too bloaty, we can deprecate it and start removing all data in // subsequent migrations pub(crate) struct UnbondedMixnodeIndex<'a> { - pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, MixId>, + pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, NodeId>, - pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, MixId>, + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, NodeId>, } impl<'a> IndexList for UnbondedMixnodeIndex<'a> { @@ -36,7 +32,7 @@ impl<'a> IndexList for UnbondedMixnodeIndex<'a> { } pub(crate) fn unbonded_mixnodes<'a>( -) -> IndexedMap<'a, MixId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { +) -> IndexedMap<'a, NodeId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { let indexes = UnbondedMixnodeIndex { owner: MultiIndex::new( |_pk, d| d.owner.clone(), @@ -71,7 +67,7 @@ impl<'a> IndexList for MixnodeBondIndex<'a> { } // mixnode_bonds() is the storage access function. -pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeBondIndex<'a>> { +pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, NodeId, MixNodeBond, MixnodeBondIndex<'a>> { let indexes = MixnodeBondIndex { owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE), identity_key: UniqueIndex::new( @@ -85,130 +81,3 @@ pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeB }; IndexedMap::new(MIXNODES_PK_NAMESPACE, indexes) } - -pub fn decrement_layer_count( - storage: &mut dyn Storage, - layer: Layer, -) -> Result<(), MixnetContractError> { - let mut layers = LAYERS.load(storage)?; - layers.decrement_layer_count(layer)?; - Ok(LAYERS.save(storage, &layers)?) -} - -pub(crate) fn assign_layer(store: &mut dyn Storage) -> StdResult { - // load current distribution - let mut layers = LAYERS.load(store)?; - - // choose the one with fewest nodes - let fewest = layers.choose_with_fewest(); - - // increment the existing count - layers.increment_layer_count(fewest); - - // and resave it - LAYERS.save(store, &layers)?; - Ok(fewest) -} - -pub(crate) fn next_mixnode_id_counter(store: &mut dyn Storage) -> StdResult { - let id: MixId = MIXNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; - MIXNODE_ID_COUNTER.save(store, &id)?; - Ok(id) -} - -pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> StdResult<()> { - LAYERS.save(storage, &LayerDistribution::default()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_dependencies; - - #[test] - fn decrementing_layer() { - let mut deps = test_helpers::init_contract(); - - // we never underflow, if it were to happen we're going to return an error instead - assert_eq!( - Err(MixnetContractError::OverflowSubtraction { - minuend: 0, - subtrahend: 1 - }), - decrement_layer_count(deps.as_mut().storage, Layer::One) - ); - - LAYERS - .save( - deps.as_mut().storage, - &LayerDistribution { - layer1: 3, - layer2: 2, - layer3: 1, - }, - ) - .unwrap(); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_ok()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - } - - #[test] - fn assigning_layer() { - let mut deps = test_helpers::init_contract(); - - let layers = LayerDistribution { - layer1: 3, - layer2: 2, - layer3: 1, - }; - LAYERS.save(deps.as_mut().storage, &layers).unwrap(); - - // always assigns layer with fewest nodes - assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(2, LAYERS.load(deps.as_ref().storage).unwrap().layer3); - - // we have 3, 2, 2, so the 2nd layer should get chosen now - assert_eq!(Layer::Two, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer2); - - // 3, 3, 2, so 3rd one again - assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer3); - } - - #[test] - fn next_id() { - let mut deps = test_helpers::init_contract(); - - for i in 1u32..1000 { - assert_eq!(i, next_mixnode_id_counter(deps.as_mut().storage).unwrap()); - } - } - - #[test] - fn initialising() { - let mut deps = mock_dependencies(); - assert!(LAYERS.load(deps.as_ref().storage).is_err()); - - initialise_storage(deps.as_mut().storage).unwrap(); - assert_eq!( - LayerDistribution::default(), - LAYERS.load(deps.as_ref().storage).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 59ebc432c0..fea26bc24e 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -1,149 +1,73 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response, Storage}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::events::{ - new_mixnode_bonding_event, new_mixnode_config_update_event, - new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, - new_pending_pledge_decrease_event, new_pending_pledge_increase_event, +use super::storage; +use crate::compat::helpers::{ + ensure_can_decrease_pledge, ensure_can_increase_pledge, ensure_can_modify_cost_params, }; -use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; -use mixnet_contract_common::{Layer, MixId, MixNode}; -use nym_contracts_common::signing::MessageSignature; - use crate::interval::storage as interval_storage; use crate::interval::storage::push_new_interval_event; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnet_contract_settings::storage::rewarding_denom; -use crate::mixnodes::helpers::{ - get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode, -}; -use crate::mixnodes::signature_helpers::verify_mixnode_bonding_signature; -use crate::signing::storage as signing_storage; +use crate::mixnodes::helpers::{get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner}; +use crate::nodes::storage as nymnodes_storage; +use crate::nodes::transactions::add_nym_node_inner; use crate::support::helpers::{ - ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond, - ensure_no_pending_pledge_changes, ensure_operating_cost_within_range, - ensure_profit_margin_within_range, validate_pledge, + ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_params_changes, + ensure_no_pending_pledge_changes, validate_pledge, }; +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_migrated_mixnode_event, new_mixnode_config_update_event, + new_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, + new_pending_pledge_decrease_event, new_pending_pledge_increase_event, +}; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; +use mixnet_contract_common::{ + MixNode, MixNodeDetails, MixnodeBondingPayload, NymNodeBond, PendingNodeChanges, +}; +use nym_contracts_common::signing::MessageSignature; -use super::storage; - -pub(crate) fn update_mixnode_layer( - mix_id: MixId, - layer: Layer, - storage: &mut dyn Storage, -) -> Result<(), MixnetContractError> { - let bond = if let Some(bond_information) = storage::mixnode_bonds().may_load(storage, mix_id)? { - bond_information - } else { - return Err(MixnetContractError::MixNodeBondNotFound { mix_id }); - }; - let mut updated_bond = bond.clone(); - updated_bond.layer = layer; - - storage::mixnode_bonds().replace(storage, bond.mix_id, Some(&updated_bond), Some(&bond))?; - Ok(()) -} - -pub fn assign_mixnode_layer( - deps: DepsMut<'_>, - info: MessageInfo, - mix_id: MixId, - layer: Layer, -) -> Result { - ensure_is_authorized(&info.sender, deps.storage)?; - - update_mixnode_layer(mix_id, layer, deps.storage)?; - - Ok(Response::default()) -} - -// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce -// so that we could return a better error message if it doesn't match? -pub(crate) fn try_add_mixnode( +pub fn try_add_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mixnode: MixNode, - cost_params: MixNodeCostParams, + mix_node: MixNode, + cost_params: NodeCostParams, owner_signature: MessageSignature, ) -> Result { - // ensure the profit margin is within the defined range - ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; + let signed_payload = MixnodeBondingPayload::new(mix_node.clone(), cost_params.clone()); - // ensure the operating cost is within the defined range - ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; - - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; - let pledge = validate_pledge(info.funds, minimum_pledge)?; - - // if the client has an active bonded mixnode or gateway, don't allow bonding - // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us - // against attempting to use different node types (i.e. gateways and mixnodes) - ensure_no_existing_bond(&info.sender, deps.storage)?; - - // there's no need to explicitly check whether there already exists mixnode with the same - // identity or sphinx keys as this is going to be done implicitly when attempting to save - // the bond information due to `UniqueIndex` constraint defined on those fields. - - // check if this sender actually owns the mixnode by checking the signature - verify_mixnode_bonding_signature( - deps.as_ref(), - info.sender.clone(), - pledge.clone(), - mixnode.clone(), - cost_params.clone(), - owner_signature, - )?; - - // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; - - let node_identity = mixnode.identity_key.clone(); - let (node_id, layer) = save_new_mixnode( - deps.storage, + // any mixnode added via 'BondMixnode' endpoint should get added as a NymNode + add_nym_node_inner( + deps, env, - mixnode, + info, + mix_node.into(), cost_params, - info.sender.clone(), - pledge.clone(), - )?; - - Ok(Response::new().add_event(new_mixnode_bonding_event( - &info.sender, - &pledge, - &node_identity, - node_id, - layer, - ))) + owner_signature, + signed_payload, + ) } -pub fn try_increase_pledge( +pub fn try_increase_mixnode_pledge( deps: DepsMut<'_>, env: Env, - info: MessageInfo, + increase: Vec, + mix_details: MixNodeDetails, ) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); - // increasing pledge is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; + ensure_can_increase_pledge(deps.storage, &mix_details)?; - ensure_bonded(&mix_details.bond_information)?; - ensure_no_pending_pledge_changes(&pending_changes)?; - - let rewarding_denom = rewarding_denom(deps.storage)?; - let pledge_increase = validate_pledge(info.funds, coin(1, rewarding_denom))?; + let rewarding_denom = &mix_details.original_pledge().denom; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase); // push the event to execute it at the end of the epoch - let epoch_event = PendingEpochEventKind::PledgeMore { + let epoch_event = PendingEpochEventKind::MixnodePledgeMore { mix_id, amount: pledge_increase, }; @@ -154,57 +78,21 @@ pub fn try_increase_pledge( Ok(Response::new().add_event(cosmos_event)) } -pub fn try_decrease_pledge( +pub fn try_decrease_mixnode_pledge( deps: DepsMut<'_>, env: Env, - info: MessageInfo, decrease_by: Coin, + mix_details: MixNodeDetails, ) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); - // decreasing pledge is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; - - ensure_bonded(&mix_details.bond_information)?; - ensure_no_pending_pledge_changes(&pending_changes)?; - - let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; - - // check that the denomination is correct - if decrease_by.denom != minimum_pledge.denom { - return Err(MixnetContractError::WrongDenom { - received: decrease_by.denom, - expected: minimum_pledge.denom, - }); - } - - // also check if the request contains non-zero amount - // (otherwise it's a no-op and we should we waste gas when resolving events?) - if decrease_by.amount.is_zero() { - return Err(MixnetContractError::ZeroCoinAmount); - } - - // decreasing pledge can't result in the new pledge being lower than the minimum amount - let new_pledge_amount = mix_details - .original_pledge() - .amount - .saturating_sub(decrease_by.amount); - if new_pledge_amount < minimum_pledge.amount { - return Err(MixnetContractError::InvalidPledgeReduction { - current: mix_details.original_pledge().amount, - decrease_by: decrease_by.amount, - minimum: minimum_pledge.amount, - denom: minimum_pledge.denom, - }); - } + ensure_can_decrease_pledge(deps.storage, &mix_details, &decrease_by)?; let cosmos_event = new_pending_pledge_decrease_event(mix_id, &decrease_by); // push the event to execute it at the end of the epoch - let epoch_event = PendingEpochEventKind::DecreasePledge { + let epoch_event = PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, }; @@ -289,57 +177,102 @@ pub(crate) fn try_update_mixnode_config( } pub(crate) fn try_update_mixnode_cost_params( - deps: DepsMut<'_>, + deps: DepsMut, env: Env, - info: MessageInfo, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, + mix_details: MixNodeDetails, ) -> Result { - // see if the node still exists - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; + let mut pending_changes = mix_details.pending_changes; + let mix_id = mix_details.mix_id(); - // changing cost params is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; + ensure_can_modify_cost_params(deps.storage, &mix_details)?; - ensure_bonded(&existing_bond)?; - - // ensure the profit margin is within the defined range - ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; - - // ensure the operating cost is within the defined range - ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; - - let cosmos_event = new_mixnode_pending_cost_params_update_event( - existing_bond.mix_id, - &info.sender, - &new_costs, - ); + let cosmos_event = new_pending_cost_params_update_event(mix_id, &new_costs); // push the interval event - let interval_event = PendingIntervalEventKind::ChangeMixCostParams { - mix_id: existing_bond.mix_id, - new_costs, - }; - push_new_interval_event(deps.storage, &env, interval_event)?; + let interval_event = PendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs }; + let interval_event_id = push_new_interval_event(deps.storage, &env, interval_event)?; + pending_changes.cost_params_change = Some(interval_event_id); + storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; Ok(Response::new().add_event(cosmos_event)) } +pub fn try_migrate_to_nymnode( + deps: DepsMut, + info: MessageInfo, +) -> Result { + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( + MixnetContractError::NoAssociatedMixNodeBond { + owner: info.sender.clone(), + }, + )?; + let node_id = mix_details.mix_id(); + let pending_changes = mix_details.pending_changes; + let mixnode_bond = mix_details.bond_information; + + if mixnode_bond.proxy.is_some() { + return Err(MixnetContractError::VestingNodeMigration); + } + + ensure_epoch_in_progress_state(deps.storage)?; + ensure_no_pending_pledge_changes(&pending_changes)?; + ensure_no_pending_params_changes(&pending_changes)?; + ensure_bonded(&mixnode_bond)?; + + let mixnode_identity = mixnode_bond.mix_node.identity_key.clone(); + + // remove mixnode bond data + storage::mixnode_bonds().replace(deps.storage, node_id, None, Some(&mixnode_bond))?; + + // NOTE: nothing happens to rewarding data as its structure hasn't changed, and it's accessible under `node_id` key + + // create nym-node entry + // note: since the starting value of nymnode counter was the same one as the final value of mixnode counter, + // we know there's definitely nothing under this key saved. + let nym_node_bond = NymNodeBond::new( + node_id, + mixnode_bond.owner, + mixnode_bond.original_pledge, + mixnode_bond.mix_node, + mixnode_bond.bonding_height, + ); + nymnodes_storage::nym_nodes().save(deps.storage, node_id, &nym_node_bond)?; + + // move pending changes + // TODO: what if node has pending PM change? + storage::PENDING_MIXNODE_CHANGES.remove(deps.storage, node_id); + nymnodes_storage::PENDING_NYMNODE_CHANGES.save( + deps.storage, + node_id, + &PendingNodeChanges::new_empty(), + )?; + + Ok(Response::new().add_event(new_migrated_mixnode_event( + &info.sender, + &mixnode_identity, + node_id, + ))) +} #[cfg(test)] pub mod tests { - use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Addr, Order, StdResult, Uint128}; - - use mixnet_contract_common::mixnode::PendingMixNodeChanges; - use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, LayerDistribution, Percent}; - + use super::*; + use crate::compat::transactions::{ + try_decrease_pledge, try_increase_pledge, try_update_cost_params, + }; use crate::contract::execute; - use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; + use crate::mixnet_contract_settings::storage::minimum_node_pledge; use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::signing::storage as signing_storage; use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::{fixtures, test_helpers}; - - use super::*; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{Addr, Order, StdResult, Uint128}; + use mixnet_contract_common::mixnode::PendingMixNodeChanges; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, Percent}; #[test] fn mixnode_add() { @@ -347,7 +280,7 @@ pub mod tests { let env = test.env(); let sender = "alice"; - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); @@ -355,7 +288,7 @@ pub mod tests { let info = mock_info(sender, &[insufficient_pledge.clone()]); let (mixnode, sig, _) = test.mixnode_with_signature(sender, Some(vec![insufficient_pledge.clone()])); - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); // we are informed that we didn't send enough funds let result = try_add_mixnode( @@ -378,7 +311,7 @@ pub mod tests { let info = mock_info(sender, &[minimum_pledge]); // if there was already a mixnode bonded by particular user - test.add_dummy_mixnode(sender, None); + test.add_legacy_mixnode(sender, None); // it fails let result = try_add_mixnode( @@ -394,7 +327,7 @@ pub mod tests { // the same holds if the user already owns a gateway let sender2 = "gateway-owner"; - test.add_dummy_gateway(sender2, None); + test.add_legacy_gateway(sender2, None); let info = mock_info(sender2, &tests::fixtures::good_mixnode_pledge()); let (mixnode, sig, _) = test.mixnode_with_signature(sender2, None); @@ -413,24 +346,26 @@ pub mod tests { let msg = ExecuteMsg::UnbondGateway {}; execute(test.deps_mut(), env.clone(), info.clone(), msg).unwrap(); - let result = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, sig); + let result = try_add_mixnode( + test.deps_mut(), + env, + info.clone(), + mixnode.clone(), + cost_params, + sig, + ); assert!(result.is_ok()); + // and the node has been added as a nym-node + let nym_node = get_node_details_by_identity(test.deps().storage, mixnode.identity_key) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender); + // make sure we got assigned the next id (note: we have already bonded a mixnode before in this test) let bond = - must_get_mixnode_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); - assert_eq!(2, bond.mix_id); - - // and make sure we're on layer 2 (because it was the next empty one) - assert_eq!(Layer::Two, bond.layer); - - // and see if the layer distribution matches our expectation - let expected = LayerDistribution { - layer1: 1, - layer2: 1, - layer3: 0, - }; - assert_eq!(expected, storage::LAYERS.load(test.deps().storage).unwrap()) + must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); + assert_eq!(2, bond.node_id); } #[test] @@ -444,7 +379,7 @@ pub mod tests { let (mixnode, signature, _) = test.mixnode_with_signature(sender, Some(pledge.clone())); // the above using cost params fixture - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); // using different parameters than what the signature was made on let mut modified_mixnode = mixnode.clone(); @@ -505,7 +440,7 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - test.immediately_unbond_mixnode(1); + test.immediately_unbond_node(1); let res = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } @@ -518,7 +453,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -527,7 +464,7 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -558,7 +495,7 @@ pub mod tests { }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events let res = try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()); @@ -587,7 +524,7 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -601,7 +538,7 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000))); + test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); let sender = mock_info(owner, &[]); let amount = test.coin(1000); try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); @@ -617,9 +554,10 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) @@ -659,7 +597,7 @@ pub mod tests { }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" update succeeds let res = try_update_mixnode_config(test.deps_mut(), info.clone(), update.clone()); @@ -688,10 +626,12 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; - let update = MixNodeCostParams { + let update = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; @@ -702,14 +642,13 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let res = - try_update_mixnode_cost_params(test.deps_mut(), env.clone(), info, update.clone()); + let res = try_update_cost_params(test.deps_mut(), env.clone(), info, update.clone()); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -724,34 +663,26 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - let update = MixNodeCostParams { + let update = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; - // try updating a non existing mixnode bond - let res = try_update_mixnode_cost_params( - test.deps_mut(), - env.clone(), - info.clone(), - update.clone(), - ); + // try updating a non-existing mixnode bond + let res = + try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked(owner) }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" update succeeds - let res = try_update_mixnode_cost_params( - test.deps_mut(), - env.clone(), - info.clone(), - update.clone(), - ); + let res = + try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); assert!(res.is_ok()); // see if the event has been pushed onto the queue @@ -781,8 +712,11 @@ pub mod tests { // but we cannot perform any updates whilst the mixnode is already unbonding try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()).unwrap(); - let res = try_update_mixnode_cost_params(test.deps_mut(), env, info, update); - assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) + let res = try_update_cost_params(test.deps_mut(), env, info, update); + assert_eq!( + res, + Err(MixnetContractError::NodeIsUnbonding { node_id: mix_id }) + ) } #[test] @@ -793,7 +727,7 @@ pub mod tests { let keypair1 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); let keypair2 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); let mixnode1 = MixNode { host: "1.2.3.4".to_string(), mix_port: 1234, @@ -852,7 +786,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -860,7 +796,7 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -886,7 +822,7 @@ pub mod tests { let res = try_increase_pledge(test.deps_mut(), env, sender); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked("not-mix-owner") }) ) @@ -913,8 +849,8 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + Err(MixnetContractError::NodeIsUnbonding { + node_id: mix_id_unbonding }) ); @@ -927,7 +863,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded_leftover }) ); @@ -939,7 +875,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded }) ) @@ -951,7 +887,7 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let sender_empty = mock_info(owner, &[]); let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty); @@ -975,7 +911,7 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -989,7 +925,7 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000))); + test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); let sender = mock_info(owner, &[]); let amount = test.coin(1000); try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); @@ -1005,9 +941,10 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) @@ -1028,7 +965,7 @@ pub mod tests { let mut test = TestSetup::new(); let env = test.env(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); let events = test.pending_epoch_events(); assert!(events.is_empty()); @@ -1040,7 +977,7 @@ pub mod tests { assert_eq!( events[0].kind, - PendingEpochEventKind::PledgeMore { + PendingEpochEventKind::MixnodePledgeMore { mix_id, amount: test.coin(1000), } @@ -1064,7 +1001,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1073,7 +1012,7 @@ pub mod tests { let owner = "mix-owner"; let decrease = test.coin(1000); - test.add_dummy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + test.add_legacy_mixnode(owner, Some(Uint128::new(100_000_000_000))); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -1100,7 +1039,7 @@ pub mod tests { let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked("not-mix-owner") }) ) @@ -1132,8 +1071,8 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + Err(MixnetContractError::NodeIsUnbonding { + node_id: mix_id_unbonding }) ); @@ -1147,7 +1086,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded_leftover }) ); @@ -1160,7 +1099,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded }) ) @@ -1172,10 +1111,10 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let pledge_amount = minimum_pledge.amount + Uint128::new(100); let pledged = test.coin(pledge_amount.u128()); - test.add_dummy_mixnode(owner, Some(pledge_amount)); + test.add_legacy_mixnode(owner, Some(pledge_amount)); let invalid_decrease = test.coin(150); let valid_decrease = test.coin(50); @@ -1210,7 +1149,7 @@ pub mod tests { let decrease = test.coin(0); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, Some(stake)); + test.add_legacy_mixnode(owner, Some(stake)); let sender = mock_info(owner, &[]); let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); @@ -1226,7 +1165,7 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, Some(stake)); + test.add_legacy_mixnode(owner, Some(stake)); let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -1240,7 +1179,7 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(stake)); + test.add_legacy_mixnode(owner, Some(stake)); let sender = mock_info(owner, &[]); let amount = test.coin(1000); try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); @@ -1256,9 +1195,10 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, Some(stake)); + let mix_id = test.add_legacy_mixnode(owner, Some(stake)); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) @@ -1284,7 +1224,7 @@ pub mod tests { let decrease = test.coin(1000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(stake)); + let mix_id = test.add_legacy_mixnode(owner, Some(stake)); let events = test.pending_epoch_events(); assert!(events.is_empty()); @@ -1296,7 +1236,7 @@ pub mod tests { assert_eq!( events[0].kind, - PendingEpochEventKind::DecreasePledge { + PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by: decrease, } diff --git a/contracts/mixnet/src/nodes/helpers.rs b/contracts/mixnet/src/nodes/helpers.rs new file mode 100644 index 0000000000..b9b2706380 --- /dev/null +++ b/contracts/mixnet/src/nodes/helpers.rs @@ -0,0 +1,183 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage as interval_storage; +use crate::nodes::storage; +use crate::nodes::storage::next_nymnode_id_counter; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Env, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::UnbondedNymNode; +use mixnet_contract_common::{ + NodeCostParams, NodeId, NodeRewarding, NymNode, NymNodeBond, NymNodeDetails, PendingNodeChanges, +}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn save_new_nymnode( + storage: &mut dyn Storage, + bonding_height: u64, + node: NymNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result { + let node_id = next_nymnode_id_counter(storage)?; + save_new_nymnode_with_id( + storage, + node_id, + bonding_height, + node, + cost_params, + owner, + pledge, + )?; + + Ok(node_id) +} + +pub(crate) fn save_new_nymnode_with_id( + storage: &mut dyn Storage, + node_id: NodeId, + bonding_height: u64, + node: NymNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result<(), MixnetContractError> { + let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); + + let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; + let node_bond = NymNodeBond::new(node_id, owner, pledge, node, bonding_height); + + // save node bond data + // note that this implicitly checks for uniqueness on identity key and owner + storage::nym_nodes().save(storage, node_id, &node_bond)?; + + // save rewarding data + rewards_storage::NYMNODE_REWARDING.save(storage, node_id, &node_rewarding)?; + + // initialise pending changes + storage::PENDING_NYMNODE_CHANGES.save(storage, node_id, &PendingNodeChanges::new_empty())?; + + Ok(()) +} + +pub(crate) fn attach_nym_node_details( + store: &dyn Storage, + bond_information: NymNodeBond, +) -> StdResult { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::NYMNODE_REWARDING.load(store, bond_information.node_id)?; + + // the same is true for the pending changes + let pending_changes = storage::PENDING_NYMNODE_CHANGES.load(store, bond_information.node_id)?; + + Ok(NymNodeDetails::new( + bond_information, + rewarding_details, + pending_changes, + )) +} + +pub(crate) fn get_node_details_by_id( + store: &dyn Storage, + mix_id: NodeId, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes().may_load(store, mix_id)? { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn get_node_details_by_owner( + store: &dyn Storage, + address: Addr, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes() + .idx + .owner + .item(store, address)? + .map(|record| record.1) + { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn get_node_details_by_identity( + store: &dyn Storage, + identity: IdentityKey, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes() + .idx + .identity_key + .item(store, identity)? + .map(|record| record.1) + { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn must_get_node_bond_by_owner( + store: &dyn Storage, + owner: &Addr, +) -> Result { + Ok(storage::nym_nodes() + .idx + .owner + .item(store, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedNodeBond { + owner: owner.clone(), + })? + .1) +} + +pub(crate) fn cleanup_post_unbond_nym_node_storage( + storage: &mut dyn Storage, + env: &Env, + current_details: &NymNodeDetails, +) -> Result<(), MixnetContractError> { + let node_id = current_details.bond_information.node_id; + // remove all bond information since we don't need it anymore + // note that "normal" remove is `may_load` followed by `replace` with a `None` + // and we have already loaded the data from the storage + storage::nym_nodes().replace( + storage, + node_id, + None, + Some(¤t_details.bond_information), + )?; + + // if there are no pending delegations to return, we can also + // purge all information regarding rewarding parameters + if current_details.rewarding_details.unique_delegations == 0 { + rewards_storage::NYMNODE_REWARDING.remove(storage, node_id); + } else { + // otherwise just set operator's tokens to zero as to indicate they have unbonded + // and already claimed those + let zeroed = current_details.rewarding_details.clear_operator(); + rewards_storage::NYMNODE_REWARDING.save(storage, node_id, &zeroed)?; + } + + let identity_key = current_details.bond_information.identity().to_owned(); + let owner = current_details.bond_information.owner.clone(); + + // save minimal information about this node + storage::unbonded_nym_nodes().save( + storage, + node_id, + &UnbondedNymNode { + identity_key, + node_id, + owner, + unbonding_height: env.block.height, + }, + )?; + + Ok(()) +} diff --git a/contracts/mixnet/src/nodes/mod.rs b/contracts/mixnet/src/nodes/mod.rs new file mode 100644 index 0000000000..7707e3b780 --- /dev/null +++ b/contracts/mixnet/src/nodes/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod helpers; +pub mod queries; +mod signature_helpers; +pub mod storage; +pub mod transactions; diff --git a/contracts/mixnet/src/nodes/queries.rs b/contracts/mixnet/src/nodes/queries.rs new file mode 100644 index 0000000000..6283d21d75 --- /dev/null +++ b/contracts/mixnet/src/nodes/queries.rs @@ -0,0 +1,259 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT, + NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT, + UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT, +}; +use crate::nodes::helpers::{ + attach_nym_node_details, get_node_details_by_id, get_node_details_by_identity, + get_node_details_by_owner, +}; +use crate::nodes::storage; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Deps, Order, StdResult, Storage}; +use cw_storage_plus::Bound; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, +}; +use mixnet_contract_common::{NodeId, NymNodeBond, NymNodeDetails}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn query_nymnode_bonds_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT) + .min(NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|node| node.node_id); + + Ok(PagedNymNodeBondsResponse { + nodes, + start_next_after, + }) +} + +pub(crate) fn query_rewarded_set_metadata( + deps: Deps<'_>, +) -> Result { + let metadata = storage::read_rewarded_set_metadata(deps.storage)?; + Ok(RolesMetadataResponse { metadata }) +} + +pub(crate) fn query_epoch_assignment( + deps: Deps<'_>, + role: Role, +) -> Result { + let metadata = storage::read_rewarded_set_metadata(deps.storage)?; + let nodes = storage::read_assigned_roles(deps.storage, role)?; + Ok(EpochAssignmentResponse { + epoch_id: metadata.epoch_id, + nodes, + }) +} + +fn attach_node_details( + storage: &dyn Storage, + read_bond: StdResult<(NodeId, NymNodeBond)>, +) -> StdResult { + match read_bond { + Ok((_, bond)) => attach_nym_node_details(storage, bond), + Err(err) => Err(err), + } +} + +pub fn query_nymnodes_details_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT) + .min(NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| attach_node_details(deps.storage, res)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|details| details.node_id()); + + Ok(PagedNymNodeDetailsResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_by_owner_paged( + deps: Deps<'_>, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .idx + .owner + .prefix(owner) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_by_identity_paged( + deps: Deps<'_>, + identity_key: String, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .idx + .identity_key + .prefix(identity_key) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_owned_nymnode(deps: Deps<'_>, address: String) -> StdResult { + let validated_addr = deps.api.addr_validate(&address)?; + + let details = get_node_details_by_owner(deps.storage, validated_addr.clone())?; + Ok(NodeOwnershipResponse { + address: validated_addr, + details, + }) +} + +pub fn query_nymnode_details(deps: Deps<'_>, node_id: NodeId) -> StdResult { + let details = get_node_details_by_id(deps.storage, node_id)?; + + Ok(NodeDetailsResponse { node_id, details }) +} + +pub fn query_nymnode_details_by_identity( + deps: Deps<'_>, + identity_key: IdentityKey, +) -> StdResult { + let details = get_node_details_by_identity(deps.storage, identity_key.clone())?; + + Ok(NodeDetailsByIdentityResponse { + identity_key, + details, + }) +} + +pub fn query_nymnode_rewarding_details( + deps: Deps<'_>, + node_id: NodeId, +) -> StdResult { + let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, node_id)?; + + Ok(NodeRewardingDetailsResponse { + node_id, + rewarding_details, + }) +} + +pub fn query_unbonded_nymnode(deps: Deps<'_>, node_id: NodeId) -> StdResult { + let details = storage::unbonded_nym_nodes().may_load(deps.storage, node_id)?; + + Ok(UnbondedNodeResponse { node_id, details }) +} + +pub fn query_stake_saturation( + deps: Deps<'_>, + node_id: NodeId, +) -> StdResult { + let node_rewarding = match rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(node_rewarding) => node_rewarding, + None => { + return Ok(StakeSaturationResponse { + node_id, + current_saturation: None, + uncapped_saturation: None, + }) + } + }; + + let rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + + Ok(StakeSaturationResponse { + node_id, + current_saturation: Some(node_rewarding.bond_saturation(&rewarding_params)), + uncapped_saturation: Some(node_rewarding.uncapped_bond_saturation(&rewarding_params)), + }) +} diff --git a/contracts/mixnet/src/nodes/signature_helpers.rs b/contracts/mixnet/src/nodes/signature_helpers.rs new file mode 100644 index 0000000000..db2e074540 --- /dev/null +++ b/contracts/mixnet/src/nodes/signature_helpers.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::storage as signing_storage; +use crate::support::helpers::decode_ed25519_identity_key; +use cosmwasm_std::{Addr, Coin, Deps}; +use mixnet_contract_common::construct_generic_node_bonding_payload; +use mixnet_contract_common::error::MixnetContractError; +use nym_contracts_common::signing::Verifier; +use nym_contracts_common::signing::{MessageSignature, SigningPurpose}; +use nym_contracts_common::IdentityKeyRef; +use serde::Serialize; + +/// Verifies the bonding signature on either a legacy mixnode, legacy gateway or a nym-node. +pub(crate) fn verify_bonding_signature( + deps: Deps<'_>, + sender: Addr, + identity_key: IdentityKeyRef, + pledge: Coin, + message: T, + signature: MessageSignature, +) -> Result<(), MixnetContractError> +where + T: SigningPurpose + Serialize, +{ + // recover the public key + let public_key = decode_ed25519_identity_key(identity_key)?; + + // reconstruct the payload + let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; + let msg = construct_generic_node_bonding_payload(nonce, sender, pledge, message); + + if deps.api.verify_message(msg, signature, &public_key)? { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } +} diff --git a/contracts/mixnet/src/nodes/storage/helpers.rs b/contracts/mixnet/src/nodes/storage/helpers.rs new file mode 100644 index 0000000000..ab59cd2617 --- /dev/null +++ b/contracts/mixnet/src/nodes/storage/helpers.rs @@ -0,0 +1,152 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; +use crate::nodes::storage::{nym_nodes, NYMNODE_ID_COUNTER}; +use cosmwasm_std::{StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; +use mixnet_contract_common::{EpochId, NodeId, NymNodeBond, RoleAssignment}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[repr(u8)] +pub enum RoleStorageBucket { + #[default] + A = 0, + B = 1, +} + +impl RoleStorageBucket { + pub fn other(&self) -> Self { + match self { + RoleStorageBucket::A => RoleStorageBucket::B, + RoleStorageBucket::B => RoleStorageBucket::A, + } + } + + pub fn swap(&self) -> Self { + self.other() + } +} + +pub(crate) fn reset_inactive_metadata( + storage: &mut dyn Storage, + epoch_id: EpochId, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + let inactive = active_bucket.other() as u8; + + ROLES_METADATA.save(storage, inactive, &RewardedSetMetadata::new(epoch_id))?; + Ok(()) +} + +pub(crate) fn save_assignment( + storage: &mut dyn Storage, + assignment: RoleAssignment, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + + // we're always assigning to the INACTIVE bucket, because it's still being built + let inactive = active_bucket.other() as u8; + + // update metadata + let mut metadata = ROLES_METADATA.load(storage, inactive)?; + let last = assignment.nodes.last().copied().unwrap_or_default(); + metadata.set_highest_id(last, assignment.role); + metadata.set_role_count(assignment.role, assignment.nodes.len() as u32); + if assignment.is_final_assignment() { + metadata.fully_assigned = true + } + ROLES_METADATA.save(storage, inactive, &metadata)?; + + // set the actual roles + Ok(ROLES.save(storage, (inactive, assignment.role), &assignment.nodes)?) +} + +pub(crate) fn read_rewarded_set_metadata( + storage: &dyn Storage, +) -> Result { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + Ok(ROLES_METADATA.load(storage, active_bucket as u8)?) +} + +pub(crate) fn read_assigned_roles( + storage: &dyn Storage, + role: Role, +) -> Result, MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + // we're always reading from the ACTIVE bucket + Ok(ROLES.load(storage, (active_bucket as u8, role))?) +} + +pub(crate) fn swap_active_role_bucket( + storage: &mut dyn Storage, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + Ok(ACTIVE_ROLES_BUCKET.save(storage, &active_bucket.swap())?) +} + +pub(crate) fn set_unbonding( + storage: &mut dyn Storage, + bond: &NymNodeBond, +) -> Result<(), MixnetContractError> { + let mut updated_bond = bond.clone(); + updated_bond.is_unbonding = true; + nym_nodes().replace(storage, bond.node_id, Some(&updated_bond), Some(bond))?; + Ok(()) +} + +pub(crate) fn next_nymnode_id_counter(store: &mut dyn Storage) -> StdResult { + let id: NodeId = NYMNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NYMNODE_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + ACTIVE_ROLES_BUCKET.save(storage, &RoleStorageBucket::default())?; + let roles = vec![ + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::EntryGateway, + Role::ExitGateway, + Role::Standby, + ]; + for role in roles { + ROLES.save(storage, (RoleStorageBucket::default() as u8, role), &vec![])?; + ROLES.save( + storage, + (RoleStorageBucket::default().other() as u8, role), + &vec![], + )? + } + + ROLES_METADATA.save( + storage, + RoleStorageBucket::default() as u8, + &Default::default(), + )?; + ROLES_METADATA.save( + storage, + RoleStorageBucket::default().other() as u8, + &Default::default(), + )?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + + #[test] + fn next_id() { + let mut deps = test_helpers::init_contract(); + + for i in 1u32..1000 { + assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap()); + } + } +} diff --git a/contracts/mixnet/src/nodes/storage/mod.rs b/contracts/mixnet/src/nodes/storage/mod.rs new file mode 100644 index 0000000000..c431e4af08 --- /dev/null +++ b/contracts/mixnet/src/nodes/storage/mod.rs @@ -0,0 +1,128 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY, NYMNODE_IDENTITY_IDX_NAMESPACE, + NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE, NYMNODE_REWARDED_SET_METADATA_NAMESPACE, + NYMNODE_ROLES_ASSIGNMENT_NAMESPACE, PENDING_NYMNODE_CHANGES_NAMESPACE, + UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, + UNBONDED_NYMNODE_PK_NAMESPACE, +}; +use crate::nodes::storage::helpers::RoleStorageBucket; +use cosmwasm_std::Addr; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; +use mixnet_contract_common::nym_node::{NymNodeBond, RewardedSetMetadata, Role, UnbondedNymNode}; +use mixnet_contract_common::{NodeId, PendingNodeChanges}; +use nym_contracts_common::IdentityKey; + +pub(crate) mod helpers; + +pub(crate) use helpers::*; + +// IMPORTANT NOTE: we're using the same storage key as we had for MIXNODE_ID_COUNTER, +// so that we could start from the old values +pub const NYMNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); + +// each nym-node has 3 storage buckets: +// - `NymNodeBondIndex` to keep track of the actual node information +// - `PENDING_NYMNODE_CHANGES` to keep track of current params/pledge changes +// - `rewards_storage::NYMNODE_REWARDING` to keep track of data needed for reward calculation + +pub const PENDING_NYMNODE_CHANGES: Map = + Map::new(PENDING_NYMNODE_CHANGES_NAMESPACE); + +pub mod rewarded_set { + use super::*; + + // role assignment period is an awkward time for querying for up-to-date data + // for example if we have assigned layer1 and layer2 but not yet touched layer3, + // the state would be inconsistent since it'd have data of layer3 from previous epoch + // + // thus we just toggle the virtual pointer between 2 buckets + // since we also don't want to keep state for all epochs. + // + // general rule of thumb: we're always READING from the active bucket, + // but we're WRITING to the inactive bucket (because it's still being built) + /// Item keeping track of the current active node assignment + pub const ACTIVE_ROLES_BUCKET: Item = + Item::new(NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY); + + // NOTES FOR FUTURE IMPLEMENTATION: + // to implement pre-announcement of nodes, you don't have to do much. literally almost nothing at all, + // you'd just have to expose the current inactive bucket and make sure to correctly invalidate it when being written to + + // it feels more efficient to have a single bulk read/write operation per role + // as opposed to storing everything under separate keys. + // however, the drawback is a potentially huge writing cost, but I don't think + // we're going to have 1k+ nodes per layer any time soon for it to be a problem + // + // note: the actual resolution of which node id corresponds to which ip/identity + // is left to up the caller + // + // storage note: we use `u8` rather than `RoleStorageBucket` in the composite key + // to avoid having to derive all required traits + /// Storage map of `(RoleStorageBucket, Role)` => set of nodes with that assigned role + pub const ROLES: Map<(u8, Role), Vec> = Map::new(NYMNODE_ROLES_ASSIGNMENT_NAMESPACE); + + /// Storage map of metadata associated with particular `RoleStorageBucket` + pub const ROLES_METADATA: Map = + Map::new(NYMNODE_REWARDED_SET_METADATA_NAMESPACE); +} + +pub(crate) struct NymNodeBondIndex<'a> { + pub(crate) owner: UniqueIndex<'a, Addr, NymNodeBond>, + + pub(crate) identity_key: UniqueIndex<'a, IdentityKey, NymNodeBond>, +} + +impl<'a> IndexList for NymNodeBondIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; + Box::new(v.into_iter()) + } +} + +// nym_nodes() is the storage access function. +pub(crate) fn nym_nodes<'a>() -> IndexedMap<'a, NodeId, NymNodeBond, NymNodeBondIndex<'a>> { + let indexes = NymNodeBondIndex { + owner: UniqueIndex::new(|d| d.owner.clone(), NYMNODE_OWNER_IDX_NAMESPACE), + identity_key: UniqueIndex::new( + |d| d.node.identity_key.clone(), + NYMNODE_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(NYMNODE_PK_NAMESPACE, indexes) +} + +// keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past nodes +// if we ever decide it's too bloaty, we can deprecate it and start removing all data in +// subsequent migrations +pub(crate) struct UnbondedNymNodeIndex<'a> { + pub(crate) owner: MultiIndex<'a, Addr, UnbondedNymNode, NodeId>, + + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedNymNode, NodeId>, +} + +impl<'a> IndexList for UnbondedNymNodeIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn unbonded_nym_nodes<'a>( +) -> IndexedMap<'a, NodeId, UnbondedNymNode, UnbondedNymNodeIndex<'a>> { + let indexes = UnbondedNymNodeIndex { + owner: MultiIndex::new( + |_pk, d| d.owner.clone(), + UNBONDED_NYMNODE_PK_NAMESPACE, + UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, + ), + identity_key: MultiIndex::new( + |_pk, d| d.identity_key.clone(), + UNBONDED_NYMNODE_PK_NAMESPACE, + UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(UNBONDED_NYMNODE_PK_NAMESPACE, indexes) +} diff --git a/contracts/mixnet/src/nodes/transactions.rs b/contracts/mixnet/src/nodes/transactions.rs new file mode 100644 index 0000000000..646bd48dd9 --- /dev/null +++ b/contracts/mixnet/src/nodes/transactions.rs @@ -0,0 +1,264 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::compat::helpers::{ + ensure_can_decrease_pledge, ensure_can_increase_pledge, ensure_can_modify_cost_params, +}; +use crate::interval::storage as interval_storage; +use crate::interval::storage::push_new_interval_event; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::nodes::helpers::{must_get_node_bond_by_owner, save_new_nymnode}; +use crate::nodes::signature_helpers::verify_bonding_signature; +use crate::nodes::storage; +use crate::nodes::storage::set_unbonding; +use crate::signing::storage as signing_storage; +use crate::support::helpers::{ + ensure_epoch_in_progress_state, ensure_no_existing_bond, ensure_operating_cost_within_range, + ensure_profit_margin_within_range, validate_pledge, +}; +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_nym_node_bonding_event, new_pending_cost_params_update_event, + new_pending_nym_node_unbonding_event, new_pending_pledge_decrease_event, + new_pending_pledge_increase_event, +}; +use mixnet_contract_common::nym_node::{NodeConfigUpdate, NymNode}; +use mixnet_contract_common::{ + NodeCostParams, NymNodeBondingPayload, NymNodeDetails, PendingEpochEventKind, + PendingIntervalEventKind, +}; +use nym_contracts_common::signing::{MessageSignature, SigningPurpose}; +use serde::Serialize; + +pub fn try_add_nym_node( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + nym_node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, +) -> Result { + // TODO: here be backwards compatibility checks for making sure there's no pre-existing mixnode/gateway + + add_nym_node_inner( + deps, + env, + info, + nym_node.clone(), + cost_params.clone(), + owner_signature, + NymNodeBondingPayload::new(nym_node, cost_params), + ) +} + +// allow bonding nym-node through mixnode/gateway entry points for backwards compatibility, +// and make sure to check correct signatures +pub(crate) fn add_nym_node_inner( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + nym_node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + signed_message_payload: T, +) -> Result +where + T: SigningPurpose + Serialize, +{ + // ensure the provided values for host and public key are not insane + nym_node.ensure_host_in_range()?; + nym_node.naive_ensure_valid_pubkey()?; + + // check if the pledge contains any funds of the appropriate denomination + let minimum_pledge = mixnet_params_storage::minimum_node_pledge(deps.storage)?; + let pledge = validate_pledge(info.funds, minimum_pledge)?; + + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; + + // if the client has an active bonded [legacy] mixnode, [legacy] gateway or a nym-node, don't allow bonding + // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us + // against attempting to use different node types (i.e. gateways and mixnodes) + ensure_no_existing_bond(&info.sender, deps.storage)?; + + // there's no need to explicitly check whether there already exists nymnode with the same + // identity as this is going to be done implicitly when attempting to save + // the bond information due to `UniqueIndex` constraint defined on that field. + + // check if this sender actually owns the node by checking the signature + verify_bonding_signature( + deps.as_ref(), + info.sender.clone(), + &nym_node.identity_key, + pledge.clone(), + signed_message_payload, + owner_signature, + )?; + + // update the signing nonce associated with this sender so that the future signature would be made on the new value + signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; + + let node_identity = nym_node.identity_key.clone(); + let node_id = save_new_nymnode( + deps.storage, + env.block.height, + nym_node, + cost_params, + info.sender.clone(), + pledge.clone(), + )?; + + Ok(Response::new().add_event(new_nym_node_bonding_event( + &info.sender, + &pledge, + &node_identity, + node_id, + ))) +} + +pub(crate) fn try_remove_nym_node( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + let existing_bond = must_get_node_bond_by_owner(deps.storage, &info.sender)?; + let pending_changes = + storage::PENDING_NYMNODE_CHANGES.load(deps.storage, existing_bond.node_id)?; + + // unbonding is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(deps.storage)?; + + // see if the proxy matches + existing_bond.ensure_bonded()?; + + // if there are any pending requests to change the pledge, wait for them to resolve before allowing the unbonding + pending_changes.ensure_no_pending_pledge_changes()?; + + // set `is_unbonding` field + set_unbonding(deps.storage, &existing_bond)?; + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::UnbondNymNode { + node_id: existing_bond.node_id, + }; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + + Ok( + Response::new().add_event(new_pending_nym_node_unbonding_event( + &existing_bond.owner, + existing_bond.identity(), + existing_bond.node_id, + )), + ) +} + +pub(crate) fn try_update_node_config( + deps: DepsMut<'_>, + info: MessageInfo, + update: NodeConfigUpdate, +) -> Result { + let existing_bond = must_get_node_bond_by_owner(deps.storage, &info.sender)?; + existing_bond.ensure_bonded()?; + + let mut updated_bond = existing_bond.clone(); + + if let Some(updated_host) = update.host { + updated_bond.node.host = updated_host; + } + + if let Some(updated_custom_http_port) = update.custom_http_port { + updated_bond.node.custom_http_port = Some(updated_custom_http_port); + } + + if update.restore_default_http_port { + updated_bond.node.custom_http_port = None + } + + storage::nym_nodes().replace( + deps.storage, + existing_bond.node_id, + Some(&updated_bond), + Some(&existing_bond), + )?; + + Ok(Response::new()) +} + +pub(crate) fn try_increase_nym_node_pledge( + deps: DepsMut<'_>, + env: Env, + increase: Vec, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_increase_pledge(deps.storage, &node_details)?; + + let rewarding_denom = &node_details.original_pledge().denom; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; + + let cosmos_event = new_pending_pledge_increase_event(node_id, &pledge_increase); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::NymNodePledgeMore { + node_id, + amount: pledge_increase, + }; + let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + pending_changes.pledge_change = Some(epoch_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn try_decrease_nym_node_pledge( + deps: DepsMut<'_>, + env: Env, + decrease_by: Coin, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_decrease_pledge(deps.storage, &node_details, &decrease_by)?; + + let cosmos_event = new_pending_pledge_decrease_event(node_id, &decrease_by); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + }; + let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + pending_changes.pledge_change = Some(epoch_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn try_update_nym_node_cost_params( + deps: DepsMut, + env: Env, + new_costs: NodeCostParams, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_modify_cost_params(deps.storage, &node_details)?; + + let cosmos_event = new_pending_cost_params_update_event(node_id, &new_costs); + + // push the interval event + let interval_event = PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs }; + let interval_event_id = push_new_interval_event(deps.storage, &env, interval_event)?; + pending_changes.cost_params_change = Some(interval_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index a0273196be..9521172bcb 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,2 +1,303 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +mod families_purge { + use cosmwasm_std::{DepsMut, Order, StdResult}; + use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; + use mixnet_contract_common::error::MixnetContractError; + use nym_contracts_common::IdentityKey; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; + pub const FAMILIES_MAP_NAMESPACE: &str = "fam2"; + pub const MEMBERS_MAP_NAMESPACE: &str = "memb2"; + + type FamilyHeadKey = IdentityKey; + + #[derive(Serialize, Deserialize, Clone)] + pub struct Family { + /// Owner of this family. + head: FamilyHead, + + /// Optional proxy (i.e. vesting contract address) used when creating the family. + proxy: Option, + + /// Human readable label for this family. + label: String, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct FamilyHead(IdentityKey); + + impl Serialize for FamilyHead { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for FamilyHead { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let inner = IdentityKey::deserialize(deserializer)?; + Ok(FamilyHead(inner)) + } + } + + pub struct FamilyIndex<'a> { + pub label: UniqueIndex<'a, FamilyHeadKey, Family>, + } + + impl<'a> IndexList for FamilyIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.label]; + Box::new(v.into_iter()) + } + } + + pub fn families<'a>() -> IndexedMap<'a, FamilyHeadKey, Family, FamilyIndex<'a>> { + let indexes = FamilyIndex { + label: UniqueIndex::new(|d| d.label.to_string(), FAMILIES_INDEX_NAMESPACE), + }; + IndexedMap::new(FAMILIES_MAP_NAMESPACE, indexes) + } + + pub const MEMBERS: Map = Map::new(MEMBERS_MAP_NAMESPACE); + + pub(crate) fn families_purge(deps: DepsMut) -> Result<(), MixnetContractError> { + // we don't care about values, we are only concerned with keys + let family_keys = families() + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + for family in family_keys { + families().remove(deps.storage, family)?; + } + + let member_keys = MEMBERS + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + for member in member_keys { + MEMBERS.remove(deps.storage, member); + } + + Ok(()) + } +} + +mod nym_nodes_usage { + use crate::constants::{CONTRACT_STATE_KEY, REWARDING_PARAMS_KEY}; + use crate::mixnet_contract_settings::storage::CONTRACT_STATE; + use crate::rewards::storage::RewardingStorage; + use crate::support::helpers::ensure_epoch_in_progress_state; + use cosmwasm_std::{Addr, Coin, DepsMut, Order, StdResult, Storage}; + use cw_storage_plus::{Item, Map}; + use mixnet_contract_common::error::MixnetContractError; + use mixnet_contract_common::reward_params::RewardedSetParams; + use mixnet_contract_common::{ + ContractState, ContractStateParams, IntervalRewardParams, MigrateMsg, NodeId, + OperatingCostRange, PendingIntervalEvent, PendingIntervalEventKind, ProfitMarginRange, + RewardingParams, + }; + use serde::{Deserialize, Serialize}; + + fn migrate_contract_state(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Serialize, Deserialize)] + struct OldContractState { + owner: Option, + rewarding_validator_address: Addr, + vesting_contract_address: Addr, + rewarding_denom: String, + params: OldContractStateParams, + } + + #[derive(Serialize, Deserialize)] + struct OldContractStateParams { + minimum_mixnode_delegation: Option, + minimum_mixnode_pledge: Coin, + minimum_gateway_pledge: Coin, + #[serde(default)] + profit_margin: ProfitMarginRange, + #[serde(default)] + interval_operating_cost: OperatingCostRange, + } + + let old_state_entry = Item::new(CONTRACT_STATE_KEY); + let old_state: OldContractState = old_state_entry.load(storage)?; + + #[allow(deprecated)] + CONTRACT_STATE.save( + storage, + &ContractState { + owner: old_state.owner, + rewarding_validator_address: old_state.rewarding_validator_address, + vesting_contract_address: old_state.vesting_contract_address, + rewarding_denom: old_state.rewarding_denom, + params: ContractStateParams { + minimum_delegation: old_state.params.minimum_mixnode_delegation, + // just use the same value for nym-node pledge as we have for mixnodes + minimum_pledge: old_state.params.minimum_mixnode_pledge, + profit_margin: old_state.params.profit_margin, + interval_operating_cost: old_state.params.interval_operating_cost, + }, + }, + )?; + + Ok(()) + } + + fn migrate_pending_interval_changes( + storage: &mut dyn Storage, + ) -> Result<(), MixnetContractError> { + // at the time of writing this migration there were just 15 pending interval events, + // so if we stay within this order of magnitude, it's quite safe to just grab all of them + let events = crate::interval::storage::PENDING_INTERVAL_EVENTS + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|row| row.into())) + .collect::>>()?; + + for event in events { + if let PendingIntervalEventKind::ChangeMixCostParams { mix_id, .. } = event.event.kind { + let mut pending = crate::mixnodes::storage::PENDING_MIXNODE_CHANGES + .may_load(storage, mix_id)? + .unwrap_or_default(); + pending.cost_params_change = Some(event.id); + crate::mixnodes::storage::PENDING_MIXNODE_CHANGES + .save(storage, mix_id, &pending)?; + } + } + + Ok(()) + } + + fn preassign_gateway_ids(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + // that one is a big if. we have ~100 gateways so we **might** be able to fit it within migration. + // if not, then we'll have to do it in batches/change our approach + + let gateways = crate::gateways::storage::gateways() + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|row| row.1)) + .collect::>>()?; + + for gateway in gateways { + let id = crate::nodes::storage::next_nymnode_id_counter(storage)?; + crate::gateways::storage::PREASSIGNED_LEGACY_IDS.save( + storage, + gateway.gateway.identity_key, + &id, + )?; + } + + Ok(()) + } + + fn cleanup_legacy_storage(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Copy, Clone, Default, Serialize, Deserialize)] + pub struct LayerDistribution { + pub layer1: u64, + pub layer2: u64, + pub layer3: u64, + } + pub const LAYERS: Item<'_, LayerDistribution> = Item::new("layers"); + + #[derive(Copy, Clone, Serialize, Deserialize)] + #[serde(deny_unknown_fields, rename_all = "snake_case")] + pub enum RewardedSetNodeStatus { + /// Node that is currently active, i.e. is expected to be used by clients for mixing packets. + #[serde(alias = "Active")] + Active, + + /// Node that is currently in standby, i.e. it's present in the rewarded set but is not active. + #[serde(alias = "Standby")] + Standby, + } + pub(crate) const REWARDED_SET: Map = Map::new("rs"); + + // remove explicit layer assignment -> got replaced with role assignment + LAYERS.remove(storage); + + // remove every node from the legacy rewarded set + let rewarded_ids = REWARDED_SET + .keys(storage, None, None, Order::Ascending) + .collect::, _>>()?; + + for node_id in rewarded_ids { + REWARDED_SET.remove(storage, node_id) + } + + Ok(()) + } + + fn migrate_rewarded_set_params(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Copy, Clone, Serialize, Deserialize)] + pub struct LegacyRewardingParams { + pub interval: IntervalRewardParams, + pub rewarded_set_size: u32, + pub active_set_size: u32, + } + pub(crate) const REWARDING_PARAMS: Item<'_, LegacyRewardingParams> = + Item::new(REWARDING_PARAMS_KEY); + + let legacy = REWARDING_PARAMS.load(storage)?; + + // our mainnet assumption. we could work around it, + // but what's the point of the extra logic if we might not need it? + if legacy.rewarded_set_size != 240 || legacy.active_set_size != 240 { + return Err(MixnetContractError::FailedMigration { + comment: "the current active or rewarded set size is not 240 (the expected value for mainnet)".to_string(), + }); + } + + let updated = RewardingParams { + interval: legacy.interval, + rewarded_set: RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, + }; + + RewardingStorage::load() + .global_rewarding_params + .save(storage, &updated)?; + + Ok(()) + } + + pub(crate) fn migrate_to_nym_nodes_usage( + deps: DepsMut<'_>, + _msg: &MigrateMsg, + ) -> Result<(), MixnetContractError> { + // ensure we're not migrating mid-epoch progression, or we're gonna have bad time + ensure_epoch_in_progress_state(deps.storage)?; + + // update the contract state structure (remove separate mixnode/gateway pledge amount) + migrate_contract_state(deps.storage)?; + + // make sure to assign pending cost params changes to mixnodes so those nodes couldn't be migrated + // to nym-nodes until the events are resolved + migrate_pending_interval_changes(deps.storage)?; + + // pre-assign NodeId to all gateways (that will be applied during nym-node migration) + // to simplify all other code during the intermediate period + preassign_gateway_ids(deps.storage)?; + + // initialise all the storage structures required by nym-nodes + crate::nodes::storage::initialise_storage(deps.storage)?; + + // update the simple active/rewarded set sizes to actually contain the distribution of roles + migrate_rewarded_set_params(deps.storage)?; + + // remove all redundant storage items + cleanup_legacy_storage(deps.storage)?; + + Ok(()) + } +} + +pub(crate) use families_purge::families_purge; +pub(crate) use nym_nodes_usage::migrate_to_nym_nodes_usage; diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 689c263efb..dfd07956ce 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -4,16 +4,18 @@ use super::storage; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; +use crate::nodes::storage::read_assigned_roles; use cosmwasm_std::{Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::helpers::IntoBaseDecimal; -use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding}; -use mixnet_contract_common::{Delegation, EpochState, EpochStatus, MixId}; +use mixnet_contract_common::helpers::{IntoBaseDecimal, NodeBond, NodeDetails}; +use mixnet_contract_common::mixnode::NodeRewarding; +use mixnet_contract_common::nym_node::Role; +use mixnet_contract_common::{Delegation, EpochState, EpochStatus, NodeId}; pub(crate) fn update_and_save_last_rewarded( storage: &mut dyn Storage, mut current_epoch_status: EpochStatus, - new_last_rewarded: MixId, + new_last_rewarded: NodeId, ) -> Result<(), MixnetContractError> { let is_done = current_epoch_status.update_last_rewarded(new_last_rewarded)?; if is_done { @@ -39,7 +41,7 @@ pub(crate) fn apply_reward_pool_changes( let epoch_reward_budget = reward_pool / interval.epochs_in_interval().into_base_decimal()? * rewarding_params.interval.interval_pool_emission; let stake_saturation_point = - staking_supply / rewarding_params.rewarded_set_size.into_base_decimal()?; + staking_supply / rewarding_params.rewarded_set_size().into_base_decimal()?; rewarding_params.interval.reward_pool = reward_pool; rewarding_params.interval.staking_supply = staking_supply; @@ -52,26 +54,29 @@ pub(crate) fn apply_reward_pool_changes( Ok(()) } -pub(crate) fn withdraw_operator_reward( +pub(crate) fn withdraw_operator_reward( store: &mut dyn Storage, - mix_details: MixNodeDetails, -) -> Result { - let mix_id = mix_details.mix_id(); - let mut mix_rewarding = mix_details.rewarding_details; - let original_pledge = mix_details.bond_information.original_pledge; - let reward = mix_rewarding.withdraw_operator_reward(&original_pledge)?; + node_details: D, +) -> Result +where + D: NodeDetails, +{ + let (bond_info, mut node_rewarding, _) = node_details.split(); + let node_id = bond_info.node_id(); + let original_pledge = bond_info.original_pledge(); + let reward = node_rewarding.withdraw_operator_reward(original_pledge)?; // save updated rewarding info - storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?; + storage::NYMNODE_REWARDING.save(store, node_id, &node_rewarding)?; Ok(reward) } pub(crate) fn withdraw_delegator_reward( store: &mut dyn Storage, delegation: Delegation, - mut mix_rewarding: MixNodeRewarding, + mut mix_rewarding: NodeRewarding, ) -> Result { - let mix_id = delegation.mix_id; + let mix_id = delegation.node_id; let mut updated_delegation = delegation.clone(); let reward = mix_rewarding.withdraw_delegator_reward(&mut updated_delegation)?; @@ -86,6 +91,47 @@ pub(crate) fn withdraw_delegator_reward( Ok(reward) } +pub(crate) fn ensure_assignment( + storage: &dyn Storage, + node_id: NodeId, + role: Role, +) -> Result<(), MixnetContractError> { + // that's a bit expensive to read the whole thing each time, but I'm not sure if there's a much better way + // (creating a reverse map would be more expensive in the long run due to writes being more costly than reads) + let assignment = read_assigned_roles(storage, role)?; + if !assignment.contains(&node_id) { + return Err(MixnetContractError::IncorrectEpochRole { node_id, role }); + } + Ok(()) +} + +// this is **ONLY** to be used in queries +// unless a better way can be figured out +pub(crate) fn expensive_role_lookup( + storage: &dyn Storage, + node_id: NodeId, +) -> Result, MixnetContractError> { + if ensure_assignment(storage, node_id, Role::EntryGateway).is_ok() { + return Ok(Some(Role::EntryGateway)); + } + if ensure_assignment(storage, node_id, Role::ExitGateway).is_ok() { + return Ok(Some(Role::ExitGateway)); + } + if ensure_assignment(storage, node_id, Role::Layer1).is_ok() { + return Ok(Some(Role::Layer1)); + } + if ensure_assignment(storage, node_id, Role::Layer2).is_ok() { + return Ok(Some(Role::Layer2)); + } + if ensure_assignment(storage, node_id, Role::Layer3).is_ok() { + return Ok(Some(Role::Layer3)); + } + if ensure_assignment(storage, node_id, Role::Standby).is_ok() { + return Ok(Some(Role::Standby)); + } + Ok(None) +} + #[cfg(test)] mod tests { use super::*; @@ -145,10 +191,7 @@ mod tests { assert_eq!( updated_rewarding_params.interval.stake_saturation_point, updated_rewarding_params.interval.staking_supply - / updated_rewarding_params - .rewarded_set_size - .into_base_decimal() - .unwrap() + / updated_rewarding_params.dec_rewarded_set_size() ); // resets changes back to 0 @@ -197,10 +240,7 @@ mod tests { assert_eq!( updated_rewarding_params2.interval.stake_saturation_point, updated_rewarding_params2.interval.staking_supply - / updated_rewarding_params2 - .rewarded_set_size - .into_base_decimal() - .unwrap() + / updated_rewarding_params2.dec_rewarded_set_size() ); // resets changes back to 0 @@ -218,7 +258,7 @@ mod tests { let pledge = Uint128::new(250_000_000); let pledge_dec = 250_000_000u32.into_base_decimal().unwrap(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(pledge)); + let mix_id = test.add_legacy_mixnode("mix-owner", Some(pledge)); // no rewards let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -229,13 +269,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -255,7 +298,7 @@ mod tests { let delegation_amount = Uint128::new(2_500_000_000); let delegation_dec = 2_500_000_000_u32.into_base_decimal().unwrap(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let delegator = "delegator"; test.add_immediate_delegation(delegator, delegation_amount, mix_id); @@ -268,13 +311,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); let delegation_pre = test.delegation(mix_id, delegator, &None); let mix_rewarding = test.mix_rewarding(mix_id); diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 87df0f9fc8..53b7ab9f99 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -1,39 +1,28 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage; +use super::{helpers, storage}; +use crate::compat; +use crate::compat::helpers::may_get_bond; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; -use crate::mixnodes; -use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::{coin, Coin, Decimal, Deps, StdResult}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::helpers::into_base_decimal; -use mixnet_contract_common::mixnode::MixNodeDetails; -use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use mixnet_contract_common::nym_node::Role; +use mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardingParams, WorkFactor, +}; use mixnet_contract_common::rewarding::helpers::truncate_reward; use mixnet_contract_common::rewarding::{ EstimatedCurrentEpochRewardResponse, PendingRewardResponse, }; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult { storage::REWARDING_PARAMS.load(deps.storage) } -fn pending_operator_reward( - mix_details: Option, -) -> StdResult { - Ok(match mix_details { - Some(mix_details) => PendingRewardResponse { - amount_staked: Some(mix_details.original_pledge().clone()), - amount_earned: Some(mix_details.pending_operator_reward()), - amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()?), - mixnode_still_fully_bonded: !mix_details.is_unbonding(), - }, - None => PendingRewardResponse::default(), - }) -} - pub fn query_pending_operator_reward( deps: Deps, owner: String, @@ -41,24 +30,20 @@ pub fn query_pending_operator_reward( let owner_address = deps.api.addr_validate(&owner)?; // in order to determine operator's reward we need to know its original pledge and thus // we have to load the entire thing - let mix_details = mixnodes::helpers::get_mixnode_details_by_owner(deps.storage, owner_address)?; - pending_operator_reward(mix_details) + compat::queries::rewards::pending_operator_reward(deps, owner_address) } pub fn query_pending_mixnode_operator_reward( deps: Deps, - mix_id: MixId, + node_id: NodeId, ) -> StdResult { - // in order to determine operator's reward we need to know its original pledge and thus - // we have to load the entire thing - let mix_details = mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)?; - pending_operator_reward(mix_details) + compat::queries::rewards::pending_operator_reward_by_id(deps, node_id) } pub fn query_pending_delegator_reward( deps: Deps, owner: String, - mix_id: MixId, + node_id: NodeId, proxy: Option, ) -> StdResult { let owner_address = deps.api.addr_validate(&owner)?; @@ -66,28 +51,32 @@ pub fn query_pending_delegator_reward( .map(|proxy| deps.api.addr_validate(&proxy)) .transpose()?; - let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + let node_rewarding = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { Some(mix_rewarding) => mix_rewarding, None => return Ok(PendingRewardResponse::default()), }; - let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(node_id, &owner_address, proxy.as_ref()); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { Some(delegation) => delegation, None => return Ok(PendingRewardResponse::default()), }; - let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation)?; - let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation)?; + let detailed_reward = node_rewarding.determine_delegation_reward(&delegation)?; + let delegator_reward = node_rewarding.pending_delegator_reward(&delegation)?; - // check if the mixnode isnt in the process of unbonding (or has already unbonded) - let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); + // check if the node isnt in the process of unbonding (or has already unbonded) + let is_bonded = may_get_bond(deps.storage, node_id)? + .map(|b| !b.is_unbonding()) + .unwrap_or_default(); + #[allow(deprecated)] Ok(PendingRewardResponse { amount_staked: Some(delegation.amount), amount_earned: Some(delegator_reward), amount_earned_detailed: Some(detailed_reward), mixnode_still_fully_bonded: is_bonded, + node_still_fully_bonded: is_bonded, }) } @@ -106,40 +95,57 @@ fn zero_reward( pub(crate) fn query_estimated_current_epoch_operator_reward( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, estimated_performance: Performance, -) -> StdResult { - let mix_details = match mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)? { + estimated_work: Option, +) -> Result { + let rewarding_details = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), - Some(mix_details) => mix_details, + Some(info) => info, }; - let amount_staked = mix_details.original_pledge().clone(); - let mix_rewarding = mix_details.rewarding_details; - let current_value = mix_rewarding.operator; + let bond = compat::helpers::get_bond(deps.storage, node_id)?; + + let amount_staked = bond.original_pledge().clone(); + let current_value = rewarding_details.operator; // if node is currently not in the rewarded set, the performance is 0, // or the node has either unbonded or is in the process of unbonding, // the calculations are trivial - the rewards are 0 - if mix_details.bond_information.is_unbonding { + if bond.is_unbonding() { return Ok(zero_reward(amount_staked, current_value)); } - let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { - None => return Ok(zero_reward(amount_staked, current_value)), - Some(node_status) => node_status, - }; - if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + + let work_factor = if let Some(work_factor) = estimated_work { + work_factor + } else { + let Some(role) = helpers::expensive_role_lookup(deps.storage, node_id)? else { + return Ok(zero_reward(amount_staked, current_value)); + }; + match role { + Role::EntryGateway | Role::Layer1 | Role::Layer2 | Role::Layer3 | Role::ExitGateway => { + rewarding_params.active_node_work() + } + Role::Standby => rewarding_params.standby_node_work(), + } + }; + + let node_reward_params = NodeRewardingParameters { + performance: estimated_performance, + work_factor, + }; + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; let interval = interval_storage::current_interval(deps.storage)?; - let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + let node_reward = rewarding_details.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = rewarding_details.determine_reward_split( node_reward, estimated_performance, interval.epochs_in_interval(), @@ -160,55 +166,69 @@ pub(crate) fn query_estimated_current_epoch_operator_reward( pub(crate) fn query_estimated_current_epoch_delegator_reward( deps: Deps<'_>, owner: String, - mix_id: MixId, - proxy: Option, + node_id: NodeId, estimated_performance: Performance, -) -> StdResult { + estimated_work: Option, +) -> Result { let owner_address = deps.api.addr_validate(&owner)?; - let proxy = proxy - .map(|proxy| deps.api.addr_validate(&proxy)) - .transpose()?; - let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { - Some(mix_rewarding) => mix_rewarding, + let rewarding_details = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + Some(info) => info, }; - let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(node_id, &owner_address, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { Some(delegation) => delegation, None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), }; let staked_dec = into_base_decimal(delegation.amount.amount)?; - let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation)?; + let current_value = staked_dec + rewarding_details.determine_delegation_reward(&delegation)?; let amount_staked = delegation.amount; - // check if the mixnode isnt in the process of unbonding (or has already unbonded) - let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); - - if !is_bonded { + if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } - // if node is currently not in the rewarded set, the performance is 0, - // or the node has either unbonded or is in the process of unbonding, - // the calculations are trivial - the rewards are 0 - let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { - None => return Ok(zero_reward(amount_staked, current_value)), - Some(node_status) => node_status, + // check if the node isnt in the process of unbonding (or has already unbonded) + let Ok(bond) = compat::helpers::get_bond(deps.storage, node_id) else { + return Ok(zero_reward(amount_staked, current_value)); }; + if bond.is_unbonding() { + return Ok(zero_reward(amount_staked, current_value)); + } + if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + + let work_factor = if let Some(work_factor) = estimated_work { + work_factor + } else { + let Some(role) = helpers::expensive_role_lookup(deps.storage, node_id)? else { + return Ok(zero_reward(amount_staked, current_value)); + }; + match role { + Role::EntryGateway | Role::Layer1 | Role::Layer2 | Role::Layer3 | Role::ExitGateway => { + rewarding_params.active_node_work() + } + Role::Standby => rewarding_params.standby_node_work(), + } + }; + + let node_reward_params = NodeRewardingParameters { + performance: estimated_performance, + work_factor, + }; + let interval = interval_storage::current_interval(deps.storage)?; - let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + let node_reward = rewarding_details.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = rewarding_details.determine_reward_split( node_reward, estimated_performance, interval.epochs_in_interval(), @@ -218,7 +238,7 @@ pub(crate) fn query_estimated_current_epoch_delegator_reward( return Ok(zero_reward(amount_staked, current_value)); } - let reward_share = current_value / mix_rewarding.delegates * reward_distribution.delegates; + let reward_share = current_value / rewarding_details.delegates * reward_distribution.delegates; Ok(EstimatedCurrentEpochRewardResponse { estimation: Some(truncate_reward(reward_share, &amount_staked.denom)), @@ -273,7 +293,7 @@ mod tests { let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); @@ -292,13 +312,13 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -318,7 +338,7 @@ mod tests { // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -342,13 +362,13 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -374,12 +394,12 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -431,7 +451,8 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); let res = @@ -451,14 +472,15 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -477,7 +499,7 @@ mod tests { // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -501,14 +523,15 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -534,14 +557,15 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -572,7 +596,8 @@ mod tests { let del3 = "delegator3"; let del4 = "delegator4"; - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(del1, 123_456_789u32, mix_id); test.add_immediate_delegation(del2, 150_000_000u32, mix_id); @@ -580,52 +605,55 @@ mod tests { test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.add_immediate_delegation(del3, 500_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(85.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass(mix_id, test_helpers::performance(5.0)); + test.legacy_reward_with_distribution_with_state_bypass( + mix_id, + test_helpers::performance(5.0), + ); test.add_immediate_delegation(del4, 5_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.add_immediate_delegation(del2, 250_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(98.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.remove_immediate_delegation(del3, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(98.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -671,7 +699,7 @@ mod tests { fn expected_current_operator( test: &TestSetup, - mix_id: MixId, + mix_id: NodeId, initial_stake: Uint128, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -691,6 +719,7 @@ mod tests { test.deps(), 42, test_helpers::performance(100.0), + None, ) .unwrap(); assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) @@ -701,11 +730,11 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -718,6 +747,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -730,11 +760,11 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -748,6 +778,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) @@ -757,11 +788,11 @@ mod tests { fn when_node_is_not_in_the_rewarded_set() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -771,6 +802,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -782,11 +814,11 @@ mod tests { fn when_estimated_performance_is_zero() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -795,6 +827,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(0.0), + None, ) .unwrap(); @@ -806,11 +839,11 @@ mod tests { fn with_correct_parameters_matches_actual_distribution() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -820,11 +853,12 @@ mod tests { test.deps(), mix_id, test_helpers::performance(95.0), + None, ) .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); @@ -850,7 +884,7 @@ mod tests { fn expected_current_delegator( test: &TestSetup, - mix_id: MixId, + mix_id: NodeId, owner: &str, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -875,11 +909,11 @@ mod tests { #[test] fn when_delegation_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -888,8 +922,8 @@ mod tests { test.deps(), "foomper".into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -899,7 +933,7 @@ mod tests { #[test] fn when_node_is_unbonding() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -907,7 +941,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -920,8 +954,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -932,7 +966,7 @@ mod tests { #[test] fn when_node_has_already_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -940,7 +974,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -954,8 +988,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -966,7 +1000,7 @@ mod tests { #[test] fn when_node_is_not_in_the_rewarded_set() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -974,7 +1008,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -984,8 +1018,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -996,7 +1030,7 @@ mod tests { #[test] fn when_estimated_performance_is_zero() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -1004,7 +1038,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -1013,8 +1047,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(0.0), + None, ) .unwrap(); @@ -1025,7 +1059,7 @@ mod tests { #[test] fn with_correct_parameters_matches_actual_distribution_for_single_delegator() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -1039,13 +1073,13 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(95.0), + None, ) .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); @@ -1067,7 +1101,7 @@ mod tests { #[test] fn with_correct_parameters_matches_actual_distribution_for_three_delegators() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake1 = Uint128::new(1_000_000_000); let initial_stake2 = Uint128::new(45_000_000_000); @@ -1084,14 +1118,14 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); test.add_immediate_delegation(del3, initial_stake3, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(85.0), ); @@ -1105,8 +1139,8 @@ mod tests { test.deps(), owner.to_string(), mix_id, - None, test_helpers::performance(95.0), + None, ) .unwrap() }) @@ -1131,7 +1165,7 @@ mod tests { let cur3 = initial_stake3_dec + est3; test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index e773e67d5d..37574f99ee 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -2,38 +2,126 @@ // SPDX-License-Identifier: Apache-2.0 use crate::constants::{ - MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, REWARDING_PARAMS_KEY, + CUMULATIVE_EPOCH_WORK_KEY, MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, + REWARDING_PARAMS_KEY, }; use crate::rewards::models::RewardPoolChange; use cosmwasm_std::{Decimal, StdResult, Storage}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::MixNodeRewarding; -use mixnet_contract_common::reward_params::RewardingParams; -use mixnet_contract_common::MixId; +use mixnet_contract_common::mixnode::NodeRewarding; +use mixnet_contract_common::reward_params::{RewardingParams, WorkFactor}; +use mixnet_contract_common::NodeId; + +// LEGACY CONSTANTS: // current parameters used for rewarding purposes pub(crate) const REWARDING_PARAMS: Item<'_, RewardingParams> = Item::new(REWARDING_PARAMS_KEY); pub(crate) const PENDING_REWARD_POOL_CHANGE: Item<'_, RewardPoolChange> = Item::new(PENDING_REWARD_POOL_KEY); -pub const MIXNODE_REWARDING: Map = - Map::new(MIXNODES_REWARDING_PK_NAMESPACE); +pub const MIXNODE_REWARDING: Map = Map::new(MIXNODES_REWARDING_PK_NAMESPACE); -pub fn reward_accounting( - storage: &mut dyn Storage, - amount: Decimal, -) -> Result<(), MixnetContractError> { - let mut pending_changes = PENDING_REWARD_POOL_CHANGE.load(storage)?; - pending_changes.removed += amount; +// we're using the same underlying key to allow seamless delegation migration +pub const NYMNODE_REWARDING: Map = MIXNODE_REWARDING; - Ok(PENDING_REWARD_POOL_CHANGE.save(storage, &pending_changes)?) +pub struct RewardingStorage<'a> { + /// Global parameters used for reward calculation, such as the current reward pool, the active set size, etc. + pub global_rewarding_params: Item<'a, RewardingParams>, + + /// All the changes to the rewarding pool that should get applied upon the **interval** finishing. + pub pending_reward_pool_change: Item<'a, RewardPoolChange>, + + /// Information associated with all nym-nodes (and legacy-mixnodes) required for reward calculation + // important note: this is using **EXACTLY** the same underlying key (and structure) as legacy mixnode rewarding + pub nym_node_rewarding_data: Map<'a, NodeId, NodeRewarding>, + + /// keeps track of total cumulative work submitted for this rewarding epoch to make sure it never goes above 1 + pub cumulative_epoch_work: Item<'a, WorkFactor>, } -pub(crate) fn initialise_storage( - storage: &mut dyn Storage, - reward_params: RewardingParams, -) -> StdResult<()> { - REWARDING_PARAMS.save(storage, &reward_params)?; - PENDING_REWARD_POOL_CHANGE.save(storage, &RewardPoolChange::default()) +impl<'a> RewardingStorage<'a> { + pub const fn new() -> RewardingStorage<'a> { + RewardingStorage { + global_rewarding_params: REWARDING_PARAMS, + pending_reward_pool_change: PENDING_REWARD_POOL_CHANGE, + nym_node_rewarding_data: NYMNODE_REWARDING, + cumulative_epoch_work: Item::new(CUMULATIVE_EPOCH_WORK_KEY), + } + } + + // an 'alias' because a `new` method might be a bit misleading since it'd suggest a brand new storage is created + // as opposed to using the same underlying data as before + pub const fn load() -> RewardingStorage<'a> { + Self::new() + } + + pub fn initialise( + &self, + storage: &mut dyn Storage, + reward_params: RewardingParams, + ) -> StdResult<()> { + self.global_rewarding_params.save(storage, &reward_params)?; + self.pending_reward_pool_change + .save(storage, &RewardPoolChange::default())?; + self.cumulative_epoch_work + .save(storage, &WorkFactor::zero())?; + + Ok(()) + } + + pub fn reset_cumulative_epoch_work( + &self, + storage: &mut dyn Storage, + ) -> Result<(), MixnetContractError> { + self.cumulative_epoch_work + .save(storage, &WorkFactor::zero())?; + Ok(()) + } + + pub fn update_cumulative_epoch_work( + &self, + storage: &mut dyn Storage, + work: Decimal, + ) -> Result<(), MixnetContractError> { + // we use a default in case this is the first run in the new contract since that value hasn't existed before + let current = self + .cumulative_epoch_work + .may_load(storage)? + .unwrap_or(WorkFactor::zero()); + let updated = current + work; + if updated > WorkFactor::one() { + return Err(MixnetContractError::TotalWorkAboveOne); + } + self.cumulative_epoch_work.save(storage, &updated)?; + Ok(()) + } + + pub fn add_pending_pool_changes( + &self, + storage: &mut dyn Storage, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + let mut pending_changes = self.pending_reward_pool_change.load(storage)?; + pending_changes.removed += amount; + self.pending_reward_pool_change + .save(storage, &pending_changes)?; + Ok(()) + } + + pub fn try_persist_node_reward( + &self, + storage: &mut dyn Storage, + node: NodeId, + updated_data: NodeRewarding, + reward: Decimal, + work: WorkFactor, + ) -> Result<(), MixnetContractError> { + self.nym_node_rewarding_data + .save(storage, node, &updated_data)?; + self.add_pending_pool_changes(storage, reward)?; + self.update_cumulative_epoch_work(storage, work)?; + + Ok(()) + } } diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index f772dca800..c5cc5c64d4 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -2,186 +2,194 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::compat::helpers::ensure_can_withdraw_rewards; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnet_contract_settings::storage::ADMIN; -use crate::mixnodes::helpers::get_mixnode_details_by_owner; -use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::helpers; use crate::rewards::helpers::update_and_save_last_rewarded; +use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ - ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, AttachSendTokens, + ensure_any_node_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, + AttachSendTokens, }; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_active_set_update_event, new_mix_rewarding_event, - new_not_found_mix_operator_rewarding_event, new_pending_active_set_update_event, + new_not_found_node_operator_rewarding_event, new_pending_active_set_update_event, new_pending_rewarding_params_update_event, new_rewarding_params_update_event, new_withdraw_delegator_reward_event, new_withdraw_operator_reward_event, new_zero_uptime_mix_operator_rewarding_event, }; use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; use mixnet_contract_common::reward_params::{ - IntervalRewardingParamsUpdate, NodeRewardParams, Performance, + ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters, }; -use mixnet_contract_common::{Delegation, EpochState, MixId}; +use mixnet_contract_common::{Delegation, EpochState, MixNodeDetails, NodeId, NymNodeDetails}; -pub(crate) fn try_reward_mixnode( +pub(crate) fn try_reward_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, - node_performance: Performance, + node_id: NodeId, + node_rewarding_params: NodeRewardingParameters, ) -> Result { + let rewarding_storage = RewardingStorage::load(); + // check whether this `info.sender` is the same one as set in `epoch_status.being_advanced_by` // if so, return `epoch_status` so we could avoid having to perform extra read from the storage let current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?; // see if the epoch has finished let interval = interval_storage::current_interval(deps.storage)?; - if !interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: interval.current_epoch_start_unix_timestamp(), - epoch_end: interval.current_epoch_end_unix_timestamp(), - }); - } + interval.ensure_current_epoch_is_over(&env)?; + let absolute_epoch_id = interval.current_epoch_absolute_id(); - if matches!(current_epoch_status.state, EpochState::Rewarding {last_rewarded, ..} if last_rewarded == mix_id) - { - return Err(MixnetContractError::MixnodeAlreadyRewarded { - mix_id, - absolute_epoch_id, - }); + if let EpochState::Rewarding { last_rewarded, .. } = current_epoch_status.state { + if last_rewarded >= node_id { + return Err(MixnetContractError::NodeAlreadyRewarded { + node_id, + absolute_epoch_id, + }); + } } // update the epoch state with this node as being rewarded most recently - // (if the transaction fails down the line, it will be reverted) - update_and_save_last_rewarded(deps.storage, current_epoch_status, mix_id)?; + // (if the transaction fails down the line, this storage write will be reverted) + update_and_save_last_rewarded(deps.storage, current_epoch_status, node_id)?; - // there's a chance of this failing to load the details if the mixnode unbonded before rewards + // there's a chance of this failing to load the details if the node unbonded before rewards // were distributed and all of its delegators are also gone - let mut mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { - Some(mix_rewarding) if mix_rewarding.still_bonded() => mix_rewarding, - // don't fail if the node has unbonded as we don't want to fail the underlying transaction + + // NOTE: legacy mixnode rewarding are stored under the same storage key + // and have the same rewarding structure thus they'd also be loaded here + let mut rewarding_info = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(rewarding_info) if rewarding_info.still_bonded() => rewarding_info, + // don't fail if the node has unbonded (or it's a legacy gateway) as we don't want to fail the underlying transaction _ => { - return Ok(Response::new() - .add_event(new_not_found_mix_operator_rewarding_event(interval, mix_id))); + return Ok( + Response::new().add_event(new_not_found_node_operator_rewarding_event( + interval, node_id, + )), + ); } }; - let prior_delegates = mix_rewarding.delegates; - let prior_unit_reward = mix_rewarding.full_reward_ratio(); + let prior_delegates = rewarding_info.delegates; + let prior_unit_reward = rewarding_info.full_reward_ratio(); // check if this node has already been rewarded for the current epoch. // unlike the previous check, this one should be a hard error since this cannot be // influenced by users actions (note that previous epoch state checks should actually already guard us against it) - if absolute_epoch_id == mix_rewarding.last_rewarded_epoch { - return Err(MixnetContractError::MixnodeAlreadyRewarded { - mix_id, + if absolute_epoch_id == rewarding_info.last_rewarded_epoch { + return Err(MixnetContractError::NodeAlreadyRewarded { + node_id, absolute_epoch_id, }); } - // again a hard error since the rewarding validator should have known not to reward this node - let node_status = interval_storage::REWARDED_SET - .load(deps.storage, mix_id) - .map_err(|_| MixnetContractError::MixnodeNotInRewardedSet { - mix_id, - absolute_epoch_id, - })?; - // no need to calculate anything as rewards are going to be 0 for everything // however, we still need to update last_rewarded_epoch field - if node_performance.is_zero() { - mix_rewarding.last_rewarded_epoch = absolute_epoch_id; - storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + if node_rewarding_params.is_zero() { + rewarding_info.last_rewarded_epoch = absolute_epoch_id; + storage::NYMNODE_REWARDING.save(deps.storage, node_id, &rewarding_info)?; return Ok( Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( - interval, mix_id, + interval, node_id, )), ); } - // make sure node's profit margin is within the allowed range, + // make sure node's cost function is within the allowed range, // if not adjust it accordingly - let params = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params; - mix_rewarding.normalise_profit_margin(params.profit_margin); - mix_rewarding.normalise_operating_cost(params.interval_operating_cost); + let params = mixnet_params_storage::state_params(deps.storage)?; + rewarding_info.normalise_cost_function(params.profit_margin, params.interval_operating_cost); - let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; - let node_reward_params = NodeRewardParams::new(node_performance, node_status.is_active()); + let global_rewarding_params = rewarding_storage + .global_rewarding_params + .load(deps.storage)?; - // calculate each step separate for easier accounting - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + // calculate each step separately for easier accounting + // + // total node reward, i.e. owner + delegates + let node_reward = rewarding_info.node_reward(&global_rewarding_params, node_rewarding_params); + + // the actual split between owner and its delegates + let reward_distribution = rewarding_info.determine_reward_split( node_reward, - node_performance, + node_rewarding_params.performance, interval.epochs_in_interval(), ); - mix_rewarding.distribute_rewards(reward_distribution, absolute_epoch_id); + // update internal accounting with the new values + rewarding_info.distribute_rewards(reward_distribution, absolute_epoch_id); - // persist changes happened to the storage - storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; - storage::reward_accounting(deps.storage, node_reward)?; + // persist the changes to the storage + rewarding_storage.try_persist_node_reward( + deps.storage, + node_id, + rewarding_info, + node_reward, + node_rewarding_params.work_factor, + )?; Ok(Response::new().add_event(new_mix_rewarding_event( interval, - mix_id, + node_id, reward_distribution, prior_delegates, prior_unit_reward, ))) } -pub(crate) fn try_withdraw_operator_reward( +pub(crate) fn try_withdraw_nym_node_operator_reward( deps: DepsMut<'_>, - info: MessageInfo, + node_details: NymNodeDetails, ) -> Result { - // we need to grab all of the node's details, so we'd known original pledge alongside - // all the earned rewards (and obviously to know if this node even exists and is still - // in the bonded state) - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( - MixnetContractError::NoAssociatedMixNodeBond { - owner: info.sender.clone(), - }, - )?; - let mix_id = mix_details.mix_id(); + ensure_can_withdraw_rewards(&node_details)?; - ensure_bonded(&mix_details.bond_information)?; + let node_id = node_details.node_id(); + let owner = node_details.bond_information.owner.clone(); + + let reward = helpers::withdraw_operator_reward(deps.storage, node_details)?; + + Ok(Response::new().add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) +} + +pub(crate) fn try_withdraw_mixnode_operator_reward( + deps: DepsMut<'_>, + mix_details: MixNodeDetails, +) -> Result { + let node_id = mix_details.mix_id(); + let owner = mix_details.bond_information.owner.clone(); + + ensure_can_withdraw_rewards(&mix_details)?; let reward = helpers::withdraw_operator_reward(deps.storage, mix_details)?; let mut response = Response::new(); // if the reward is zero, don't track or send anything - there's no point if !reward.amount.is_zero() { - response = response.send_tokens(&info.sender, reward.clone()) + response = response.send_tokens(&owner, reward.clone()) } - Ok(response.add_event(new_withdraw_operator_reward_event( - &info.sender, - reward, - mix_id, - ))) + Ok(response.add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) } pub(crate) fn try_withdraw_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + node_id: NodeId, ) -> Result { // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + let storage_key = Delegation::generate_storage_key(node_id, &info.sender, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { None => { - return Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + return Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: info.sender.into_string(), proxy: None, }); @@ -189,21 +197,15 @@ pub(crate) fn try_withdraw_delegator_reward( Some(delegation) => delegation, }; - // grab associated mixnode rewarding details + // grab associated node rewarding details let mix_rewarding = - storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( - "mixnode rewarding got removed from the storage whilst there's still an existing delegation" + storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)?.ok_or(MixnetContractError::inconsistent_state( + "nym-node/legacy mixnode rewarding got removed from the storage whilst there's still an existing delegation" ))?; // see if the mixnode is not in the process of unbonding or whether it has already unbonded // (in that case the expected path of getting your tokens back is via undelegation) - match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { - Some(mix_bond) if mix_bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }); - } - None => return Err(MixnetContractError::MixnodeHasUnbonded { mix_id }), - _ => (), - }; + ensure_any_node_bonded(deps.storage, node_id)?; let reward = helpers::withdraw_delegator_reward(deps.storage, delegation, mix_rewarding)?; let mut response = Response::new(); @@ -216,54 +218,46 @@ pub(crate) fn try_withdraw_delegator_reward( Ok(response.add_event(new_withdraw_delegator_reward_event( &info.sender, reward, - mix_id, + node_id, ))) } -pub(crate) fn try_update_active_set_size( +pub(crate) fn try_update_active_set_distribution( deps: DepsMut<'_>, env: Env, info: MessageInfo, - active_set_size: u32, + update: ActiveSetUpdate, force_immediately: bool, ) -> Result { ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; - if active_set_size == 0 { - return Err(MixnetContractError::ZeroActiveSet); - } + let mut rewarding_params = RewardingStorage::load() + .global_rewarding_params + .load(deps.storage)?; - if active_set_size > rewarding_params.rewarded_set_size { - return Err(MixnetContractError::InvalidActiveSetSize); - } + // make sure the values could theoretically be applied in the current context + rewarding_params.validate_active_set_update(update)?; let interval = interval_storage::current_interval(deps.storage)?; - if force_immediately || interval.is_current_epoch_over(&env) { - rewarding_params.try_change_active_set_size(active_set_size)?; - storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; - Ok(Response::new().add_event(new_active_set_update_event( - env.block.height, - active_set_size, - ))) - } else { - // updating active sety size is only allowed if the epoch is currently not in the process of being advanced - // (unless the force flag was used) - ensure_epoch_in_progress_state(deps.storage)?; - // push the epoch event - let epoch_event = PendingEpochEventKind::UpdateActiveSetSize { - new_size: active_set_size, - }; - push_new_epoch_event(deps.storage, &env, epoch_event)?; - let time_left = interval.secs_until_current_interval_end(&env); - Ok( - Response::new().add_event(new_pending_active_set_update_event( - active_set_size, - time_left, - )), - ) + // perform the change immediately + if force_immediately || interval.is_current_epoch_over(&env) { + rewarding_params.try_change_active_set(update)?; + storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + return Ok(Response::new().add_event(new_active_set_update_event(env.block.height, update))); } + + // otherwise push the event onto the queue to get executed when the epoch concludes + + // updating active set is only allowed if the epoch is currently not in the process of being advanced + // (unless the force flag was used) + ensure_epoch_in_progress_state(deps.storage)?; + + // push the epoch event + let epoch_event = PendingEpochEventKind::UpdateActiveSet { update }; + push_new_epoch_event(deps.storage, &env, epoch_event)?; + let time_left = interval.secs_until_current_interval_end(&env); + Ok(Response::new().add_event(new_pending_active_set_update_event(update, time_left))) } pub(crate) fn try_update_rewarding_params( @@ -311,29 +305,83 @@ pub(crate) fn try_update_rewarding_params( #[cfg(test)] pub mod tests { - use cosmwasm_std::testing::mock_info; - - use crate::mixnodes::storage as mixnodes_storage; - use crate::support::tests::test_helpers; - use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::support::tests::fixtures::active_set_update_fixture; + use crate::support::tests::test_helpers; + use cosmwasm_std::testing::mock_info; #[cfg(test)] mod mixnode_rewarding { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::{find_attribute, FindAttribute, TestSetup}; use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::events::{ MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY, OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY, ZERO_PERFORMANCE_VALUE, }; use mixnet_contract_common::helpers::compare_decimals; - use mixnet_contract_common::{EpochStatus, RewardedSetNodeStatus}; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::Performance; + use mixnet_contract_common::EpochStatus; - use crate::interval::pending_events; - use crate::support::tests::test_helpers::{find_attribute, TestSetup}; + // a simple wrapper to streamline checking for rewarding results + trait TestRewarding { + #[deprecated] + fn legacy_rewarding( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> Result; - use super::*; + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result; + + fn assert_rewarding(&mut self, node_id: NodeId, performance: Performance) -> Response; + } + + impl TestRewarding for TestSetup { + fn legacy_rewarding( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> Result { + // this is rather temporary (I hope...) + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + + self.execute_rewarding( + node_id, + NodeRewardingParameters { + performance, + work_factor, + }, + ) + } + + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result { + let sender = self.rewarding_validator(); + self.execute_fn( + |deps, env, info| try_reward_node(deps, env, info, node_id, rewarding_params), + sender, + ) + } + + #[track_caller] + fn assert_rewarding(&mut self, node_id: NodeId, performance: Performance) -> Response { + let caller = std::panic::Location::caller(); + self.legacy_rewarding(node_id, performance) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + } #[cfg(test)] mod epoch_state_is_correctly_updated { @@ -342,15 +390,15 @@ pub mod tests { #[test] fn when_target_mixnode_unbonded() { let mut test = TestSetup::new(); - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); - let mix_id_never_existed = 42; + let node_id_unbonded = test.add_rewarded_legacy_mixnode("mix-owner-unbonded", None); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); + let node_id_never_existed = 42; test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ]); test.start_epoch_transition(); @@ -360,77 +408,54 @@ pub mod tests { // since before performing the nym-api should clear out the event queue // manually adjust delegation info as to indicate the rewarding information shouldnt get removed - let mut rewarding_details = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id_unbonded_leftover) + let mut rewarding_details = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id_unbonded_leftover) .unwrap(); rewarding_details.delegates = Decimal::raw(12345); rewarding_details.unique_delegations = 1; - storage::MIXNODE_REWARDING + storage::NYMNODE_REWARDING .save( test.deps_mut().storage, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, &rewarding_details, ) .unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded) .unwrap(); pending_events::unbond_mixnode( test.deps_mut(), &env, 123, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, ) .unwrap(); - let env = test.env(); - let sender = test.rewarding_validator(); let performance = test_helpers::performance(100.0); - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id_unbonded, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_unbonded, performance); assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id_unbonded, - final_node_id: mix_id_never_existed, + last_rewarded: node_id_unbonded, + final_node_id: node_id_never_existed, }, interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state ); - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id_unbonded_leftover, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_unbonded_leftover, performance); assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id_unbonded_leftover, - final_node_id: mix_id_never_existed, + last_rewarded: node_id_unbonded_leftover, + final_node_id: node_id_never_existed, }, interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state ); - try_reward_mixnode( - test.deps_mut(), - env, - sender, - mix_id_never_existed, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_never_existed, performance); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -442,16 +467,14 @@ pub mod tests { #[test] fn when_target_mixnode_has_zero_performance() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let zero_performance = test_helpers::performance(0.); - let env = test.env(); - let sender = test.rewarding_validator(); - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, zero_performance).unwrap(); + let zero_performance = test_helpers::performance(0.); + test.assert_rewarding(node_id, zero_performance); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -463,16 +486,14 @@ pub mod tests { #[test] fn when_theres_only_one_node_to_reward() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); + let performance = test_helpers::performance(100.0); + test.assert_rewarding(node_id, performance); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -487,36 +508,27 @@ pub mod tests { let mut ids = Vec::new(); for i in 0..100 { - let mix_id = test.add_dummy_mixnode(&format!("mix-owner{i}"), None); - ids.push(mix_id); + let node_id = test.add_rewarded_legacy_mixnode(&format!("mix-owner{i}"), None); + ids.push(node_id); } test.skip_to_next_epoch_end(); test.force_change_rewarded_set(ids.clone()); test.start_epoch_transition(); let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); - for mix_id in ids { - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); + for node_id in ids { + test.assert_rewarding(node_id, performance); let current_state = interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state; - if mix_id == 100 { + if node_id == 100 { assert_eq!(EpochState::ReconcilingEvents, current_state) } else { assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id, + last_rewarded: node_id, final_node_id: 100, }, current_state @@ -531,12 +543,13 @@ pub mod tests { let bad_states = vec![ EpochState::InProgress, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { let mut test = TestSetup::new(); - let rewarding_validator = test.rewarding_validator(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -545,15 +558,9 @@ pub mod tests { test.skip_to_current_epoch_end(); test.force_change_rewarded_set(vec![1, 2, 3]); - let env = test.env(); - let res = try_reward_mixnode( - test.deps_mut(), - env, - rewarding_validator, - 1, - test_helpers::performance(100.), - ); + let res = test.legacy_rewarding(1, test_helpers::performance(100.0)); + assert_eq!( res, Err(MixnetContractError::UnexpectedNonRewardingEpochState { @@ -566,39 +573,39 @@ pub mod tests { #[test] fn can_only_be_performed_by_specified_rewarding_validator() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let some_sender = mock_info("foomper", &[]); // skip time to when the following epoch is over (since mixnodes are not eligible for rewarding // in the same epoch they're bonded and we need the rewarding epoch to be over) test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.); + let params = test.legacy_rewarding_params(node_id, 100.); let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, some_sender, mix_id, performance); + let res = try_reward_node(test.deps_mut(), env, some_sender, node_id, params); assert_eq!(res, Err(MixnetContractError::Unauthorized)); // good address (sanity check) let env = test.env(); let sender = test.rewarding_validator(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = try_reward_node(test.deps_mut(), env, sender, node_id, params); assert!(res.is_ok()); } #[test] fn can_only_be_performed_if_node_is_fully_bonded() { let mut test = TestSetup::new(); - let mix_id_never_existed = 42; - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + let node_id_never_existed = 42; + let node_id_unbonded = test.add_rewarded_legacy_mixnode("mix-owner-unbonded", None); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ]); test.start_epoch_transition(); @@ -608,43 +615,34 @@ pub mod tests { // since before performing the nym-api should clear out the event queue // manually adjust delegation info as to indicate the rewarding information shouldnt get removed - let mut rewarding_details = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id_unbonded_leftover) + let mut rewarding_details = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id_unbonded_leftover) .unwrap(); rewarding_details.delegates = Decimal::raw(12345); rewarding_details.unique_delegations = 1; - storage::MIXNODE_REWARDING + storage::NYMNODE_REWARDING .save( test.deps_mut().storage, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, &rewarding_details, ) .unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded).unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded).unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); - let env = test.env(); - let sender = test.rewarding_validator(); let performance = test_helpers::performance(100.0); - for &mix_id in &[ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + for &node_id in &[ + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ] { - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); + let res = test.assert_rewarding(node_id, performance); let reason = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), NO_REWARD_REASON_KEY, &res, ); @@ -656,16 +654,14 @@ pub mod tests { fn can_only_be_performed_once_epoch_is_over() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let sender = test.rewarding_validator(); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); // node is in the active set BUT the current epoch has just begun test.skip_to_next_epoch(); - test.force_change_rewarded_set(vec![mix_id]); - let performance = test_helpers::performance(100.); + test.force_change_rewarded_set(vec![node_id]); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + let performance = test_helpers::performance(100.); + let res = test.legacy_rewarding(node_id, performance); assert!(matches!( res, Err(MixnetContractError::EpochInProgress { .. }) @@ -674,199 +670,157 @@ pub mod tests { // epoch is over (sanity check) test.skip_to_current_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = test.legacy_rewarding(node_id, performance); assert!(res.is_ok()); } #[test] fn can_only_be_performed_for_nodes_in_rewarded_set() { - let mut test = TestSetup::new(); - - let active_mix_id = test.add_dummy_mixnode("mix-owner-active", None); - let standby_mix_id = test.add_dummy_mixnode("mix-owner-standby", None); - let inactive_mix_id = test.add_dummy_mixnode("mix-owner-inactive", None); - let sender = test.rewarding_validator(); - - test.skip_to_next_epoch_end(); - - // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - active_mix_id, - &RewardedSetNodeStatus::Active, - ) - .unwrap(); - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - standby_mix_id, - &RewardedSetNodeStatus::Standby, - ) - .unwrap(); - - // actually add one more dummy node with high id so we wouldn't go into the next state - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - 9001, - &RewardedSetNodeStatus::Standby, - ) - .unwrap(); - test.start_epoch_transition(); - - let performance = test_helpers::performance(100.); - let env = test.env(); - let res_active = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - active_mix_id, - performance, - ); - let res_standby = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - standby_mix_id, - performance, - ); - let res_inactive = - try_reward_mixnode(test.deps_mut(), env, sender, inactive_mix_id, performance); - - assert!(res_active.is_ok()); - assert!(res_standby.is_ok()); - assert!(matches!( - res_inactive, - Err(MixnetContractError::MixnodeNotInRewardedSet { mix_id, .. }) if mix_id == inactive_mix_id - )); + // let mut test = TestSetup::new(); + // + // let active_node_id = test.add_rewarded_legacy_mixnode("mix-owner-active", None); + // let standby_node_id = test.add_rewarded_legacy_mixnode("mix-owner-standby", None); + // let inactive_node_id = test.add_rewarded_legacy_mixnode("mix-owner-inactive", None); + // let sender = test.rewarding_validator(); + // + // test.skip_to_next_epoch_end(); + // + // // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive + // interval_storage::REWARDED_SET + // .save( + // test.deps_mut().storage, + // active_node_id, + // &RewardedSetNodeStatus::Active, + // ) + // .unwrap(); + // interval_storage::REWARDED_SET + // .save( + // test.deps_mut().storage, + // standby_node_id, + // &RewardedSetNodeStatus::Standby, + // ) + // .unwrap(); + // + // // actually add one more dummy node with high id so we wouldn't go into the next state + // interval_storage::REWARDED_SET + // .save( + // test.deps_mut().storage, + // 9001, + // &RewardedSetNodeStatus::Standby, + // ) + // .unwrap(); + // test.start_epoch_transition(); + // + // let performance = test_helpers::performance(100.); + // let env = test.env(); + // let res_active = try_reward_node( + // test.deps_mut(), + // env.clone(), + // sender.clone(), + // active_node_id, + // performance, + // ); + // let res_standby = try_reward_node( + // test.deps_mut(), + // env.clone(), + // sender.clone(), + // standby_node_id, + // performance, + // ); + // let res_inactive = + // try_reward_node(test.deps_mut(), env, sender, inactive_node_id, performance); + // + // assert!(res_active.is_ok()); + // assert!(res_standby.is_ok()); + // assert!(matches!( + // res_inactive, + // Err(MixnetContractError::MixnodeNotInRewardedSet { node_id, .. }) if node_id == inactive_node_id + // )); } #[test] fn can_only_be_performed_once_per_node_per_epoch() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id, 42]); + test.force_change_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); let performance = test_helpers::performance(100.); - let env = test.env(); - let sender = test.rewarding_validator(); // first rewarding - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ); - assert!(res.is_ok()); + test.assert_rewarding(node_id, performance); // second rewarding - let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + let res = test.legacy_rewarding(node_id, performance); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id )); // in the following epoch we're good again test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = test.legacy_rewarding(node_id, performance); assert!(res.is_ok()); } #[test] fn requires_nonzero_performance_score() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id, 42]); + test.force_change_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); let zero_performance = test_helpers::performance(0.); let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); // first rewarding - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - zero_performance, - ) - .unwrap(); - let reason = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - NO_REWARD_REASON_KEY, - &res, - ); + let res = test.assert_rewarding(node_id, zero_performance); + let reason = res.attribute(MixnetEventType::NodeRewarding, NO_REWARD_REASON_KEY); assert_eq!(ZERO_PERFORMANCE_VALUE, reason); - // sanity check: it's still treated as rewarding, so you we can't reward the node again + // sanity check: it's still treated as rewarding, so we can't reward the node again // with different performance for the same epoch - let res = try_reward_mixnode( - test.deps_mut(), - env, - sender.clone(), - mix_id, - zero_performance, - ); + let res = test.legacy_rewarding(node_id, zero_performance); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id )); // but in the next epoch, as always, we're good again test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); + let res = test.assert_rewarding(node_id, performance); // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) - let operator = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - OPERATOR_REWARD_KEY, - &res, - ); + let operator = res.attribute(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); assert!(!operator.is_empty()); assert_ne!("0", operator); - let delegates = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - DELEGATES_REWARD_KEY, - &res, - ); + let delegates = res.attribute(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); assert_eq!("0", delegates); } #[test] fn correctly_accounts_for_rewards_distributed() { let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", None); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", None); + let node_id3 = test.add_rewarded_legacy_mixnode("mix-owner3", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + test.force_change_rewarded_set(vec![node_id1, node_id2, node_id3]); test.start_epoch_transition(); let performance = test_helpers::performance(98.0); - let env = test.env(); - let sender = test.rewarding_validator(); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), node_id3); let change = storage::PENDING_REWARD_POOL_CHANGE .load(test.deps().storage) @@ -877,36 +831,17 @@ pub mod tests { let mut total_operator = Decimal::zero(); let mut total_delegates = Decimal::zero(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let before = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id) + for &node_id in &[node_id1, node_id2, node_id3] { + let before = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id) .unwrap(); - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); - let operator: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - OPERATOR_REWARD_KEY, - &res, - ) - .parse() - .unwrap(); - let delegates: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - DELEGATES_REWARD_KEY, - &res, - ) - .parse() - .unwrap(); + let res = test.assert_rewarding(node_id, performance); + let operator = res.decimal(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); + let delegates = res.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); - let after = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id) + let after = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id) .unwrap(); // also the values emitted via events are consistent with the actual values! @@ -936,20 +871,21 @@ pub mod tests { let operator3 = Uint128::new(12_345_000_000); let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", Some(operator3)); + let global_rewarding_params = test.rewarding_params(); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", Some(operator1)); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", Some(operator2)); + let node_id3 = test.add_rewarded_legacy_mixnode("mix-owner3", Some(operator3)); test.skip_to_next_epoch_end(); test.start_epoch_transition(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + test.force_change_rewarded_set(vec![node_id1, node_id2, node_id3]); let performance = test_helpers::performance(98.0); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), node_id3); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); @@ -957,13 +893,16 @@ pub mod tests { // repeat the rewarding the same set of delegates for few epochs for _ in 0..10 { test.start_epoch_transition(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let mut sim = test.instantiate_simulator(mix_id); - let dist = test.reward_with_distribution(mix_id, performance); - let node_params = NodeRewardParams { + for &node_id in &[node_id1, node_id2, node_id3] { + let mut sim = test.instantiate_simulator(node_id); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, performance, - in_active_set: true, - }; + ); + let node_params = NodeRewardingParameters::new( + performance, + global_rewarding_params.active_node_work(), + ); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -975,11 +914,11 @@ pub mod tests { // add few more delegations and repeat it // (note: we're not concerned about whether particular delegation owner got the correct amount, // this is checked in other unit tests) - test.add_immediate_delegation("delegator1", Uint128::new(50_000_000), mix_id1); - test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(50_000_000), node_id1); + test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), node_id2); - test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), mix_id3); - test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), mix_id3); + test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), node_id3); + test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), node_id3); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); @@ -987,13 +926,16 @@ pub mod tests { let performance = test_helpers::performance(12.3); for _ in 0..10 { test.start_epoch_transition(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let mut sim = test.instantiate_simulator(mix_id); - let dist = test.reward_with_distribution(mix_id, performance); - let node_params = NodeRewardParams { + for &node_id in &[node_id1, node_id2, node_id3] { + let mut sim = test.instantiate_simulator(node_id); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, performance, - in_active_set: true, - }; + ); + let node_params = NodeRewardingParameters::new( + performance, + global_rewarding_params.active_node_work(), + ); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -1009,78 +951,53 @@ pub mod tests { let operator2 = Uint128::new(12_345_000_000); let mut test = TestSetup::new(); - let sender = test.rewarding_validator(); + let global_rewarding_params = test.rewarding_params(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", Some(operator1)); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", Some(operator2)); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2]); + test.force_change_rewarded_set(vec![node_id1, node_id2]); let performance = test_helpers::performance(98.0); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id1); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id1); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id1); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id1); - let del11 = test.delegation(mix_id1, "delegator1", &None); - let del12 = test.delegation(mix_id1, "delegator2", &None); - let del21 = test.delegation(mix_id2, "delegator1", &None); + let del11 = test.delegation(node_id1, "delegator1", &None); + let del12 = test.delegation(node_id1, "delegator2", &None); + let del21 = test.delegation(node_id2, "delegator1", &None); for _ in 0..10 { test.start_epoch_transition(); // we know from the previous tests that actual rewarding distribution matches the simulator - let mut sim1 = test.instantiate_simulator(mix_id1); - let mut sim2 = test.instantiate_simulator(mix_id2); + let mut sim1 = test.instantiate_simulator(node_id1); + let mut sim2 = test.instantiate_simulator(node_id2); - let node_params = NodeRewardParams { + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap(); let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap(); - let env = test.env(); + let actual_prior1 = test.mix_rewarding(node_id1); + let actual_prior2 = test.mix_rewarding(node_id2); - let actual_prior1 = test.mix_rewarding(mix_id1); - let actual_prior2 = test.mix_rewarding(mix_id2); + let res1 = test.assert_rewarding(node_id1, performance); - let res1 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id1, - performance, - ) - .unwrap(); + let prior_delegates1 = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward1 = + res1.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates1, actual_prior1.delegates); - - let delegates_reward1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward1, dist1.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1111,40 +1028,17 @@ pub mod tests { None, ); - let res2 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id2, - performance, - ) - .unwrap(); + let res2 = test.assert_rewarding(node_id2, performance); + + let prior_delegates2 = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward2 = + res2.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates2, actual_prior2.delegates); - - let delegates_reward2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward2, dist2.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1168,66 +1062,41 @@ pub mod tests { } // add more delegations and check few more epochs (so that the delegations would start from non-default unit delegation value) - test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id1); - test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id2); + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), node_id1); + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), node_id2); - let del13 = test.delegation(mix_id1, "delegator3", &None); - let del23 = test.delegation(mix_id2, "delegator3", &None); + let del13 = test.delegation(node_id1, "delegator3", &None); + let del23 = test.delegation(node_id2, "delegator3", &None); for _ in 0..10 { test.start_epoch_transition(); // we know from the previous tests that actual rewarding distribution matches the simulator - let mut sim1 = test.instantiate_simulator(mix_id1); - let mut sim2 = test.instantiate_simulator(mix_id2); + let mut sim1 = test.instantiate_simulator(node_id1); + let mut sim2 = test.instantiate_simulator(node_id2); - let node_params = NodeRewardParams { + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap(); let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap(); - let env = test.env(); + let actual_prior1 = test.mix_rewarding(node_id1); + let actual_prior2 = test.mix_rewarding(node_id2); - let actual_prior1 = test.mix_rewarding(mix_id1); - let actual_prior2 = test.mix_rewarding(mix_id2); + let res1 = test.assert_rewarding(node_id1, performance); - let res1 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id1, - performance, - ) - .unwrap(); + let prior_delegates1 = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward1 = + res1.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates1, actual_prior1.delegates); - - let delegates_reward1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward1, dist1.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1266,40 +1135,17 @@ pub mod tests { None, ); - let res2 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id2, - performance, - ) - .unwrap(); + let res2 = test.assert_rewarding(node_id2, performance); + + let prior_delegates2 = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward2 = + res2.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates2, actual_prior2.delegates); - - let delegates_reward2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward2, dist2.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1349,10 +1195,10 @@ pub mod tests { fn can_only_be_done_if_delegation_exists() { let mut test = TestSetup::new(); // add relatively huge stake so that the reward would be high enough to offset operating costs - let mix_id1 = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id2 = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id1 = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id2 = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -1361,36 +1207,42 @@ pub mod tests { let sender2 = mock_info(delegator2, &[]); // note that there's no delegation from delegator1 towards mix1 - test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id1); + test.add_immediate_delegation(delegator2, 100_000_000u128, node_id1); - test.add_immediate_delegation(delegator1, 100_000_000u128, mix_id2); - test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator1, 100_000_000u128, node_id2); + test.add_immediate_delegation(delegator2, 100_000_000u128, node_id2); // perform some rewarding so that we'd have non-zero rewards test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2]); + test.force_change_rewarded_set(vec![node_id1, node_id2]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); - test.reward_with_distribution(mix_id2, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id1, + test_helpers::performance(100.0), + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id2, + test_helpers::performance(100.0), + ); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id1); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id1); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id: mix_id1, + Err(MixnetContractError::NodeDelegationNotFound { + node_id: node_id1, address: delegator1.to_string(), proxy: None, }) ); // sanity check for other ones - let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id2); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, node_id2); assert!(res.is_ok()); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id1); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), node_id1); assert!(res.is_ok()); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id2); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, node_id2); assert!(res.is_ok()); } @@ -1398,38 +1250,44 @@ pub mod tests { fn tokens_are_only_sent_if_reward_is_nonzero() { let mut test = TestSetup::new(); // add relatively huge stake so that the reward would be high enough to offset operating costs - let mix_id1 = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id2 = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id1 = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id2 = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); // very low stake so operating cost would be higher than total reward let low_stake_id = - test.add_dummy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); + test.add_rewarded_legacy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); let delegator = "delegator"; let sender = mock_info(delegator, &[]); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id1); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id1); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id2); test.add_immediate_delegation(delegator, 1_000u128, low_stake_id); // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, low_stake_id]); + test.force_change_rewarded_set(vec![node_id1, low_stake_id]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); - test.reward_with_distribution(low_stake_id, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id1, + test_helpers::performance(100.0), + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + low_stake_id, + test_helpers::performance(100.0), + ); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id1).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id1).unwrap(); assert!(matches!( &res1.messages[0].msg, CosmosMsg::Bank(BankMsg::Send { to_address, amount }) if to_address == delegator && !amount[0].amount.is_zero() ),); let res2 = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id2).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id2).unwrap(); assert!(res2.messages.is_empty()); let res3 = @@ -1442,27 +1300,33 @@ pub mod tests { // note: if node has unbonded or is in the process of unbonding, the expected // way of getting back the rewards is to completely undelegate let mut test = TestSetup::new(); - let mix_id_unbonding = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonding = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonded_leftover = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator = "delegator"; let sender = mock_info(delegator, &[]); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonding); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonded_leftover); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonding); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonded_leftover); let performance = test_helpers::performance(100.0); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { test.start_epoch_transition(); - test.reward_with_distribution(mix_id_unbonding, performance); - test.reward_with_distribution(mix_id_unbonded_leftover, performance); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonding, + performance, + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonded_leftover, + performance, + ); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1471,32 +1335,32 @@ pub mod tests { // start unbonding the first node and fully unbond the other let mut bond = mixnodes_storage::mixnode_bonds() - .load(test.deps().storage, mix_id_unbonding) + .load(test.deps().storage, node_id_unbonding) .unwrap(); bond.is_unbonding = true; mixnodes_storage::mixnode_bonds() - .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .save(test.deps_mut().storage, node_id_unbonding, &bond) .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); let res = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id_unbonding); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id_unbonding); assert_eq!( res, Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + mix_id: node_id_unbonding }) ); let res = - try_withdraw_delegator_reward(test.deps_mut(), sender, mix_id_unbonded_leftover); + try_withdraw_delegator_reward(test.deps_mut(), sender, node_id_unbonded_leftover); assert_eq!( res, - Err(MixnetContractError::MixnodeHasUnbonded { - mix_id: mix_id_unbonded_leftover + Err(MixnetContractError::NymNodeBondNotFound { + node_id: node_id_unbonded_leftover }) ); } @@ -1504,10 +1368,10 @@ pub mod tests { #[test] fn correctly_determines_earned_share_and_resets_reward_ratio() { let mut test = TestSetup::new(); - let mix_id_single = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id_quad = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id_single = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id_quad = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -1525,28 +1389,34 @@ pub mod tests { let amount_quad3 = 250_000_000u128; let amount_quad4 = 500_000_000u128; - test.add_immediate_delegation(delegator1, amount_single, mix_id_single); + test.add_immediate_delegation(delegator1, amount_single, node_id_single); - test.add_immediate_delegation(delegator1, amount_quad1, mix_id_quad); - test.add_immediate_delegation(delegator2, amount_quad2, mix_id_quad); - test.add_immediate_delegation(delegator3, amount_quad3, mix_id_quad); - test.add_immediate_delegation(delegator4, amount_quad4, mix_id_quad); + test.add_immediate_delegation(delegator1, amount_quad1, node_id_quad); + test.add_immediate_delegation(delegator2, amount_quad2, node_id_quad); + test.add_immediate_delegation(delegator3, amount_quad3, node_id_quad); + test.add_immediate_delegation(delegator4, amount_quad4, node_id_quad); let performance = test_helpers::performance(100.0); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_single, mix_id_quad]); + test.force_change_rewarded_set(vec![node_id_single, node_id_quad]); // accumulate some rewards let mut accumulated_single = Decimal::zero(); let mut accumulated_quad = Decimal::zero(); for _ in 0..10 { test.start_epoch_transition(); - let dist = test.reward_with_distribution(mix_id_single, performance); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_single, + performance, + ); // sanity check to make sure test is actually doing what it's supposed to be doing assert!(!dist.delegates.is_zero()); accumulated_single += dist.delegates; - let dist = test.reward_with_distribution(mix_id_quad, performance); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_quad, + performance, + ); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); @@ -1554,30 +1424,32 @@ pub mod tests { test.set_epoch_in_progress_state(); } - let before = test.read_delegation(mix_id_single, delegator1, None); + let before = test.read_delegation(node_id_single, delegator1, None); assert_eq!(before.cumulative_reward_ratio, Decimal::zero()); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_single) + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id_single) .unwrap(); let (_, reward) = test_helpers::get_bank_send_msg(&res1).unwrap(); assert_eq!(truncate_reward_amount(accumulated_single), reward[0].amount); - let after = test.read_delegation(mix_id_single, delegator1, None); + let after = test.read_delegation(node_id_single, delegator1, None); assert_ne!(after.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after.cumulative_reward_ratio, - test.mix_rewarding(mix_id_single).total_unit_reward + test.mix_rewarding(node_id_single).total_unit_reward ); // withdraw first two rewards. note that due to scaling we expect second reward to be 4x the first one - let before1 = test.read_delegation(mix_id_quad, delegator1, None); - let before2 = test.read_delegation(mix_id_quad, delegator2, None); + let before1 = test.read_delegation(node_id_quad, delegator1, None); + let before2 = test.read_delegation(node_id_quad, delegator2, None); assert_eq!(before1.cumulative_reward_ratio, Decimal::zero()); assert_eq!(before2.cumulative_reward_ratio, Decimal::zero()); - let res1 = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_quad) - .unwrap(); + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id_quad) + .unwrap(); let (_, reward1) = test_helpers::get_bank_send_msg(&res1).unwrap(); - let res2 = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id_quad) - .unwrap(); + let res2 = + try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), node_id_quad) + .unwrap(); let (_, reward2) = test_helpers::get_bank_send_msg(&res2).unwrap(); // the seeming "error" comes from reward truncation, // say "actual" reward1 was `100.9`, while reward2 was 4x that, i.e. `403.6 @@ -1592,34 +1464,37 @@ pub mod tests { Uint128::new(4), ); - let after1 = test.read_delegation(mix_id_quad, delegator1, None); + let after1 = test.read_delegation(node_id_quad, delegator1, None); assert_ne!(after1.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after1.cumulative_reward_ratio, - test.mix_rewarding(mix_id_quad).total_unit_reward + test.mix_rewarding(node_id_quad).total_unit_reward ); - let after2 = test.read_delegation(mix_id_quad, delegator2, None); + let after2 = test.read_delegation(node_id_quad, delegator2, None); assert_ne!(after2.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after2.cumulative_reward_ratio, - test.mix_rewarding(mix_id_quad).total_unit_reward + test.mix_rewarding(node_id_quad).total_unit_reward ); // accumulate some more for _ in 0..10 { test.start_epoch_transition(); - let dist = test.reward_with_distribution(mix_id_quad, performance); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_quad, + performance, + ); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); } - let before1_new = test.read_delegation(mix_id_quad, delegator1, None); - let before2_new = test.read_delegation(mix_id_quad, delegator2, None); - let before3 = test.read_delegation(mix_id_quad, delegator3, None); - let before4 = test.read_delegation(mix_id_quad, delegator4, None); + let before1_new = test.read_delegation(node_id_quad, delegator1, None); + let before2_new = test.read_delegation(node_id_quad, delegator2, None); + let before3 = test.read_delegation(node_id_quad, delegator3, None); + let before4 = test.read_delegation(node_id_quad, delegator4, None); assert_eq!( before1_new.cumulative_reward_ratio, @@ -1633,10 +1508,10 @@ pub mod tests { assert_eq!(before4.cumulative_reward_ratio, Decimal::zero()); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender1, node_id_quad).unwrap(); let (_, reward1_new) = test_helpers::get_bank_send_msg(&res1).unwrap(); let res2 = - try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender2, node_id_quad).unwrap(); let (_, reward2_new) = test_helpers::get_bank_send_msg(&res2).unwrap(); // the ratio between first and second delegator is still the same @@ -1647,10 +1522,10 @@ pub mod tests { ); let res3 = - try_withdraw_delegator_reward(test.deps_mut(), sender3, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender3, node_id_quad).unwrap(); let (_, reward3) = test_helpers::get_bank_send_msg(&res3).unwrap(); let res4 = - try_withdraw_delegator_reward(test.deps_mut(), sender4, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender4, node_id_quad).unwrap(); let (_, reward4) = test_helpers::get_bank_send_msg(&res4).unwrap(); // (and so is the ratio between 3rd and 4th) @@ -1677,6 +1552,7 @@ pub mod tests { #[cfg(test)] mod withdrawing_operator_reward { use super::*; + use crate::compat::transactions::try_withdraw_operator_reward; use crate::interval::pending_events; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Uint128}; @@ -1686,18 +1562,22 @@ pub mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); + let node_id = + test.add_rewarded_legacy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); let sender = mock_info("random-guy", &[]); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, + test_helpers::performance(100.0), + ); let res = try_withdraw_operator_reward(test.deps_mut(), sender.clone()); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: sender.sender }) ) @@ -1709,17 +1589,21 @@ pub mod tests { let owner1 = "mix-owner1"; let owner2 = "mix-owner2"; - let mix_id1 = test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); - test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let node_id1 = + test.add_rewarded_legacy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + test.add_rewarded_legacy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); let sender1 = mock_info(owner1, &[]); let sender2 = mock_info(owner2, &[]); // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1]); + test.force_change_rewarded_set(vec![node_id1]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id1, + test_helpers::performance(100.0), + ); let res1 = try_withdraw_operator_reward(test.deps_mut(), sender1).unwrap(); assert!(matches!( @@ -1740,23 +1624,29 @@ pub mod tests { let owner2 = "mix-owner2"; let sender1 = mock_info(owner1, &[]); let sender2 = mock_info(owner2, &[]); - let mix_id_unbonding = - test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonding = + test.add_rewarded_legacy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); // add some delegation to the second node so that it wouldn't be cleared upon unbonding - test.add_immediate_delegation("delegator", 100_000_000u128, mix_id_unbonded_leftover); + test.add_immediate_delegation("delegator", 100_000_000u128, node_id_unbonded_leftover); let performance = test_helpers::performance(100.0); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { test.start_epoch_transition(); - test.reward_with_distribution(mix_id_unbonding, performance); - test.reward_with_distribution(mix_id_unbonded_leftover, performance); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonding, + performance, + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonded_leftover, + performance, + ); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1765,29 +1655,29 @@ pub mod tests { // start unbonding the first node and fully unbond the other let mut bond = mixnodes_storage::mixnode_bonds() - .load(test.deps().storage, mix_id_unbonding) + .load(test.deps().storage, node_id_unbonding) .unwrap(); bond.is_unbonding = true; mixnodes_storage::mixnode_bonds() - .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .save(test.deps_mut().storage, node_id_unbonding, &bond) .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); let res = try_withdraw_operator_reward(test.deps_mut(), sender1); assert_eq!( res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + Err(MixnetContractError::NodeIsUnbonding { + node_id: node_id_unbonding }) ); let res = try_withdraw_operator_reward(test.deps_mut(), sender2); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked(owner2) }) ); @@ -1797,6 +1687,7 @@ pub mod tests { #[cfg(test)] mod updating_active_set { use cw_controllers::AdminError::NotAdmin; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::EpochStatus; use crate::support::tests::test_helpers::TestSetup; @@ -1811,7 +1702,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1825,11 +1718,11 @@ pub mod tests { interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env.clone(), owner.clone(), - 100, + active_set_update_fixture(), false, ); assert!(matches!( @@ -1837,8 +1730,13 @@ pub mod tests { Err(MixnetContractError::EpochAdvancementInProgress { .. }) )); - let res_forced = - try_update_active_set_size(test.deps_mut(), env.clone(), owner, 100, true); + let res_forced = try_update_active_set_distribution( + test.deps_mut(), + env.clone(), + owner, + active_set_update_fixture(), + true, + ); assert!(res_forced.is_ok()) } } @@ -1852,19 +1750,31 @@ pub mod tests { let random = mock_info("random-guy", &[]); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env.clone(), rewarding_validator, - 42, + active_set_update_fixture(), false, ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); - let res = try_update_active_set_size(test.deps_mut(), env.clone(), random, 42, false); + let res = try_update_active_set_distribution( + test.deps_mut(), + env.clone(), + random, + active_set_update_fixture(), + false, + ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner, + active_set_update_fixture(), + false, + ); assert!(res.is_ok()) } @@ -1876,14 +1786,18 @@ pub mod tests { let rewarded_set_size = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .rewarded_set_size; + .rewarded_set_size(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner.clone(), - rewarded_set_size + 1, + ActiveSetUpdate { + entry_gateways: rewarded_set_size, + exit_gateways: rewarded_set_size, + mixnodes: rewarded_set_size * 3, + }, false, ); assert_eq!(res, Err(MixnetContractError::InvalidActiveSetSize)); @@ -1892,11 +1806,15 @@ pub mod tests { // (make sure we start with the fresh state) let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner.clone(), - rewarded_set_size, + ActiveSetUpdate { + entry_gateways: rewarded_set_size / 3, + exit_gateways: rewarded_set_size / 3, + mixnodes: rewarded_set_size / 3, + }, false, ); assert!(res.is_ok()); @@ -1904,11 +1822,15 @@ pub mod tests { // as well as if its any value lower than that let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner, - rewarded_set_size - 100, + ActiveSetUpdate { + entry_gateways: 1, + exit_gateways: 1, + mixnodes: 3, + }, false, ); assert!(res.is_ok()); @@ -1922,27 +1844,37 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); test.skip_to_current_interval_end(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, false); + + let update = active_set_update_fixture(); + let expected = update.active_set_size(); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner.clone(), + update, + false, + ); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected); // sanity check for "normal" case let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + let res = + try_update_active_set_distribution(test.deps_mut(), env, owner, update, false); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_eq!(old, new); } @@ -1954,16 +1886,19 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, true); + + let update = active_set_update_fixture(); + let expected = update.active_set_size(); + let res = try_update_active_set_distribution(test.deps_mut(), env, owner, update, true); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected); } #[test] @@ -1974,29 +1909,39 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + + let issued_update = active_set_update_fixture(); + let expected_updated = issued_update.active_set_size(); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner, + issued_update, + false, + ); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_eq!(old, new); // make sure it's actually saved to pending events let events = test.pending_epoch_events(); - assert!( - matches!(events[0].kind, PendingEpochEventKind::UpdateActiveSetSize { new_size } if new_size == 42) - ); + let PendingEpochEventKind::UpdateActiveSet { update } = events[0].kind else { + panic!("unexpected epoch event") + }; + assert_eq!(update, issued_update); test.execute_all_pending_events(); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected_updated); } } @@ -2005,6 +1950,8 @@ pub mod tests { use cosmwasm_std::Decimal; use cw_controllers::AdminError::NotAdmin; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::RewardedSetParams; use mixnet_contract_common::EpochStatus; use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; @@ -2019,7 +1966,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; let update = IntervalRewardingParamsUpdate { @@ -2029,7 +1978,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; for bad_state in bad_states { @@ -2076,7 +2030,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let env = test.env(); @@ -2109,7 +2068,7 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let env = test.env(); @@ -2129,7 +2088,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2141,7 +2105,7 @@ pub mod tests { assert!(res.is_ok()); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); // sanity check for "normal" case let mut test = TestSetup::new(); @@ -2164,7 +2128,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2173,7 +2142,7 @@ pub mod tests { assert!(res.is_ok()); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); } #[test] @@ -2188,7 +2157,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2200,14 +2174,18 @@ pub mod tests { // make sure it's actually saved to pending events let events = test.pending_interval_events(); - assert!( - matches!(events[0].kind,PendingIntervalEventKind::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123)) - ); + let PendingIntervalEventKind::UpdateRewardingParams { update } = events[0].kind else { + panic!("unexpected epoch event") + }; + let Some(rewarded_set_update) = update.rewarded_set_params else { + panic!("no rewarded set updates"); + }; + assert_eq!(rewarded_set_update.rewarded_set_size(), 669); test.execute_all_pending_events(); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); } #[test] @@ -2229,7 +2207,7 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let env = test.env(); diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 19ddf2bb6e..9d8e158e76 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -3,11 +3,15 @@ use crate::gateways::storage as gateways_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::must_get_mixnode_bond_by_owner; use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::helpers::must_get_node_bond_by_owner; +use crate::nodes::storage as nymnodes_storage; use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::PendingMixNodeChanges; -use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond}; +use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond, NodeId}; +use nym_contracts_common::IdentityKey; use nym_contracts_common::Percent; // helper trait to attach `Msg` to a response if it's provided @@ -132,40 +136,6 @@ pub(crate) fn ensure_epoch_in_progress_state( Ok(()) } -// pub(crate) fn ensure_mix_rewarding_state(storage: &dyn Storage) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::Rewarding { .. }) { -// return Err(MixnetContractError::EpochNotInMixRewardingState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } -// -// pub(crate) fn ensure_event_reconciliation_state( -// storage: &dyn Storage, -// ) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::ReconcilingEvents) { -// return Err(MixnetContractError::EpochNotInEventReconciliationState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } -// -// pub(crate) fn ensure_epoch_advancement_state( -// storage: &dyn Storage, -// ) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::AdvancingEpoch) { -// return Err(MixnetContractError::EpochNotInAdvancementState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } - pub(crate) fn ensure_is_authorized( sender: &Addr, storage: &dyn Storage, @@ -212,12 +182,68 @@ pub(crate) fn ensure_no_pending_pledge_changes( Ok(()) } +pub(crate) fn ensure_no_pending_params_changes( + pending_changes: &PendingMixNodeChanges, +) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = pending_changes.cost_params_change { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) +} + +/// get identity key of the currently bonded legacy mixnode or nym-node +#[allow(dead_code)] +pub(crate) fn get_bond_identity( + storage: &dyn Storage, + owner: &Addr, +) -> Result { + // legacy mixnode + if let Ok(bond) = must_get_mixnode_bond_by_owner(storage, owner) { + return Ok(bond.mix_node.identity_key); + } + // current nym-node + must_get_node_bond_by_owner(storage, owner).map(|b| b.node.identity_key) +} + +/// Checks whether a nym-node or a legacy mixnode with the provided id is currently bonded +pub(crate) fn ensure_any_node_bonded( + storage: &dyn Storage, + node_id: NodeId, +) -> Result<(), MixnetContractError> { + // legacy mixnode + if let Some(mixnode_bond) = mixnodes_storage::mixnode_bonds().may_load(storage, node_id)? { + return if mixnode_bond.is_unbonding { + Err(MixnetContractError::MixnodeIsUnbonding { mix_id: node_id }) + } else { + Ok(()) + }; + } + + // current nym-node + match nymnodes_storage::nym_nodes().may_load(storage, node_id)? { + None => return Err(MixnetContractError::NymNodeBondNotFound { node_id }), + Some(bond) if bond.is_unbonding => { + return Err(MixnetContractError::NodeIsUnbonding { node_id }) + } + _ => Ok(()), + } +} + // check if the target address has already bonded a mixnode or gateway, // in either case, return an appropriate error pub(crate) fn ensure_no_existing_bond( sender: &Addr, storage: &dyn Storage, ) -> Result<(), MixnetContractError> { + if nymnodes_storage::nym_nodes() + .idx + .owner + .item(storage, sender.clone())? + .is_some() + { + return Err(MixnetContractError::AlreadyOwnsNymNode); + } + if mixnodes_storage::mixnode_bonds() .idx .owner diff --git a/contracts/mixnet/src/support/legacy.rs b/contracts/mixnet/src/support/legacy.rs new file mode 100644 index 0000000000..755fb6cc8b --- /dev/null +++ b/contracts/mixnet/src/support/legacy.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/mixnet/src/support/mod.rs b/contracts/mixnet/src/support/mod.rs index f27f7eecd0..41cb1691e3 100644 --- a/contracts/mixnet/src/support/mod.rs +++ b/contracts/mixnet/src/support/mod.rs @@ -2,4 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod helpers; +pub(crate) mod legacy; + +const foo: &str = "remove that hack"; +// #[cfg(test)] pub(crate) mod tests; diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index 5a3d5e38a6..d76b9598b8 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -1,7 +1,11 @@ -use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::constants::INITIAL_PLEDGE_AMOUNT; use cosmwasm_std::{coin, Coin}; -use mixnet_contract_common::mixnode::MixNodeCostParams; -use mixnet_contract_common::{Gateway, MixNode, Percent}; +use mixnet_contract_common::mixnode::NodeCostParams; +use mixnet_contract_common::reward_params::ActiveSetUpdate; +use mixnet_contract_common::{ + Gateway, MixNode, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; pub const TEST_COIN_DENOM: &str = "unym"; @@ -17,10 +21,11 @@ pub fn mix_node_fixture() -> MixNode { } } -pub fn mix_node_cost_params_fixture() -> MixNodeCostParams { - MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: coin(40_000_000, TEST_COIN_DENOM), +pub fn node_cost_params_fixture() -> NodeCostParams { + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_COIN_DENOM), } } @@ -36,16 +41,25 @@ pub fn gateway_fixture() -> Gateway { } } -pub fn good_mixnode_pledge() -> Vec { +pub fn good_node_plegge() -> Vec { vec![Coin { denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }] } -pub fn good_gateway_pledge() -> Vec { - vec![Coin { - denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, - }] +pub fn good_mixnode_pledge() -> Vec { + good_node_plegge() +} + +pub fn good_gateway_pledge() -> Vec { + good_node_plegge() +} + +pub fn active_set_update_fixture() -> ActiveSetUpdate { + ActiveSetUpdate { + entry_gateways: 30, + exit_gateways: 30, + mixnodes: 30, + } } diff --git a/contracts/mixnet/src/support/tests/legacy.rs b/contracts/mixnet/src/support/tests/legacy.rs new file mode 100644 index 0000000000..d74808cdf1 --- /dev/null +++ b/contracts/mixnet/src/support/tests/legacy.rs @@ -0,0 +1,65 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::gateways::storage as gateways_storage; +use crate::gateways::storage::PREASSIGNED_LEGACY_IDS; +use crate::interval::storage as interval_storage; +use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::storage::next_nymnode_id_counter; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Env, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{ + Gateway, GatewayBond, MixNode, MixNodeBond, NodeCostParams, NodeId, NodeRewarding, +}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn save_new_mixnode( + storage: &mut dyn Storage, + env: Env, + mixnode: MixNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result { + let mix_id = next_nymnode_id_counter(storage)?; + let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); + + let mixnode_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; + let mixnode_bond = MixNodeBond { + mix_id, + owner, + original_pledge: pledge, + mix_node: mixnode, + proxy: None, + bonding_height: env.block.height, + is_unbonding: false, + }; + + // save mixnode bond data + // note that this implicitly checks for uniqueness on identity key, sphinx key and owner + mixnodes_storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; + + // save rewarding data + rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; + + Ok(mix_id) +} + +pub(crate) fn save_new_gateway( + storage: &mut dyn Storage, + env: Env, + gateway: Gateway, + owner: Addr, + pledge: Coin, +) -> Result { + let gateway_identity = gateway.identity_key.clone(); + let bond = GatewayBond::new(pledge.clone(), owner.clone(), env.block.height, gateway); + + gateways_storage::gateways().save(storage, bond.identity(), &bond)?; + + let id = next_nymnode_id_counter(storage)?; + PREASSIGNED_LEGACY_IDS.save(storage, gateway_identity.clone(), &id)?; + + Ok(gateway_identity) +} diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs deleted file mode 100644 index 61a0815d91..0000000000 --- a/contracts/mixnet/src/support/tests/messages.rs +++ /dev/null @@ -1,36 +0,0 @@ -use cosmwasm_std::{Coin, Deps}; -use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey}; -use nym_crypto::asymmetric::identity; -use rand_chacha::rand_core::{CryptoRng, RngCore}; - -use crate::support::tests; -use crate::support::tests::test_helpers::{ed25519_sign_message, gateway_bonding_sign_payload}; - -pub(crate) fn valid_bond_gateway_msg( - mut rng: impl RngCore + CryptoRng, - deps: Deps<'_>, - stake: Vec, - sender: &str, -) -> (ExecuteMsg, IdentityKey) { - let keypair = identity::KeyPair::new(&mut rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - - let gateway = Gateway { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::gateway_fixture() - }; - - let msg = gateway_bonding_sign_payload(deps, sender, gateway.clone(), stake); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let identity_key = keypair.public_key().to_base58_string(); - ( - ExecuteMsg::BondGateway { - gateway, - owner_signature, - }, - identity_key, - ) -} diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index dee121b2f0..de41b21d43 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -3,40 +3,46 @@ #[cfg(test)] pub mod fixtures; -#[cfg(test)] -pub mod messages; +pub(crate) mod legacy; #[cfg(test)] pub mod test_helpers { use crate::constants; - use crate::contract::instantiate; - use crate::delegations::queries::query_mixnode_delegations_paged; + use crate::contract::{execute, instantiate}; + use crate::delegations::queries::query_node_delegations_paged; use crate::delegations::storage as delegations_storage; - use crate::delegations::transactions::try_delegate_to_mixnode; - use crate::families::transactions::{try_create_family, try_join_family}; - use crate::gateways::transactions::try_add_gateway; + use crate::delegations::transactions::try_delegate_to_node; use crate::interval::transactions::{ perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, }; use crate::interval::{pending_events, storage as interval_storage}; - use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage}; use crate::mixnet_contract_settings::storage::{ - minimum_gateway_pledge, minimum_mixnode_pledge, rewarding_denom, - rewarding_validator_address, + self as mixnet_params_storage, minimum_node_pledge, }; + use crate::mixnet_contract_settings::storage::{rewarding_denom, rewarding_validator_address}; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; - use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode}; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::nodes::storage as nymnodes_storage; + use crate::nodes::storage::helpers::RoleStorageBucket; + use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; + use crate::nodes::storage::{read_assigned_roles, save_assignment, swap_active_role_bucket}; + use crate::nodes::transactions::{try_add_nym_node, try_remove_nym_node}; + use crate::rewards::helpers::expensive_role_lookup; use crate::rewards::queries::{ query_pending_delegator_reward, query_pending_mixnode_operator_reward, }; use crate::rewards::storage as rewards_storage; - use crate::rewards::transactions::try_reward_mixnode; + use crate::rewards::storage::RewardingStorage; + use crate::rewards::transactions::try_reward_node; use crate::signing::storage as signing_storage; + use crate::support::helpers::ensure_no_existing_bond; use crate::support::tests; use crate::support::tests::fixtures::{ - good_gateway_pledge, good_mixnode_pledge, TEST_COIN_DENOM, + good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM, }; + use crate::support::tests::{legacy, test_helpers}; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; @@ -48,21 +54,25 @@ pub mod test_helpers { use cosmwasm_std::{Deps, OwnedDeps}; use cosmwasm_std::{DepsMut, MessageInfo}; use cosmwasm_std::{Env, Response, Timestamp, Uint128}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, }; - use mixnet_contract_common::families::FamilyHead; - use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode}; + use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode}; + use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; - use mixnet_contract_common::reward_params::{Performance, RewardingParams}; + use mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor, + }; use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode; use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ - construct_family_join_permit, Delegation, EpochEventId, EpochState, EpochStatus, Gateway, + Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, Gateway, GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg, - Interval, MixId, MixNode, MixNodeBond, MixnodeBondingPayload, Percent, - RewardedSetNodeStatus, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + Interval, MixNode, MixNodeBond, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, + NymNodeBondingPayload, Percent, RoleAssignment, SignableGatewayBondingMsg, + SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, @@ -72,8 +82,34 @@ pub mod test_helpers { use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use serde::Serialize; + use std::collections::HashMap; + use std::fmt::Debug; + use std::str::FromStr; use std::time::Duration; + pub enum NodeQueryType { + ById(NodeId), + ByIdentity(IdentityKey), + ByOwner(Addr), + } + + impl From for NodeQueryType { + fn from(value: NodeId) -> Self { + NodeQueryType::ById(value) + } + } + + impl From for NodeQueryType { + fn from(value: IdentityKey) -> Self { + NodeQueryType::ByIdentity(value) + } + } + impl From for NodeQueryType { + fn from(value: Addr) -> Self { + NodeQueryType::ByOwner(value) + } + } + pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { if a > b { assert!(a - b <= leeway) @@ -132,6 +168,99 @@ pub mod test_helpers { self.env.clone() } + #[allow(unused)] + pub fn execute( + &mut self, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + let env = self.env.clone(); + execute(self.deps_mut(), env, info, msg) + } + + #[allow(unused)] + pub fn execute_no_funds( + &mut self, + sender: impl Into, + msg: ExecuteMsg, + ) -> Result { + self.execute(self.mock_info(sender), msg) + } + + #[allow(unused)] + pub fn execute_fn( + &mut self, + exec_fn: F, + info: MessageInfo, + ) -> Result + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let env = self.env().clone(); + exec_fn(self.deps_mut(), env, info) + } + + #[allow(unused)] + pub fn execute_fn_no_funds( + &mut self, + exec_fn: F, + sender: impl Into, + ) -> Result + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let info = self.mock_info(sender); + self.execute_fn(exec_fn, info) + } + + #[allow(unused)] + #[track_caller] + pub fn assert_simple_execution(&mut self, exec_fn: F, info: MessageInfo) -> Response + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let caller = std::panic::Location::caller(); + self.execute_fn(exec_fn, info) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + + #[allow(unused)] + #[track_caller] + pub fn assert_simple_execution_no_funds( + &mut self, + exec_fn: F, + sender: impl Into, + ) -> Response + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let caller = std::panic::Location::caller(); + self.execute_fn_no_funds(exec_fn, sender) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + + pub fn get_node_id(&self, query_type: impl Into) -> NodeId { + match query_type.into() { + NodeQueryType::ById(id) => id, + NodeQueryType::ByIdentity(identity) => { + get_node_details_by_identity(&self.deps.storage, identity) + .unwrap() + .unwrap() + .node_id() + } + NodeQueryType::ByOwner(owner) => { + must_get_node_bond_by_owner(&self.deps.storage, &owner) + .unwrap() + .node_id + } + } + } + + #[allow(unused)] + pub fn mock_info(&self, sender: impl Into) -> MessageInfo { + mock_info(&sender.into(), &[]) + } + pub fn rewarding_validator(&self) -> MessageInfo { self.rewarding_validator.clone() } @@ -158,77 +287,54 @@ pub mod test_helpers { interval_storage::current_interval(self.deps().storage).unwrap() } - pub fn rewarded_set(&self) -> Vec<(MixId, RewardedSetNodeStatus)> { - interval_storage::REWARDED_SET - .range(self.deps().storage, None, None, Order::Ascending) - .map(|res| res.unwrap()) - .collect::>() - } - - pub fn generate_family_join_permit( - &mut self, - family_owner_keys: &identity::KeyPair, - member_node: IdentityKeyRef, - ) -> MessageSignature { - let identity = family_owner_keys.public_key().to_base58_string(); - - let head_mixnode = mixnodes_storage::mixnode_bonds() - .idx - .identity_key - .item(self.deps().storage, identity.clone()) - .unwrap() - .map(|record| record.1) - .unwrap(); - - let family_head = FamilyHead::new(&identity); - let owner = head_mixnode.owner; - - let nonce = signing_storage::get_signing_nonce(self.deps().storage, owner).unwrap(); - - let msg = construct_family_join_permit(nonce, family_head, member_node.to_owned()); - - let sig_bytes = family_owner_keys - .private_key() - .sign(msg.to_plaintext().unwrap()) - .to_bytes(); - MessageSignature::from(sig_bytes.as_ref()) + pub fn active_roles_bucket(&self) -> RoleStorageBucket { + ACTIVE_ROLES_BUCKET.load(self.deps().storage).unwrap() } #[allow(unused)] - pub fn join_family( + pub fn active_roles_metadata(&self) -> RewardedSetMetadata { + let bucket = self.active_roles_bucket().other(); + ROLES_METADATA + .load(self.deps().storage, bucket as u8) + .unwrap() + } + + pub fn inactive_roles_metadata(&self) -> RewardedSetMetadata { + let bucket = self.active_roles_bucket().other(); + ROLES_METADATA + .load(self.deps().storage, bucket as u8) + .unwrap() + } + + #[allow(unused)] + pub fn active_roles(&self, role: Role) -> Vec { + let bucket = self.active_roles_bucket().other(); + ROLES + .load(self.deps().storage, (bucket as u8, role)) + .unwrap() + } + + pub fn inactive_roles(&self, role: Role) -> Vec { + let bucket = self.active_roles_bucket().other(); + ROLES + .load(self.deps().storage, (bucket as u8, role)) + .unwrap() + } + + pub fn max_role_count(&self, role: Role) -> u32 { + RewardingStorage::load() + .global_rewarding_params + .load(self.deps().storage) + .unwrap() + .rewarded_set + .maximum_role_count(role) + } + + pub fn set_pending_pledge_change( &mut self, - member: &str, - member_keys: &identity::KeyPair, - head_keys: &identity::KeyPair, + mix_id: NodeId, + event_id: Option, ) { - let member_identity = member_keys.public_key().to_base58_string(); - let head_identity = head_keys.public_key().to_base58_string(); - - let join_permit = self.generate_family_join_permit(head_keys, &member_identity); - let family_head = FamilyHead::new(head_identity); - - try_join_family( - self.deps_mut(), - mock_info(member, &[]), - join_permit, - family_head, - ) - .unwrap(); - } - - #[allow(dead_code)] - pub fn create_dummy_mixnode_with_new_family( - &mut self, - head: &str, - label: &str, - ) -> (MixId, identity::KeyPair) { - let (mix_id, keys) = self.add_dummy_mixnode_with_keypair(head, None); - - try_create_family(self.deps_mut(), mock_info(head, &[]), label.to_string()).unwrap(); - (mix_id, keys) - } - - pub fn set_pending_pledge_change(&mut self, mix_id: MixId, event_id: Option) { let mut changes = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(self.deps().storage, mix_id) .unwrap_or_default(); @@ -239,77 +345,206 @@ pub mod test_helpers { .unwrap(); } - pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option) -> MixId { - let stake = self.make_mix_pledge(stake); - let (mixnode, owner_signature, _) = - self.mixnode_with_signature(owner, Some(stake.clone())); + pub fn immediately_assign_lowest_mix_layer(&mut self, node_id: NodeId) -> Role { + let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); + + let mut layer1 = read_assigned_roles(&self.deps.storage, Role::Layer1).unwrap(); + let mut layer2 = read_assigned_roles(&self.deps.storage, Role::Layer2).unwrap(); + let mut layer3 = read_assigned_roles(&self.deps.storage, Role::Layer3).unwrap(); + let l1 = layer1.len(); + let l2 = layer2.len(); + let l3 = layer3.len(); + + if l1 <= l2 && l1 <= l3 { + layer1.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, Role::Layer1), + &layer1, + ) + .unwrap(); + + Role::Layer1 + } else if l2 <= l3 && l2 <= l1 { + layer2.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, Role::Layer2), + &layer2, + ) + .unwrap(); + + Role::Layer2 + } else { + layer3.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, Role::Layer3), + &layer3, + ) + .unwrap(); + + Role::Layer3 + } + } + + pub fn add_rewarded_set_nymnode( + &mut self, + owner: &str, + stake: Option, + ) -> (NymNodeBond, MessageSignature, KeyPair) { + let res = self.add_nymnode(owner, stake); + let id = res.0.node_id; + self.immediately_assign_lowest_mix_layer(id); + + res + } + + pub fn add_rewarded_set_nymnode_id( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + self.add_rewarded_set_nymnode(owner, stake).0.node_id + } + + pub fn add_nymnode( + &mut self, + owner: &str, + stake: Option, + ) -> (NymNodeBond, MessageSignature, KeyPair) { + let stake = self.make_node_pledge(stake); + let (node, owner_signature, keypair) = + self.node_with_signature(owner, Some(stake.clone())); let info = mock_info(owner, stake.as_ref()); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - let env = self.env(); - try_add_mixnode( + try_add_nym_node( self.deps_mut(), env, - info, - mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner_signature, + info.clone(), + node, + tests::fixtures::node_cost_params_fixture(), + owner_signature.clone(), ) .unwrap(); - // newly added mixnode gets assigned the current counter + 1 - current_id_counter + 1 + let bond = must_get_node_bond_by_owner(&self.deps.storage, &info.sender).unwrap(); + + (bond, owner_signature, keypair) } - pub fn add_dummy_gateway(&mut self, sender: &str, stake: Option) -> IdentityKey { - let stake = self.make_gateway_pledge(stake); - let (gateway, owner_signature) = - self.gateway_with_signature(sender, Some(stake.clone())); + pub fn add_dummy_nymnode(&mut self, owner: &str, stake: Option) -> NodeId { + self.add_nymnode(owner, stake).0.node_id + } - let info = mock_info(sender, &stake); - let key = gateway.identity_key.clone(); + #[allow(unused)] + pub fn add_dummy_nym_node_with_keypair( + &mut self, + owner: &str, + stake: Option, + ) -> (NodeId, identity::KeyPair) { + let (bond, _, keypair) = self.add_nymnode(owner, stake); + (bond.node_id, keypair) + } + + pub fn add_legacy_mixnode(&mut self, owner: &str, stake: Option) -> NodeId { + let stake = self.make_mix_pledge(stake); + let (mixnode, _, _) = self.mixnode_with_signature(owner, Some(stake.clone())); + + let info = mock_info(owner, stake.as_ref()); let env = self.env(); - try_add_gateway(self.deps_mut(), env, info, gateway, owner_signature).unwrap(); - key + + ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap(); + signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone()) + .unwrap(); + let node_id = legacy::save_new_mixnode( + &mut self.deps.storage, + env, + mixnode, + tests::fixtures::node_cost_params_fixture(), + info.sender, + info.funds[0].clone(), + ) + .unwrap(); + + node_id + } + + pub fn add_rewarded_legacy_mixnode( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + let node_id = self.add_legacy_mixnode(owner, stake); + self.immediately_assign_lowest_mix_layer(node_id); + + node_id + } + + pub fn add_legacy_gateway(&mut self, sender: &str, stake: Option) -> IdentityKey { + let stake = self.make_gateway_pledge(stake); + let (gateway, _) = self.gateway_with_signature(sender, Some(stake.clone())); + + let env = self.env(); + let info = mock_info(sender, &stake); + + legacy::save_new_gateway( + &mut self.deps.storage, + env, + gateway, + info.sender, + info.funds[0].clone(), + ) + .unwrap() + } + + pub fn save_legacy_gateway(&mut self, gateway: Gateway, info: &MessageInfo) { + let env = self.env(); + + legacy::save_new_gateway( + &mut self.deps.storage, + env, + gateway, + info.sender.clone(), + info.funds[0].clone(), + ) + .unwrap(); } pub fn add_dummy_mixnodes(&mut self, n: usize) { for i in 0..n { - self.add_dummy_mixnode(&format!("owner{i}"), None); + self.add_legacy_mixnode(&format!("owner{i}"), None); } } pub fn add_dummy_gateways(&mut self, n: usize) { for i in 0..n { - self.add_dummy_gateway(&format!("owner{i}"), None); + self.add_legacy_gateway(&format!("owner{i}"), None); } } - pub fn make_mix_pledge(&self, stake: Option) -> Vec { + pub fn make_node_pledge(&self, stake: Option) -> Vec { let stake = match stake { Some(amount) => { let denom = rewarding_denom(self.deps().storage).unwrap(); Coin { denom, amount } } - None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), + None => minimum_node_pledge(self.deps.as_ref().storage).unwrap(), }; vec![stake] } + pub fn make_mix_pledge(&self, stake: Option) -> Vec { + self.make_node_pledge(stake) + } + pub fn make_gateway_pledge(&self, stake: Option) -> Vec { - let stake = match stake { - Some(amount) => { - let denom = rewarding_denom(self.deps().storage).unwrap(); - Coin { denom, amount } - } - None => minimum_gateway_pledge(self.deps.as_ref().storage).unwrap(), - }; - vec![stake] + self.make_node_pledge(stake) } pub fn mixnode_bonding_signature( @@ -328,42 +563,48 @@ pub mod test_helpers { &mut self, owner: &str, stake: Option, - ) -> (MixId, identity::KeyPair) { + ) -> (NodeId, identity::KeyPair) { let stake = self.make_mix_pledge(stake); + let (mixnode, _, keypair) = self.mixnode_with_signature(owner, Some(stake.clone())); - let keypair = identity::KeyPair::new(&mut self.rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - - let mixnode = MixNode { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }; - - let msg = - mixnode_bonding_sign_payload(self.deps(), owner, mixnode.clone(), stake.clone()); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let info = mock_info(owner, &stake); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - + let info = mock_info(owner, stake.as_ref()); let env = self.env(); - try_add_mixnode( - self.deps_mut(), + + ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap(); + signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone()) + .unwrap(); + let node_id = legacy::save_new_mixnode( + &mut self.deps.storage, env, - info, mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner_signature, + tests::fixtures::node_cost_params_fixture(), + info.sender, + info.funds[0].clone(), ) .unwrap(); - // newly added mixnode gets assigned the current counter + 1 - (current_id_counter + 1, keypair) + (node_id, keypair) + } + + pub fn node_with_signature( + &mut self, + sender: &str, + stake: Option>, + ) -> (NymNode, MessageSignature, KeyPair) { + let stake = stake.unwrap_or(good_node_plegge()); + + let keypair = identity::KeyPair::new(&mut self.rng); + let identity_key = keypair.public_key().to_base58_string(); + + let node = NymNode { + host: "1.2.3.4".to_string(), + custom_http_port: None, + identity_key, + }; + let msg = nymnode_bonding_sign_payload(self.deps(), sender, node.clone(), stake); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + + (node, owner_signature, keypair) } pub fn mixnode_with_signature( @@ -390,7 +631,7 @@ pub mod test_helpers { pub fn gateway_with_signature( &mut self, - sender: &str, + sender: impl Into, stake: Option>, ) -> (Gateway, MessageSignature) { let stake = stake.unwrap_or(good_gateway_pledge()); @@ -405,13 +646,18 @@ pub mod test_helpers { ..tests::fixtures::gateway_fixture() }; - let msg = gateway_bonding_sign_payload(self.deps(), sender, gateway.clone(), stake); + let msg = gateway_bonding_sign_payload( + self.deps(), + sender.into().as_str(), + gateway.clone(), + stake, + ); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); (gateway, owner_signature) } - pub fn start_unbonding_mixnode(&mut self, mix_id: MixId) { + pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) { let bond_details = mixnodes_storage::mixnode_bonds() .load(self.deps().storage, mix_id) .unwrap(); @@ -425,17 +671,45 @@ pub mod test_helpers { .unwrap(); } - pub fn immediately_unbond_mixnode(&mut self, mix_id: MixId) { + pub fn start_unbonding_nymnode(&mut self, node_id: NodeId) { + let bond_details = nymnodes_storage::nym_nodes() + .load(self.deps().storage, node_id) + .unwrap(); + + let env = self.env(); + try_remove_nym_node( + self.deps_mut(), + env, + mock_info(bond_details.owner.as_str(), &[]), + ) + .unwrap(); + } + + #[track_caller] + pub fn immediately_unbond_node(&mut self, node: impl Into) { + let node_id = self.get_node_id(node); + let env = self.env(); + pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id) + .unwrap(); + } + + pub fn immediately_unbond_mixnode(&mut self, mix_id: NodeId) { let env = self.env(); pending_events::unbond_mixnode(self.deps_mut(), &env, env.block.height, mix_id) .unwrap(); } + pub fn immediately_unbond_nymnode(&mut self, node_id: NodeId) { + let env = self.env(); + pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id) + .unwrap(); + } + pub fn add_immediate_delegation( &mut self, delegator: &str, amount: impl Into, - target: MixId, + target: NodeId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { @@ -459,7 +733,7 @@ pub mod test_helpers { &mut self, delegator: &str, amount: impl Into, - target: MixId, + target: NodeId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { @@ -470,7 +744,7 @@ pub mod test_helpers { delegate(self.deps_mut(), env, delegator, vec![amount], target) } - pub fn remove_immediate_delegation(&mut self, delegator: &str, target: MixId) { + pub fn remove_immediate_delegation(&mut self, delegator: &str, target: NodeId) { let height = self.env.block.height; pending_events::undelegate(self.deps_mut(), height, Addr::unchecked(delegator), target) .unwrap(); @@ -482,6 +756,12 @@ pub mod test_helpers { try_begin_epoch_transition(self.deps_mut(), env, sender).unwrap(); } + pub fn epoch_state(&self) -> EpochState { + interval_storage::current_epoch_status(self.deps().storage) + .unwrap() + .state + } + pub fn set_epoch_in_progress_state(&mut self) { let being_advanced_by = self.rewarding_validator.sender.clone(); interval_storage::save_current_epoch_status( @@ -506,20 +786,22 @@ pub mod test_helpers { .unwrap(); } - pub fn set_epoch_advancement_state(&mut self) { + pub fn set_epoch_role_assignment_state(&mut self) { let being_advanced_by = self.rewarding_validator.sender.clone(); interval_storage::save_current_epoch_status( self.deps_mut().storage, &EpochStatus { being_advanced_by, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }, ) .unwrap(); } #[allow(unused)] - pub fn pending_operator_reward(&mut self, mix: MixId) -> Decimal { + pub fn pending_operator_reward(&mut self, mix: NodeId) -> Decimal { query_pending_mixnode_operator_reward(self.deps(), mix) .unwrap() .amount_earned_detailed @@ -527,7 +809,7 @@ pub mod test_helpers { } #[allow(unused)] - pub fn pending_delegator_reward(&mut self, delegator: &str, target: MixId) -> Decimal { + pub fn pending_delegator_reward(&mut self, delegator: &str, target: NodeId) -> Decimal { query_pending_delegator_reward(self.deps(), delegator.into(), target, None) .unwrap() .amount_earned_detailed @@ -583,16 +865,52 @@ pub mod test_helpers { self.set_epoch_in_progress_state(); } - pub fn force_change_rewarded_set(&mut self, nodes: Vec) { - let active_set_size = rewards_storage::REWARDING_PARAMS - .load(self.deps().storage) - .unwrap() - .active_set_size; - interval_storage::update_rewarded_set(self.deps_mut().storage, active_set_size, nodes) - .unwrap(); + pub fn reset_role_assignment(&mut self) { + let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); + + for role in vec![ + Role::EntryGateway, + Role::ExitGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ] { + ROLES + .save(&mut self.deps.storage, (active_bucket as u8, role), &vec![]) + .unwrap(); + } } - pub fn instantiate_simulator(&self, node: MixId) -> Simulator { + // note: this does NOT assign gateway role + pub fn force_change_rewarded_set(&mut self, nodes: Vec) { + // reset current assignment + self.reset_role_assignment(); + let mut roles = HashMap::new(); + for node in nodes { + let role = self.immediately_assign_lowest_mix_layer(node); + let assigned = roles.entry(role).or_insert(Vec::new()); + assigned.push(node) + } + + // we cheat a bit to write to the 'active' bucket instead + swap_active_role_bucket(self.deps_mut().storage).unwrap(); + + for (role, assignment) in roles { + save_assignment( + self.deps_mut().storage, + RoleAssignment { + role, + nodes: assignment, + }, + ) + .unwrap(); + } + + swap_active_role_bucket(self.deps_mut().storage).unwrap(); + } + + pub fn instantiate_simulator(&self, node: NodeId) -> Simulator { simulator_from_single_node_state(self.deps(), node) } @@ -615,39 +933,164 @@ pub mod test_helpers { .collect::>() } + pub fn active_node_work(&self) -> WorkFactor { + self.rewarding_params().active_node_work() + } + + pub fn standby_node_work(&self) -> WorkFactor { + self.rewarding_params().standby_node_work() + } + + pub fn active_node_params(&self, performance: f32) -> NodeRewardingParameters { + NodeRewardingParameters { + performance: test_helpers::performance(performance), + work_factor: self.active_node_work(), + } + } + + pub fn standby_node_params(&self, performance: f32) -> NodeRewardingParameters { + NodeRewardingParameters { + performance: test_helpers::performance(performance), + work_factor: self.standby_node_work(), + } + } + + pub fn reward_with_distribution_ignore_state( + &mut self, + node_id: NodeId, + params: NodeRewardingParameters, + ) -> RewardDistribution { + self.reward_with_distribution_with_state_bypass( + node_id, + params.performance, + params.work_factor, + ) + } + pub fn reward_with_distribution_with_state_bypass( &mut self, - mix_id: MixId, + node_id: NodeId, performance: Performance, + work_factor: WorkFactor, ) -> RewardDistribution { let initial_status = interval_storage::current_epoch_status(self.deps().storage).unwrap(); self.start_epoch_transition(); - let res = self.reward_with_distribution(mix_id, performance); + let res = self.reward_with_distribution( + node_id, + NodeRewardingParameters::new(performance, work_factor), + ); interval_storage::save_current_epoch_status(self.deps_mut().storage, &initial_status) .unwrap(); res } + #[deprecated] + pub fn legacy_reward_with_distribution_with_state_bypass( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> RewardDistribution { + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + self.reward_with_distribution_with_state_bypass(node_id, performance, work_factor) + } + + #[track_caller] + pub fn node_role(&self, node_id: NodeId) -> Role { + if read_assigned_roles(&self.deps.storage, Role::EntryGateway) + .unwrap() + .contains(&node_id) + { + Role::EntryGateway + } else if read_assigned_roles(&self.deps.storage, Role::ExitGateway) + .unwrap() + .contains(&node_id) + { + Role::ExitGateway + } else if read_assigned_roles(&self.deps.storage, Role::Layer1) + .unwrap() + .contains(&node_id) + { + Role::Layer1 + } else if read_assigned_roles(&self.deps.storage, Role::Layer2) + .unwrap() + .contains(&node_id) + { + Role::Layer2 + } else if read_assigned_roles(&self.deps.storage, Role::Layer3) + .unwrap() + .contains(&node_id) + { + Role::Layer3 + } else if read_assigned_roles(&self.deps.storage, Role::Standby) + .unwrap() + .contains(&node_id) + { + Role::Standby + } else { + let caller = std::panic::Location::caller(); + panic!("{caller}: no assigned roles") + } + } + + pub fn legacy_rewarding_params( + &self, + node_id: NodeId, + performance: f32, + ) -> NodeRewardingParameters { + let performance = test_helpers::performance(performance); + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + NodeRewardingParameters { + performance, + work_factor, + } + } + + pub fn get_legacy_rewarding_node_work_factor(&self, node_id: NodeId) -> Decimal { + let global_rewarding_params = self.rewarding_params(); + let work_factor = + match expensive_role_lookup(self.deps.as_ref().storage, node_id).unwrap() { + None => Decimal::zero(), + Some(Role::Standby) => global_rewarding_params.standby_node_work(), + _ => global_rewarding_params.active_node_work(), + }; + work_factor + } + + pub fn legacy_reward_with_distribution_and_legacy_work_factor( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> RewardDistribution { + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + self.reward_with_distribution( + node_id, + NodeRewardingParameters { + performance, + work_factor, + }, + ) + } + pub fn reward_with_distribution( &mut self, - mix_id: MixId, - performance: Performance, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, ) -> RewardDistribution { let env = self.env(); let sender = self.rewarding_validator(); let res = - try_reward_mixnode(self.deps_mut(), env, sender, mix_id, performance).unwrap(); + try_reward_node(self.deps_mut(), env, sender, node_id, rewarding_params).unwrap(); let operator: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), OPERATOR_REWARD_KEY, &res, ) .parse() .unwrap(); let delegates: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), DELEGATES_REWARD_KEY, &res, ) @@ -662,7 +1105,7 @@ pub mod test_helpers { pub fn read_delegation( &mut self, - mix: MixId, + mix: NodeId, owner: &str, proxy: Option<&str>, ) -> Delegation { @@ -675,19 +1118,25 @@ pub mod test_helpers { .unwrap() } - pub fn mix_rewarding(&self, node: MixId) -> MixNodeRewarding { + pub fn mix_rewarding(&self, node: NodeId) -> NodeRewarding { rewards_storage::MIXNODE_REWARDING .load(self.deps().storage, node) .unwrap() } #[allow(unused)] - pub fn mix_bond(&self, mix_id: MixId) -> MixNodeBond { + pub fn mix_bond(&self, mix_id: NodeId) -> MixNodeBond { mixnode_bonds().load(self.deps().storage, mix_id).unwrap() } - pub fn delegation(&self, mix: MixId, owner: &str, proxy: &Option) -> Delegation { - read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy).unwrap() + #[track_caller] + pub fn delegation(&self, mix: NodeId, owner: &str, proxy: &Option) -> Delegation { + let caller = std::panic::Location::caller(); + + read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy) + .unwrap_or_else(|| { + panic!("{caller} failed with: delegation for {mix}/{owner} doesn't exist") + }) } } @@ -707,11 +1156,11 @@ pub mod test_helpers { } } - pub fn simulator_from_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator { + pub fn simulator_from_single_node_state(deps: Deps<'_>, node: NodeId) -> Simulator { let mix_rewarding = rewards_storage::MIXNODE_REWARDING .load(deps.storage, node) .unwrap(); - let delegations = query_mixnode_delegations_paged(deps, node, None, None).unwrap(); + let delegations = query_node_delegations_paged(deps, node, None, None).unwrap(); if delegations.delegations.len() as u32 == constants::DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT { @@ -769,6 +1218,50 @@ pub mod test_helpers { panic!("did not find the attribute") } + pub(crate) trait FindAttribute { + fn attribute(&self, event_type: E, attribute: &str) -> String + where + E: Into>, + S: Into; + + fn parsed_attribute(&self, event_type: E, attribute: &str) -> T + where + E: Into>, + S: Into, + T: FromStr, + ::Err: Debug; + + fn decimal(&self, event_type: E, attribute: &str) -> Decimal + where + E: Into>, + S: Into, + { + self.parsed_attribute(event_type, attribute) + } + } + + impl FindAttribute for Response { + fn attribute(&self, event_type: E, attribute: &str) -> String + where + E: Into>, + S: Into, + { + find_attribute(event_type.into(), attribute, self) + } + + fn parsed_attribute(&self, event_type: E, attribute: &str) -> T + where + E: Into>, + S: Into, + T: FromStr, + ::Err: Debug, + { + find_attribute(event_type.into(), attribute, self) + .parse() + .unwrap() + } + } + // using floats in tests is fine // (what it does is converting % value, like 12.34 into `Performance` (`Percent`) // which internally is represented by decimal `0.1234` @@ -842,7 +1335,7 @@ pub mod test_helpers { // (gateway, owner_signature) // } - pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: MixId, n: usize) { + pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) { for i in 0..n { pending_events::delegate( deps.branch(), @@ -916,7 +1409,7 @@ pub mod test_helpers { deps: DepsMut<'_>, identity_key: Option<&str>, owner: &str, - ) -> MixId { + ) -> NodeId { let id = loop { let candidate = rng.next_u32(); if !mixnodes_storage::unbonded_mixnodes().has(deps.storage, candidate) { @@ -943,13 +1436,28 @@ pub mod test_helpers { id } + pub fn nymnode_bonding_sign_payload( + deps: Deps<'_>, + owner: &str, + node: NymNode, + stake: Vec, + ) -> SignableNymNodeBondingMsg { + let cost_params = tests::fixtures::node_cost_params_fixture(); + let nonce = + signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); + + let payload = NymNodeBondingPayload::new(node, cost_params); + let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload); + SignableNymNodeBondingMsg::new(nonce, content) + } + pub fn mixnode_bonding_sign_payload( deps: Deps<'_>, owner: &str, mixnode: MixNode, stake: Vec, ) -> SignableMixNodeBondingMsg { - let cost_params = tests::fixtures::mix_node_cost_params_fixture(); + let cost_params = tests::fixtures::node_cost_params_fixture(); let nonce = signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); @@ -972,6 +1480,15 @@ pub mod test_helpers { SignableGatewayBondingMsg::new(nonce, content) } + fn intial_rewarded_set_params() -> RewardedSetParams { + RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 50, + } + } + fn initial_rewarding_params() -> InitialRewardingParams { let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; @@ -983,8 +1500,7 @@ pub mod test_helpers { sybil_resistance: Percent::from_percentage_value(30).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_size: 240, - active_set_size: 100, + rewarded_set_params: intial_rewarded_set_params(), } } @@ -1006,14 +1522,14 @@ pub mod test_helpers { deps } - pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec, mix_id: MixId) { + pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec, mix_id: NodeId) { let info = mock_info(sender, &stake); - try_delegate_to_mixnode(deps, env, info, mix_id).unwrap(); + try_delegate_to_node(deps, env, info, mix_id).unwrap(); } pub(crate) fn read_delegation( storage: &dyn Storage, - mix: MixId, + mix: NodeId, owner: &Addr, proxy: &Option, ) -> Option { diff --git a/contracts/mixnet/src/testing/legacy.rs b/contracts/mixnet/src/testing/legacy.rs new file mode 100644 index 0000000000..38a919fd33 --- /dev/null +++ b/contracts/mixnet/src/testing/legacy.rs @@ -0,0 +1,12 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn legacy_mixnode_bonding() { + todo!() + } +} diff --git a/contracts/mixnet/src/testing/mod.rs b/contracts/mixnet/src/testing/mod.rs index 30de4b3136..ac99770b50 100644 --- a/contracts/mixnet/src/testing/mod.rs +++ b/contracts/mixnet/src/testing/mod.rs @@ -2,3 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod transactions; + +// the purpose of that module is to keep track of tests of legacy features that will eventually be phased out +// such as standalone mixnode/gateway bonding +pub(crate) mod legacy; diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs index 3faf826ec0..cfdc18fcf6 100644 --- a/contracts/mixnet/src/vesting_migration.rs +++ b/contracts/mixnet/src/vesting_migration.rs @@ -10,7 +10,7 @@ use crate::support::helpers::{ }; use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; pub(crate) fn try_migrate_vested_mixnode( @@ -61,7 +61,7 @@ pub(crate) fn try_migrate_vested_mixnode( pub(crate) fn try_migrate_vested_delegation( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { ensure_epoch_in_progress_state(deps.storage)?; diff --git a/contracts/vesting/src/queries.rs b/contracts/vesting/src/queries.rs index 17330c6dbe..a744db56ad 100644 --- a/contracts/vesting/src/queries.rs +++ b/contracts/vesting/src/queries.rs @@ -8,7 +8,7 @@ use crate::vesting::StorableVestingAccountExt; use contracts_common::{get_build_information, ContractBuildInformation}; use cosmwasm_std::{Coin, Deps, Env, Order, StdResult, Timestamp, Uint128}; use cw_storage_plus::Bound; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::{ Account, AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData, VestingCoinsResponse, @@ -259,7 +259,7 @@ pub fn try_get_withdrawn_coins( pub fn try_get_delegation_times( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let owner = deps.api.addr_validate(vesting_account_address)?; let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; @@ -277,7 +277,7 @@ pub fn try_get_delegation_times( pub fn try_get_all_delegations( deps: Deps<'_>, - start_after: Option<(u32, MixId, BlockTimestampSecs)>, + start_after: Option<(u32, NodeId, BlockTimestampSecs)>, limit: Option, ) -> Result { let limit = limit.unwrap_or(100).min(200) as usize; @@ -314,7 +314,7 @@ pub fn try_get_all_delegations( pub fn try_get_delegation( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, block_timestamp_secs: BlockTimestampSecs, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; @@ -333,7 +333,7 @@ pub fn try_get_delegation( pub fn try_get_delegation_amount( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index d6d363d36f..a2122de58f 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cosmwasm_std::{Coin, Order}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{IdentityKey, MixId}; +use mixnet_contract_common::{IdentityKey, NodeId}; use vesting_contract_common::account::VestingAccountStorageKey; use vesting_contract_common::{Account, PledgeData, VestingContractError}; @@ -40,7 +40,7 @@ pub const _OLD_DELEGATIONS: Map< /// Storage map containing information about tokens delegated towards particular mixnodes /// in the mixnet contract with given vesting account. -pub const DELEGATIONS: Map<'_, (VestingAccountStorageKey, MixId, BlockTimestampSecs), Uint128> = +pub const DELEGATIONS: Map<'_, (VestingAccountStorageKey, NodeId, BlockTimestampSecs), Uint128> = Map::new("dlg_v2"); /// Explicit contract admin that is allowed, among other things, to create new vesting accounts. @@ -53,7 +53,7 @@ pub const MIXNET_CONTRACT_ADDRESS: Item<'_, Addr> = Item::new("mix"); pub const MIX_DENOM: Item<'_, String> = Item::new("den"); pub fn save_delegation( - key: (VestingAccountStorageKey, MixId, BlockTimestampSecs), + key: (VestingAccountStorageKey, NodeId, BlockTimestampSecs), amount: Uint128, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { @@ -68,7 +68,7 @@ pub fn save_delegation( } pub fn remove_delegation( - key: (VestingAccountStorageKey, MixId, BlockTimestampSecs), + key: (VestingAccountStorageKey, NodeId, BlockTimestampSecs), storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { DELEGATIONS.remove(storage, key); @@ -76,7 +76,7 @@ pub fn remove_delegation( } pub fn load_delegation_timestamps( - prefix: (VestingAccountStorageKey, MixId), + prefix: (VestingAccountStorageKey, NodeId), storage: &dyn Storage, ) -> Result, VestingContractError> { let block_timestamps = DELEGATIONS @@ -87,7 +87,7 @@ pub fn load_delegation_timestamps( } pub fn count_subdelegations_for_mix( - prefix: (VestingAccountStorageKey, MixId), + prefix: (VestingAccountStorageKey, NodeId), storage: &dyn Storage, ) -> u32 { DELEGATIONS diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 8e4c7c6a62..d4fb13e330 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -2,7 +2,7 @@ use contracts_common::signing::MessageSignature; use cosmwasm_std::{Coin, Env, Response, Storage}; use mixnet_contract_common::{ gateway::GatewayConfigUpdate, - mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, + mixnode::{MixNodeConfigUpdate, NodeCostParams}, Gateway, MixNode, }; use vesting_contract_common::VestingContractError; @@ -16,7 +16,7 @@ pub trait MixnodeBondingAccount { fn try_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, env: &Env, @@ -58,7 +58,7 @@ pub trait MixnodeBondingAccount { fn try_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, storage: &mut dyn Storage, ) -> Result; fn try_track_migrated_mixnode( diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 2e7b5a65d3..b0d2154537 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -1,17 +1,17 @@ use cosmwasm_std::{Coin, Env, Response, Storage, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::VestingContractError; pub trait DelegatingAccount { fn try_claim_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; fn try_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, env: &Env, storage: &mut dyn Storage, @@ -19,7 +19,7 @@ pub trait DelegatingAccount { fn try_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; @@ -30,7 +30,7 @@ pub trait DelegatingAccount { fn track_delegation( &self, block_height: u64, - mix_id: MixId, + mix_id: NodeId, // Save some gas by passing it in current_balance: Uint128, delegation: Coin, @@ -40,13 +40,13 @@ pub trait DelegatingAccount { // vesting account performs an undelegation. fn track_undelegation( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; fn track_migrated_delegation( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; } diff --git a/contracts/vesting/src/traits/mod.rs b/contracts/vesting/src/traits/mod.rs index d1126bb92c..60cff028d4 100644 --- a/contracts/vesting/src/traits/mod.rs +++ b/contracts/vesting/src/traits/mod.rs @@ -1,9 +1,7 @@ pub mod bonding_account; pub mod delegating_account; -pub mod node_families; pub mod vesting_account; pub use self::bonding_account::{GatewayBondingAccount, MixnodeBondingAccount}; pub use self::delegating_account::DelegatingAccount; -pub use self::node_families::NodeFamilies; pub use self::vesting_account::VestingAccount; diff --git a/contracts/vesting/src/traits/node_families.rs b/contracts/vesting/src/traits/node_families.rs deleted file mode 100644 index f5af460d17..0000000000 --- a/contracts/vesting/src/traits/node_families.rs +++ /dev/null @@ -1,32 +0,0 @@ -use contracts_common::signing::MessageSignature; -use cosmwasm_std::{Response, Storage}; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::IdentityKeyRef; -use vesting_contract_common::VestingContractError; - -pub trait NodeFamilies { - fn try_create_family( - &self, - storage: &dyn Storage, - label: String, - ) -> Result; - - fn try_join_family( - &self, - storage: &dyn Storage, - join_permit: MessageSignature, - family_head: FamilyHead, - ) -> Result; - - fn try_leave_family( - &self, - storage: &dyn Storage, - family_head: FamilyHead, - ) -> Result; - - fn try_head_kick_member( - &self, - storage: &dyn Storage, - member: IdentityKeyRef<'_>, - ) -> Result; -} diff --git a/contracts/vesting/src/transactions.rs b/contracts/vesting/src/transactions.rs index a51f084862..09e3ac9a8a 100644 --- a/contracts/vesting/src/transactions.rs +++ b/contracts/vesting/src/transactions.rs @@ -6,14 +6,13 @@ use crate::storage::{ account_from_address, save_account, ADMIN, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, }; use crate::traits::{ - DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, NodeFamilies, VestingAccount, + DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, }; use crate::vesting::{populate_vesting_periods, StorableVestingAccountExt}; use contracts_common::signing::MessageSignature; use cosmwasm_std::{coin, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Timestamp}; -use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ - Gateway, GatewayConfigUpdate, MixId, MixNode, MixNodeConfigUpdate, MixNodeCostParams, + Gateway, GatewayConfigUpdate, MixNode, MixNodeConfigUpdate, NodeCostParams, NodeId, }; use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, @@ -24,40 +23,6 @@ use vesting_contract_common::events::{ }; use vesting_contract_common::{Account, PledgeCap, VestingContractError, VestingSpecification}; -pub fn try_create_family( - info: MessageInfo, - deps: DepsMut, - label: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_create_family(deps.storage, label) -} -pub fn try_join_family( - info: MessageInfo, - deps: DepsMut, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_join_family(deps.storage, join_permit, family_head) -} -pub fn try_leave_family( - info: MessageInfo, - deps: DepsMut, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_leave_family(deps.storage, family_head) -} -pub fn try_kick_family_member( - info: MessageInfo, - deps: DepsMut, - member: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_head_kick_member(deps.storage, &member) -} - /// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens. /// /// Callable by ADMIN only, see [instantiate]. @@ -99,7 +64,7 @@ pub fn try_update_gateway_config( } pub fn try_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, info: MessageInfo, deps: DepsMut, ) -> Result { @@ -273,7 +238,7 @@ pub fn try_track_migrate_mixnode( /// Track vesting delegation being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. pub fn try_track_migrate_delegation( owner: &str, - mix_id: MixId, + mix_id: NodeId, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { @@ -288,7 +253,7 @@ pub fn try_track_migrate_delegation( /// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_mixnode( mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, amount: Coin, info: MessageInfo, @@ -392,7 +357,7 @@ pub fn try_track_reward( /// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards. pub fn try_track_undelegation( address: &str, - mix_id: MixId, + mix_id: NodeId, amount: Coin, info: MessageInfo, deps: DepsMut<'_>, @@ -408,7 +373,7 @@ pub fn try_track_undelegation( /// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. pub fn try_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, info: MessageInfo, @@ -452,7 +417,7 @@ pub fn try_claim_operator_reward( pub fn try_claim_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; @@ -461,7 +426,7 @@ pub fn try_claim_delegator_reward( /// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, info: MessageInfo, deps: DepsMut<'_>, diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index d01bea6789..167707401a 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -5,7 +5,7 @@ use crate::traits::DelegatingAccount; use crate::vesting::account::StorableVestingAccountExt; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::ExecuteMsg as MixnetExecuteMsg; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::events::{ new_vesting_delegation_event, new_vesting_undelegation_event, }; @@ -16,7 +16,7 @@ use super::Account; impl DelegatingAccount for Account { fn try_claim_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { let msg = MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { @@ -32,7 +32,7 @@ impl DelegatingAccount for Account { fn try_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, coin: Coin, env: &Env, storage: &mut dyn Storage, @@ -74,7 +74,7 @@ impl DelegatingAccount for Account { fn try_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { if !self.any_delegation_for_mix(mix_id, storage) { @@ -99,7 +99,7 @@ impl DelegatingAccount for Account { fn track_delegation( &self, block_timestamp_secs: u64, - mix_id: MixId, + mix_id: NodeId, current_balance: Uint128, delegation: Coin, storage: &mut dyn Storage, @@ -116,7 +116,7 @@ impl DelegatingAccount for Account { fn track_undelegation( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { @@ -128,7 +128,7 @@ impl DelegatingAccount for Account { fn track_migrated_delegation( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { let delegation = self.total_delegations_for_mix(mix_id, storage)?; diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index ee14207d3e..fb3ede091f 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -8,7 +8,7 @@ use crate::vesting::account::StorableVestingAccountExt; use contracts_common::signing::MessageSignature; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::mixnode::MixNodeConfigUpdate; -use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use vesting_contract_common::events::{ new_vesting_decrease_pledge_event, new_vesting_mixnode_bonding_event, @@ -35,7 +35,7 @@ impl MixnodeBondingAccount for Account { fn try_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, env: &Env, @@ -206,7 +206,7 @@ impl MixnodeBondingAccount for Account { fn try_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, storage: &mut dyn Storage, ) -> Result { let msg = MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index c09b6057b5..4353b748eb 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -10,14 +10,13 @@ use crate::storage::{ }; use crate::traits::VestingAccount; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::account::VestingAccountStorageKey; use vesting_contract_common::{Account, PledgeCap, PledgeData, VestingContractError}; mod delegating_account; mod gateway_bonding_account; mod mixnode_bonding_account; -mod node_families; mod vesting_account; fn generate_storage_key( @@ -152,20 +151,20 @@ pub(crate) trait StorableVestingAccountExt: VestingAccount { fn remove_gateway_pledge(&self, storage: &mut dyn Storage) -> Result<(), VestingContractError>; - fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool; + fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool; - fn num_subdelegations_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> u32; + fn num_subdelegations_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> u32; fn remove_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; #[allow(dead_code)] fn total_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; @@ -273,7 +272,7 @@ impl StorableVestingAccountExt for Account { remove_gateway_pledge(self.storage_key(), storage) } - fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool { + fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool { DELEGATIONS .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) @@ -281,13 +280,13 @@ impl StorableVestingAccountExt for Account { .is_some() } - fn num_subdelegations_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> u32 { + fn num_subdelegations_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> u32 { count_subdelegations_for_mix((self.storage_key(), mix_id), storage) } fn remove_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { // note that the limit is implicitly set to `MAX_PER_MIX_DELEGATIONS` @@ -302,7 +301,7 @@ impl StorableVestingAccountExt for Account { fn total_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { Ok(DELEGATIONS diff --git a/contracts/vesting/src/vesting/account/node_families.rs b/contracts/vesting/src/vesting/account/node_families.rs deleted file mode 100644 index 649531688b..0000000000 --- a/contracts/vesting/src/vesting/account/node_families.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::Account; -use crate::{storage::MIXNET_CONTRACT_ADDRESS, traits::NodeFamilies}; -use contracts_common::signing::MessageSignature; -use cosmwasm_std::{wasm_execute, Response, Storage}; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, IdentityKeyRef}; -use vesting_contract_common::VestingContractError; - -impl NodeFamilies for Account { - fn try_create_family( - &self, - storage: &dyn Storage, - label: String, - ) -> Result { - let msg = MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address: self.owner_address().into_string(), - label, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_join_family( - &self, - storage: &dyn Storage, - join_permit: MessageSignature, - family_head: FamilyHead, - ) -> Result { - let msg = MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address: self.owner_address().to_string(), - join_permit, - family_head, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_leave_family( - &self, - storage: &dyn Storage, - family_head: FamilyHead, - ) -> Result { - let msg = MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address: self.owner_address().to_string(), - family_head, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_head_kick_member( - &self, - storage: &dyn Storage, - member: IdentityKeyRef<'_>, - ) -> Result { - let msg = MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address: self.owner_address().to_string(), - member: member.to_string(), - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } -} diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index d8da405008..33cfc2baec 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -26,7 +26,7 @@ mod tests { use contracts_common::signing::MessageSignature; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coin, coins, Addr, Coin, Timestamp, Uint128}; - use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::{Gateway, MixNode, Percent}; use vesting_contract_common::messages::ExecuteMsg; use vesting_contract_common::{Account, PledgeCap, VestingSpecification}; @@ -428,7 +428,7 @@ mod tests { version: "0.10.0".to_string(), }; - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin { denom: "NYM".to_string(), diff --git a/explorer-api/explorer-api-requests/src/lib.rs b/explorer-api/explorer-api-requests/src/lib.rs index 2109348899..3ea6ed7200 100644 --- a/explorer-api/explorer-api-requests/src/lib.rs +++ b/explorer-api/explorer-api-requests/src/lib.rs @@ -1,6 +1,6 @@ use nym_api_requests::models::NodePerformance; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{Addr, Coin, Gateway, Layer, MixId, MixNode}; +use nym_mixnet_contract_common::{Addr, Coin, Gateway, LegacyMixLayer, MixNode, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,13 +14,13 @@ pub enum MixnodeStatus { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct PrettyDetailedMixNodeBond { - pub mix_id: MixId, + pub mix_id: NodeId, pub location: Option, pub status: MixnodeStatus, pub pledge_amount: Coin, pub total_delegation: Coin, pub owner: Addr, - pub layer: Layer, + pub layer: LegacyMixLayer, pub mix_node: MixNode, pub stake_saturation: f32, pub uncapped_saturation: f32, diff --git a/explorer-api/src/gateways/models.rs b/explorer-api/src/gateways/models.rs index a3ed406067..c3a2cd9062 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -3,7 +3,8 @@ use crate::{cache::Cache, location::LocationCacheItem}; use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; -use nym_mixnet_contract_common::{GatewayBond, IdentityKey}; +use nym_mixnet_contract_common::IdentityKey; +use nym_validator_client::legacy::LegacyGatewayBondWithId; use serde::Serialize; use std::{sync::Arc, time::SystemTime}; use tokio::sync::RwLock; @@ -11,7 +12,7 @@ use tokio::sync::RwLock; use super::location::GatewayLocationCache; pub(crate) struct GatewayCache { - pub(crate) gateways: Cache, + pub(crate) gateways: Cache, } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -37,20 +38,20 @@ impl ThreadsafeGatewayCache { fn create_detailed_gateway( &self, - bond: GatewayBond, + bond: LegacyGatewayBondWithId, location: Option<&LocationCacheItem>, ) -> PrettyDetailedGatewayBond { PrettyDetailedGatewayBond { - pledge_amount: bond.pledge_amount, - owner: bond.owner, - block_height: bond.block_height, - gateway: bond.gateway, - proxy: bond.proxy, + pledge_amount: bond.bond.pledge_amount, + owner: bond.bond.owner, + block_height: bond.bond.block_height, + gateway: bond.bond.gateway, + proxy: bond.bond.proxy, location: location.and_then(|l| l.location.clone()), } } - pub(crate) async fn get_gateways(&self) -> Vec { + pub(crate) async fn get_gateways(&self) -> Vec { self.gateways.read().await.gateways.get_all() } @@ -106,7 +107,7 @@ impl ThreadsafeGatewayCache { .insert(identy_key, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn update_cache(&self, gateways: Vec) { + pub(crate) async fn update_cache(&self, gateways: Vec) { let mut guard = self.gateways.write().await; for gateway in gateways { diff --git a/explorer-api/src/mix_node/delegations.rs b/explorer-api/src/mix_node/delegations.rs index feaefcf5c0..fc9420c88c 100644 --- a/explorer-api/src/mix_node/delegations.rs +++ b/explorer-api/src/mix_node/delegations.rs @@ -4,12 +4,12 @@ use super::models::SummedDelegations; use crate::client::ThreadsafeValidatorClient; use itertools::Itertools; -use nym_mixnet_contract_common::{Delegation, MixId}; +use nym_mixnet_contract_common::{Delegation, NodeId}; use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; pub(crate) async fn get_single_mixnode_delegations( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Vec { match client .0 @@ -27,7 +27,7 @@ pub(crate) async fn get_single_mixnode_delegations( pub(crate) async fn get_single_mixnode_delegations_summed( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Vec { let delegations_by_owner = get_single_mixnode_delegations(client, mix_id) .await diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 0b1c901a40..31d5e78c93 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -5,12 +5,12 @@ use crate::client::ThreadsafeValidatorClient; use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; use nym_contracts_common::truncate_decimal; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::client::NymApiClientExt; pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Option { let stake_saturation = client .0 diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index e00ed8ff82..2a44097275 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -10,7 +10,7 @@ use crate::mix_node::models::{ }; use crate::state::ExplorerApiStateContext; use nym_explorer_api_requests::PrettyDetailedMixNodeBond; -use nym_mixnet_contract_common::{Delegation, MixId}; +use nym_mixnet_contract_common::{Delegation, NodeId}; use reqwest::Error as ReqwestError; use rocket::response::status::NotFound; use rocket::serde::json::Json; @@ -47,7 +47,7 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result")] pub(crate) async fn get_by_id( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Result, NotFound> { match state.inner.mixnodes.get_detailed_mixnode(mix_id).await { @@ -59,7 +59,7 @@ pub(crate) async fn get_by_id( #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations(&state.inner.validator_client, mix_id).await) @@ -68,7 +68,7 @@ pub(crate) async fn get_delegations( #[openapi(tag = "mix_node")] #[get("//delegations/summed")] pub(crate) async fn get_delegations_summed( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, mix_id).await) @@ -77,7 +77,7 @@ pub(crate) async fn get_delegations_summed( #[openapi(tag = "mix_node")] #[get("//description")] pub(crate) async fn get_description( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.clone().get_description(mix_id).await { @@ -125,7 +125,7 @@ pub(crate) async fn get_description( #[openapi(tag = "mix_node")] #[get("//stats")] pub(crate) async fn get_stats( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.get_node_stats(mix_id).await { @@ -170,7 +170,7 @@ pub(crate) async fn get_stats( #[openapi(tag = "mix_node")] #[get("//economic-dynamics-stats")] pub(crate) async fn get_economic_dynamics_stats( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.get_econ_stats(mix_id).await { diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 4adfa6dd86..d7b79ad71a 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -3,7 +3,7 @@ use crate::cache::Cache; use nym_mixnet_contract_common::Delegation; -use nym_mixnet_contract_common::{Addr, Coin, MixId}; +use nym_mixnet_contract_common::{Addr, Coin, NodeId}; use nym_validator_client::models::SelectionChance; use serde::Deserialize; use serde::Serialize; @@ -14,7 +14,7 @@ use tokio::sync::RwLock; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct SummedDelegations { pub owner: Addr, - pub mix_id: MixId, + pub mix_id: NodeId, pub amount: Coin, } @@ -40,9 +40,9 @@ impl SummedDelegations { } pub(crate) struct MixNodeCache { - pub(crate) descriptions: Cache, - pub(crate) node_stats: Cache, - pub(crate) econ_stats: Cache, + pub(crate) descriptions: Cache, + pub(crate) node_stats: Cache, + pub(crate) econ_stats: Cache, } #[derive(Clone)] @@ -61,19 +61,19 @@ impl ThreadsafeMixNodeCache { } } - pub(crate) async fn get_description(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_description(&self, mix_id: NodeId) -> Option { self.inner.read().await.descriptions.get(&mix_id) } - pub(crate) async fn get_node_stats(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_node_stats(&self, mix_id: NodeId) -> Option { self.inner.read().await.node_stats.get(&mix_id) } - pub(crate) async fn get_econ_stats(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_econ_stats(&self, mix_id: NodeId) -> Option { self.inner.read().await.econ_stats.get(&mix_id) } - pub(crate) async fn set_description(&self, mix_id: MixId, description: NodeDescription) { + pub(crate) async fn set_description(&self, mix_id: NodeId, description: NodeDescription) { self.inner .write() .await @@ -81,11 +81,11 @@ impl ThreadsafeMixNodeCache { .set(mix_id, description); } - pub(crate) async fn set_node_stats(&self, mix_id: MixId, node_stats: NodeStats) { + pub(crate) async fn set_node_stats(&self, mix_id: NodeId, node_stats: NodeStats) { self.inner.write().await.node_stats.set(mix_id, node_stats); } - pub(crate) async fn set_econ_stats(&self, mix_id: MixId, econ_stats: EconomicDynamicsStats) { + pub(crate) async fn set_econ_stats(&self, mix_id: NodeId, econ_stats: EconomicDynamicsStats) { self.inner.write().await.econ_stats.set(mix_id, econ_stats); } } @@ -147,11 +147,11 @@ fn get_common_owner(delegations: &[Delegation]) -> Option { Some(owner) } -fn get_common_mix_id(delegations: &[Delegation]) -> Option { - let mix_id = delegations.iter().next()?.mix_id; +fn get_common_mix_id(delegations: &[Delegation]) -> Option { + let mix_id = delegations.iter().next()?.node_id; if delegations .iter() - .any(|delegation| delegation.mix_id != mix_id) + .any(|delegation| delegation.node_id != mix_id) { log::warn!("Unexpected different node identities when summing delegations"); return None; diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index de3c2e2fd6..4fdd1714d4 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -1,8 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use crate::location::LocationCache; -pub(crate) type MixnodeLocationCache = LocationCache; +pub(crate) type MixnodeLocationCache = LocationCache; diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index fe5ae7fde8..4ffd99304f 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -1,24 +1,20 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use super::location::MixnodeLocationCache; +use crate::helpers::best_effort_small_dec_to_f64; +use crate::location::LocationCacheItem; +use crate::mix_nodes::CACHE_ENTRY_TTL; +use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; +use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; +use nym_mixnet_contract_common::NodeId; +use nym_validator_client::models::MixNodeBondAnnotated; +use serde::Serialize; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, SystemTime}; - -use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; -use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; -use nym_mixnet_contract_common::MixId; -use serde::Serialize; use tokio::sync::{RwLock, RwLockReadGuard}; -use crate::helpers::best_effort_small_dec_to_f64; -use crate::location::LocationCacheItem; -use nym_validator_client::models::MixNodeBondAnnotated; - -use super::location::MixnodeLocationCache; -use super::utils::family_numerical_id; -use crate::mix_nodes::CACHE_ENTRY_TTL; - #[derive(Clone, Debug, Serialize, JsonSchema)] pub(crate) struct MixNodeActiveSetSummary { pub active: usize, @@ -35,9 +31,9 @@ pub(crate) struct MixNodeSummary { #[derive(Clone, Debug)] pub(crate) struct MixNodesResult { pub(crate) valid_until: SystemTime, - pub(crate) all_mixnodes: HashMap, - active_mixnodes: HashSet, - rewarded_mixnodes: HashSet, + pub(crate) all_mixnodes: HashMap, + active_mixnodes: HashSet, + rewarded_mixnodes: HashSet, } impl MixNodesResult { @@ -50,7 +46,7 @@ impl MixNodesResult { } } - fn determine_node_status(&self, mix_id: MixId) -> MixnodeStatus { + fn determine_node_status(&self, mix_id: NodeId) -> MixnodeStatus { if self.active_mixnodes.contains(&mix_id) { MixnodeStatus::Active } else if self.rewarded_mixnodes.contains(&mix_id) { @@ -64,7 +60,7 @@ impl MixNodesResult { self.valid_until >= SystemTime::now() } - fn get_mixnode(&self, mix_id: MixId) -> Option { + fn get_mixnode(&self, mix_id: NodeId) -> Option { if self.is_valid() { self.all_mixnodes.get(&mix_id).cloned() } else { @@ -72,7 +68,7 @@ impl MixNodesResult { } } - fn get_mixnodes(&self) -> Option> { + fn get_mixnodes(&self) -> Option> { if self.is_valid() { Some(self.all_mixnodes.clone()) } else { @@ -102,7 +98,7 @@ impl ThreadsafeMixNodesCache { } } - pub(crate) async fn is_location_valid(&self, mix_id: MixId) -> bool { + pub(crate) async fn is_location_valid(&self, mix_id: NodeId) -> bool { self.locations .read() .await @@ -116,7 +112,7 @@ impl ThreadsafeMixNodesCache { self.locations.read().await.clone() } - pub(crate) async fn set_location(&self, mix_id: MixId, location: Option) { + pub(crate) async fn set_location(&self, mix_id: NodeId, location: Option) { // cache the location for this mix node so that it can be used when the mix node list is refreshed self.locations .write() @@ -124,25 +120,23 @@ impl ThreadsafeMixNodesCache { .insert(mix_id, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn get_mixnode(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_mixnode(&self, mix_id: NodeId) -> Option { self.mixnodes.read().await.get_mixnode(mix_id) } - pub(crate) async fn get_mixnodes(&self) -> Option> { + pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } fn create_detailed_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, mixnodes_guard: &RwLockReadGuard<'_, MixNodesResult>, location: Option<&LocationCacheItem>, node: &MixNodeBondAnnotated, ) -> PrettyDetailedMixNodeBond { - let denom = &node.mixnode_details.original_pledge().denom; let rewarding_info = &node.mixnode_details.rewarding_details; - - let family_id = node.family.as_ref().map(family_numerical_id); + let denom = &rewarding_info.cost_params.interval_operating_cost.denom; PrettyDetailedMixNodeBond { mix_id, @@ -162,14 +156,14 @@ impl ThreadsafeMixNodesCache { estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy), operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(), profit_margin_percent: rewarding_info.cost_params.profit_margin_percent, - family_id, + family_id: None, blacklisted: node.blacklisted, } } pub(crate) async fn get_detailed_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Option { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; @@ -197,8 +191,8 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn update_cache( &self, all_bonds: Vec, - rewarded_nodes: HashSet, - active_nodes: HashSet, + rewarded_nodes: HashSet, + active_nodes: HashSet, ) { let mut guard = self.mixnodes.write().await; guard.all_mixnodes = all_bonds diff --git a/explorer-api/src/mix_nodes/utils.rs b/explorer-api/src/mix_nodes/utils.rs index 5273d66f55..2742fd2415 100644 --- a/explorer-api/src/mix_nodes/utils.rs +++ b/explorer-api/src/mix_nodes/utils.rs @@ -1,13 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use isocountry::CountryCode; -use nym_mixnet_contract_common::families::FamilyHead; -use rand::Rng; -use rand_pcg::Pcg64; -use rand_seeder::Seeder; - use crate::location::GeoLocation; +use isocountry::CountryCode; #[allow(dead_code)] pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String { @@ -22,10 +17,3 @@ pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String } } } - -// We don't need numerical IDs anywhere, so to avoid modifying the contract storage again and -// since this is for explorer ergonomics, it will generate a deterministic random u16 based on the family Identity. -pub(crate) fn family_numerical_id(fh: &FamilyHead) -> u16 { - let mut rng: Pcg64 = Seeder::from(fh.identity()).make_rng(); - rng.gen() -} diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index c3cd4107f1..952e681340 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::{MixId, MixNode}; +use nym_mixnet_contract_common::{NodeId, MixNode}; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; @@ -23,7 +23,7 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec, Open #[openapi(tag = "ping")] #[get("/")] pub(crate) async fn index( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.ping.clone().get(mix_id).await { diff --git a/explorer-api/src/ping/models.rs b/explorer-api/src/ping/models.rs index 9aebf7d5fd..10d6d832ce 100644 --- a/explorer-api/src/ping/models.rs +++ b/explorer-api/src/ping/models.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime}; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use serde::Serialize; use tokio::sync::RwLock; -pub(crate) type PingCache = HashMap; +pub(crate) type PingCache = HashMap; const PING_TTL: Duration = Duration::from_secs(60 * 5); // 5 mins, before port check will be re-tried (only while pending) const CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour, to cache result from port check @@ -24,7 +24,7 @@ impl ThreadsafePingCache { } } - pub(crate) async fn get(&self, mix_id: MixId) -> Option { + pub(crate) async fn get(&self, mix_id: NodeId) -> Option { self.inner .read() .await @@ -44,7 +44,7 @@ impl ThreadsafePingCache { }) } - pub(crate) async fn set_pending(&self, mix_id: MixId) { + pub(crate) async fn set_pending(&self, mix_id: NodeId) { self.inner.write().await.insert( mix_id, PingCacheItem { @@ -55,7 +55,7 @@ impl ThreadsafePingCache { ); } - pub(crate) async fn set(&self, mix_id: MixId, item: PingResponse) { + pub(crate) async fn set(&self, mix_id: NodeId, item: PingResponse) { self.inner.write().await.insert( mix_id, PingCacheItem { diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 8ecee92ae2..b007fae76d 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,7 +3,7 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; @@ -39,7 +39,7 @@ pub struct ExplorerApiState { } impl ExplorerApiState { - pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option { self.mixnodes.get_mixnode(mix_id).await } } diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index b0b9a9701b..52c0192538 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -1,8 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::GatewayBond; use nym_task::TaskClient; +use nym_validator_client::legacy::LegacyGatewayBondWithId; use nym_validator_client::models::MixNodeBondAnnotated; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse}; @@ -28,13 +28,12 @@ impl ExplorerApiTasks { F: FnOnce(&'a QueryHttpRpcValidatorClient) -> Fut, Fut: Future, ValidatorClientError>>, { - let bonds = match f(&self.state.inner.validator_client.0).await { - Ok(result) => result, - Err(err) => { + let bonds = f(&self.state.inner.validator_client.0) + .await + .unwrap_or_else(|err| { error!("Unable to retrieve mixnode bonds: {err}"); vec![] - } - }; + }); info!("Fetched {} mixnode bonds", bonds.len()); bonds @@ -48,7 +47,9 @@ impl ExplorerApiTasks { .await } - async fn retrieve_all_gateways(&self) -> Result, ValidatorClientError> { + async fn retrieve_all_gateways( + &self, + ) -> Result, ValidatorClientError> { info!("About to retrieve all gateways..."); self.state .inner diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 194be8cc2d..48521ecf46 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -5,13 +5,9 @@ name = "nym-api" license = "GPL-3.0" version = "1.1.44" -authors = [ - "Dave Hrycyszyn ", - "Jędrzej Stuczyński ", - "Drazen Urch ", -] +authors.workspace = true edition = "2021" -rust-version = "1.76.0" +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -31,16 +27,12 @@ humantime-serde = { workspace = true } k256 = { workspace = true, features = [ "ecdsa-core", ] } # needed for the Verifier trait; pull whatever version is used by other dependencies -log = { workspace = true } pin-project = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } reqwest = { workspace = true, features = ["json"] } -rocket = { workspace = true, features = ["json"] } -rocket_cors = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tap = { workspace = true } thiserror = { workspace = true } time = { workspace = true, features = ["serde-human-readable", "parsing"] } tokio = { workspace = true, features = [ @@ -66,20 +58,17 @@ sqlx = { workspace = true, features = [ "time", ] } -okapi = { workspace = true, features = ["impl_json_schema"] } -rocket_okapi = { workspace = true, features = ["swagger"] } schemars = { workspace = true, features = ["preserve_order"] } zeroize = { workspace = true } # for axum server -axum = { workspace = true, features = ["tokio"], optional = true } -axum-extra = { workspace = true, features = ["typed-header"], optional = true } -tower-http = { workspace = true, features = ["cors", "trace"], optional = true } -utoipa = { workspace = true, features = ["axum_extras", "time"], optional = true } -utoipa-swagger-ui = { workspace = true, features = ["axum"], optional = true} -utoipauto = { workspace = true, optional = true } -tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } -tracing = { workspace = true, optional = true } +axum = { workspace = true, features = ["tokio"] } +axum-extra = { workspace = true, features = ["typed-header"] } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipauto = { workspace = true } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +tracing = { workspace = true } ## ephemera-specific #actix-web = "4" @@ -112,7 +101,7 @@ cw4 = { workspace = true } nym-dkg = { path = "../common/dkg", features = ["cw-types"] } nym-gateway-client = { path = "../common/client-libs/gateway-client" } nym-inclusion-probability = { path = "../common/inclusion-probability" } -nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"]} +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] } nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" } nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } @@ -121,29 +110,19 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] } +nym-api-requests = { path = "nym-api-requests" } nym-validator-client = { path = "../common/client-libs/validator-client" } -nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi"] } +nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi", "basic_tracing"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } nym-types = { path = "../common/types" } nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } +nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] } [features] no-reward = [] v2-performance = [] generate-ts = ["ts-rs"] -axum = ["dep:axum", - "axum-extra", - "tower-http", - "utoipa", - "utoipauto", - "tracing-subscriber", - "tracing", - "utoipa-swagger-ui", - "nym-http-api-common/utoipa", - "nym-mixnet-contract-common/utoipa" -] [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/nym-api/Rocket.toml b/nym-api/Rocket.toml deleted file mode 100644 index a094d2157a..0000000000 --- a/nym-api/Rocket.toml +++ /dev/null @@ -1,7 +0,0 @@ -[default] -limits = { forms = "64 kB", json = "1 MiB" } -port = 8080 -address = "127.0.0.1" - -[release] -address = "0.0.0.0" \ No newline at end of file diff --git a/nym-api/build.rs b/nym-api/build.rs index cdd97f9505..d6e0c45770 100644 --- a/nym-api/build.rs +++ b/nym-api/build.rs @@ -1,4 +1,4 @@ -use sqlx::{Connection, SqliteConnection}; +use sqlx::{Connection, FromRow, SqliteConnection}; use std::env; #[tokio::main] @@ -15,6 +15,42 @@ async fn main() { .await .expect("Failed to perform SQLx migrations"); + #[derive(FromRow)] + struct Exists { + exists: bool, + } + + // check if it was already run + let res: Exists = sqlx::query_as("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'") + .fetch_one(&mut conn) + .await + .unwrap(); + + let already_run = res.exists; + + // execute the manual v3 migration + // it's performed on an empty storage, so we don't need to actually make any network queries + if !already_run { + sqlx::query( + r#" + CREATE TABLE gateway_details_temp + ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + node_id INTEGER NOT NULL UNIQUE, + identity VARCHAR NOT NULL UNIQUE + ); + + DROP TABLE gateway_details; + ALTER TABLE gateway_details_temp RENAME TO gateway_details; + + INSERT INTO v3_migration_info(id) VALUES (0); + "#, + ) + .execute(&mut conn) + .await + .expect("failed to update post v3 migration tables"); + } + #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); diff --git a/nym-api/migrations/20240726120000_v3_changes.sql b/nym-api/migrations/20240726120000_v3_changes.sql new file mode 100644 index 0000000000..652729d215 --- /dev/null +++ b/nym-api/migrations/20240726120000_v3_changes.sql @@ -0,0 +1,24 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +-- we don't have to be keeping track of the gateway owner; it will make things easier in code +ALTER TABLE gateway_details DROP COLUMN owner; +ALTER TABLE mixnode_details DROP COLUMN owner; + +-- NOTE: this column is made `NOT NULL UNIQUE` in code during `migrate_v3_database` call! +ALTER TABLE gateway_details ADD node_id INTEGER; + +-- a hacky table-flag to indicate whether the v3 migration has been run +CREATE TABLE v3_migration_info ( + id INTEGER PRIMARY KEY CHECK (id = 0) +); + +--CREATE TABLE node_historical_performance ( +-- contract_node_id INTEGER NOT NULL, +-- date DATE NOT NULL, +-- performance FLOAT NOT NULL, +-- +-- UNIQUE(contract_node_id, date); +--) \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 70943d6fba..f2873b9173 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -11,9 +11,9 @@ bs58 = { workspace = true } cosmrs = { workspace = true } cosmwasm-std = { workspace = true } getset = { workspace = true } -rocket = { workspace = true, optional = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } sha2.workspace = true tendermint = { workspace = true } thiserror.workspace = true @@ -32,12 +32,9 @@ nym-ecash-time = { path = "../../common/ecash-time" } nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] } +nym-network-defaults = { path = "../../common/network-defaults" } -[dev-dependencies] -serde_json.workspace = true - [features] default = [] -rocket-traits = ["rocket"] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/legacy.rs b/nym-api/nym-api-requests/src/legacy.rs new file mode 100644 index 0000000000..f7af56252c --- /dev/null +++ b/nym-api/nym-api-requests/src/legacy.rs @@ -0,0 +1,92 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use cosmwasm_std::Decimal; +use nym_mixnet_contract_common::mixnode::PendingMixNodeChanges; +use nym_mixnet_contract_common::{ + GatewayBond, LegacyMixLayer, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use utoipa::ToSchema; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyGatewayBondWithId { + // we need to flatten it so that consumers of endpoints that returned `GatewayBond` wouldn't break + #[serde(flatten)] + pub bond: GatewayBond, + pub node_id: NodeId, +} + +impl Deref for LegacyGatewayBondWithId { + type Target = GatewayBond; + fn deref(&self) -> &Self::Target { + &self.bond + } +} + +impl From for GatewayBond { + fn from(value: LegacyGatewayBondWithId) -> Self { + value.bond + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyMixNodeBondWithLayer { + // we need to flatten it so that consumers of endpoints that returned `MixNodeBond` wouldn't break + #[serde(flatten)] + pub bond: MixNodeBond, + + pub layer: LegacyMixLayer, +} + +impl Deref for LegacyMixNodeBondWithLayer { + type Target = MixNodeBond; + fn deref(&self) -> &Self::Target { + &self.bond + } +} + +impl From for MixNodeBond { + fn from(value: LegacyMixNodeBondWithLayer) -> Self { + value.bond + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct LegacyMixNodeDetailsWithLayer { + /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + pub bond_information: LegacyMixNodeBondWithLayer, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + #[serde(default)] + pub pending_changes: PendingMixNodeChanges, +} + +impl LegacyMixNodeDetailsWithLayer { + pub fn mix_id(&self) -> NodeId { + self.bond_information.mix_id + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } +} + +impl From for MixNodeDetails { + fn from(value: LegacyMixNodeDetailsWithLayer) -> Self { + MixNodeDetails { + bond_information: value.bond_information.into(), + rewarding_details: value.rewarding_details, + pending_changes: value.pending_changes, + } + } +} diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 7ea5db1f6d..664eb59fff 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; pub mod constants; pub mod ecash; mod helpers; +pub mod legacy; pub mod models; pub mod nym_nodes; pub mod pagination; diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index f42c8cb764..072a2aa549 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -2,17 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 use crate::helpers::unix_epoch; -use crate::nym_nodes::NodeRole; +use crate::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{ - GatewayBond, IdentityKey, Interval, MixId, MixNode, MixNodeBond, Percent, RewardedSetNodeStatus, +use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent}; +use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT}; +use nym_node_requests::api::v1::authenticator::models::Authenticator; +use nym_node_requests::api::v1::gateway::models::Wireguard; +use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; +use nym_node_requests::api::v1::node::models::{ + AuxiliaryDetails, BinaryBuildInformationOwned, NodeRoles, }; -use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, BinaryBuildInformationOwned}; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; @@ -21,7 +26,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToResponse, ToSchema}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -66,18 +71,6 @@ pub enum MixnodeStatus { Inactive, // in neither the rewarded set nor the active set, but is bonded NotFound, // doesn't even exist in the bonded set } - -impl From for Option { - fn from(status: MixnodeStatus) -> Self { - match status { - MixnodeStatus::Active => Some(RewardedSetNodeStatus::Active), - MixnodeStatus::Standby => Some(RewardedSetNodeStatus::Standby), - MixnodeStatus::Inactive => None, - MixnodeStatus::NotFound => None, - } - } -} - impl MixnodeStatus { pub fn is_active(&self) -> bool { *self == MixnodeStatus::Active @@ -91,7 +84,7 @@ impl MixnodeStatus { ts(export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts") )] pub struct MixnodeCoreStatusResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub count: i32, } @@ -126,23 +119,58 @@ pub struct NodePerformance { pub last_24h: Performance, } -#[derive(ToSchema)] -#[schema(title = "MixNodeDetails")] -pub struct MixNodeDetailsSchema { +// imo for now there's no point in exposing more than that, +// nym-api shouldn't be calculating apy or stake saturation for you. +// it should just return its own metrics (performance) and then you can do with it as you wish +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct NodeAnnotation { + pub last_24h_performance: Performance, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct AnnotationResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub annotation: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct NodePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub performance: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct NodeDatePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + pub performance: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[schema(title = "LegacyMixNodeDetailsWithLayer")] +pub struct LegacyMixNodeDetailsWithLayerSchema { /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + #[schema(example = "unimplemented schema")] pub bond_information: String, /// Details used for computation of rewarding related data. + #[schema(example = "unimplemented schema")] pub rewarding_details: String, /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + #[schema(example = "unimplemented schema")] pub pending_changes: String, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct MixNodeBondAnnotated { - #[schema(value_type = MixNodeDetailsSchema)] - pub mixnode_details: MixNodeDetails, + #[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)] + pub mixnode_details: LegacyMixNodeDetailsWithLayer, #[schema(value_type = String)] pub stake_saturation: StakeSaturation, #[schema(value_type = String)] @@ -153,7 +181,6 @@ pub struct MixNodeBondAnnotated { pub node_performance: NodePerformance, pub estimated_operator_apy: Decimal, pub estimated_delegators_apy: Decimal, - pub family: Option, pub blacklisted: bool, // a rather temporary thing until we query self-described endpoints of mixnodes @@ -166,7 +193,7 @@ impl MixNodeBondAnnotated { &self.mixnode_details.bond_information.mix_node } - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.mixnode_details.mix_id() } @@ -177,11 +204,15 @@ impl MixNodeBondAnnotated { pub fn owner(&self) -> &Addr { self.mixnode_details.bond_information.owner() } + + pub fn version(&self) -> &str { + &self.mixnode_details.bond_information.mix_node.version + } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct GatewayBondAnnotated { - pub gateway_bond: GatewayBond, + pub gateway_bond: LegacyGatewayBondWithId, #[serde(default)] pub self_described: Option, @@ -196,12 +227,16 @@ pub struct GatewayBondAnnotated { } impl GatewayBondAnnotated { + pub fn version(&self) -> &str { + &self.gateway_bond.gateway.version + } + pub fn identity(&self) -> &String { - self.gateway_bond.identity() + self.gateway_bond.bond.identity() } pub fn owner(&self) -> &Addr { - self.gateway_bond.owner() + self.gateway_bond.bond.owner() } } @@ -240,7 +275,7 @@ pub struct RewardEstimationResponse { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct UptimeResponse { #[schema(value_type = u32)] - pub mix_id: MixId, + pub mix_id: NodeId, // The same as node_performance.last_24h. Legacy pub avg_uptime: u8, pub node_performance: NodePerformance, @@ -349,7 +384,7 @@ pub struct AllInclusionProbabilitiesResponse { #[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] pub struct InclusionProbability { #[schema(value_type = u32)] - pub mix_id: MixId, + pub mix_id: NodeId, pub in_active: f64, pub in_reserve: f64, } @@ -358,7 +393,7 @@ type Uptime = u8; #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeStatusReportResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub identity: IdentityKey, pub owner: String, pub most_recent: Uptime, @@ -378,8 +413,40 @@ pub struct GatewayStatusReportResponse { pub last_day: Uptime, } +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct PerformanceHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse, +} + +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct UptimeHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse, +} + #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HistoricalUptimeResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + + pub uptime: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct HistoricalPerformanceResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + + pub performance: f64, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct OldHistoricalUptimeResponse { pub date: String, #[schema(value_type = u8)] pub uptime: Uptime, @@ -387,17 +454,17 @@ pub struct HistoricalUptimeResponse { #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeUptimeHistoryResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct GatewayUptimeHistoryResponse { pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(ToSchema)] @@ -543,14 +610,70 @@ impl JsonSchema for OffsetDateTimeJsonSchemaWrapper { } } -// this struct is getting quite bloated... #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NymNodeDescription { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub contract_node_type: DescribedNodeType, + pub description: NymNodeData, +} + +impl NymNodeDescription { + pub fn version(&self) -> &str { + &self.description.build_information.build_version + } + + pub fn entry_information(&self) -> BasicEntryInformation { + BasicEntryInformation { + hostname: self.description.host_information.hostname.clone(), + ws_port: self.description.mixnet_websockets.ws_port, + wss_port: self.description.mixnet_websockets.wss_port, + } + } + + pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { + let keys = &self.description.host_information.keys; + let entry = if self.description.declared_role.can_operate_entry_gateway() { + Some(self.entry_information()) + } else { + None + }; + + SkimmedNode { + node_id: self.node_id, + ed25519_identity_pubkey: keys.ed25519.clone(), + ip_addresses: self.description.host_information.ip_address.clone(), + mix_port: self.description.mix_port(), + x25519_sphinx_pubkey: keys.x25519.clone(), + // we can't use the declared roles, we have to take whatever was provided in the contract. + // why? say this node COULD operate as an exit, but it might be the case the contract decided + // to assign it an ENTRY role only. we have to use that one instead. + role, + entry, + performance, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum DescribedNodeType { + LegacyMixnode, + LegacyGateway, + NymNode, +} + +// this struct is getting quite bloated... +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NymNodeData { #[serde(default)] pub last_polled: OffsetDateTimeJsonSchemaWrapper, pub host_information: HostInformation, + #[serde(default)] + pub declared_role: NodeRoles, + #[serde(default)] pub auxiliary_details: AuxiliaryDetails, @@ -571,25 +694,33 @@ pub struct NymNodeDescription { // for now we only care about their ws/wss situation, nothing more pub mixnet_websockets: WebSockets, - - #[serde(default = "default_node_role")] - pub role: NodeRole, } -// For backwards compatibility, set a slightly artificial default -fn default_node_role() -> NodeRole { - NodeRole::Inactive +impl NymNodeData { + pub fn mix_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .mix_port + .unwrap_or(DEFAULT_MIX_LISTENING_PORT) + } + + pub fn verloc_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .verloc_port + .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT) + } } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct DescribedGateway { - pub bond: GatewayBond, - pub self_described: Option, +pub struct LegacyDescribedGateway { + pub bond: LegacyGatewayBondWithId, + pub self_described: Option, } -impl From for DescribedGateway { - fn from(bond: GatewayBond) -> Self { - DescribedGateway { +impl From for LegacyDescribedGateway { + fn from(bond: LegacyGatewayBondWithId) -> Self { + LegacyDescribedGateway { bond, self_described: None, } @@ -597,14 +728,14 @@ impl From for DescribedGateway { } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct DescribedMixNode { - pub bond: MixNodeBond, - pub self_described: Option, +pub struct LegacyDescribedMixNode { + pub bond: LegacyMixNodeBondWithLayer, + pub self_described: Option, } -impl From for DescribedMixNode { - fn from(bond: MixNodeBond) -> Self { - DescribedMixNode { +impl From for LegacyDescribedMixNode { + fn from(bond: LegacyMixNodeBondWithLayer) -> Self { + LegacyDescribedMixNode { bond, self_described: None, } @@ -626,18 +757,45 @@ pub struct IpPacketRouterDetails { pub address: String, } +// works for current simple case. +impl From for IpPacketRouterDetails { + fn from(value: IpPacketRouter) -> Self { + IpPacketRouterDetails { + address: value.address, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct AuthenticatorDetails { /// address of the embedded authenticator pub address: String, } +// works for current simple case. +impl From for AuthenticatorDetails { + fn from(value: Authenticator) -> Self { + AuthenticatorDetails { + address: value.address, + } + } +} #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct WireguardDetails { pub port: u16, pub public_key: String, } +// works for current simple case. +impl From for WireguardDetails { + fn from(value: Wireguard) -> Self { + WireguardDetails { + port: value.port, + public_key: value.public_key, + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct ApiHealthResponse { pub status: ApiStatus, diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 3a4ea40ca9..7499c3f83b 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::{ - GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, + GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeData, OffsetDateTimeJsonSchemaWrapper, }; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; use std::net::IpAddr; +use time::OffsetDateTime; use utoipa::ToSchema; #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] @@ -16,9 +17,23 @@ pub struct CachedNodesResponse { pub nodes: Vec, } +impl From> for CachedNodesResponse { + fn from(nodes: Vec) -> Self { + CachedNodesResponse::new(nodes) + } +} + +impl CachedNodesResponse { + pub fn new(nodes: Vec) -> Self { + CachedNodesResponse { + refreshed_at: OffsetDateTime::now_utc().into(), + nodes, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "kebab-case")] -#[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))] pub enum NodeRoleQueryParam { ActiveMixnode, @@ -56,8 +71,6 @@ pub struct BasicEntryInformation { pub wss_port: Option, } -type NodeId = MixId; - // the bare minimum information needed to construct sphinx packets #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SkimmedNode { @@ -82,9 +95,16 @@ pub struct SkimmedNode { } impl SkimmedNode { + pub fn get_mix_layer(&self) -> Option { + match self.role { + NodeRole::Mixnode { layer } => Some(layer), + _ => None, + } + } + pub fn from_described_gateway( annotated: &GatewayBondAnnotated, - description: Option<&NymNodeDescription>, + description: Option<&NymNodeData>, ) -> Self { let mut base: SkimmedNode = annotated.into(); let Some(description) = description else { @@ -129,15 +149,15 @@ impl<'a> From<&'a MixNodeBondAnnotated> for SkimmedNode { impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode { fn from(value: &'a GatewayBondAnnotated) -> Self { SkimmedNode { - node_id: MixId::MAX, + node_id: value.gateway_bond.node_id, ip_addresses: value.ip_addresses.clone(), - ed25519_identity_pubkey: value.gateway_bond.identity().clone(), - mix_port: value.gateway_bond.gateway.mix_port, - x25519_sphinx_pubkey: value.gateway_bond.gateway.sphinx_key.clone(), + ed25519_identity_pubkey: value.gateway_bond.bond.identity().clone(), + mix_port: value.gateway_bond.bond.gateway.mix_port, + x25519_sphinx_pubkey: value.gateway_bond.bond.gateway.sphinx_key.clone(), role: NodeRole::EntryGateway, entry: Some(BasicEntryInformation { hostname: None, - ws_port: value.gateway_bond.gateway.clients_port, + ws_port: value.gateway_bond.bond.gateway.clients_port, wss_port: None, }), performance: value.node_performance.last_24h, @@ -159,5 +179,5 @@ pub struct FullFatNode { pub expanded: SemiSkimmedNode, // kinda temporary for now to make as few changes as possible for now - pub self_described: Option, + pub self_described: Option, } diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs index 7a2537ebee..87e401da9d 100644 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ b/nym-api/src/circulating_supply_api/cache/mod.rs @@ -6,7 +6,6 @@ use cosmwasm_std::Addr; use nym_api_requests::models::CirculatingSupplyResponse; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; -use rocket::fairing::AdHoc; use std::ops::Deref; use std::{ sync::{atomic::AtomicBool, Arc}, @@ -15,6 +14,7 @@ use std::{ use thiserror::Error; use tokio::sync::RwLock; use tokio::time; +use tracing::{error, info}; mod data; pub(crate) mod refresher; @@ -67,13 +67,6 @@ impl CirculatingSupplyCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub(crate) fn stage(mix_denom: String) -> AdHoc { - AdHoc::on_ignite("Circulating Supply Cache Stage", |rocket| async { - rocket.manage(Self::new(mix_denom)) - }) - } - pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) { let mut cache = self.data.write().await; @@ -81,10 +74,10 @@ impl CirculatingSupplyCache { circulating_supply.amount -= mixmining_reserve.amount; circulating_supply.amount -= vesting_tokens.amount; - log::info!("Updating circulating supply cache"); - log::info!("the mixmining reserve is now {mixmining_reserve}"); - log::info!("the number of tokens still vesting is now {vesting_tokens}"); - log::info!("the circulating supply is now {circulating_supply}"); + info!("Updating circulating supply cache"); + info!("the mixmining reserve is now {mixmining_reserve}"); + info!("the number of tokens still vesting is now {vesting_tokens}"); + info!("the circulating supply is now {circulating_supply}"); cache.mixmining_reserve.unchecked_update(mixmining_reserve); cache.vesting_tokens.unchecked_update(vesting_tokens); diff --git a/nym-api/src/circulating_supply_api/cache/refresher.rs b/nym-api/src/circulating_supply_api/cache/refresher.rs index f983390356..60d7c2bafc 100644 --- a/nym-api/src/circulating_supply_api/cache/refresher.rs +++ b/nym-api/src/circulating_supply_api/cache/refresher.rs @@ -11,6 +11,7 @@ use std::collections::HashSet; use std::sync::atomic::Ordering; use std::time::Duration; use tokio::time; +use tracing::{error, trace}; pub(crate) struct CirculatingSupplyCacheRefresher { nyxd_client: Client, diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index 973d1fc64e..3e49659b55 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -1,15 +1,13 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::models::{AxumErrorResponse, AxumResult}, - v2::AxumAppState, -}; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; use axum::{extract, Router}; use nym_api_requests::models::CirculatingSupplyResponse; use nym_validator_client::nyxd::Coin; -pub(crate) fn circulating_supply_routes() -> Router { +pub(crate) fn circulating_supply_routes() -> Router { Router::new() .route("/", axum::routing::get(get_full_circulating_supply)) .route( @@ -31,7 +29,7 @@ pub(crate) fn circulating_supply_routes() -> Router { ) )] async fn get_full_circulating_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { match state .circulating_supply_cache() @@ -52,7 +50,7 @@ async fn get_full_circulating_supply( ) )] async fn get_total_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { let full_circulating_supply = match state .circulating_supply_cache() @@ -75,7 +73,7 @@ async fn get_total_supply( ) )] async fn get_circulating_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { let full_circulating_supply = match state .circulating_supply_cache() diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 2bfefcc35f..9c125815cb 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -1,28 +1,22 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; - -use crate::support::{config, nyxd}; - use self::cache::refresher::CirculatingSupplyCacheRefresher; +use crate::support::{config, nyxd}; +use nym_task::TaskManager; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; +// pub(crate) mod routes; -/// Merges the routes with http information and returns it to Rocket for serving -pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_full_circulating_supply, - routes::get_total_supply, - routes::get_circulating_supply - ] -} +// /// Merges the routes with http information and returns it to Rocket for serving +// pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: routes::get_full_circulating_supply, +// routes::get_total_supply, +// routes::get_circulating_supply +// ] +// } /// Spawn the circulating supply cache refresher. pub(crate) fn start_cache_refresh( diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index f552698d86..9e6018550c 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -3,7 +3,6 @@ use crate::ecash::error::{EcashError, Result}; use crate::ecash::state::EcashState; -use log::trace; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; @@ -14,6 +13,7 @@ use rocket::serde::json::Json; use rocket::State as RocketState; use rocket_okapi::openapi; use time::Date; +use tracing::trace; // routes with globally aggregated keys, signatures, etc. diff --git a/nym-api/src/ecash/api_routes/aggregation_axum.rs b/nym-api/src/ecash/api_routes/aggregation_axum.rs index 4a03313580..d956f2897d 100644 --- a/nym-api/src/ecash/api_routes/aggregation_axum.rs +++ b/nym-api/src/ecash/api_routes/aggregation_axum.rs @@ -5,10 +5,9 @@ use crate::ecash::api_routes::helpers::EpochIdParam; use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Path; use axum::{Json, Router}; -use log::trace; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; @@ -18,10 +17,11 @@ use nym_validator_client::nym_api::rfc_3339_date; use serde::Deserialize; use std::sync::Arc; use time::Date; +use tracing::trace; use utoipa::IntoParams; /// routes with globally aggregated keys, signatures, etc. -pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { +pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { Router::new() .route( "/master-verification-key:epoch_id", diff --git a/nym-api/src/ecash/api_routes/handlers.rs b/nym-api/src/ecash/api_routes/handlers.rs index b40ccf9773..5b9d6cee46 100644 --- a/nym-api/src/ecash/api_routes/handlers.rs +++ b/nym-api/src/ecash/api_routes/handlers.rs @@ -6,11 +6,11 @@ use crate::ecash::api_routes::issued_axum::issued_routes; use crate::ecash::api_routes::partial_signing_axum::partial_signing_routes; use crate::ecash::api_routes::spending_axum::spending_routes; use crate::ecash::state::EcashState; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Router; use std::sync::Arc; -pub(crate) fn ecash_routes(ecash_state: Arc) -> Router { +pub(crate) fn ecash_routes(ecash_state: Arc) -> Router { Router::new() .merge(aggregation_routes(Arc::clone(&ecash_state))) .merge(issued_routes(Arc::clone(&ecash_state))) diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index 4469bf11d0..6fba7d43dd 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -27,7 +27,6 @@ pub(crate) fn build_credentials_response( Ok(IssuedCredentialsResponse { credentials }) } -#[cfg(feature = "axum")] #[derive(serde::Deserialize, utoipa::IntoParams)] #[into_params(parameter_in = Path)] pub(super) struct EpochIdParam { diff --git a/nym-api/src/ecash/api_routes/issued_axum.rs b/nym-api/src/ecash/api_routes/issued_axum.rs index 72fa55e39f..95c1439600 100644 --- a/nym-api/src/ecash/api_routes/issued_axum.rs +++ b/nym-api/src/ecash/api_routes/issued_axum.rs @@ -6,7 +6,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::ecash::storage::EcashStorageExt; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Path; use axum::{Json, Router}; use nym_api_requests::ecash::models::{ @@ -17,7 +17,7 @@ use serde::Deserialize; use std::sync::Arc; use utoipa::IntoParams; -pub(crate) fn issued_routes(ecash_state: Arc) -> Router { +pub(crate) fn issued_routes(ecash_state: Arc) -> Router { Router::new() .route( "/epoch-credentials/:epoch", diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs index 910e253288..9c0082748c 100644 --- a/nym-api/src/ecash/api_routes/mod.rs +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -1,18 +1,14 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -pub(crate) mod aggregation; +// pub(crate) mod aggregation; mod helpers; -pub(crate) mod issued; -pub(crate) mod partial_signing; -pub(crate) mod spending; +// pub(crate) mod issued; +// pub(crate) mod partial_signing; +// pub(crate) mod spending; -cfg_if::cfg_if! { - if #[cfg(feature = "axum")] { - pub(crate) mod aggregation_axum; - pub(crate) mod handlers; - pub(crate) mod issued_axum; - pub(crate) mod partial_signing_axum; - pub(crate) mod spending_axum; - } -} +pub(crate) mod aggregation_axum; +pub(crate) mod handlers; +pub(crate) mod issued_axum; +pub(crate) mod partial_signing_axum; +pub(crate) mod spending_axum; diff --git a/nym-api/src/ecash/api_routes/partial_signing_axum.rs b/nym-api/src/ecash/api_routes/partial_signing_axum.rs index fe93ddc921..e42ffcf6e0 100644 --- a/nym-api/src/ecash/api_routes/partial_signing_axum.rs +++ b/nym-api/src/ecash/api_routes/partial_signing_axum.rs @@ -6,7 +6,7 @@ use crate::ecash::error::EcashError; use crate::ecash::helpers::blind_sign; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Path; use axum::{Json, Router}; use nym_api_requests::ecash::{ @@ -19,9 +19,10 @@ use serde::Deserialize; use std::ops::Deref; use std::sync::Arc; use time::Date; +use tracing::{debug, trace}; use utoipa::IntoParams; -pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { +pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { Router::new() .route( "/blind-sign", diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index b5eedbcdbe..1ea24efde6 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -78,16 +78,16 @@ pub async fn verify_ticket( ) { IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? IdentifyResult::DuplicatePayInfo(_) => { - log::warn!("Identical payInfo"); + warn!("Identical payInfo"); return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); } IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { //Actual double spending - log::warn!( + warn!( "Double spending attempt for key {}", pub_key.to_base58_string() ); - log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + error!("UNIMPLEMENTED: blacklisting the double spend key"); return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); } } diff --git a/nym-api/src/ecash/api_routes/spending_axum.rs b/nym-api/src/ecash/api_routes/spending_axum.rs index aca64b7f0c..a5110a52fc 100644 --- a/nym-api/src/ecash/api_routes/spending_axum.rs +++ b/nym-api/src/ecash/api_routes/spending_axum.rs @@ -4,7 +4,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; use nym_api_requests::ecash::models::{ @@ -18,8 +18,9 @@ use std::ops::Deref; use std::sync::Arc; use time::macros::time; use time::{OffsetDateTime, Time}; +use tracing::{error, warn}; -pub(crate) fn spending_routes(ecash_state: Arc) -> Router { +pub(crate) fn spending_routes(ecash_state: Arc) -> Router { Router::new() .route( "/verify-ecash-ticket", @@ -111,16 +112,16 @@ async fn verify_ticket( ) { IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? IdentifyResult::DuplicatePayInfo(_) => { - log::warn!("Identical payInfo"); + warn!("Identical payInfo"); return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); } IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { //Actual double spending - log::warn!( + warn!( "Double spending attempt for key {}", pub_key.to_base58_string() ); - log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + error!("UNIMPLEMENTED: blacklisting the double spend key"); return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); } } diff --git a/nym-api/src/ecash/client.rs b/nym-api/src/ecash/client.rs index e0970f2155..805252808a 100644 --- a/nym-api/src/ecash/client.rs +++ b/nym-api/src/ecash/client.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::error::Result; +use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{ diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs index a539cb12c2..fd70c592eb 100644 --- a/nym-api/src/ecash/comm.rs +++ b/nym-api/src/ecash/comm.rs @@ -5,6 +5,7 @@ use crate::ecash::client::Client; use crate::ecash::error::{EcashError, Result}; use crate::ecash::helpers::CachedImmutableEpochItem; use crate::{ecash, nyxd}; +use async_trait::async_trait; use nym_coconut_dkg_common::types::{Epoch, EpochId}; use nym_dkg::Threshold; use nym_validator_client::EcashApiClient; diff --git a/nym-api/src/ecash/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs index 6b806bf792..474d6ab0b1 100644 --- a/nym-api/src/ecash/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -10,10 +10,11 @@ use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use rand::{CryptoRng, RngCore}; use std::path::Path; use thiserror::__private::AsDisplay; +use tracing::warn; pub(crate) fn init_bte_keypair( rng: &mut R, - config: &config::CoconutSigner, + config: &config::EcashSigner, ) -> anyhow::Result<()> { let dkg_params = nym_dkg::bte::setup(); let kp = DkgKeyPair::new(&dkg_params, rng); @@ -27,7 +28,7 @@ pub(crate) fn init_bte_keypair( .context("DKG BTE keypair store failure") } -pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result { +pub(crate) fn load_bte_keypair(config: &config::EcashSigner) -> anyhow::Result { nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new( &config.storage_paths.decryption_key_path, &config.storage_paths.public_key_with_proof_path, @@ -36,25 +37,25 @@ pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result } pub(crate) fn load_ecash_keypair_if_exists( - config: &config::CoconutSigner, + config: &config::EcashSigner, ) -> anyhow::Result> { - if !config.storage_paths.coconut_key_path.exists() { + if !config.storage_paths.ecash_key_path.exists() { return Ok(None); } // first attempt to load ecash keys directly, // if that fails fallback to coconut keys and perform migration if let Ok(ecash_key) = - nym_pemstore::load_key::(&config.storage_paths.coconut_key_path) + nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) { return Ok(Some(ecash_key)); } - if let Ok(legacy_coconut_key) = nym_pemstore::load_key::( - &config.storage_paths.coconut_key_path, - ) { + if let Ok(legacy_coconut_key) = + nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) + { let migrated_key: KeyPairWithEpoch = legacy_coconut_key.into(); - nym_pemstore::store_key(&migrated_key, &config.storage_paths.coconut_key_path) + nym_pemstore::store_key(&migrated_key, &config.storage_paths.ecash_key_path) .context("migrated key storage failure")?; return Ok(Some(migrated_key)); diff --git a/nym-api/src/ecash/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs index 6dd454f10d..25cba6b48d 100644 --- a/nym-api/src/ecash/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -18,6 +18,7 @@ use std::path::PathBuf; use std::time::Duration; use time::OffsetDateTime; use tokio::time::{interval, MissedTickBehavior}; +use tracing::{debug, error, info, trace, warn}; mod error; pub(crate) mod keys; @@ -32,7 +33,7 @@ pub(crate) struct DkgController { impl DkgController { pub(crate) fn new( - config: &config::CoconutSigner, + config: &config::EcashSigner, nyxd_client: nyxd::Client, coconut_keypair: CoconutKeyPair, dkg_keypair: DkgKeyPair, @@ -52,7 +53,7 @@ impl DkgController { Ok(DkgController { dkg_client: DkgClient::new(nyxd_client), - coconut_key_path: config.storage_paths.coconut_key_path.clone(), + coconut_key_path: config.storage_paths.ecash_key_path.clone(), state: State::new( config.storage_paths.dkg_persistent_state_path.clone(), persistent_state, @@ -304,7 +305,7 @@ impl DkgController { } pub(crate) fn start( - config: &config::CoconutSigner, + config: &config::EcashSigner, nyxd_client: nyxd::Client, coconut_keypair: CoconutKeyPair, dkg_bte_keypair: DkgKeyPair, diff --git a/nym-api/src/ecash/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs index bb8c17d92b..a0ae4ac840 100644 --- a/nym-api/src/ecash/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -6,7 +6,6 @@ use crate::ecash::dkg::controller::keys::archive_coconut_keypair; use crate::ecash::dkg::controller::DkgController; use crate::ecash::error::EcashError; use crate::ecash::keys::KeyPairWithEpoch; -use log::debug; use nym_coconut_dkg_common::dealing::{chunk_dealing, DealingChunkInfo, MAX_DEALING_CHUNK_SIZE}; use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; use nym_dkg::{Dealing, Scalar}; @@ -15,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Formatter}; use std::path::PathBuf; use thiserror::Error; +use tracing::{debug, error, info, warn}; enum DealingGeneration { Fresh { number: u32 }, diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index 23291806b8..f3e69e1fe6 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -8,7 +8,6 @@ use crate::ecash::dkg::state::key_derivation::{DealerRejectionReason, Derivation use crate::ecash::error::EcashError; use crate::ecash::keys::KeyPairWithEpoch; use cosmwasm_std::Addr; -use log::debug; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex}; use nym_compact_ecash::scheme::keygen::SecretKeyAuth; @@ -25,6 +24,8 @@ use rand::{CryptoRng, RngCore}; use std::collections::{BTreeMap, HashMap}; use std::ops::Deref; use thiserror::Error; +use tracing::debug; +use tracing::{error, info, warn}; #[derive(Debug, Error)] pub enum KeyDerivationError { diff --git a/nym-api/src/ecash/dkg/key_finalization.rs b/nym-api/src/ecash/dkg/key_finalization.rs index 9ba5ff958a..b23e2312ba 100644 --- a/nym-api/src/ecash/dkg/key_finalization.rs +++ b/nym-api/src/ecash/dkg/key_finalization.rs @@ -7,6 +7,7 @@ use cw3::Status; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; use thiserror::Error; +use tracing::{debug, error, info, warn}; #[derive(Debug, Error)] pub enum KeyFinalizationError { diff --git a/nym-api/src/ecash/dkg/key_validation.rs b/nym-api/src/ecash/dkg/key_validation.rs index b99bacd962..aff0ea90ac 100644 --- a/nym-api/src/ecash/dkg/key_validation.rs +++ b/nym-api/src/ecash/dkg/key_validation.rs @@ -13,6 +13,7 @@ use nym_compact_ecash::{ use rand::{CryptoRng, RngCore}; use std::collections::HashMap; use thiserror::Error; +use tracing::{debug, error, info, warn}; fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool { if voted_yes && chain_vote == Vote::Yes { diff --git a/nym-api/src/ecash/dkg/public_key.rs b/nym-api/src/ecash/dkg/public_key.rs index b7f7d3e6ac..afbdf145dc 100644 --- a/nym-api/src/ecash/dkg/public_key.rs +++ b/nym-api/src/ecash/dkg/public_key.rs @@ -3,10 +3,11 @@ use crate::ecash::dkg::controller::DkgController; use crate::ecash::error::EcashError; -use log::debug; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; use thiserror::Error; +use tracing::debug; +use tracing::info; #[derive(Debug, Error)] pub enum PublicKeySubmissionError { diff --git a/nym-api/src/ecash/dkg/state/mod.rs b/nym-api/src/ecash/dkg/state/mod.rs index eb1cca637e..065cdb3f24 100644 --- a/nym-api/src/ecash/dkg/state/mod.rs +++ b/nym-api/src/ecash/dkg/state/mod.rs @@ -10,7 +10,6 @@ use crate::ecash::dkg::state::registration::{DkgParticipant, ParticipantState, R use crate::ecash::error::EcashError; use crate::ecash::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; use cosmwasm_std::Addr; -use log::debug; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::EpochId; use nym_crypto::asymmetric::identity; @@ -20,6 +19,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use time::OffsetDateTime; +use tracing::{debug, warn}; use url::Url; pub(crate) mod dealing_exchange; diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index 94201847dc..e5c7a9fd89 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -15,14 +15,6 @@ use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITL use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; -use okapi::openapi3::Responses; -use rocket::http::{ContentType, Status}; -use rocket::response::Responder; -use rocket::{response, Request, Response}; -use rocket_okapi::gen::OpenApiGenerator; -use rocket_okapi::response::OpenApiResponderInner; -use rocket_okapi::util::ensure_status_code_exists; -use std::io::Cursor; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; @@ -219,24 +211,24 @@ pub enum EcashError { UnknownTicketBookType(#[from] UnknownTicketType), } -impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { - let err_msg = self.to_string(); - Response::build() - .header(ContentType::Plain) - .sized_body(err_msg.len(), Cursor::new(err_msg)) - .status(Status::BadRequest) - .ok() - } -} - -impl OpenApiResponderInner for EcashError { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 400); - Ok(responses) - } -} +// impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { +// fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { +// let err_msg = self.to_string(); +// Response::build() +// .header(ContentType::Plain) +// .sized_body(err_msg.len(), Cursor::new(err_msg)) +// .status(Status::BadRequest) +// .ok() +// } +// } +// +// impl OpenApiResponderInner for EcashError { +// fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { +// let mut responses = Responses::default(); +// ensure_status_code_exists(&mut responses, 400); +// Ok(responses) +// } +// } #[derive(Debug, Error)] pub enum RedemptionError { diff --git a/nym-api/src/ecash/mod.rs b/nym-api/src/ecash/mod.rs index 662822114f..2855f8dd7a 100644 --- a/nym-api/src/ecash/mod.rs +++ b/nym-api/src/ecash/mod.rs @@ -1,11 +1,6 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - pub(crate) mod api_routes; pub(crate) mod client; pub(crate) mod comm; @@ -22,26 +17,26 @@ pub(crate) mod tests; // equivalent of 100nym pub(crate) const MINIMUM_BALANCE: u128 = 100_000000; -pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { - if enabled { - openapi_get_routes_spec![ - settings: - api_routes::partial_signing::post_blind_sign, - api_routes::partial_signing::partial_expiration_date_signatures, - api_routes::partial_signing::partial_coin_indices_signatures, - api_routes::spending::verify_ticket, - api_routes::spending::batch_redeem_tickets, - api_routes::spending::double_spending_filter_v1, - api_routes::issued::epoch_credentials, - api_routes::issued::issued_credential, - api_routes::issued::issued_credentials, - api_routes::aggregation::master_verification_key, - api_routes::aggregation::coin_indices_signatures, - api_routes::aggregation::expiration_date_signatures - ] - } else { - openapi_get_routes_spec![ - settings: - ] - } -} +// pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { +// if enabled { +// openapi_get_routes_spec![ +// settings: +// api_routes::partial_signing::post_blind_sign, +// api_routes::partial_signing::partial_expiration_date_signatures, +// api_routes::partial_signing::partial_coin_indices_signatures, +// api_routes::spending::verify_ticket, +// api_routes::spending::batch_redeem_tickets, +// api_routes::spending::double_spending_filter_v1, +// api_routes::issued::epoch_credentials, +// api_routes::issued::issued_credential, +// api_routes::issued::issued_credentials, +// api_routes::aggregation::master_verification_key, +// api_routes::aggregation::coin_indices_signatures, +// api_routes::aggregation::expiration_date_signatures +// ] +// } else { +// openapi_get_routes_spec![ +// settings: +// ] +// } +// } diff --git a/nym-api/src/ecash/state/helpers.rs b/nym-api/src/ecash/state/helpers.rs index 2fcfd18a31..b96ab53099 100644 --- a/nym-api/src/ecash/state/helpers.rs +++ b/nym-api/src/ecash/state/helpers.rs @@ -16,6 +16,7 @@ use std::future::Future; use time::ext::NumericalDuration; use time::Date; use tokio::sync::Mutex; +use tracing::{debug, error, info, warn}; // attempt to completely rebuild the bloomfilter data for given day async fn try_rebuild_today_bloomfilter( @@ -23,10 +24,10 @@ async fn try_rebuild_today_bloomfilter( params: BloomfilterParameters, storage: &NymApiStorage, ) -> Result { - log::info!("rebuilding bloomfilter for {today}"); + info!("rebuilding bloomfilter for {today}"); let tickets = storage.get_all_spent_tickets_on(today).await?; - log::debug!( + debug!( "there are {} tickets to insert into the filter", tickets.len() ); @@ -45,7 +46,7 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( start: Date, days: i64, ) -> Result { - log::info!( + info!( "attempting to rebuild partial bloomfilter starting at {start} which includes {days} days" ); @@ -56,11 +57,11 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( .try_load_partial_bloomfilter_bitmap(date, params_id) .await? else { - log::warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least {days} day(s) since 'ecash'-based zk-nyms were introduced this is expected)"); + warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least {days} day(s) since 'ecash'-based zk-nyms were introduced this is expected)"); continue; }; if !filter_builder.add_bytes(&bitmap) { - log::error!( + error!( "failed to add bitmap from {date} to the global bloomfilter. it may be malformed!" ); } @@ -71,16 +72,16 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( pub(super) async fn try_rebuild_bloomfilter( storage: &NymApiStorage, ) -> Result { - log::info!("attempting to rebuild the double spending bloomfilter..."); + info!("attempting to rebuild the double spending bloomfilter..."); let today = ecash_today().date(); let (params_id, params) = storage.get_double_spending_filter_params().await?; - log::info!("will use the following parameters: {params:?}"); + info!("will use the following parameters: {params:?}"); // we're never going to have persisted data for 'today'. we need to rebuild it from scratch let today_filter = try_rebuild_today_bloomfilter(today, params, storage).await?; - log::info!("attempting to rebuild the global filter"); + info!("attempting to rebuild the global filter"); let mut global_filter = prepare_partial_bloomfilter_builder( storage, params, @@ -91,9 +92,7 @@ pub(super) async fn try_rebuild_bloomfilter( .await?; if !global_filter.add_bytes(&today_filter.dump_bitmap()) { - log::error!( - "failed to add bitmap from {today} to the global bloomfilter. it may be malformed!" - ); + error!("failed to add bitmap from {today} to the global bloomfilter. it may be malformed!"); } Ok(TicketDoubleSpendingFilter::new( @@ -141,7 +140,7 @@ where match f(api).await { Ok(partial_share) => shares.lock().await.push(partial_share), Err(err) => { - log::warn!("failed to obtain partial threshold data from API: {disp}: {err}") + warn!("failed to obtain partial threshold data from API: {disp}: {err}") } } }) diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index fd3b5080b1..52faecbf3f 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -13,6 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use time::{Date, OffsetDateTime}; use tokio::sync::RwLock; +use tracing::debug; pub(crate) struct TicketDoubleSpendingFilter { built_on: Date, @@ -144,7 +145,7 @@ impl LocalEcashState { if should_export { tokio::spawn(async move { - log::debug!("exporting bloomfilter bitmap"); + debug!("exporting bloomfilter bitmap"); let new = filter.read().await.export_global_bitmap(); let mut exported_guard = exported.data.write().await; exported_guard.last_exported_at = OffsetDateTime::now_utc(); diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index b55117ea1e..27a878f061 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -47,6 +47,7 @@ use nym_validator_client::EcashApiClient; use time::ext::NumericalDuration; use time::{Date, Duration, OffsetDateTime}; use tokio::sync::RwLockReadGuard; +use tracing::{debug, error, info, warn}; pub(crate) mod auxiliary; pub(crate) mod bloom; @@ -169,7 +170,7 @@ impl EcashState { }); } - log::info!( + info!( "attempting to establish master coin index signatures for epoch {epoch_id}..." ); @@ -263,7 +264,7 @@ impl EcashState { // because if it was a past epoch we **do** have those keys. // they're just archived - log::error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); + error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); return Err(EcashError::InvalidSigningKeyEpoch { requested: epoch_id, available: signing_keys.issued_for_epoch, @@ -550,7 +551,7 @@ impl EcashState { pub(crate) async fn accept_proposal(&self, proposal_id: u64) -> Result<()> { //SW NOTE: What to do if this fails if let Err(err) = self.aux.client.vote_proposal(proposal_id, true, None).await { - log::debug!("failed to vote on proposal {proposal_id}: {err}"); + debug!("failed to vote on proposal {proposal_id}: {err}"); } Ok(()) @@ -755,7 +756,7 @@ impl EcashState { // sanity check because this should NEVER happen, // but when it inevitably does, we don't want to crash if spending_date != yesterday { - log::error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); + error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); } // this shouldn't be happening too often, so it's fine to interact with the storage @@ -771,7 +772,7 @@ impl EcashState { return Ok(guard.insert_global_only(serial_number)); } - log::info!("we need to advance our bloomfilter"); + info!("we need to advance our bloomfilter"); let previous_bitmap = guard.export_today_bitmap(); // archive the BF for today's date diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs index a77b4dbb20..0e5169fe5c 100644 --- a/nym-api/src/ecash/storage/manager.rs +++ b/nym-api/src/ecash/storage/manager.rs @@ -6,6 +6,7 @@ use crate::ecash::storage::models::{ StoredBloomfilterParams, TicketProvider, VerifiedTicket, }; use crate::support::storage::manager::StorageManager; +use async_trait::async_trait; use nym_coconut_dkg_common::types::EpochId; use nym_ecash_contract_common::deposit::DepositId; use time::{Date, OffsetDateTime}; diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs index cd7fc26f40..5c15f2b2f6 100644 --- a/nym-api/src/ecash/storage/mod.rs +++ b/nym-api/src/ecash/storage/mod.rs @@ -12,6 +12,7 @@ use crate::ecash::storage::models::{ }; use crate::node_status_api::models::NymApiStorageError; use crate::support::storage::NymApiStorage; +use async_trait::async_trait; use nym_api_requests::ecash::models::Pagination; use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; @@ -24,6 +25,7 @@ use nym_crypto::asymmetric::identity; use nym_ecash_contract_common::deposit::DepositId; use nym_validator_client::nyxd::AccountId; use time::{Date, OffsetDateTime}; +use tracing::info; mod helpers; pub(crate) mod manager; diff --git a/nym-api/src/epoch_operations/error.rs b/nym-api/src/epoch_operations/error.rs index a9f0c8ad99..65fc822dff 100644 --- a/nym-api/src/epoch_operations/error.rs +++ b/nym-api/src/epoch_operations/error.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::NymApiStorageError; -use nym_mixnet_contract_common::{EpochState, MixId}; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{EpochState, NodeId}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; use nym_validator_client::ValidatorClientError; @@ -23,7 +24,10 @@ pub enum RewardingError { }, #[error("it seems the current epoch is in mid-rewarding state (last rewarded is {last_rewarded}). With our current nym-api this shouldn't have been possible. Manual intervention is required.")] - MidMixRewarding { last_rewarded: MixId }, + MidNodeRewarding { last_rewarded: NodeId }, + + #[error("it seems the current epoch is in mid-role assignment state (next role to assign is {next}). With our current nym-api this shouldn't have been possible. Manual intervention is required.")] + MidRoleAssignment { next: Role }, // #[error("There were no mixnodes to reward (network is dead)")] // NoMixnodesToReward, @@ -42,12 +46,16 @@ pub enum RewardingError { #[from] source: std::num::TryFromIntError, }, + #[error("{source}")] WeightedError { #[from] source: rand::distributions::WeightedError, }, + #[error("could not obtain the current interval rewarding parameters")] + RewardingParamsRetrievalFailure, + #[error("{0}")] GenericError(#[from] anyhow::Error), } diff --git a/nym-api/src/epoch_operations/event_reconciliation.rs b/nym-api/src/epoch_operations/event_reconciliation.rs index 47230463c9..302763e68b 100644 --- a/nym-api/src/epoch_operations/event_reconciliation.rs +++ b/nym-api/src/epoch_operations/event_reconciliation.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::RewardedSetUpdater; +use crate::EpochAdvancer; use nym_mixnet_contract_common::EpochState; use std::cmp::max; +use tracing::{error, info, warn}; -impl RewardedSetUpdater { +impl EpochAdvancer { pub(super) async fn reconcile_epoch_events(&self) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; match epoch_status.state { @@ -18,17 +19,17 @@ impl RewardedSetUpdater { operation: "reconciling epoch events".to_string(), }) } - EpochState::AdvancingEpoch => { + EpochState::RoleAssignment { .. } => { warn!("we seem to have crashed mid epoch operations... no need to reconcile events as we've already done that!"); Ok(()) } EpochState::ReconcilingEvents => { - log::info!("Reconciling all pending epoch events..."); + info!("Reconciling all pending epoch events..."); if let Err(err) = self._reconcile_epoch_events().await { - log::error!("FAILED to reconcile epoch events... - {err}"); + error!("FAILED to reconcile epoch events... - {err}"); Err(err) } else { - log::info!("Reconciled all pending epoch events... SUCCESS"); + info!("Reconciled all pending epoch events... SUCCESS"); Ok(()) } } diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index 61ea0defeb..a4f5966615 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -1,24 +1,41 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::epoch_operations::RewardedSetUpdater; +use crate::epoch_operations::EpochAdvancer; use cosmwasm_std::{Decimal, Fraction}; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{ExecuteMsg, Interval, MixId}; +use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor}; +use nym_mixnet_contract_common::{ExecuteMsg, Interval, NodeId, RewardedSet, RewardingParams}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub(crate) struct MixnodeWithPerformance { - pub(crate) mix_id: MixId, - +pub(crate) struct NodeWithPerformance { + pub(crate) node_id: NodeId, pub(crate) performance: Performance, } -impl From for ExecuteMsg { - fn from(mix_reward: MixnodeWithPerformance) -> Self { - ExecuteMsg::RewardMixnode { - mix_id: mix_reward.mix_id, - performance: mix_reward.performance, +impl NodeWithPerformance { + pub fn with_work(self, work_factor: WorkFactor) -> RewardedNodeWithParams { + RewardedNodeWithParams { + node_id: self.node_id, + params: NodeRewardingParameters { + performance: self.performance, + work_factor, + }, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct RewardedNodeWithParams { + pub(crate) node_id: NodeId, + pub(crate) params: NodeRewardingParameters, +} + +impl From for ExecuteMsg { + fn from(node_reward: RewardedNodeWithParams) -> Self { + ExecuteMsg::RewardNode { + node_id: node_reward.node_id, + params: node_reward.params, } } } @@ -37,36 +54,122 @@ pub(super) fn stake_to_f64(stake: Decimal) -> f64 { } } -impl RewardedSetUpdater { - pub(crate) async fn load_performance( +impl EpochAdvancer { + pub(crate) async fn load_mixnode_performance( &self, interval: &Interval, - mix_id: MixId, - ) -> MixnodeWithPerformance { + node_id: NodeId, + ) -> NodeWithPerformance { let uptime = self .storage .get_average_mixnode_uptime_in_the_last_24hrs( - mix_id, + node_id, interval.current_epoch_end_unix_timestamp(), ) .await .unwrap_or_default(); - MixnodeWithPerformance { - mix_id, + NodeWithPerformance { + node_id, performance: uptime.into(), } } - pub(crate) async fn load_nodes_performance( + pub(crate) async fn load_gateway_performance( &self, interval: &Interval, - nodes: &[MixId], - ) -> Vec { - let mut with_performance = Vec::with_capacity(nodes.len()); - for mix_id in nodes { - with_performance.push(self.load_performance(interval, *mix_id).await) + node_id: NodeId, + ) -> NodeWithPerformance { + let uptime = self + .storage + .get_average_gateway_uptime_in_the_last_24hrs( + node_id, + interval.current_epoch_end_unix_timestamp(), + ) + .await + .unwrap_or_default(); + + NodeWithPerformance { + node_id, + performance: uptime.into(), } + } + + pub(crate) async fn load_any_performance( + &self, + interval: &Interval, + node_id: NodeId, + ) -> NodeWithPerformance { + // currently we can't do much better without new network monitor + let mix_performance = self.load_mixnode_performance(interval, node_id).await; + if !mix_performance.performance.is_zero() { + return mix_performance; + } + + self.load_gateway_performance(interval, node_id).await + } + + pub(crate) async fn load_nodes_for_rewarding( + &self, + interval: &Interval, + nodes: &RewardedSet, + // we only need reward parameters for active set work factor and rewarded/active set sizes; + // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync + global_rewarding_params: RewardingParams, + ) -> Vec { + // currently we are using constant omega for nodes, but that will change with tickets + // or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere + let active_node_work_factor = global_rewarding_params.active_node_work(); + let standby_node_work_factor = global_rewarding_params.standby_node_work(); + + // SANITY CHECK: + let standby_share = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap() + * standby_node_work_factor; + let active_share = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap() + * active_node_work_factor; + let total_work = standby_share + active_share; + + // this HAS TO blow up. there's no recovery + assert!(total_work <= Decimal::one(), "work calculation logic is flawed! somehow the total work in the system is greater than 1!"); + + let mut with_performance = Vec::with_capacity(nodes.rewarded_set_size()); + + // all the active set mixnodes + for node_id in nodes + .layer1 + .iter() + .chain(nodes.layer2.iter()) + .chain(nodes.layer3.iter()) + { + with_performance.push( + self.load_mixnode_performance(interval, *node_id) + .await + .with_work(active_node_work_factor), + ) + } + + // all the active set gateways + for node_id in nodes + .entry_gateways + .iter() + .chain(nodes.exit_gateways.iter()) + { + with_performance.push( + self.load_gateway_performance(interval, *node_id) + .await + .with_work(active_node_work_factor), + ) + } + + // all the standby nodes + for node_id in &nodes.standby { + with_performance.push( + self.load_any_performance(interval, *node_id) + .await + .with_work(standby_node_work_factor), + ) + } + with_performance } } diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index 952bb5ca1f..e7ccdf7240 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// there is couple of reasons for putting this in a separate module: +// there is a couple of reasons for putting this in a separate module: // 1. I didn't feel it fit well in nym contract "cache". It seems like purpose of cache is to just keep updating local data // rather than attempting to change global view (i.e. the active set) // @@ -12,17 +12,20 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::ONE_DAY; use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; use error::RewardingError; -pub(crate) use helpers::MixnodeWithPerformance; +pub(crate) use helpers::RewardedNodeWithParams; use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval}; use nym_task::{TaskClient, TaskManager}; use std::collections::HashSet; use std::time::Duration; use tokio::time::sleep; +use tracing::{error, info, trace, warn}; pub(crate) mod error; mod event_reconciliation; @@ -31,13 +34,16 @@ mod rewarded_set_assignment; mod rewarding; mod transition_beginning; -pub struct RewardedSetUpdater { +// naming things is difficult, ok? +// this is struct responsible for advancing an epoch +pub struct EpochAdvancer { nyxd_client: Client, nym_contract_cache: NymContractCache, + described_cache: SharedCache, storage: NymApiStorage, } -impl RewardedSetUpdater { +impl EpochAdvancer { pub(crate) async fn current_interval_details( &self, ) -> Result { @@ -47,71 +53,77 @@ impl RewardedSetUpdater { pub(crate) fn new( nyxd_client: Client, nym_contract_cache: NymContractCache, + described_cache: SharedCache, storage: NymApiStorage, ) -> Self { - RewardedSetUpdater { + EpochAdvancer { nyxd_client, nym_contract_cache, + described_cache, storage, } } #[allow(clippy::doc_lazy_continuation)] // This is where the epoch gets advanced, and all epoch related transactions originate - /// Upon each epoch having finished the following actions are executed by this nym-api: - /// 1. it computes the rewards for each node using the ephemera channel for the epoch that - /// ended - /// 2. it queries the mixnet contract to check the current `EpochState` in order to figure out whether - /// a different nym-api has already started epoch transition (not yet applicable) - /// 3. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: - /// - if successful, the address of this validator is going to be saved as being responsible for progressing this epoch. - /// What it means in practice is that once we have multiple instances of nym-api running, - /// only this one will try to perform the rest of the actions. It will also allow it to - /// more easily recover in case of crashes. - /// - the `EpochState` changes to `Rewarding`, meaning the nym-api will now be allowed to send - /// `RewardMixnode` transactions. However, it's not going to be able anything else like `ReconcileEpochEvents` - /// until that is done. - /// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over, - /// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished. - /// 4. it obtains the current rewarded set and for each node in there (**SORTED BY MIX_ID!!**), - /// it sends (in a single batch) `RewardMixnode` message with the measured performance. - /// Once the final message gets executed, the mixnet contract automatically transitions - /// the state to `ReconcilingEvents`. - /// 5. it obtains the number of pending epoch and interval events and repeatedly sends - /// `ReconcileEpochEvents` transaction until all of them are resolved. - /// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`. - /// 6. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) - /// determines the new rewarded set. It then assigns layers to the provided nodes taking - /// family information into consideration. Finally it sends `AdvanceCurrentEpoch` message - /// containing the set and layer information thus rolling over the epoch and changing the state - /// to `InProgress`. - /// 7. it purges old (older than 48h) measurement data - /// 8. the whole process repeats once the new epoch finishes + // TODO: make sure this is still up to date + // /// Upon each epoch having finished the following actions are executed by this nym-api: + // /// 1. it computes the rewards for each node using the ephemera channel for the epoch that + // /// ended + // /// 2. it queries the mixnet contract to check the current `EpochState` in order to figure out whether + // /// a different nym-api has already started epoch transition (not yet applicable) + // /// 3. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: + // /// - if successful, the address of this validator is going to be saved as being responsible for progressing this epoch. + // /// What it means in practice is that once we have multiple instances of nym-api running, + // /// only this one will try to perform the rest of the actions. It will also allow it to + // /// more easily recover in case of crashes. + // /// - the `EpochState` changes to `Rewarding`, meaning the nym-api will now be allowed to send + // /// `RewardNode` transactions. However, it's not going to be able anything else like `ReconcileEpochEvents` + // /// until that is done. + // /// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over, + // /// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished. + // /// 4. it obtains the current rewarded set and for each node in there (**SORTED BY NODE_ID!!**), + // /// it sends (in a single batch) `RewardMixnode` message with the measured performance. + // /// Once the final message gets executed, the mixnet contract automatically transitions + // /// the state to `ReconcilingEvents`. + // /// 5. it obtains the number of pending epoch and interval events and repeatedly sends + // /// `ReconcileEpochEvents` transaction until all of them are resolved. + // /// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`. + // /// 6. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) + // /// determines the new rewarded set. It then assigns roles to the provided nodes taking + // /// family information into consideration. Finally, it sends `AssignRole` message + // /// containing the role assignment information thus (after each role has been assigned) + // /// rolling over the epoch and changing the state to `InProgress`. + // /// 7. it purges old (older than 48h) measurement data + // /// 8. the whole process repeats once the new epoch finishes async fn perform_epoch_operations(&mut self, interval: Interval) -> Result<(), RewardingError> { - let mut rewards = self.nodes_to_reward(interval).await; - rewards.sort_by_key(|a| a.mix_id); + let mut rewards = self.nodes_to_reward(interval).await?; + rewards.sort_by_key(|a| a.node_id); - log::info!("The current epoch has finished."); - log::info!( + info!("The current epoch has finished."); + info!( "Interval id: {}, epoch id: {} (absolute epoch id: {})", interval.current_interval_id(), interval.current_epoch_id(), interval.current_epoch_absolute_id() ); - log::info!( + info!( "The current epoch has lasted from {} until {}", interval.current_epoch_start(), interval.current_epoch_end() ); - log::info!("Performing all epoch operations..."); + info!("Performing all epoch operations..."); let epoch_end = interval.current_epoch_end(); - let all_mixnodes = self.nym_contract_cache.mixnodes_filtered().await; - if all_mixnodes.is_empty() { - // that's a bit weird, but - log::warn!("there don't seem to be any mixnodes on the network!") + let legacy_mixnodes = self.nym_contract_cache.legacy_mixnodes_filtered().await; + let legacy_gateways = self.nym_contract_cache.legacy_gateways_filtered().await; + let nym_nodes = self.nym_contract_cache.nym_nodes_filtered().await; + + if legacy_mixnodes.is_empty() && legacy_gateways.is_empty() && nym_nodes.is_empty() { + // that's a bit weird, but ok + warn!("there don't seem to be any nodes on the network!") } let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -133,23 +145,33 @@ impl RewardedSetUpdater { } // Reward all the nodes in the still current, soon to be previous rewarded set - log::info!("Rewarding the current rewarded set..."); - self.reward_current_rewarded_set(&rewards, interval).await?; + info!("Rewarding the current rewarded set..."); + self.reward_current_rewarded_set(rewards, interval).await?; // note: those operations don't really have to be atomic, so it's fine to send them // as separate transactions self.reconcile_epoch_events().await?; - self.update_rewarded_set_and_advance_epoch(interval, &all_mixnodes) - .await?; + self.update_rewarded_set_and_advance_epoch( + interval, + &legacy_mixnodes, + &legacy_gateways, + &nym_nodes, + ) + .await?; - log::info!("Purging old node statuses from the storage..."); + info!("Purging old node statuses from the storage..."); let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp(); self.storage.purge_old_statuses(cutoff).await?; Ok(()) } - async fn update_blacklist(&mut self, interval: &Interval) -> Result<(), RewardingError> { + // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. + // instead clients are meant to be filtering out them themselves based on the provided scores. + async fn update_legacy_node_blacklist( + &mut self, + interval: &Interval, + ) -> Result<(), RewardingError> { info!("Updating blacklists"); let mut mix_blacklist_add = HashSet::new(); @@ -183,15 +205,16 @@ impl RewardedSetUpdater { for gateway in gateways { if gateway.value() <= 50.0 { - gate_blacklist_add.insert(gateway.identity().to_string()); + gate_blacklist_add.insert(gateway.node_id()); } else { - gate_blacklist_remove.insert(gateway.identity().to_string()); + gate_blacklist_remove.insert(gateway.node_id()); } } self.nym_contract_cache .update_gateways_blacklist(gate_blacklist_add, gate_blacklist_remove) .await; + Ok(()) } @@ -219,7 +242,7 @@ impl RewardedSetUpdater { return Some(current_interval.interval); } else { let time_left = current_interval.time_until_current_epoch_end(); - log::info!( + info!( "Waiting for epoch change, it should take approximately {}s", time_left.as_secs() ); @@ -244,15 +267,19 @@ impl RewardedSetUpdater { } pub(crate) async fn run(&mut self, mut shutdown: TaskClient) -> Result<(), RewardingError> { + info!("waiting for initial contract cache values before we can start rewarding"); self.nym_contract_cache.wait_for_initial_values().await; + info!("waiting for initial self-described cache values before we can start rewarding"); + self.described_cache.naive_wait_for_initial_values().await; + while !shutdown.is_shutdown() { let interval_details = match self.wait_until_epoch_end(&mut shutdown).await { // received a shutdown None => return Ok(()), Some(interval) => interval, }; - if let Err(err) = self.update_blacklist(&interval_details).await { + if let Err(err) = self.update_legacy_node_blacklist(&interval_details).await { error!("failed to update the node blacklist - {err}"); continue; } @@ -268,11 +295,16 @@ impl RewardedSetUpdater { pub(crate) fn start( nyxd_client: Client, nym_contract_cache: &NymContractCache, - storage: NymApiStorage, + described_cache: SharedCache, + storage: &NymApiStorage, shutdown: &TaskManager, ) { - let mut rewarded_set_updater = - RewardedSetUpdater::new(nyxd_client, nym_contract_cache.to_owned(), storage); + let mut rewarded_set_updater = EpochAdvancer::new( + nyxd_client, + nym_contract_cache.to_owned(), + described_cache, + storage.to_owned(), + ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await }); } diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 08a794768c..de5e3b0f92 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -3,26 +3,38 @@ use crate::epoch_operations::error::RewardingError; use crate::epoch_operations::helpers::stake_to_f64; -use crate::RewardedSetUpdater; +use crate::EpochAdvancer; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{ - EpochState, IdentityKey, Interval, Layer, LayerAssignment, MixId, MixNodeDetails, -}; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_mixnet_contract_common::helpers::IntoBaseDecimal; +use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams}; +use nym_mixnet_contract_common::{EpochState, Interval, NodeId, NymNodeDetails, RewardedSet}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; -use std::collections::HashMap; +use std::collections::HashSet; +use tracing::{debug, error, info, warn}; + +#[derive(Debug, Clone, PartialEq)] +enum AvailableRole { + // legacy mixnodes + nym-nodes in mixing mode + Mix, + + // legacy gateways + nym-nodes in entry or exit mode + EntryGateway, + + // nym-nodes in exit mode + ExitGateway, +} #[derive(Debug, Clone)] -struct MixnodeWithStakeAndPerformance { - mix_id: MixId, - identity: IdentityKey, +struct NodeWithStakeAndPerformance { + node_id: NodeId, + available_roles: Vec, total_stake: Decimal, performance: Performance, } -impl MixnodeWithStakeAndPerformance { +impl NodeWithStakeAndPerformance { fn to_selection_weight(&self) -> f64 { let scaled_performance = match self.performance.checked_pow(20) { Ok(perf) => perf, @@ -35,150 +47,281 @@ impl MixnodeWithStakeAndPerformance { let scaled_stake = self.total_stake * scaled_performance; stake_to_f64(scaled_stake) } -} -impl RewardedSetUpdater { - // Needs to run for active and reserve sets separatley, as it does not preserve order - async fn determine_layers( - &self, - set: &[MixnodeWithStakeAndPerformance], - ) -> Result, RewardingError> { - let mut assignments = Vec::with_capacity(set.len()); - let target_layer_count = set.len() / 3; - - let mix_to_family = self.nym_contract_cache.mix_to_family().await.to_vec(); - - let mix_to_family = mix_to_family - .into_iter() - .collect::>(); - - let mut regular_nodes = Vec::with_capacity(set.len()); - - let mut families = HashMap::new(); - - for node in set.iter() { - if let Some(fh) = mix_to_family.get(&node.identity) { - let family: &mut Vec = families.entry(fh.identity()).or_default(); - family.push(node.mix_id) - } else { - regular_nodes.push(node.mix_id) - } - } - - let mut layers = HashMap::new(); - layers.insert(Layer::One, Vec::with_capacity(target_layer_count)); - layers.insert(Layer::Two, Vec::with_capacity(target_layer_count)); - layers.insert(Layer::Three, Vec::with_capacity(target_layer_count)); - - // Assign all members of a family to same layer - for (_head, members) in families.iter_mut() { - let smallest_layer = layers - .iter() - .min_by_key(|(_layer, members)| members.len()) - .map(|(layer, _members)| *layer) - .unwrap_or(Layer::One); - - let entry = layers.entry(smallest_layer).or_default(); - if entry.len() + members.len() <= target_layer_count { - entry.extend_from_slice(members) - } - } - - // Assign nodes with no families into layers - for mix_id in regular_nodes.drain(..) { - let smallest_layer = layers - .iter() - .min_by_key(|(_layer, members)| members.len()) - .map(|(layer, _members)| *layer) - .unwrap_or(Layer::One); - - let entry = layers.entry(smallest_layer).or_default(); - if entry.len() < target_layer_count { - entry.push(mix_id) - } - } - - for (layer, members) in layers { - let layer_assignments = members - .into_iter() - .map(|mix_id| LayerAssignment::new(mix_id, layer)); - assignments.extend(layer_assignments); - } - Ok(assignments) + fn can_operate_mixnode(&self) -> bool { + self.available_roles.contains(&AvailableRole::Mix) } + fn can_operate_entry_gateway(&self) -> bool { + self.available_roles.contains(&AvailableRole::EntryGateway) + } + + fn can_operate_exit_gateway(&self) -> bool { + self.available_roles.contains(&AvailableRole::ExitGateway) + } +} + +impl EpochAdvancer { fn determine_rewarded_set( &self, - mixnodes: Vec, - nodes_to_select: u32, - ) -> Result, RewardingError> { - if mixnodes.is_empty() { - return Ok(Vec::new()); + nodes: Vec, + spec: RewardedSetParams, + ) -> Result { + if nodes.is_empty() { + warn!("there are no nodes for assignment!"); + return Ok(RewardedSet::default()); } let mut rng = OsRng; - // generate list of mixnodes and their relatively weight (by total stake) - let choices = mixnodes + // generate list of nodes and their relatively weight (by total stake scaled by performance) + let all_choices = nodes .into_iter() - .map(|mix| { - let weight = mix.to_selection_weight(); - (mix, weight) + .map(|node| { + let weight = node.to_selection_weight(); + (node, weight) }) .collect::>(); - // the unwrap here is fine as an error can only be thrown under one of the following conditions: - // - our mixnode list is empty - we have already checked for that - // - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128 - // - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond - // - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one) - Ok(choices - .choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1)? - .map(|(mix, _weight)| mix.clone()) - .collect()) + // 1. determine entry gateways + let entry_eligible = all_choices + .iter() + .filter(|node| node.0.can_operate_entry_gateway()) + .collect::>(); + let entry_gateways = entry_eligible + .choose_multiple_weighted(&mut rng, spec.entry_gateways as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 2. determine exit gateways + let exit_eligible = all_choices + .iter() + .filter(|node| { + node.0.can_operate_exit_gateway() && !entry_gateways.contains(&node.0.node_id) + }) + .collect::>(); + let exit_gateways = exit_eligible + .choose_multiple_weighted(&mut rng, spec.exit_gateways as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 3. determine mixnodes + let mix_eligible = all_choices + .iter() + .filter(|node| { + node.0.can_operate_mixnode() + && !exit_gateways.contains(&node.0.node_id) + && !entry_gateways.contains(&node.0.node_id) + }) + .collect::>(); + let mixnodes = mix_eligible + .choose_multiple_weighted(&mut rng, spec.mixnodes as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 4. determine standby + let standby_eligible = all_choices + .iter() + .filter(|node| { + exit_gateways.contains(&node.0.node_id) + && !entry_gateways.contains(&node.0.node_id) + && !mixnodes.contains(&node.0.node_id) + }) + .collect::>(); + let standby = standby_eligible + .choose_multiple_weighted(&mut rng, spec.standby as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 5. split mixnodes into the layers: just shuffle the selected nodes and select every 3rd into each layer + let mut mixnodes_vec = mixnodes.into_iter().collect::>(); + mixnodes_vec.shuffle(&mut rng); + + let mut layer1 = Vec::new(); + let mut layer2 = Vec::new(); + let mut layer3 = Vec::new(); + + for (i, mix) in mixnodes_vec.iter().enumerate() { + match i % 3 { + 0 => layer1.push(*mix), + 1 => layer2.push(*mix), + 2 => layer3.push(*mix), + n => panic!("we have broken maths! somehow {i} % 3 == {n}!"), + } + } + + if entry_gateways.len() != spec.entry_gateways as usize { + warn!( + "we didn't manage to select {} entry gateways. we only got {}", + spec.entry_gateways, + entry_gateways.len() + ) + } + + if exit_gateways.len() != spec.exit_gateways as usize { + warn!( + "we didn't manage to select {} exit gateways. we only got {}", + spec.exit_gateways, + exit_gateways.len() + ) + } + + if mixnodes_vec.len() != spec.mixnodes as usize { + warn!( + "we didn't manage to select {} mixnodes. we only got {}", + spec.mixnodes, + mixnodes_vec.len() + ) + } + + if standby.len() != spec.standby as usize { + warn!( + "we didn't manage to select {} standby nodes. we only got {}", + spec.standby, + standby.len() + ) + } + + Ok(RewardedSet { + entry_gateways: entry_gateways.into_iter().collect(), + exit_gateways: exit_gateways.into_iter().collect(), + layer1, + layer2, + layer3, + standby, + }) } async fn attach_performance( &self, interval: Interval, - mixnodes: &[MixNodeDetails], - ) -> Vec { - let mut with_performance = Vec::with_capacity(mixnodes.len()); - for mix in mixnodes { - with_performance.push(MixnodeWithStakeAndPerformance { - mix_id: mix.mix_id(), - identity: mix.bond_information.identity().to_owned(), - total_stake: mix.total_stake(), - performance: self - .load_performance(&interval, mix.mix_id()) - .await - .performance, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + ) -> Vec { + let mut with_performance = + Vec::with_capacity(legacy_mixnodes.len() + legacy_gateways.len() + nym_nodes.len()); + + for mix in legacy_mixnodes { + let total_stake = mix.total_stake(); + let performance = self + .load_mixnode_performance(&interval, mix.mix_id()) + .await + .performance; + debug!( + "legacy mixnode {}: stake: {total_stake}, performance: {performance}", + mix.mix_id() + ); + + with_performance.push(NodeWithStakeAndPerformance { + node_id: mix.mix_id(), + available_roles: vec![AvailableRole::Mix], + total_stake, + performance, }) } + for gateway in legacy_gateways { + let total_stake = gateway + .bond + .pledge_amount + .amount + .into_base_decimal() + .unwrap_or_default(); + let performance = self + .load_gateway_performance(&interval, gateway.node_id) + .await + .performance; + debug!( + "legacy gateway {}: stake: {total_stake}, performance: {performance}", + gateway.node_id + ); + + with_performance.push(NodeWithStakeAndPerformance { + node_id: gateway.node_id, + available_roles: vec![AvailableRole::EntryGateway], + total_stake, + performance, + }) + } + + // SAFETY: the cache MUST HAVE been initialised before now + let described_cache = self.described_cache.get().await.unwrap(); + + for nym_node in nym_nodes { + let node_id = nym_node.node_id(); + let total_stake = nym_node.total_stake(); + let performance = self + .load_any_performance(&interval, nym_node.node_id()) + .await + .performance; + debug!("nym-node {node_id}: stake: {total_stake}, performance: {performance}",); + + let Some(self_described) = described_cache.get_description(&node_id) else { + warn!("we don't have self described information for nym-node {node_id}. it is not going to get considered for rewarded set selection."); + continue; + }; + + let mut available_roles = Vec::new(); + if self_described.declared_role.can_operate_mixnode() { + available_roles.push(AvailableRole::Mix) + } + if self_described.declared_role.can_operate_entry_gateway() { + available_roles.push(AvailableRole::EntryGateway) + } + if self_described.declared_role.can_operate_exit_gateway() { + available_roles.push(AvailableRole::ExitGateway) + } + + if available_roles.is_empty() { + warn!("nym-node {node_id} can't operate under any mode!"); + continue; + } + + with_performance.push(NodeWithStakeAndPerformance { + node_id: nym_node.node_id(), + available_roles, + total_stake, + performance, + }) + } + with_performance } pub(super) async fn update_rewarded_set_and_advance_epoch( &self, current_interval: Interval, - all_mixnodes: &[MixNodeDetails], + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; match epoch_status.state { - EpochState::AdvancingEpoch => { - log::info!("Advancing epoch and updating the rewarded set..."); + EpochState::RoleAssignment { next } => { + // with how the nym-api is currently coded, this should never happen as we're always + // assigning roles to ALL nodes at once, but who knows what we might decide to do in the future... + if !next.is_first() { + return Err(RewardingError::MidRoleAssignment { next }); + } + + info!("attempting to assign the rewarded set for the upcoming epoch..."); let nodes_with_performance = self - .attach_performance(current_interval, all_mixnodes) + .attach_performance( + current_interval, + legacy_mixnodes, + legacy_gateways, + nym_nodes, + ) .await; if let Err(err) = self ._update_rewarded_set_and_advance_epoch(nodes_with_performance) .await { - log::error!("FAILED to advance the current epoch... - {err}"); + error!("FAILED to assign the rewarded set... - {err}"); Err(err) } else { - log::info!("Advanced the epoch and updated the rewarded set... SUCCESS"); + info!("Advanced the epoch and updated the rewarded set... SUCCESS"); Ok(()) } } @@ -195,54 +338,21 @@ impl RewardedSetUpdater { async fn _update_rewarded_set_and_advance_epoch( &self, - all_mixnodes: Vec, + all_nodes: Vec, ) -> Result<(), RewardingError> { // we grab rewarding parameters here as they might have gotten updated when performing epoch actions let rewarding_parameters = self.nyxd_client.get_current_rewarding_parameters().await?; - debug!("Rewarding paremeters: {:?}", rewarding_parameters); + debug!("Rewarding parameters: {rewarding_parameters:?}"); let new_rewarded_set = - self.determine_rewarded_set(all_mixnodes, rewarding_parameters.rewarded_set_size)?; + self.determine_rewarded_set(all_nodes, rewarding_parameters.rewarded_set)?; debug!("New rewarded set: {:?}", new_rewarded_set); - let empty = vec![]; - - let (active_set, reserve_set) = if new_rewarded_set.len() - <= rewarding_parameters.active_set_size as usize - { - warn!("Active set size ({}) is greater then rewarded set len ({}), there will be no reserve set", rewarding_parameters.active_set_size, new_rewarded_set.len()); - (new_rewarded_set.as_slice(), empty.as_slice()) - } else { - new_rewarded_set.split_at(rewarding_parameters.active_set_size as usize) - }; - - let mut active_set_layer_assignments = self.determine_layers(active_set).await?; - debug!( - "Active set layer assignments: {:?}", - active_set_layer_assignments - ); - let reserve_set_layer_assignments = self.determine_layers(reserve_set).await?; - debug!( - "Reserve set layer assignments: {:?}", - reserve_set_layer_assignments - ); - - active_set_layer_assignments.extend(reserve_set_layer_assignments); - - debug!( - "Rewarded set layer assignments: {:?}", - active_set_layer_assignments - ); - self.nyxd_client - .advance_current_epoch( - active_set_layer_assignments, - rewarding_parameters.active_set_size, - ) + .send_role_assignment_messages(new_rewarded_set) .await?; - Ok(()) } } diff --git a/nym-api/src/epoch_operations/rewarding.rs b/nym-api/src/epoch_operations/rewarding.rs index 2f8ae6160b..624ec520b4 100644 --- a/nym-api/src/epoch_operations/rewarding.rs +++ b/nym-api/src/epoch_operations/rewarding.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::epoch_operations::helpers::MixnodeWithPerformance; -use crate::RewardedSetUpdater; -use nym_mixnet_contract_common::{EpochState, Interval, MixId}; +use crate::epoch_operations::helpers::RewardedNodeWithParams; +use crate::EpochAdvancer; +use nym_mixnet_contract_common::{EpochState, Interval}; +use tracing::{error, info, warn}; -impl RewardedSetUpdater { +impl EpochAdvancer { pub(super) async fn reward_current_rewarded_set( &self, - to_reward: &[MixnodeWithPerformance], + to_reward: Vec, current_interval: Interval, ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -22,27 +23,27 @@ impl RewardedSetUpdater { operation: "mix rewarding".to_string(), }) } - EpochState::ReconcilingEvents | EpochState::AdvancingEpoch => { - warn!("we seem to have crashed mid epoch operations... no need to reward mixnodes as we've already done that! (or this could be a false positive if there were no nodes to reward - to fix this warning later)"); + EpochState::ReconcilingEvents | EpochState::RoleAssignment { .. } => { + warn!("we seem to have crashed mid epoch operations... no need to reward nodes as we've already done that! (or this could be a false positive if there were no nodes to reward - to fix this warning later)"); Ok(()) } EpochState::Rewarding { last_rewarded, .. } => { - log::info!("Rewarding the current rewarded set..."); + info!("Rewarding the current rewarded set..."); // with how the nym-api is currently coded, this should never happen as we're always - // rewarding ALL mixnodes at once, but who knows what we might decide to do in the future... + // rewarding ALL nodes at once, but who knows what we might decide to do in the future... if last_rewarded != 0 { - return Err(RewardingError::MidMixRewarding { last_rewarded }); + return Err(RewardingError::MidNodeRewarding { last_rewarded }); } if let Err(err) = self ._reward_current_rewarded_set(to_reward, current_interval) .await { - log::error!("FAILED to reward rewarded set - {err}"); + error!("FAILED to reward rewarded set: {err}"); Err(err) } else { - log::info!("Rewarded current rewarded set... SUCCESS"); + info!("Rewarded current rewarded set... SUCCESS"); Ok(()) } } @@ -51,41 +52,55 @@ impl RewardedSetUpdater { async fn _reward_current_rewarded_set( &self, - to_reward: &[MixnodeWithPerformance], + to_reward: Vec, current_interval: Interval, ) -> Result<(), RewardingError> { if to_reward.is_empty() { error!("There are no nodes to reward in this epoch - we shouldn't have been in the 'Rewarding' state!"); - } else if let Err(err) = self.nyxd_client.send_rewarding_messages(to_reward).await { + } else if let Err(err) = self.nyxd_client.send_rewarding_messages(&to_reward).await { error!( - "failed to perform mixnode rewarding for epoch {}! Error encountered: {err}", + "failed to perform node rewarding for epoch {}! Error encountered: {err}", current_interval.current_epoch_absolute_id(), ); return Err(err.into()); } - log::info!("rewarded {} mixnodes...", to_reward.len()); + info!("rewarded {} nodes...", to_reward.len()); Ok(()) } - pub(crate) async fn nodes_to_reward(&self, interval: Interval) -> Vec { - // try to get current up to date view of the network bypassing the cache + pub(crate) async fn nodes_to_reward( + &self, + interval: Interval, + ) -> Result, RewardingError> { + // try to get current up-to-date view of the network bypassing the cache // in case the epochs were significantly shortened for the purposes of testing - let rewarded_set: Vec = match self.nyxd_client.get_rewarded_set_mixnodes().await { - Ok(nodes) => nodes.into_iter().map(|(id, _)| id).collect::>(), + let rewarded_set = match self.nyxd_client.get_rewarded_set_nodes().await { + Ok(rewarded_set) => rewarded_set, Err(err) => { - warn!("failed to obtain the current rewarded set - {err}. falling back to the cached version"); + warn!("failed to obtain the current rewarded set: {err}. falling back to the cached version"); self.nym_contract_cache - .rewarded_set() + .rewarded_set_owned() .await .into_inner() - .into_iter() - .map(|node| node.mix_id()) - .collect::>() } }; - self.load_nodes_performance(&interval, &rewarded_set).await + // we only need reward parameters for active set work factor and rewarded/active set sizes; + // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync + let Some(reward_params) = self + .nym_contract_cache + .interval_reward_params() + .await + .into_inner() + else { + error!("failed to obtain the current interval rewarding parameters. can't determine rewards without them"); + return Err(RewardingError::RewardingParamsRetrievalFailure); + }; + + Ok(self + .load_nodes_for_rewarding(&interval, &rewarded_set, reward_params) + .await) } } diff --git a/nym-api/src/epoch_operations/transition_beginning.rs b/nym-api/src/epoch_operations/transition_beginning.rs index f6e8f87b6a..b57779dfca 100644 --- a/nym-api/src/epoch_operations/transition_beginning.rs +++ b/nym-api/src/epoch_operations/transition_beginning.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::epoch_operations::RewardedSetUpdater; +use crate::epoch_operations::EpochAdvancer; +use tracing::{error, info}; -impl RewardedSetUpdater { +impl EpochAdvancer { // returns boolean indicating whether we should bother continuing pub(super) async fn begin_epoch_transition(&self) -> Result { info!("starting the epoch transition..."); @@ -13,14 +14,14 @@ impl RewardedSetUpdater { // wasn't faster than us let epoch_status = self.nyxd_client.get_current_epoch_status().await?; if !epoch_status.is_in_progress() { - log::error!("FAILED to begin epoch progression: {err}"); + error!("FAILED to begin epoch progression: {err}"); Err(err) } else { error!("another nym-api ({}) is already advancing the epoch... but we shouldn't have other nym-apis yet!", epoch_status.being_advanced_by); Ok(false) } } else { - log::info!("Begun epoch transition... SUCCESS"); + info!("Begun epoch transition... SUCCESS"); Ok(true) } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 3121ce18f2..8534879327 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,36 +1,16 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// TODO rocket remove -#![allow(deprecated)] - -#[macro_use] -extern crate rocket; - -use crate::ecash::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, -}; -use crate::epoch_operations::RewardedSetUpdater; -use crate::network::models::NetworkDetails; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; -use crate::support::caching::cache::SharedCache; +use crate::epoch_operations::EpochAdvancer; use crate::support::cli; -use crate::support::config::Config; use crate::support::storage; -use crate::support::storage::NymApiStorage; use ::nym_config::defaults::setup_env; -use circulating_supply_api::cache::CirculatingSupplyCache; use clap::Parser; -use ecash::dkg::controller::DkgController; use node_status_api::NodeStatusCache; -use nym_bin_common::logging::setup_logging; -use nym_config::defaults::NymNetworkDetails; +use nym_bin_common::logging::setup_tracing_logger; use nym_contract_cache::cache::NymContractCache; -use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; -use rand::rngs::OsRng; -use support::{http, nyxd}; +use support::nyxd; +use tracing::{info, trace}; mod circulating_supply_api; mod ecash; @@ -43,14 +23,7 @@ pub(crate) mod nym_contract_cache; pub(crate) mod nym_nodes; mod status; pub(crate) mod support; - -#[cfg(feature = "axum")] -mod v2; - -struct ShutdownHandles { - task_manager_handle: TaskManager, - rocket_handle: rocket::Shutdown, -} +mod v3_migration; // TODO rocket: remove all such Todos once rocket is phased out completely #[tokio::main] @@ -60,143 +33,13 @@ async fn main() -> Result<(), anyhow::Error> { console_subscriber::init(); }} - setup_logging(); - // TODO rocket: replace with tracing logger once rocket is eliminated from code + setup_tracing_logger(); info!("Starting nym api..."); let args = cli::Cli::parse(); - trace!("{:#?}", args); + trace!("args: {:#?}", args); setup_env(args.config_env_file.as_ref()); args.execute().await } - -async fn start_nym_api_tasks(config: Config) -> anyhow::Result { - let nyxd_client = nyxd::Client::new(&config); - let connected_nyxd = config.get_nyxd_url(); - let nym_network_details = NymNetworkDetails::new_from_env(); - let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - - let coconut_keypair_wrapper = ecash::keys::KeyPair::new(); - - // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { - let issued_for = loaded_keys.issued_for_epoch; - coconut_keypair_wrapper.set(loaded_keys).await; - - if can_validate_coconut_keys(&nyxd_client, issued_for).await? { - coconut_keypair_wrapper.validate() - } - } - - let identity_keypair = config.base.storage_paths.load_identity()?; - let identity_public_key = *identity_keypair.public_key(); - - // let's build our rocket! - let rocket = http::setup_rocket( - &config, - network_details, - nyxd_client.clone(), - identity_keypair, - coconut_keypair_wrapper.clone(), - ) - .await?; - - // setup shutdowns - let shutdown = TaskManager::new(10); - - // Rocket handles shutdown on its own, but its shutdown handling should be incorporated - // with that of the rest of the tasks. Currently its runtime is forcefully terminated once - // nym-api exits. - let rocket_shutdown_handle = rocket.shutdown(); - - // get references to the managed state - let nym_contract_cache_state = rocket.state::().unwrap(); - let node_status_cache_state = rocket.state::().unwrap(); - let circulating_supply_cache_state = rocket.state::().unwrap(); - let storage = if let Some(storage) = rocket.state::() { - storage.to_owned() - } else { - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await? - }; - let described_nodes_state = rocket.state::>().unwrap(); - - // start note describe cache refresher - // we should be doing the below, but can't due to our current startup structure - // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); - // let cache = refresher.get_shared_cache(); - node_describe_cache::new_refresher_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), - described_nodes_state.to_owned(), - ) - .named("node-self-described-data-refresher") - .start(shutdown.subscribe_named("node-self-described-data-refresher")); - - // start all the caches first - let nym_contract_cache_listener = nym_contract_cache::start_refresher( - &config.node_status_api, - nym_contract_cache_state, - nyxd_client.clone(), - &shutdown, - ); - - node_status_api::start_cache_refresh( - &config.node_status_api, - nym_contract_cache_state, - node_status_cache_state, - storage.to_owned(), - nym_contract_cache_listener, - &shutdown, - ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - circulating_supply_cache_state, - &shutdown, - ); - - // start dkg task - if config.coconut_signer.enabled { - let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?; - - DkgController::start( - &config.coconut_signer, - nyxd_client.clone(), - coconut_keypair_wrapper, - dkg_bte_keypair, - identity_public_key, - OsRng, - &shutdown, - )?; - } - - // and then only start the uptime updater (and the monitor itself, duh) - // if the monitoring if it's enabled - if config.network_monitor.enabled { - network_monitor::start::( - &config.network_monitor, - nym_contract_cache_state, - &storage, - nyxd_client.clone(), - &shutdown, - ) - .await; - - HistoricalUptimeUpdater::start(storage.to_owned(), &shutdown); - - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - RewardedSetUpdater::start(nyxd_client, nym_contract_cache_state, storage, &shutdown); - } - } - // Launch the rocket, serve http endpoints and finish the startup - tokio::spawn(rocket.launch()); - - Ok(ShutdownHandles { - task_manager_handle: shutdown, - rocket_handle: rocket_shutdown_handle, - }) -} diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index 0cb27ca92d..7bb77bc3fc 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::network::models::{ContractInformation, NetworkDetails}; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::{extract, Router}; use nym_contracts_common::ContractBuildInformation; use std::collections::HashMap; use utoipa::ToSchema; -pub(crate) fn nym_network_routes() -> Router { +pub(crate) fn nym_network_routes() -> Router { Router::new() .route("/details", axum::routing::get(network_details)) .route("/nym-contracts", axum::routing::get(nym_contracts)) @@ -27,7 +27,7 @@ pub(crate) fn nym_network_routes() -> Router { ) )] async fn network_details( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json { state.network_details().to_owned().into() } @@ -56,7 +56,7 @@ pub(crate) struct ContractVersionSchemaResponse { ) )] async fn nym_contracts( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json>> { let info = state.nym_contract_cache().contract_details().await; info.iter() @@ -82,7 +82,7 @@ async fn nym_contracts( ) )] async fn nym_contracts_detailed( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json>> { let info = state.nym_contract_cache().contract_details().await; info.iter() diff --git a/nym-api/src/network/mod.rs b/nym-api/src/network/mod.rs index 1f91dea3fd..8595827dd0 100644 --- a/nym-api/src/network/mod.rs +++ b/nym-api/src/network/mod.rs @@ -1,20 +1,14 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -#[cfg(feature = "axum")] pub(crate) mod handlers; pub(crate) mod models; -mod routes; - -pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::network_details, - routes::nym_contracts, - routes::nym_contracts_detailed - ] -} +// mod routes; +// +// pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: routes::network_details, +// routes::nym_contracts, +// routes::nym_contracts_detailed +// ] +// } diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 7cdaa1d052..66d0324c26 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -5,8 +5,7 @@ use nym_config::defaults::NymNetworkDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] +#[derive(Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] pub struct NetworkDetails { pub(crate) connected_nyxd: String, pub(crate) network: NymNetworkDetails, @@ -21,8 +20,7 @@ impl NetworkDetails { } } -#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] -#[derive(Serialize, Deserialize, Clone, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub struct ContractInformation { pub(crate) address: Option, diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index cc308a627f..699498b287 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -11,8 +11,10 @@ use crate::network_monitor::monitor::receiver::{ use crate::network_monitor::monitor::sender::PacketSender; use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; +use crate::node_describe_cache::DescribedNodes; use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; +use crate::support::caching::cache::SharedCache; use crate::support::{config, nyxd}; use futures::channel::mpsc; use nym_bandwidth_controller::BandwidthController; @@ -23,6 +25,7 @@ use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; use nym_task::TaskManager; use std::sync::Arc; +use tracing::info; pub(crate) mod gateways_reader; pub(crate) mod monitor; @@ -33,7 +36,8 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( config: &'a config::NetworkMonitor, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache: &NymContractCache, + described_cache: SharedCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, ) -> NetworkMonitorBuilder<'a> { @@ -41,7 +45,8 @@ pub(crate) fn setup<'a>( config, nyxd_client, storage.to_owned(), - nym_contract_cache_state.to_owned(), + nym_contract_cache.clone(), + described_cache, ) } @@ -49,7 +54,8 @@ pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a config::NetworkMonitor, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, } impl<'a> NetworkMonitorBuilder<'a> { @@ -57,13 +63,15 @@ impl<'a> NetworkMonitorBuilder<'a> { config: &'a config::NetworkMonitor, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, ) -> Self { NetworkMonitorBuilder { config, nyxd_client, node_status_storage, - validator_cache, + contract_cache, + described_cache, } } @@ -84,7 +92,8 @@ impl<'a> NetworkMonitorBuilder<'a> { mpsc::unbounded(); let packet_preparer = new_packet_preparer( - self.validator_cache, + self.contract_cache, + self.described_cache, self.config.debug.per_node_test_packets, Arc::clone(&ack_key), *identity_keypair.public_key(), @@ -158,14 +167,16 @@ impl NetworkMonitorRunnables { } fn new_packet_preparer( - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, per_node_test_packets: usize, ack_key: Arc, self_public_identity: identity::PublicKey, self_public_encryption: encryption::PublicKey, ) -> PacketPreparer { PacketPreparer::new( - validator_cache, + contract_cache, + described_cache, per_node_test_packets, ack_key, self_public_identity, @@ -218,12 +229,19 @@ fn new_packet_receiver( // TODO: 2) how do we make it non-async as other 'start' methods? pub(crate) async fn start( config: &config::NetworkMonitor, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache: &NymContractCache, + described_cache: SharedCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, shutdown: &TaskManager, ) { - let monitor_builder = setup(config, nym_contract_cache_state, storage, nyxd_client); + let monitor_builder = setup( + config, + nym_contract_cache, + described_cache, + storage, + nyxd_client, + ); info!("Starting network monitor..."); let runnables: NetworkMonitorRunnables = monitor_builder.build().await; runnables.spawn_tasks(shutdown); diff --git a/nym-api/src/network_monitor/monitor/gateways_pinger.rs b/nym-api/src/network_monitor/monitor/gateways_pinger.rs index a114088024..ed09c2d090 100644 --- a/nym-api/src/network_monitor/monitor/gateways_pinger.rs +++ b/nym-api/src/network_monitor/monitor/gateways_pinger.rs @@ -3,12 +3,12 @@ use crate::network_monitor::monitor::gateway_clients_cache::ActiveGatewayClients; use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; -use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH; use nym_task::TaskClient; use std::time::Duration; use tokio::time::{sleep, Instant}; +use tracing::{debug, info, trace, warn}; // TODO: should it perhaps be moved to config along other timeout values? const PING_TIMEOUT: Duration = Duration::from_secs(3); diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index e4c35d8f4f..1fdb0ad601 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -9,13 +9,14 @@ use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use crate::storage::NymApiStorage; use crate::support::config; -use log::{debug, error, info}; +use nym_mixnet_contract_common::NodeId; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; use nym_task::TaskClient; use std::collections::{HashMap, HashSet}; use std::process; use tokio::time::{sleep, Duration, Instant}; +use tracing::{debug, error, info, trace}; pub(crate) mod gateway_clients_cache; pub(crate) mod gateways_pinger; @@ -169,11 +170,11 @@ impl Monitor { .collect() } - fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet) { + fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet) { for mix in route.topology().mixes_as_vec() { - blacklist.insert(mix.identity_key.to_base58_string()); + blacklist.insert(mix.mix_id); } - blacklist.insert(route.gateway_identity().to_base58_string()); + blacklist.insert(route.gateway().node_id); } async fn prepare_test_routes(&mut self) -> Option> { diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 73a11b4d60..0f139b05d0 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -3,23 +3,29 @@ use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; +use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; use crate::nym_contract_cache::cache::NymContractCache; -use log::info; +use crate::support::caching::cache::SharedCache; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; +use nym_api_requests::models::NymNodeDescription; use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::{GatewayBond, Layer, MixNodeBond}; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId, RewardedSet}; use nym_node_tester_utils::node::TestableNode; use nym_node_tester_utils::NodeTester; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::{PacketSize, PacketType}; +use nym_topology::gateway::GatewayConversionError; +use nym_topology::mix::MixnodeConversionError; use nym_topology::{gateway, mix}; -use rand::{rngs::ThreadRng, seq::SliceRandom, thread_rng, Rng}; +use rand::prelude::SliceRandom; +use rand::{rngs::ThreadRng, thread_rng, Rng}; use std::collections::{HashMap, HashSet}; - use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::Duration; +use tracing::{error, info, trace}; const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200); const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200); @@ -69,7 +75,8 @@ pub(crate) struct PreparedPackets { #[derive(Clone)] pub(crate) struct PacketPreparer { - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, /// Number of test packets sent to each node per_node_test_packets: usize, @@ -85,14 +92,16 @@ pub(crate) struct PacketPreparer { impl PacketPreparer { pub(crate) fn new( - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, per_node_test_packets: usize, ack_key: Arc, self_public_identity: identity::PublicKey, self_public_encryption: encryption::PublicKey, ) -> Self { PacketPreparer { - validator_cache, + contract_cache, + described_cache, per_node_test_packets, ack_key, self_public_identity, @@ -138,15 +147,16 @@ impl PacketPreparer { } pub(crate) async fn wait_for_validator_cache_initial_values(&self, minimum_full_routes: usize) { - // wait for the cache to get initialised - self.validator_cache.wait_for_initial_values().await; + // wait for the caches to get initialised + self.contract_cache.wait_for_initial_values().await; + self.described_cache.naive_wait_for_initial_values().await; // now wait for at least `minimum_full_routes` mixnodes per layer and `minimum_full_routes` gateway to be online info!("Waiting for minimal topology to be online"); let initialisation_backoff = Duration::from_secs(30); loop { - let gateways = self.validator_cache.gateways_all().await; - let mixnodes = self.validator_cache.mixnodes_all_basic().await; + let gateways = self.contract_cache.legacy_gateways_all().await; + let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; if gateways.len() < minimum_full_routes { self.topology_wait_backoff(initialisation_backoff).await; @@ -159,9 +169,9 @@ impl PacketPreparer { for mix in mixnodes { match mix.layer { - Layer::One => layer1_count += 1, - Layer::Two => layer2_count += 1, - Layer::Three => layer3_count += 1, + LegacyMixLayer::One => layer1_count += 1, + LegacyMixLayer::Two => layer2_count += 1, + LegacyMixLayer::Three => layer3_count += 1, } } @@ -176,64 +186,184 @@ impl PacketPreparer { } } - async fn all_mixnodes_and_gateways(&self) -> (Vec, Vec) { + async fn all_legacy_mixnodes_and_gateways( + &self, + ) -> ( + Vec, + Vec, + ) { info!("Obtaining network topology..."); - let mixnodes = self.validator_cache.mixnodes_all_basic().await; - let gateways = self.validator_cache.gateways_all().await; + let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; + let gateways = self.contract_cache.legacy_gateways_all().await; (mixnodes, gateways) } - async fn filtered_mixnodes_and_gateways(&self) -> (Vec, Vec) { + async fn filtered_legacy_mixnodes_and_gateways( + &self, + ) -> ( + Vec, + Vec, + ) { info!("Obtaining network topology..."); - let mixnodes = self.validator_cache.mixnodes_filtered_basic().await; - let gateways = self.validator_cache.gateways_filtered().await; + let mixnodes = self.contract_cache.legacy_mixnodes_filtered_basic().await; + let gateways = self.contract_cache.legacy_gateways_filtered().await; (mixnodes, gateways) } - pub(crate) fn try_parse_mix_bond(&self, mix: &MixNodeBond) -> Result { - let identity = mix.mix_node.identity_key.clone(); - mix.try_into().map_err(|_| identity) + pub(crate) fn try_parse_mix_bond( + &self, + bond: &LegacyMixNodeBondWithLayer, + ) -> Result { + fn parse_bond( + bond: &LegacyMixNodeBondWithLayer, + ) -> Result { + let host = mix::LegacyNode::parse_host(&bond.mix_node.host)?; + + // try to completely resolve the host in the mix situation to avoid doing it every + // single time we want to construct a path + let mix_host = mix::LegacyNode::extract_mix_host(&host, bond.mix_node.mix_port)?; + + Ok(mix::LegacyNode { + mix_id: bond.mix_id, + host, + mix_host, + identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key)?, + layer: bond.layer, + version: bond.mix_node.version.as_str().into(), + }) + } + + let identity = bond.mix_node.identity_key.clone(); + parse_bond(bond).map_err(|_| identity) } pub(crate) fn try_parse_gateway_bond( &self, - gateway: &GatewayBond, - ) -> Result { + gateway: &LegacyGatewayBondWithId, + ) -> Result { + fn parse_bond( + bond: &LegacyGatewayBondWithId, + ) -> Result { + let host = gateway::LegacyNode::parse_host(&bond.gateway.host)?; + + // try to completely resolve the host in the mix situation to avoid doing it every + // single time we want to construct a path + let mix_host = gateway::LegacyNode::extract_mix_host(&host, bond.gateway.mix_port)?; + + Ok(gateway::LegacyNode { + node_id: bond.node_id, + host, + mix_host, + clients_ws_port: bond.gateway.clients_port, + clients_wss_port: None, + identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, + version: bond.gateway.version.as_str().into(), + }) + } + let identity = gateway.gateway.identity_key.clone(); - gateway.try_into().map_err(|_| identity) + parse_bond(gateway).map_err(|_| identity) + } + + fn layered_mixes<'a, R: Rng>( + &self, + rng: &mut R, + blacklist: &mut HashSet, + rewarded_set: &RewardedSet, + legacy_mixnodes: Vec, + mixing_nym_nodes: impl Iterator + 'a, + ) -> HashMap> { + let mut layered_mixes = HashMap::new(); + for mix in legacy_mixnodes { + let layer = mix.layer; + let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); + let Ok(parsed_node) = self.try_parse_mix_bond(&mix) else { + blacklist.insert(mix.mix_id); + continue; + }; + layer_mixes.push(parsed_node) + } + + for mixing_nym_node in mixing_nym_nodes { + let Some(parsed_node) = self.nym_node_to_legacy_mix(rng, rewarded_set, mixing_nym_node) + else { + continue; + }; + let layer = parsed_node.layer; + let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); + layer_mixes.push(parsed_node) + } + + layered_mixes + } + + fn all_gateways<'a>( + &self, + blacklist: &mut HashSet, + legacy_gateways: Vec, + gateway_capable_nym_nodes: impl Iterator + 'a, + ) -> Vec { + let mut gateways = Vec::new(); + for gateway in legacy_gateways { + let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) else { + blacklist.insert(gateway.node_id); + continue; + }; + gateways.push(parsed_node) + } + + for gateway_capable_node in gateway_capable_nym_nodes { + let Some(parsed_node) = self.nym_node_to_legacy_gateway(gateway_capable_node) else { + continue; + }; + gateways.push(parsed_node) + } + + gateways } - // gets rewarded nodes // chooses n random nodes from each layer (and gateway) such that they are not on the blacklist - // if failed to parsed => onto the blacklist they go + // if failed to get parsed => onto the blacklist they go // if generated fewer than n, blacklist will be updated by external function with correctly generated // routes so that they wouldn't be reused pub(crate) async fn prepare_test_routes( &self, n: usize, - blacklist: &mut HashSet, + blacklist: &mut HashSet, ) -> Option> { - let (mixnodes, gateways) = self.filtered_mixnodes_and_gateways().await; - // separate mixes into layers for easier selection - let mut layered_mixes = HashMap::new(); - for mix in mixnodes { - let layer = mix.layer; - let mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - mixes.push(mix) - } + let (legacy_mixnodes, legacy_gateways) = self.filtered_legacy_mixnodes_and_gateways().await; + let rewarded_set = self.contract_cache.rewarded_set().await?; - // get all nodes from each layer... - let l1 = layered_mixes.get(&Layer::One)?; - let l2 = layered_mixes.get(&Layer::Two)?; - let l3 = layered_mixes.get(&Layer::Three)?; + let descriptions = self.described_cache.get().await.ok()?; + + let mixing_nym_nodes = descriptions.mixing_nym_nodes(); + // last I checked `gatewaying` wasn't a word : ) + let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes(); - // try to choose n nodes from each of them (+ gateways)... let mut rng = thread_rng(); + // separate mixes into layers for easier selection + let layered_mixes = self.layered_mixes( + &mut rng, + blacklist, + &rewarded_set, + legacy_mixnodes, + mixing_nym_nodes, + ); + let gateways = self.all_gateways(blacklist, legacy_gateways, gateway_capable_nym_nodes); + + // get all nodes from each layer... + let l1 = layered_mixes.get(&LegacyMixLayer::One)?; + let l2 = layered_mixes.get(&LegacyMixLayer::Two)?; + let l3 = layered_mixes.get(&LegacyMixLayer::Three)?; + + // try to choose n nodes from each of them (+ gateways)... let rand_l1 = l1.choose_multiple(&mut rng, n).collect::>(); let rand_l2 = l2.choose_multiple(&mut rng, n).collect::>(); let rand_l3 = l3.choose_multiple(&mut rng, n).collect::>(); @@ -258,36 +388,18 @@ impl PacketPreparer { trace!("Generating test routes..."); let mut routes = Vec::new(); for i in 0..most_available { - let Ok(node_1) = self.try_parse_mix_bond(rand_l1[i]) else { - blacklist.insert(rand_l1[i].identity().to_owned()); - continue; - }; - - let Ok(node_2) = self.try_parse_mix_bond(rand_l2[i]) else { - blacklist.insert(rand_l2[i].identity().to_owned()); - continue; - }; - - let Ok(node_3) = self.try_parse_mix_bond(rand_l3[i]) else { - blacklist.insert(rand_l3[i].identity().to_owned()); - continue; - }; - - let Ok(gateway) = self.try_parse_gateway_bond(rand_gateways[i]) else { - blacklist.insert(rand_gateways[i].identity().to_owned()); - continue; - }; + let node_1 = rand_l1[i].clone(); + let node_2 = rand_l2[i].clone(); + let node_3 = rand_l3[i].clone(); + let gateway = rand_gateways[i].clone(); routes.push(TestRoute::new(rng.gen(), node_1, node_2, node_3, gateway)) } - info!( - "The following routes will be used for testing: {:#?}", - routes - ); + info!("The following routes will be used for testing: {routes:#?}"); Some(routes) } - fn create_packet_sender(&self, gateway: &gateway::Node) -> Recipient { + fn create_packet_sender(&self, gateway: &gateway::LegacyNode) -> Recipient { Recipient::new( self.self_public_identity, self.self_public_encryption, @@ -325,20 +437,16 @@ impl PacketPreparer { fn filter_outdated_and_malformed_mixnodes( &self, - nodes: Vec, - ) -> (Vec, Vec) { + nodes: Vec, + ) -> (Vec, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for mixnode in nodes { - if let Ok(parsed_node) = (&mixnode).try_into() { + if let Ok(parsed_node) = self.try_parse_mix_bond(&mixnode) { parsed_nodes.push(parsed_node) } else { invalid_nodes.push(InvalidNode::Malformed { - node: TestableNode::new_mixnode( - mixnode.identity().to_owned(), - mixnode.owner.clone().into_string(), - mixnode.mix_id, - ), + node: TestableNode::new_mixnode(mixnode.identity().to_owned(), mixnode.mix_id), }); } } @@ -347,18 +455,18 @@ impl PacketPreparer { fn filter_outdated_and_malformed_gateways( &self, - nodes: Vec, - ) -> (Vec, Vec) { + nodes: Vec, + ) -> (Vec<(gateway::LegacyNode, NodeId)>, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for gateway in nodes { - if let Ok(parsed_node) = (&gateway).try_into() { - parsed_nodes.push(parsed_node) + if let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) { + parsed_nodes.push((parsed_node, gateway.node_id)) } else { invalid_nodes.push(InvalidNode::Malformed { node: TestableNode::new_gateway( - gateway.identity().to_owned(), - gateway.owner.clone().into_string(), + gateway.bond.identity().to_owned(), + gateway.node_id, ), }); } @@ -366,6 +474,43 @@ impl PacketPreparer { (parsed_nodes, invalid_nodes) } + fn nym_node_to_legacy_mix( + &self, + rng: &mut R, + rewarded_set: &RewardedSet, + mixing_nym_node: &NymNodeDescription, + ) -> Option { + let maybe_explicit_layer = rewarded_set + .try_get_mix_layer(&mixing_nym_node.node_id) + .and_then(|layer| LegacyMixLayer::try_from(layer).ok()); + + let layer = match maybe_explicit_layer { + Some(layer) => layer, + None => { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + + // if nym-node doesn't have a layer assigned, since it's either standby or inactive, + // we have to choose one randomly for the testing purposes + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(rng).copied().unwrap() + } + }; + + mixing_nym_node.try_to_topology_mix_node(layer).ok() + } + + fn nym_node_to_legacy_gateway( + &self, + gateway_capable_node: &NymNodeDescription, + ) -> Option { + gateway_capable_node.try_to_topology_gateway().ok() + } + pub(super) async fn prepare_test_packets( &mut self, test_nonce: u64, @@ -373,17 +518,38 @@ impl PacketPreparer { // TODO: Maybe do this _packet_type: PacketType, ) -> PreparedPackets { - // only test mixnodes that are rewarded, i.e. that will be rewarded in this interval. - // (remember that "idle" nodes are still part of that set) - // we don't care about other nodes, i.e. nodes that are bonded but will not get - // any reward during the current rewarding interval - let (mixnodes, gateways) = self.all_mixnodes_and_gateways().await; + let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await; + let rewarded_set = self.contract_cache.rewarded_set().await; + + let descriptions = self + .described_cache + .get() + .await + .expect("the cache must have been initialised!"); + let mixing_nym_nodes = descriptions.mixing_nym_nodes(); + let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes(); let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes); let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways); - let tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::>(); - let tested_gateways = gateways.iter().map(|node| node.into()).collect::>(); + let mut tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::>(); + let mut tested_gateways = gateways.iter().map(|node| node.into()).collect::>(); + + // try to add nym-nodes into the fold + if let Some(rewarded_set) = rewarded_set { + let mut rng = thread_rng(); + for mix in mixing_nym_nodes { + if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, &mix) { + tested_mixnodes.push(TestableNode::from(&parsed)); + } + } + } + + for gateway in gateway_capable_nym_nodes { + if let Some(parsed) = self.nym_node_to_legacy_gateway(&gateway) { + tested_gateways.push((&parsed, gateway.node_id).into()) + } + } let packets_to_create = (test_routes.len() * self.per_node_test_packets) * (tested_mixnodes.len() + tested_gateways.len()); @@ -405,6 +571,7 @@ impl PacketPreparer { // 1. the topology is definitely valid (otherwise we wouldn't be here) // 2. the recipient is specified (by calling **mix**_tester) // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet + #[allow(clippy::unwrap_used)] let mixnode_test_packets = mix_tester .mixnodes_test_packets( &mixnodes, @@ -421,7 +588,7 @@ impl PacketPreparer { gateway_packets.push_packets(mix_packets); // and generate test packets for gateways (note the variable recipient) - for gateway in &gateways { + for (gateway, node_id) in &gateways { let recipient = self.create_packet_sender(gateway); let gateway_identity = gateway.identity_key; let gateway_address = gateway.clients_address(); @@ -430,9 +597,11 @@ impl PacketPreparer { // 1. the topology is definitely valid (otherwise we wouldn't be here) // 2. the recipient is specified // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet + #[allow(clippy::unwrap_used)] let gateway_test_packets = mix_tester - .gateway_test_packets( + .legacy_gateway_test_packets( gateway, + *node_id, route_ext, self.per_node_test_packets as u32, Some(recipient), diff --git a/nym-api/src/network_monitor/monitor/processor.rs b/nym-api/src/network_monitor/monitor/processor.rs index 84ac4b3aeb..0008bfa901 100644 --- a/nym-api/src/network_monitor/monitor/processor.rs +++ b/nym-api/src/network_monitor/monitor/processor.rs @@ -7,7 +7,6 @@ use crate::network_monitor::ROUTE_TESTING_TEST_NONCE; use futures::channel::mpsc; use futures::lock::{Mutex, MutexGuard}; use futures::{SinkExt, StreamExt}; -use log::warn; use nym_crypto::asymmetric::encryption; use nym_node_tester_utils::error::NetworkTestingError; use nym_node_tester_utils::processor::TestPacketProcessor; @@ -16,6 +15,7 @@ use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError}; use std::mem; use std::sync::Arc; use thiserror::Error; +use tracing::{debug, error, trace, warn}; pub(crate) type ReceivedProcessorSender = mpsc::UnboundedSender; pub(crate) type ReceivedProcessorReceiver = mpsc::UnboundedReceiver; diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index be835fcc41..d6dcbf4b85 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -8,6 +8,7 @@ use futures::StreamExt; use nym_crypto::asymmetric::identity; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; use nym_task::TaskClient; +use tracing::trace; pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender; pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver; diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index e2c5bf8e4d..0b371f1003 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -11,7 +11,6 @@ use futures::channel::mpsc; use futures::stream::{self, FuturesUnordered, StreamExt}; use futures::task::Context; use futures::{Future, Stream}; -use log::{debug, info, trace, warn}; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; @@ -30,6 +29,7 @@ use std::pin::Pin; use std::sync::Arc; use std::task::Poll; use std::time::Duration; +use tracing::{debug, info, trace, warn}; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); diff --git a/nym-api/src/network_monitor/monitor/summary_producer.rs b/nym-api/src/network_monitor/monitor/summary_producer.rs index 0c09e196f6..5f8449f75f 100644 --- a/nym-api/src/network_monitor/monitor/summary_producer.rs +++ b/nym-api/src/network_monitor/monitor/summary_producer.rs @@ -5,7 +5,7 @@ use crate::network_monitor::monitor::preparer::InvalidNode; use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use nym_node_tester_utils::node::{NodeType, TestableNode}; -use nym_types::monitoring::{GatewayResult, MixnodeResult}; +use nym_types::monitoring::NodeResult; use std::collections::HashMap; use std::fmt::{Display, Formatter}; @@ -60,8 +60,8 @@ impl TestReport { fn new( total_sent: usize, total_received: usize, - mixnode_results: &[MixnodeResult], - gateway_results: &[GatewayResult], + mixnode_results: &[NodeResult], + gateway_results: &[NodeResult], route_results: &[RouteResult], ) -> Self { let mut exceptional_mixnodes = 0; @@ -206,8 +206,8 @@ impl Display for TestReport { } pub(crate) struct TestSummary { - pub(crate) mixnode_results: Vec, - pub(crate) gateway_results: Vec, + pub(crate) mixnode_results: Vec, + pub(crate) gateway_results: Vec, pub(crate) route_results: Vec, } @@ -291,16 +291,10 @@ impl SummaryProducer { let performance = received as f32 / per_node_expected as f32 * 100.0; let reliability = performance.round() as u8; + let result = NodeResult::new(node.node_id, node.encoded_identity, reliability); match node.typ { - NodeType::Mixnode { mix_id } => { - let res = - MixnodeResult::new(mix_id, node.encoded_identity, node.owner, reliability); - mixnode_results.push(res) - } - NodeType::Gateway => { - let res = GatewayResult::new(node.encoded_identity, node.owner, reliability); - gateway_results.push(res) - } + NodeType::Mixnode => mixnode_results.push(result), + NodeType::Gateway => gateway_results.push(result), } } diff --git a/nym-api/src/network_monitor/test_packet.rs b/nym-api/src/network_monitor/test_packet.rs index 6e5c433ff8..1b1bfbbeda 100644 --- a/nym-api/src/network_monitor/test_packet.rs +++ b/nym-api/src/network_monitor/test_packet.rs @@ -24,7 +24,7 @@ impl NymApiTestMessageExt { pub fn mix_plaintexts( &self, - node: &mix::Node, + node: &mix::LegacyNode, test_packets: u32, ) -> Result>, NetworkTestingError> { NodeTestMessage::mix_plaintexts(node, test_packets, *self) diff --git a/nym-api/src/network_monitor/test_route/mod.rs b/nym-api/src/network_monitor/test_route/mod.rs index 477251f80d..224f751357 100644 --- a/nym-api/src/network_monitor/test_route/mod.rs +++ b/nym-api/src/network_monitor/test_route/mod.rs @@ -16,10 +16,10 @@ pub(crate) struct TestRoute { impl TestRoute { pub(crate) fn new( id: u64, - l1_mix: mix::Node, - l2_mix: mix::Node, - l3_mix: mix::Node, - gateway: gateway::Node, + l1_mix: mix::LegacyNode, + l2_mix: mix::LegacyNode, + l3_mix: mix::LegacyNode, + gateway: gateway::LegacyNode, ) -> Self { let layered_mixes = [ (1u8, vec![l1_mix]), @@ -39,19 +39,19 @@ impl TestRoute { self.id } - pub(crate) fn gateway(&self) -> &gateway::Node { + pub(crate) fn gateway(&self) -> &gateway::LegacyNode { &self.nodes.gateways()[0] } - pub(crate) fn layer_one_mix(&self) -> &mix::Node { + pub(crate) fn layer_one_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&1).unwrap()[0] } - pub(crate) fn layer_two_mix(&self) -> &mix::Node { + pub(crate) fn layer_two_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&2).unwrap()[0] } - pub(crate) fn layer_three_mix(&self) -> &mix::Node { + pub(crate) fn layer_three_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&3).unwrap()[0] } diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index f56646e6fc..2b9b8d0564 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -1,26 +1,29 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_describe_cache::query_helpers::query_for_described_data; use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; use crate::support::config; use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; +use async_trait::async_trait; use futures::{stream, StreamExt}; -use nym_api_requests::models::{ - AuthenticatorDetails, IpPacketRouterDetails, NetworkRequesterDetails, NymNodeDescription, - WireguardDetails, -}; -use nym_api_requests::nym_nodes::NodeRole; -use nym_config::defaults::{mainnet, DEFAULT_NYM_NODE_HTTP_PORT}; -use nym_contracts_common::IdentityKey; +use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_crypto::asymmetric::{ed25519, encryption, identity, x25519}; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use nym_topology::gateway::GatewayConversionError; +use nym_topology::mix::MixnodeConversionError; +use nym_topology::{gateway, mix, NetworkAddress}; use std::collections::HashMap; +use std::net::SocketAddr; +use std::time::Duration; use thiserror::Error; -use time::OffsetDateTime; +use tracing::{debug, error}; -// type alias for ease of use -pub type DescribedNodes = HashMap; +mod query_helpers; #[derive(Debug, Error)] pub enum NodeDescribeCacheError { @@ -30,30 +33,155 @@ pub enum NodeDescribeCacheError { source: UninitialisedCache, }, - #[error("gateway {gateway} has provided malformed host information ({host}: {source}")] + #[error("node {node_id} has provided malformed host information ({host}: {source}")] MalformedHost { host: String, - gateway: IdentityKey, + node_id: NodeId, #[source] source: NymNodeApiClientError, }, - #[error("gateway '{gateway}' with host '{host}' doesn't seem to expose any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] - NoHttpPortsAvailable { host: String, gateway: IdentityKey }, + #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] + NoHttpPortsAvailable { host: String, node_id: NodeId }, - #[error("failed to query gateway '{gateway}': {source}")] + #[error("failed to query node {node_id}: {source}")] ApiFailure { - gateway: IdentityKey, + node_id: NodeId, #[source] source: NymNodeApiClientError, }, // TODO: perhaps include more details here like whether key/signature/payload was malformed - #[error("could not verify signed host information for gateway '{gateway}'")] - MissignedHostInformation { gateway: IdentityKey }, + #[error("could not verify signed host information for node {node_id}")] + MissignedHostInformation { node_id: NodeId }, +} + +// this exists because I've been moving things around quite a lot and now the place that holds the type +// doesn't have relevant dependencies for proper impl +pub(crate) trait NodeDescriptionTopologyExt { + fn try_to_topology_mix_node( + &self, + layer: LegacyMixLayer, + ) -> Result; + + fn try_to_topology_gateway(&self) -> Result; +} + +impl NodeDescriptionTopologyExt for NymNodeDescription { + // TODO: this might have to be moved around + fn try_to_topology_mix_node( + &self, + layer: LegacyMixLayer, + ) -> Result { + let keys = &self.description.host_information.keys; + let ips = &self.description.host_information.ip_address; + if ips.is_empty() { + return Err(MixnodeConversionError::NoIpAddressesProvided { + mixnode: keys.ed25519.clone(), + }); + } + + let host = match &self.description.host_information.hostname { + None => NetworkAddress::IpAddr(ips[0]), + Some(hostname) => NetworkAddress::Hostname(hostname.clone()), + }; + + // get ip from the self-reported values so we wouldn't need to do any hostname resolution + // (which doesn't really work in wasm) + let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); + + Ok(mix::LegacyNode { + mix_id: self.node_id, + host, + mix_host, + identity_key: ed25519::PublicKey::from_base58_string(&keys.ed25519)?, + sphinx_key: x25519::PublicKey::from_base58_string(&keys.x25519)?, + layer, + version: self + .description + .build_information + .build_version + .as_str() + .into(), + }) + } + + fn try_to_topology_gateway(&self) -> Result { + let keys = &self.description.host_information.keys; + + let ips = &self.description.host_information.ip_address; + if ips.is_empty() { + return Err(GatewayConversionError::NoIpAddressesProvided { + gateway: keys.ed25519.clone(), + }); + } + + let host = match &self.description.host_information.hostname { + None => NetworkAddress::IpAddr(ips[0]), + Some(hostname) => NetworkAddress::Hostname(hostname.clone()), + }; + + // get ip from the self-reported values so we wouldn't need to do any hostname resolution + // (which doesn't really work in wasm) + let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); + + Ok(gateway::LegacyNode { + node_id: self.node_id, + host, + mix_host, + clients_ws_port: self.description.mixnet_websockets.ws_port, + clients_wss_port: self.description.mixnet_websockets.wss_port, + identity_key: identity::PublicKey::from_base58_string( + &self.description.host_information.keys.ed25519, + )?, + sphinx_key: encryption::PublicKey::from_base58_string( + &self.description.host_information.keys.x25519, + )?, + version: self + .description + .build_information + .build_version + .as_str() + .into(), + }) + } +} + +pub struct DescribedNodes { + nodes: HashMap, +} + +impl DescribedNodes { + pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> { + self.nodes.get(node_id).map(|n| &n.description) + } + + pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> { + self.nodes.get(node_id) + } + + pub fn all_nodes<'a>(&'a self) -> impl Iterator + 'a { + self.nodes.values() + } + + pub fn mixing_nym_nodes<'a>(&'a self) -> impl Iterator + 'a { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.can_operate_mixnode()) + } + + pub fn gateway_capable_nym_nodes<'a>( + &'a self, + ) -> impl Iterator + 'a { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.can_operate_entry_gateway()) + } } pub struct NodeDescriptionProvider { @@ -79,35 +207,42 @@ impl NodeDescriptionProvider { async fn try_get_client( host: &str, - identity_key: &IdentityKey, - port: Option, + node_id: NodeId, + custom_port: Option, ) -> Result { // first try the standard port in case the operator didn't put the node behind the proxy, // then default https (443) // finally default http (80) let mut addresses_to_try = vec![ - format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), - format!("http://{host}:8000"), - format!("https://{host}"), - format!("http://{host}"), + format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node + format!("https://{host}"), // node behind https proxy (443) + format!("http://{host}"), // node behind http proxy (80) ]; - if let Some(port) = port { + // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via + // the 'custom_port' since it should have been present in the contract. + + if let Some(port) = custom_port { addresses_to_try.insert(0, format!("http://{host}:{port}")); } for address in addresses_to_try { // if provided host was malformed, no point in continuing - let client = match nym_node_requests::api::Client::new_url(address, None) { + let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { + b.with_timeout(Duration::from_secs(5)) + .with_user_agent("nym-api-describe-cache") + .build() + }) { Ok(client) => client, Err(err) => { return Err(NodeDescribeCacheError::MalformedHost { host: host.to_string(), - gateway: identity_key.clone(), + node_id, source: err, }); } }; + if let Ok(health) = client.get_health().await { if health.status.is_up() { return Ok(client); @@ -117,149 +252,75 @@ async fn try_get_client( Err(NodeDescribeCacheError::NoHttpPortsAvailable { host: host.to_string(), - gateway: identity_key.to_string(), + node_id, }) } async fn try_get_description( data: RefreshData, -) -> Result<(IdentityKey, NymNodeDescription), NodeDescribeCacheError> { - let client = try_get_client(&data.host(), &data.identity_key(), data.port()).await?; +) -> Result { + let client = try_get_client(&data.host, data.node_id, data.port).await?; - let host_info = - client - .get_host_information() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().to_string(), - source: err, - })?; + let map_query_err = |err| NodeDescribeCacheError::ApiFailure { + node_id: data.node_id, + source: err, + }; + + let host_info = client.get_host_information().await.map_err(map_query_err)?; if !host_info.verify_host_information() { return Err(NodeDescribeCacheError::MissignedHostInformation { - gateway: data.identity_key().clone(), + node_id: data.node_id, }); } - let build_info = - client - .get_build_information() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - })?; + let node_info = query_for_described_data(&client, data.node_id).await?; + let description = node_info.into_node_description(host_info.data); - // this can be an old node that hasn't yet exposed this - let auxiliary_details = client.get_auxiliary_details().await.inspect_err(|err| { - debug!("could not obtain auxiliary details of node {}: {err} is it running an old version?", data.identity_key()); - }).unwrap_or_default(); - - let websockets = - client - .get_mixnet_websockets() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - })?; - - let network_requester = - if let Ok(nr) = client.get_network_requester().await { - let exit_policy = client.get_exit_policy().await.map_err(|err| { - NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - } - })?; - let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL; - - Some(NetworkRequesterDetails { - address: nr.address, - uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy, - }) - } else { - None - }; - - let ip_packet_router = if let Ok(ipr) = client.get_ip_packet_router().await { - Some(IpPacketRouterDetails { - address: ipr.address, - }) - } else { - None - }; - - let authenticator = if let Ok(auth) = client.get_authenticator().await { - Some(AuthenticatorDetails { - address: auth.address, - }) - } else { - None - }; - - let wireguard = if let Ok(wg) = client.get_wireguard().await { - Some(WireguardDetails { - port: wg.port, - public_key: wg.public_key, - }) - } else { - None - }; - - let description = NymNodeDescription { - host_information: host_info.data.into(), - last_polled: OffsetDateTime::now_utc().into(), - build_information: build_info, - network_requester, - ip_packet_router, - authenticator, - wireguard, - mixnet_websockets: websockets.into(), - auxiliary_details, - role: data.role(), - }; - - Ok((data.identity_key().clone(), description)) + Ok(NymNodeDescription { + node_id: data.node_id, + contract_node_type: data.node_type, + description, + }) } struct RefreshData { host: String, - identity_key: IdentityKey, - role: NodeRole, + node_id: NodeId, + node_type: DescribedNodeType, + port: Option, } impl RefreshData { - pub fn new(host: String, identity_key: IdentityKey, role: NodeRole, port: Option) -> Self { + pub fn new( + host: impl Into, + node_type: DescribedNodeType, + node_id: NodeId, + port: Option, + ) -> Self { RefreshData { - host, - identity_key, - role, + host: host.into(), + node_id, + node_type, port, } } - pub fn host(&self) -> String { - self.host.clone() - } - - pub fn identity_key(&self) -> IdentityKey { - self.identity_key.clone() - } - - pub fn port(&self) -> Option { - self.port - } - - pub fn role(&self) -> NodeRole { - self.role.clone() + async fn try_refresh(self) -> Option { + match try_get_description(self).await { + Ok(description) => Some(description), + Err(err) => { + debug!("failed to obtain node self-described data: {err}"); + None + } + } } } #[async_trait] impl CacheItemProvider for NodeDescriptionProvider { - type Item = HashMap; + type Item = DescribedNodes; type Error = NodeDescribeCacheError; async fn wait_until_ready(&self) { @@ -267,58 +328,62 @@ impl CacheItemProvider for NodeDescriptionProvider { } async fn try_refresh(&self) -> Result { - let mut host_id_pairs = self - .contract_cache - .gateways_all() - .await - .into_iter() - .map(|full| { - RefreshData::new( - full.gateway.host, - full.gateway.identity_key, - NodeRole::EntryGateway, - None, - ) - }) - .collect::>(); + // we need to query: + // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) + // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) + // - nym-nodes - host_id_pairs.extend( - self.contract_cache - .mixnodes_all() - .await - .into_iter() - .map(|full| { - RefreshData::new( - full.bond_information.mix_node.host, - full.bond_information.mix_node.identity_key, - NodeRole::Mixnode { - layer: full.bond_information.layer.into(), - }, - Some(full.bond_information.mix_node.mix_port), - ) - }) - .collect::>(), - ); + let mut nodes_to_query = Vec::new(); - if host_id_pairs.is_empty() { - return Ok(HashMap::new()); + match self.contract_cache.all_cached_legacy_mixnodes().await { + None => error!("failed to obtain mixnodes information from the cache"), + Some(legacy_mixnodes) => { + for node in &**legacy_mixnodes { + nodes_to_query.push(RefreshData::new( + &node.bond_information.mix_node.host, + DescribedNodeType::LegacyMixnode, + node.mix_id(), + Some(node.bond_information.mix_node.http_api_port), + )) + } + } } - let node_description = stream::iter(host_id_pairs.into_iter().map(try_get_description)) - .buffer_unordered(self.batch_size) - .filter_map(|res| async move { - match res { - Ok((identity, description)) => Some((identity, description)), - Err(err) => { - debug!("failed to obtain gateway self-described data: {err}"); - None - } + match self.contract_cache.all_cached_legacy_gateways().await { + None => error!("failed to obtain gateways information from the cache"), + Some(legacy_gateways) => { + for node in &**legacy_gateways { + nodes_to_query.push(RefreshData::new( + &node.bond.gateway.host, + DescribedNodeType::LegacyGateway, + node.node_id, + None, + )) } - }) + } + } + + match self.contract_cache.all_cached_nym_nodes().await { + None => error!("failed to obtain nym-nodes information from the cache"), + Some(nym_nodes) => { + for node in &**nym_nodes { + nodes_to_query.push(RefreshData::new( + &node.bond_information.node.host, + DescribedNodeType::NymNode, + node.node_id(), + node.bond_information.node.custom_http_port, + )) + } + } + } + + let nodes = stream::iter(nodes_to_query.into_iter().map(|n| n.try_refresh())) + .buffer_unordered(self.batch_size) + .filter_map(|x| async move { x.map(|d| (d.node_id, d)) }) .collect::>() .await; - Ok(node_description) + Ok(DescribedNodes { nodes }) } } @@ -327,8 +392,6 @@ impl CacheItemProvider for NodeDescriptionProvider { pub(crate) fn new_refresher( config: &config::TopologyCacher, contract_cache: NymContractCache, - // hehe. we can't do that yet - // network_gateways: SharedCache>, ) -> CacheRefresher { CacheRefresher::new( Box::new( @@ -342,8 +405,6 @@ pub(crate) fn new_refresher( pub(crate) fn new_refresher_with_initial_value( config: &config::TopologyCacher, contract_cache: NymContractCache, - // hehe. we can't do that yet - // network_gateways: SharedCache>, initial: SharedCache, ) -> CacheRefresher { CacheRefresher::new_with_initial_value( diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs new file mode 100644 index 0000000000..cdb9e04d4e --- /dev/null +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -0,0 +1,242 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node_describe_cache::NodeDescribeCacheError; +use futures::future::{maybe_done, MaybeDone}; +use futures::{FutureExt, TryFutureExt}; +use nym_api_requests::models::{ + AuthenticatorDetails, HostInformation, IpPacketRouterDetails, NetworkRequesterDetails, + NymNodeData, WebSockets, WireguardDetails, +}; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_config::defaults::mainnet; +use nym_mixnet_contract_common::NodeId; +use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; +use nym_node_requests::api::Client; +use pin_project::pin_project; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use time::OffsetDateTime; +use tracing::debug; + +async fn network_requester_future( + client: &Client, +) -> Result, NymNodeApiClientError> { + let Ok(nr) = client.get_network_requester().await else { + return Ok(None); + }; + + client.get_exit_policy().await.map(|exit_policy| { + let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL; + Some(NetworkRequesterDetails { + address: nr.address, + uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy, + }) + }) +} + +pub(crate) async fn query_for_described_data( + client: &Client, + node_id: NodeId, +) -> Result { + let map_query_err = |source| NodeDescribeCacheError::ApiFailure { node_id, source }; + + // all of those should be happening concurrently. + NodeDescribedInfoMegaFuture::new( + client.get_build_information().map_err(map_query_err), + client.get_roles().map_err(map_query_err), + client.get_auxiliary_details() + .inspect_err(|err| { + // old nym-nodes will not have this field, so use the default instead + debug!("could not obtain auxiliary details of node {node_id}: {err} is it running an old version?") + }) + .unwrap_or_else(|_| AuxiliaryDetails::default()), + client.get_mixnet_websockets().ok_into().map_err(map_query_err), + network_requester_future(client).map_err(map_query_err), + // `ok_into` ultimately calls `IpPacketRouter::into` to transform it into `IpPacketRouterDetails` + client.get_ip_packet_router().ok_into().map(Result::ok), + client.get_authenticator().ok_into().map(Result::ok), + client.get_wireguard().ok_into().map(Result::ok) + ) + .await +} + +// just a helper to have named fields as opposed to a mega tuple +// could I have used something more sophisticated? sure. +// is this code disgusting? yes. does it work? also yes +// (note: I've just mostly copied code from `futures-util::generate` macro where +// they derive code for `join2`, `join3`, etc.) +#[pin_project] +struct NodeDescribedInfoMegaFuture +where + F1: Future, + F2: Future, + F3: Future, + F4: Future, + F5: Future, + F6: Future, + F7: Future, + F8: Future, +{ + #[pin] + build_info: MaybeDone, + #[pin] + roles: MaybeDone, + #[pin] + auxiliary_details: MaybeDone, + #[pin] + websockets: MaybeDone, + #[pin] + network_requester: MaybeDone, + #[pin] + ipr: MaybeDone, + #[pin] + authenticator: MaybeDone, + #[pin] + wireguard: MaybeDone, +} + +impl Future + for NodeDescribedInfoMegaFuture +where + F1: Future>, + F2: Future>, + F3: Future, + F4: Future>, + F5: Future, NodeDescribeCacheError>>, + F6: Future>, + F7: Future>, + F8: Future>, +{ + type Output = Result; + + // SAFETY: we've explicitly checked all futures have completed thus the unwraps are fine + #[allow(clippy::unwrap_used)] + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut all_done = true; + let mut futures = self.project(); + + all_done &= futures.build_info.as_mut().poll(cx).is_ready(); + all_done &= futures.roles.as_mut().poll(cx).is_ready(); + all_done &= futures.auxiliary_details.as_mut().poll(cx).is_ready(); + all_done &= futures.websockets.as_mut().poll(cx).is_ready(); + all_done &= futures.network_requester.as_mut().poll(cx).is_ready(); + all_done &= futures.ipr.as_mut().poll(cx).is_ready(); + all_done &= futures.authenticator.as_mut().poll(cx).is_ready(); + all_done &= futures.wireguard.as_mut().poll(cx).is_ready(); + + if all_done { + Poll::Ready( + ResolvedNodeDescribedInfo { + build_info: futures.build_info.take_output().unwrap(), + roles: futures.roles.take_output().unwrap(), + auxiliary_details: futures.auxiliary_details.take_output().unwrap(), + websockets: futures.websockets.take_output().unwrap(), + network_requester: futures.network_requester.take_output().unwrap(), + ipr: futures.ipr.take_output().unwrap(), + authenticator: futures.authenticator.take_output().unwrap(), + wireguard: futures.wireguard.take_output().unwrap(), + } + .try_unwrap(), + ) + } else { + Poll::Pending + } + } +} + +impl NodeDescribedInfoMegaFuture +where + F1: Future, + F2: Future, + F3: Future, + F4: Future, + F5: Future, + F6: Future, + F7: Future, + F8: Future, +{ + // okay. the fact I have to bypass clippy here means it wasn't a good idea to create this abomination after all + #[allow(clippy::too_many_arguments)] + fn new( + build_info: F1, + roles: F2, + auxiliary_details: F3, + websockets: F4, + network_requester: F5, + ipr: F6, + authenticator: F7, + wireguard: F8, + ) -> Self { + NodeDescribedInfoMegaFuture { + build_info: maybe_done(build_info), + roles: maybe_done(roles), + auxiliary_details: maybe_done(auxiliary_details), + websockets: maybe_done(websockets), + network_requester: maybe_done(network_requester), + ipr: maybe_done(ipr), + authenticator: maybe_done(authenticator), + wireguard: maybe_done(wireguard), + } + } +} + +struct ResolvedNodeDescribedInfo { + build_info: Result, + roles: Result, + // TODO: in the future make it return a Result as well. + auxiliary_details: AuxiliaryDetails, + websockets: Result, + network_requester: Result, NodeDescribeCacheError>, + ipr: Option, + authenticator: Option, + wireguard: Option, +} + +impl ResolvedNodeDescribedInfo { + fn try_unwrap(self) -> Result { + Ok(UnwrappedResolvedNodeDescribedInfo { + build_info: self.build_info?, + roles: self.roles?, + auxiliary_details: self.auxiliary_details, + websockets: self.websockets?, + network_requester: self.network_requester?, + ipr: self.ipr, + authenticator: self.authenticator, + wireguard: self.wireguard, + }) + } +} + +pub(crate) struct UnwrappedResolvedNodeDescribedInfo { + pub(crate) build_info: BinaryBuildInformationOwned, + pub(crate) roles: NodeRoles, + pub(crate) auxiliary_details: AuxiliaryDetails, + pub(crate) websockets: WebSockets, + pub(crate) network_requester: Option, + pub(crate) ipr: Option, + pub(crate) authenticator: Option, + pub(crate) wireguard: Option, +} + +impl UnwrappedResolvedNodeDescribedInfo { + pub(crate) fn into_node_description( + self, + host_info: impl Into, + ) -> NymNodeData { + NymNodeData { + host_information: host_info.into(), + last_polled: OffsetDateTime::now_utc().into(), + build_information: self.build_info, + network_requester: self.network_requester, + ip_packet_router: self.ipr, + authenticator: self.authenticator, + wireguard: self.wireguard, + mixnet_websockets: self.websockets, + auxiliary_details: self.auxiliary_details, + declared_role: self.roles, + } + } +} diff --git a/nym-api/src/node_status_api/cache/data.rs b/nym-api/src/node_status_api/cache/data.rs index b2e6512417..d8b2cda51e 100644 --- a/nym-api/src/node_status_api/cache/data.rs +++ b/nym-api/src/node_status_api/cache/data.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated}; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; use nym_contracts_common::IdentityKey; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; use crate::support::caching::Cache; @@ -12,11 +12,14 @@ use super::inclusion_probabilities::InclusionProbabilities; #[derive(Default)] pub(crate) struct NodeStatusCacheData { - pub(crate) mixnodes_annotated: Cache>, - pub(crate) rewarded_set_annotated: Cache>, - pub(crate) active_set_annotated: Cache>, + pub(crate) legacy_gateway_mapping: Cache>, - pub(crate) gateways_annotated: Cache>, + /// Basic annotation for **all** nodes, i.e. legacy + nym-nodes + pub(crate) node_annotations: Cache>, + + /// Annotations as before, just for legacy things + pub(crate) mixnodes_annotated: Cache>, + pub(crate) gateways_annotated: Cache>, // Estimated active set inclusion probabilities from Monte Carlo simulation pub(crate) inclusion_probabilities: Cache, diff --git a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs index 4d53793d36..28bde0e1e2 100644 --- a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs +++ b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs @@ -1,12 +1,13 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::InclusionProbability; use nym_contracts_common::truncate_decimal; -use nym_mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams}; +use nym_mixnet_contract_common::{NodeId, RewardingParams}; use serde::Serialize; use std::time::Duration; -use tap::TapFallible; +use tracing::error; const MAX_SIMULATION_SAMPLES: u64 = 5000; const MAX_SIMULATION_TIME_SEC: u64 = 15; @@ -22,13 +23,13 @@ pub(crate) struct InclusionProbabilities { impl InclusionProbabilities { pub(crate) fn compute( - mixnodes: &[MixNodeDetails], + mixnodes: &[LegacyMixNodeDetailsWithLayer], params: RewardingParams, ) -> Option { compute_inclusion_probabilities(mixnodes, params) } - pub(crate) fn node(&self, mix_id: MixId) -> Option<&InclusionProbability> { + pub(crate) fn node(&self, mix_id: NodeId) -> Option<&InclusionProbability> { self.inclusion_probabilities .iter() .find(|x| x.mix_id == mix_id) @@ -36,11 +37,11 @@ impl InclusionProbabilities { } fn compute_inclusion_probabilities( - mixnodes: &[MixNodeDetails], + mixnodes: &[LegacyMixNodeDetailsWithLayer], params: RewardingParams, ) -> Option { - let active_set_size = params.active_set_size; - let standby_set_size = params.rewarded_set_size - active_set_size; + let active_set_size = params.active_set_size(); + let standby_set_size = params.rewarded_set.standby; // Unzip list of total bonds into ids and bonds. // We need to go through this zip/unzip procedure to make sure we have matching identities @@ -57,7 +58,7 @@ fn compute_inclusion_probabilities( Duration::from_secs(MAX_SIMULATION_TIME_SEC), &mut rng, ) - .tap_err(|err| error!("{err}")) + .inspect_err(|err| error!("{err}")) .ok()?; Some(InclusionProbabilities { @@ -69,7 +70,9 @@ fn compute_inclusion_probabilities( }) } -fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec, Vec) { +fn unzip_into_mixnode_ids_and_total_bonds( + mixnodes: &[LegacyMixNodeDetailsWithLayer], +) -> (Vec, Vec) { mixnodes .iter() .map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128())) @@ -77,7 +80,7 @@ fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec Vec { ids.iter() diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 99d4358802..262755ef3b 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -4,15 +4,15 @@ use self::data::NodeStatusCacheData; use self::inclusion_probabilities::InclusionProbabilities; use crate::support::caching::Cache; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, MixnodeStatus}; -use nym_contracts_common::{IdentityKey, IdentityKeyRef}; -use nym_mixnet_contract_common::MixId; -use rocket::fairing::AdHoc; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_contracts_common::IdentityKey; +use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; use std::{sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::RwLockReadGuard; use tokio::{sync::RwLock, time}; +use tracing::error; const CACHE_TIMEOUT_MS: u64 = 100; @@ -47,27 +47,22 @@ impl NodeStatusCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub fn stage() -> AdHoc { - AdHoc::on_ignite("Node Status Cache", |rocket| async { - rocket.manage(Self::new()) - }) - } - /// Updates the cache with the latest data. async fn update( &self, - mixnodes: HashMap, - rewarded_set: Vec, - active_set: Vec, - gateways: HashMap, + legacy_gateway_mapping: HashMap, + node_annotations: HashMap, + mixnodes: HashMap, + gateways: HashMap, inclusion_probabilities: InclusionProbabilities, ) { match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.write()).await { Ok(mut cache) => { cache.mixnodes_annotated.unchecked_update(mixnodes); - cache.rewarded_set_annotated.unchecked_update(rewarded_set); - cache.active_set_annotated.unchecked_update(active_set); + cache + .legacy_gateway_mapping + .unchecked_update(legacy_gateway_mapping); + cache.node_annotations.unchecked_update(node_annotations); cache.gateways_annotated.unchecked_update(gateways); cache .inclusion_probabilities @@ -104,10 +99,25 @@ impl NodeStatusCache { } } - pub(crate) async fn active_mixnodes_cache( + pub(crate) async fn node_annotations( &self, - ) -> Option>>> { - self.get(|c| &c.active_set_annotated).await + ) -> Option>>> { + self.get(|c| &c.node_annotations).await + } + + pub(crate) async fn map_identity_to_node_id(&self, identity: &str) -> Option { + self.inner + .read() + .await + .legacy_gateway_mapping + .get(identity) + .copied() + } + + pub(crate) async fn annotated_legacy_mixnodes( + &self, + ) -> Option>>> { + self.get(|c| &c.mixnodes_annotated).await } pub(crate) async fn mixnodes_annotated_full(&self) -> Option> { @@ -122,24 +132,14 @@ impl NodeStatusCache { Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } - pub(crate) async fn mixnode_annotated(&self, mix_id: MixId) -> Option { + pub(crate) async fn mixnode_annotated(&self, mix_id: NodeId) -> Option { let mixnodes = self.get(|c| &c.mixnodes_annotated).await?; mixnodes.get(&mix_id).cloned() } - pub(crate) async fn rewarded_set_annotated(&self) -> Option>> { - self.get_owned(|c| c.rewarded_set_annotated.clone_cache()) - .await - } - - pub(crate) async fn active_set_annotated(&self) -> Option>> { - self.get_owned(|c| c.active_set_annotated.clone_cache()) - .await - } - - pub(crate) async fn gateways_cache( + pub(crate) async fn annotated_legacy_gateways( &self, - ) -> Option>>> { + ) -> Option>>> { self.get(|c| &c.gateways_annotated).await } @@ -155,41 +155,13 @@ impl NodeStatusCache { Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } - pub(crate) async fn gateway_annotated( - &self, - gateway_id: IdentityKeyRef<'_>, - ) -> Option { + pub(crate) async fn gateway_annotated(&self, node_id: NodeId) -> Option { let gateways = self.get(|c| &c.gateways_annotated).await?; - gateways.get(gateway_id).cloned() + gateways.get(&node_id).cloned() } pub(crate) async fn inclusion_probabilities(&self) -> Option> { self.get_owned(|c| c.inclusion_probabilities.clone_cache()) .await } - - pub async fn mixnode_details( - &self, - mix_id: MixId, - ) -> (Option, MixnodeStatus) { - // it might not be the most optimal to possibly iterate the entire vector to find (or not) - // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) - - let active_set = &self.active_set_annotated().await.unwrap().into_inner(); - if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { - return (Some(bond.clone()), MixnodeStatus::Active); - } - - let rewarded_set = &self.rewarded_set_annotated().await.unwrap().into_inner(); - if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { - return (Some(bond.clone()), MixnodeStatus::Standby); - } - - let all_bonded = &self.mixnodes_annotated_filtered().await.unwrap(); - if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { - (Some(bond.clone()), MixnodeStatus::Inactive) - } else { - (None, MixnodeStatus::NotFound) - } - } } diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index d20f3329b4..b60f4d34a6 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -1,59 +1,23 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::helpers::RewardedSetStatus; use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; use crate::support::storage::NymApiStorage; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodePerformance}; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::{reward_params::Performance, Interval, MixId}; -use nym_mixnet_contract_common::{ - GatewayBond, IdentityKey, MixNodeDetails, RewardedSetNodeStatus, RewardingParams, +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ + GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, NodePerformance, }; +use nym_mixnet_contract_common::{reward_params::Performance, Interval, NodeId, RewardedSet}; +use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; use nym_topology::NetworkAddress; use std::collections::{HashMap, HashSet}; use std::net::ToSocketAddrs; use std::str::FromStr; -pub(super) fn to_rewarded_set_node_status( - rewarded_set: &[MixNodeDetails], - active_set: &[MixNodeDetails], -) -> HashMap { - let mut rewarded_set_node_status: HashMap = rewarded_set - .iter() - .map(|m| (m.mix_id(), RewardedSetNodeStatus::Standby)) - .collect(); - for mixnode in active_set { - *rewarded_set_node_status - .get_mut(&mixnode.mix_id()) - .expect("All active nodes are rewarded nodes") = RewardedSetNodeStatus::Active; - } - rewarded_set_node_status -} - -pub(super) fn split_into_active_and_rewarded_set( - mixnodes_annotated: &HashMap, - rewarded_set_node_status: &HashMap, -) -> (Vec, Vec) { - let rewarded_set: Vec<_> = mixnodes_annotated - .values() - .filter(|mixnode| rewarded_set_node_status.get(&mixnode.mix_id()).is_some()) - .cloned() - .collect(); - let active_set: Vec<_> = rewarded_set - .iter() - .filter(|mixnode| { - rewarded_set_node_status - .get(&mixnode.mix_id()) - .map_or(false, RewardedSetNodeStatus::is_active) - }) - .cloned() - .collect(); - (rewarded_set, active_set) -} - pub(super) async fn get_mixnode_performance_from_storage( storage: &NymApiStorage, - mix_id: MixId, + mix_id: NodeId, epoch: Interval, ) -> Option { storage @@ -68,12 +32,12 @@ pub(super) async fn get_mixnode_performance_from_storage( pub(super) async fn get_gateway_performance_from_storage( storage: &NymApiStorage, - gateway_id: &str, + node_id: NodeId, epoch: Interval, ) -> Option { storage .get_average_gateway_uptime_in_the_last_24hrs( - gateway_id, + node_id, epoch.current_epoch_end_unix_timestamp(), ) .await @@ -81,19 +45,25 @@ pub(super) async fn get_gateway_performance_from_storage( .map(Into::into) } -pub(super) async fn annotate_nodes_with_details( +// TODO: this might have to be moved to a different file if other places also rely on this functionality +fn get_rewarded_set_status(rewarded_set: &RewardedSet, node_id: NodeId) -> RewardedSetStatus { + if rewarded_set.is_standby(&node_id) { + RewardedSetStatus::Standby + } else if rewarded_set.is_active_mixnode(&node_id) { + RewardedSetStatus::Active + } else { + RewardedSetStatus::Inactive + } +} + +pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( storage: &NymApiStorage, - mixnodes: Vec, + mixnodes: Vec, interval_reward_params: RewardingParams, current_interval: Interval, - rewarded_set: &HashMap, - mix_to_family: Vec<(IdentityKey, FamilyHead)>, - blacklist: &HashSet, -) -> HashMap { - let mix_to_family = mix_to_family - .into_iter() - .collect::>(); - + rewarded_set: &RewardedSet, + blacklist: &HashSet, +) -> HashMap { let mut annotated = HashMap::new(); for mixnode in mixnodes { let stake_saturation = mixnode @@ -104,7 +74,7 @@ pub(super) async fn annotate_nodes_with_details( .rewarding_details .uncapped_bond_saturation(&interval_reward_params); - let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).copied(); + let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id()); // If the performance can't be obtained, because the nym-api was not started with // the monitoring (and hence, storage), then reward estimates will be all zero @@ -147,10 +117,6 @@ pub(super) async fn annotate_nodes_with_details( let (estimated_operator_apy, estimated_delegators_apy) = compute_apy_from_reward(&mixnode, reward_estimate, current_interval); - let family = mix_to_family - .get(mixnode.bond_information.identity()) - .cloned(); - annotated.insert( mixnode.mix_id(), MixNodeBondAnnotated { @@ -162,7 +128,6 @@ pub(super) async fn annotate_nodes_with_details( node_performance, estimated_operator_apy, estimated_delegators_apy, - family, ip_addresses, }, ); @@ -170,35 +135,33 @@ pub(super) async fn annotate_nodes_with_details( annotated } -pub(crate) async fn annotate_gateways_with_details( +pub(crate) async fn annotate_legacy_gateways_with_details( storage: &NymApiStorage, - gateway_bonds: Vec, + gateway_bonds: Vec, current_interval: Interval, - blacklist: &HashSet, -) -> HashMap { + blacklist: &HashSet, +) -> HashMap { let mut annotated = HashMap::new(); for gateway_bond in gateway_bonds { - let performance = get_gateway_performance_from_storage( - storage, - gateway_bond.identity(), - current_interval, - ) - .await - .unwrap_or_default(); + let performance = + get_gateway_performance_from_storage(storage, gateway_bond.node_id, current_interval) + .await + .unwrap_or_default(); let node_performance = storage - .construct_gateway_report(gateway_bond.identity()) + .construct_gateway_report(gateway_bond.node_id) .await .map(NodePerformance::from) .ok() .unwrap_or_default(); // safety: this conversion is infallible - let ip_addresses = match NetworkAddress::from_str(&gateway_bond.gateway.host).unwrap() { + let ip_addresses = match NetworkAddress::from_str(&gateway_bond.bond.gateway.host).unwrap() + { NetworkAddress::IpAddr(ip) => vec![ip], NetworkAddress::Hostname(hostname) => { // try to resolve it - (hostname.as_str(), gateway_bond.gateway.mix_port) + (hostname.as_str(), gateway_bond.bond.gateway.mix_port) .to_socket_addrs() .map(|iter| iter.map(|s| s.ip()).collect::>()) .unwrap_or_default() @@ -206,9 +169,9 @@ pub(crate) async fn annotate_gateways_with_details( }; annotated.insert( - gateway_bond.identity().to_string(), + gateway_bond.node_id, GatewayBondAnnotated { - blacklisted: blacklist.contains(&gateway_bond.gateway.identity_key), + blacklisted: blacklist.contains(&gateway_bond.node_id), gateway_bond, self_described: None, performance, @@ -219,3 +182,72 @@ pub(crate) async fn annotate_gateways_with_details( } annotated } + +pub(crate) async fn produce_node_annotations( + storage: &NymApiStorage, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + current_interval: Interval, +) -> HashMap { + let mut annotations = HashMap::new(); + + for legacy_mix in legacy_mixnodes { + let perf = storage + .get_average_mixnode_uptime_in_the_last_24hrs( + legacy_mix.mix_id(), + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + legacy_mix.mix_id(), + NodeAnnotation { + last_24h_performance: perf, + }, + ); + } + + for legacy_gateway in legacy_gateways { + let perf = storage + .get_average_gateway_uptime_in_the_last_24hrs( + legacy_gateway.node_id, + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + legacy_gateway.node_id, + NodeAnnotation { + last_24h_performance: perf, + }, + ); + } + + for nym_node in nym_nodes { + let perf = storage + .get_average_node_uptime_in_the_last_24hrs( + nym_node.node_id(), + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + nym_node.node_id(), + NodeAnnotation { + last_24h_performance: perf, + }, + ); + } + + annotations +} diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index ad07919e62..aa664885a9 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use super::NodeStatusCache; +use crate::node_status_api::cache::node_sets::produce_node_annotations; use crate::{ node_status_api::cache::{ inclusion_probabilities::InclusionProbabilities, node_sets::{ - annotate_gateways_with_details, annotate_nodes_with_details, - split_into_active_and_rewarded_set, to_rewarded_set_node_status, + annotate_legacy_gateways_with_details, annotate_legacy_mixnodes_nodes_with_details, }, NodeStatusCacheError, }, @@ -16,9 +16,11 @@ use crate::{ support::caching::CacheNotification, }; use nym_task::TaskClient; +use std::collections::HashMap; use std::time::Duration; use tokio::sync::watch; use tokio::time; +use tracing::{debug, error, info, trace}; // Long running task responsible for keeping the node status cache up-to-date. pub struct NodeStatusCacheRefresher { @@ -56,14 +58,14 @@ impl NodeStatusCacheRefresher { tokio::select! { biased; _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } // Update node status cache when the contract cache / validator cache is updated Ok(_) = self.contract_cache_listener.changed() => { tokio::select! { _ = self.update_on_notify(&mut fallback_interval) => (), _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } } } @@ -73,18 +75,18 @@ impl NodeStatusCacheRefresher { tokio::select! { _ = self.update_on_timer() => (), _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } } } } } - log::info!("NodeStatusCacheRefresher: Exiting"); + info!("NodeStatusCacheRefresher: Exiting"); } /// Updates the node status cache when the contract cache / validator cache is updated async fn update_on_notify(&self, fallback_interval: &mut time::Interval) { - log::debug!( + debug!( "Validator cache event detected: {:?}", &*self.contract_cache_listener.borrow(), ); @@ -94,31 +96,28 @@ impl NodeStatusCacheRefresher { /// Updates the node status cache when the fallback interval is reached async fn update_on_timer(&self) { - log::debug!("Timed trigger for the node status cache"); + debug!("Timed trigger for the node status cache"); let have_contract_cache_data = *self.contract_cache_listener.borrow() != CacheNotification::Start; if have_contract_cache_data { let _ = self.refresh().await; } else { - log::trace!( - "Skipping updating node status cache, is the contract cache not yet available?" - ); + trace!("Skipping updating node status cache, is the contract cache not yet available?"); } } /// Refreshes the node status cache by fetching the latest data from the contract cache async fn refresh(&self) -> Result<(), NodeStatusCacheError> { - log::info!("Updating node status cache"); + info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.contract_cache.mixnodes_all().await; + let mixnode_details = self.contract_cache.legacy_mixnodes_all().await; let interval_reward_params = self.contract_cache.interval_reward_params().await; let current_interval = self.contract_cache.current_interval().await; - let rewarded_set = self.contract_cache.rewarded_set().await; - let active_set = self.contract_cache.active_set().await; - let mix_to_family = self.contract_cache.mix_to_family().await; - let gateway_bonds = self.contract_cache.gateways_all().await; + let rewarded_set = self.contract_cache.rewarded_set_owned().await; + let gateway_bonds = self.contract_cache.legacy_gateways_all().await; + let nym_nodes = self.contract_cache.nym_nodes().await; // get blacklists let mixnodes_blacklist = self.contract_cache.mixnodes_blacklist().await; @@ -138,24 +137,33 @@ impl NodeStatusCacheRefresher { NodeStatusCacheError::SimulationFailed })?; + let mut legacy_gateway_mapping = HashMap::new(); + for gateway in &gateway_bonds { + legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); + } + // Create annotated data - let rewarded_set_node_status = to_rewarded_set_node_status(&rewarded_set, &active_set); - let mixnodes_annotated = annotate_nodes_with_details( + + let node_annotations = produce_node_annotations( + &self.storage, + &mixnode_details, + &gateway_bonds, + &nym_nodes, + current_interval, + ) + .await; + + let mixnodes_annotated = annotate_legacy_mixnodes_nodes_with_details( &self.storage, mixnode_details, interval_reward_params, current_interval, - &rewarded_set_node_status, - mix_to_family.to_vec(), + &rewarded_set, &mixnodes_blacklist, ) .await; - // Create the annotated rewarded and active sets - let (rewarded_set, active_set) = - split_into_active_and_rewarded_set(&mixnodes_annotated, &rewarded_set_node_status); - - let gateways_annotated = annotate_gateways_with_details( + let gateways_annotated = annotate_legacy_gateways_with_details( &self.storage, gateway_bonds, current_interval, @@ -166,9 +174,9 @@ impl NodeStatusCacheRefresher { // Update the cache self.cache .update( + legacy_gateway_mapping, + node_annotations, mixnodes_annotated, - rewarded_set, - active_set, gateways_annotated, inclusion_probabilities, ) diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index 2a385b2812..f42cd59d84 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -1,9 +1,9 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Router; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use utoipa::IntoParams; @@ -11,7 +11,7 @@ pub(crate) mod network_monitor; pub(crate) mod unstable; pub(crate) mod without_monitor; -pub(crate) fn node_status_routes(network_monitor: bool) -> Router { +pub(crate) fn node_status_routes(network_monitor: bool) -> Router { // in the minimal variant we would not have access to endpoints relying on existence // of the network monitor and the associated storage let without_network_monitor = without_monitor::mandatory_routes(); @@ -28,5 +28,5 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router #[derive(Deserialize, IntoParams)] #[into_params(parameter_in = Path)] struct MixIdParam { - mix_id: MixId, + mix_id: NodeId, } diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs index ea38422c17..5f71eac079 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -4,13 +4,13 @@ use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, - _gateway_uptime_history, _get_gateway_avg_uptime, _get_gateways_detailed, - _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, _get_mixnode_reward_estimation, - _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report, - _mixnode_uptime_history, + _gateway_uptime_history, _get_gateway_avg_uptime, _get_legacy_gateways_detailed, + _get_legacy_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, + _get_mixnode_reward_estimation, _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, + _mixnode_report, _mixnode_uptime_history, }; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; use axum::Json; use axum::Router; @@ -25,7 +25,9 @@ use utoipa::IntoParams; use super::unstable; -pub(super) fn network_monitor_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(super) fn network_monitor_routes() -> Router { Router::new() .nest( "/gateway/:identity", @@ -92,9 +94,10 @@ pub(super) fn network_monitor_routes() -> Router { (status = 200, body = GatewayStatusReportResponse) ) )] +#[deprecated] async fn gateway_report( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _gateway_report(state.node_status_cache(), &identity).await?, @@ -109,12 +112,13 @@ async fn gateway_report( (status = 200, body = GatewayUptimeHistoryResponse) ) )] +#[deprecated] async fn gateway_uptime_history( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( - _gateway_uptime_history(state.storage(), &identity).await?, + _gateway_uptime_history(state.storage(), state.nym_contract_cache(), &identity).await?, )) } @@ -135,10 +139,11 @@ struct SinceQueryParams { (status = 200, body = GatewayCoreStatusResponse) ) )] +#[deprecated] async fn gateway_core_status_count( Path(identity): Path, Query(SinceQueryParams { since }): Query, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _gateway_core_status_count(state.storage(), &identity, since).await?, @@ -153,9 +158,10 @@ async fn gateway_core_status_count( (status = 200, body = GatewayUptimeResponse) ) )] +#[deprecated] async fn get_gateway_avg_uptime( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_gateway_avg_uptime(state.node_status_cache(), &identity).await?, @@ -173,9 +179,10 @@ async fn get_gateway_avg_uptime( (status = 200, body = MixnodeStatusReportResponse) ) )] +#[deprecated] async fn mixnode_report( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _mixnode_report(state.node_status_cache(), mix_id).await?, @@ -193,12 +200,13 @@ async fn mixnode_report( (status = 200, body = MixnodeUptimeHistoryResponse) ) )] +#[deprecated] async fn mixnode_uptime_history( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( - _mixnode_uptime_history(state.storage(), mix_id).await?, + _mixnode_uptime_history(state.storage(), state.nym_contract_cache(), mix_id).await?, )) } @@ -213,10 +221,11 @@ async fn mixnode_uptime_history( (status = 200, body = MixnodeCoreStatusResponse) ) )] +#[deprecated] async fn mixnode_core_status_count( Path(MixIdParam { mix_id }): Path, Query(SinceQueryParams { since }): Query, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _mixnode_core_status_count(state.storage(), mix_id, since).await?, @@ -234,9 +243,10 @@ async fn mixnode_core_status_count( (status = 200, body = RewardEstimationResponse) ) )] +#[deprecated] async fn get_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_reward_estimation( @@ -260,9 +270,10 @@ async fn get_mixnode_reward_estimation( (status = 200, body = RewardEstimationResponse) ) )] +#[deprecated] async fn compute_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, Json(user_reward_param): Json, ) -> AxumResult> { Ok(Json( @@ -287,9 +298,10 @@ async fn compute_mixnode_reward_estimation( (status = 200, body = UptimeResponse) ) )] +#[deprecated] async fn get_mixnode_avg_uptime( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?, @@ -304,8 +316,9 @@ async fn get_mixnode_avg_uptime( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_mixnodes_detailed_unfiltered( - State(state): State, + State(state): State, ) -> Json> { Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) } @@ -318,10 +331,11 @@ pub async fn get_mixnodes_detailed_unfiltered( (status = 200, body = GatewayBondAnnotated) ) )] +#[deprecated] pub async fn get_gateways_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_gateways_detailed(state.node_status_cache()).await) + Json(_get_legacy_gateways_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -332,8 +346,9 @@ pub async fn get_gateways_detailed( (status = 200, body = GatewayBondAnnotated) ) )] +#[deprecated] pub async fn get_gateways_detailed_unfiltered( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_gateways_detailed_unfiltered(state.node_status_cache()).await) + Json(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) } diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index b8a30639b4..7e30a4ca31 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -3,19 +3,20 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::helpers::PaginationRequest; +use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; -use crate::v2::AxumAppState; use axum::extract::{Path, Query, State}; use axum::Json; use nym_api_requests::models::{ GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute, }; use nym_api_requests::pagination::Pagination; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use std::cmp::min; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; +use tracing::{error, trace}; pub type DbId = i64; @@ -103,7 +104,7 @@ const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100; const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; async fn _mixnode_test_results( - mix_id: MixId, + mix_id: NodeId, page: u32, per_page: u32, info_cache: &NodeInfoCache, @@ -162,9 +163,9 @@ async fn _mixnode_test_results( } pub async fn mixnode_test_results( - Path(mix_id): Path, + Path(mix_id): Path, Query(pagination): Query, - State(state): State, + State(state): State, ) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( @@ -252,7 +253,7 @@ async fn _gateway_test_results( pub async fn gateway_test_results( Path(gateway_identity): Path, Query(pagination): Query, - State(state): State, + State(state): State, ) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs index b431d38913..c981075a00 100644 --- a/nym-api/src/node_status_api/handlers/without_monitor.rs +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -3,34 +3,44 @@ use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ - _get_active_set_detailed, _get_mixnode_inclusion_probabilities, - _get_mixnode_inclusion_probability, _get_mixnode_stake_saturation, _get_mixnode_status, - _get_mixnodes_detailed, _get_rewarded_set_detailed, + _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, + _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, + _get_mixnode_stake_saturation, _get_mixnode_status, _get_rewarded_set_legacy_mixnodes_detailed, }; -use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; use axum::extract::{Path, State}; +use axum::routing::{get, post}; use axum::Json; use axum::Router; use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeStatusResponse, StakeSaturationResponse, }; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use nym_types::monitoring::MonitorMessage; +use tracing::error; -pub(super) fn mandatory_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(super) fn mandatory_routes() -> Router { Router::new() + .route( + "/submit-gateway-monitoring-results", + post(submit_gateway_monitoring_results), + ) + .route( + "/submit-node-monitoring-results", + post(submit_node_monitoring_results), + ) .nest( "/mixnode/:mix_id", Router::new() - .route("/status", axum::routing::get(get_mixnode_status)) - .route( - "/stake-saturation", - axum::routing::get(get_mixnode_stake_saturation), - ) + .route("/status", get(get_mixnode_status)) + .route("/stake-saturation", get(get_mixnode_stake_saturation)) .route( "/inclusion-probability", - axum::routing::get(get_mixnode_inclusion_probability), + get(get_mixnode_inclusion_probability), ), ) .merge( @@ -39,21 +49,105 @@ pub(super) fn mandatory_routes() -> Router { Router::new() .route( "/inclusion-probability", - axum::routing::get(get_mixnode_inclusion_probabilities), + get(get_mixnode_inclusion_probabilities), ) - .route("/detailed", axum::routing::get(get_mixnodes_detailed)) - .route( - "/rewarded/detailed", - axum::routing::get(get_rewarded_set_detailed), - ) - .route( - "/active/detailed", - axum::routing::get(get_active_set_detailed), - ), + .route("/detailed", get(get_mixnodes_detailed)) + .route("/rewarded/detailed", get(get_rewarded_set_detailed)) + .route("/active/detailed", get(get_active_set_detailed)), ), ) } +#[utoipa::path( + tag = "status", + post, + path = "/v1/status/submit-gateway-monitoring-results", + responses( + (status = 200), + (status = 400, body = ErrorResponse, description = "TBD"), + (status = 403, body = ErrorResponse, description = "TBD"), + (status = 500, body = ErrorResponse, description = "TBD"), + ), +)] +pub(crate) async fn submit_gateway_monitoring_results( + State(state): State, + Json(message): Json, +) -> AxumResult<()> { + if !message.is_in_allowed() { + return Err(AxumErrorResponse::forbidden( + "Monitor not registered to submit results", + )); + } + + if !message.timely() { + return Err(AxumErrorResponse::bad_request("Message is too old")); + } + + if !message.verify() { + return Err(AxumErrorResponse::bad_request("invalid signature")); + } + + match state + .storage + .manager + .submit_gateway_statuses_v2(message.results()) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to submit gateway monitoring results: {err}"); + Err(AxumErrorResponse::internal_msg( + "failed to submit gateway monitoring results", + )) + } + } +} + +#[utoipa::path( + tag = "status", + post, + path = "/v1/status/submit-node-monitoring-results", + responses( + (status = 200), + (status = 400, body = ErrorResponse, description = "TBD"), + (status = 403, body = ErrorResponse, description = "TBD"), + (status = 500, body = ErrorResponse, description = "TBD"), + ), +)] +pub(crate) async fn submit_node_monitoring_results( + State(state): State, + Json(message): Json, +) -> AxumResult<()> { + if !message.is_in_allowed() { + return Err(AxumErrorResponse::forbidden( + "Monitor not registered to submit results", + )); + } + + if !message.timely() { + return Err(AxumErrorResponse::bad_request("Message is too old")); + } + + if !message.verify() { + return Err(AxumErrorResponse::bad_request("invalid signature")); + } + + match state + .storage + .manager + .submit_mixnode_statuses_v2(message.results()) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to submit node monitoring results: {err}"); + Err(AxumErrorResponse::internal_msg( + "failed to submit node monitoring results", + )) + } + } +} + #[utoipa::path( tag = "status", get, @@ -65,9 +159,10 @@ pub(super) fn mandatory_routes() -> Router { (status = 200, body = MixnodeStatusResponse) ) )] +#[deprecated] async fn get_mixnode_status( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> Json { Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) } @@ -83,9 +178,10 @@ async fn get_mixnode_status( (status = 200, body = StakeSaturationResponse) ) )] +#[deprecated] async fn get_mixnode_stake_saturation( - Path(mix_id): Path, - State(state): State, + Path(mix_id): Path, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_stake_saturation( @@ -108,9 +204,10 @@ async fn get_mixnode_stake_saturation( (status = 200, body = InclusionProbabilityResponse) ) )] +#[deprecated] async fn get_mixnode_inclusion_probability( - Path(mix_id): Path, - State(state): State, + Path(mix_id): Path, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?, @@ -125,8 +222,9 @@ async fn get_mixnode_inclusion_probability( (status = 200, body = AllInclusionProbabilitiesResponse) ) )] +#[deprecated] async fn get_mixnode_inclusion_probabilities( - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_inclusion_probabilities(state.node_status_cache()).await?, @@ -141,10 +239,11 @@ async fn get_mixnode_inclusion_probabilities( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_mixnodes_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_mixnodes_detailed(state.node_status_cache()).await) + Json(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -155,10 +254,17 @@ pub async fn get_mixnodes_detailed( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_rewarded_set_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_rewarded_set_detailed(state.node_status_cache()).await) + Json( + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) } #[utoipa::path( @@ -169,8 +275,15 @@ pub async fn get_rewarded_set_detailed( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_active_set_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_active_set_detailed(state.node_status_cache()).await) + Json( + _get_active_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) } diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 0d706b1156..c3308eaee4 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -11,25 +11,62 @@ use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, + MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; -use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; +use nym_mixnet_contract_common::NodeId; + +pub(crate) enum RewardedSetStatus { + Active, + Standby, + Inactive, +} + +impl From for RewardedSetStatus { + fn from(value: MixnodeStatus) -> Self { + match value { + MixnodeStatus::Active => RewardedSetStatus::Active, + MixnodeStatus::Standby => RewardedSetStatus::Standby, + // for all intents and purposes, missing node is treated as inactive for rewarding (since it wouldn't get anything + MixnodeStatus::Inactive => RewardedSetStatus::Inactive, + MixnodeStatus::NotFound => RewardedSetStatus::Inactive, + } + } +} + +async fn gateway_identity_to_node_id( + cache: &NodeStatusCache, + identity: &str, +) -> AxumResult { + let node_id = cache + .map_identity_to_node_id(identity) + .await + .ok_or(AxumErrorResponse::not_found("gateway bond not found"))?; + Ok(node_id) +} async fn get_gateway_bond_annotated( cache: &NodeStatusCache, - identity: &str, + node_id: NodeId, ) -> AxumResult { cache - .gateway_annotated(identity) + .gateway_annotated(node_id) .await .ok_or(AxumErrorResponse::not_found("gateway bond not found")) } +async fn get_gateway_bond_annotated_by_identity( + cache: &NodeStatusCache, + identity: &str, +) -> AxumResult { + let node_id = gateway_identity_to_node_id(cache, identity).await?; + get_gateway_bond_annotated(cache, node_id).await +} + async fn get_mixnode_bond_annotated( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { cache .mixnode_annotated(mix_id) @@ -41,7 +78,7 @@ pub(crate) async fn _gateway_report( cache: &NodeStatusCache, identity: &str, ) -> AxumResult { - let gateway = get_gateway_bond_annotated(cache, identity).await?; + let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; Ok(GatewayStatusReportResponse { identity: gateway.identity().to_owned(), @@ -54,13 +91,24 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, + nym_contract_cache: &NymContractCache, identity: &str, ) -> AxumResult { - storage - .get_gateway_uptime_history(identity) + let history = storage + .get_gateway_uptime_history_by_identity(identity) .await - .map(GatewayUptimeHistoryResponse::from) - .map_err(AxumErrorResponse::not_found) + .map_err(AxumErrorResponse::not_found)?; + + let owner = nym_contract_cache + .legacy_gateway_owner(history.node_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("could not determine gateway owner"))?; + + Ok(GatewayUptimeHistoryResponse { + identity: history.identity, + owner, + history: history.history.into_iter().map(Into::into).collect(), + }) } pub(crate) async fn _gateway_core_status_count( @@ -69,7 +117,7 @@ pub(crate) async fn _gateway_core_status_count( since: Option, ) -> AxumResult { let count = storage - .get_core_gateway_status_count(identity, since) + .get_core_gateway_status_count_by_identity(identity, since) .await .map_err(AxumErrorResponse::not_found)?; @@ -81,7 +129,7 @@ pub(crate) async fn _gateway_core_status_count( pub(crate) async fn _mixnode_report( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; @@ -97,18 +145,30 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - mix_id: MixId, + nym_contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - storage + let history = storage .get_mixnode_uptime_history(mix_id) .await - .map(MixnodeUptimeHistoryResponse::from) - .map_err(AxumErrorResponse::not_found) + .map_err(AxumErrorResponse::not_found)?; + + let owner = nym_contract_cache + .legacy_gateway_owner(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("could not determine mixnode owner"))?; + + Ok(MixnodeUptimeHistoryResponse { + mix_id, + identity: history.identity, + owner, + history: history.history.into_iter().map(Into::into).collect(), + }) } pub(crate) async fn _mixnode_core_status_count( storage: &NymApiStorage, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> AxumResult { let count = storage @@ -121,7 +181,7 @@ pub(crate) async fn _mixnode_core_status_count( pub(crate) async fn _get_mixnode_status( cache: &NymContractCache, - mix_id: MixId, + mix_id: NodeId, ) -> MixnodeStatusResponse { MixnodeStatusResponse { status: cache.mixnode_status(mix_id).await, @@ -129,157 +189,161 @@ pub(crate) async fn _get_mixnode_status( } pub(crate) async fn _get_mixnode_reward_estimation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, status) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let status = contract_cache.mixnode_status(mix_id).await; + let mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + let current_interval = contract_cache + .current_interval() + .await + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) - } + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + mixnode.performance, + status.into(), + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _compute_mixnode_reward_estimation( user_reward_param: &ComputeRewardEstParam, - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; - if let Some(mut mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let mut mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + let current_interval = contract_cache + .current_interval() + .await + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => Some(RewardedSetNodeStatus::Active), - Some(false) => Some(RewardedSetNodeStatus::Standby), - None => actual_status.into(), - }; + // For these parameters we either use the provided ones, or fall back to the system ones + let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); + let status = match user_reward_param.active_in_rewarded_set { + Some(true) => RewardedSetStatus::Active, + Some(false) => RewardedSetStatus::Standby, + None => { + let actual_status = contract_cache.mixnode_status(mix_id).await; + actual_status.into() } + }; - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost.clone(); - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(AxumErrorResponse::unprocessable_entity( - "Pledge plus delegation too large", - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) + if let Some(pledge_amount) = user_reward_param.pledge_amount { + mixnode.mixnode_details.rewarding_details.operator = + Decimal::from_ratio(pledge_amount, 1u64); } + if let Some(total_delegation) = user_reward_param.total_delegation { + mixnode.mixnode_details.rewarding_details.delegates = + Decimal::from_ratio(total_delegation, 1u64); + } + + if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .profit_margin_percent = profit_margin_percent; + } + + if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .interval_operating_cost = interval_operating_cost.clone(); + } + + if mixnode.mixnode_details.rewarding_details.operator + + mixnode.mixnode_details.rewarding_details.delegates + > reward_params.interval.staking_supply + { + return Err(AxumErrorResponse::unprocessable_entity( + "Pledge plus delegation too large", + )); + } + + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + performance, + status, + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _get_mixnode_stake_saturation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, _) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - // Recompute the stake saturation just so that we can confidently state that the `as_at` - // field is consistent and correct. Luckily this is very cheap. - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let rewarding_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - Ok(StakeSaturationResponse { - saturation: mixnode - .mixnode_details - .rewarding_details - .bond_saturation(&rewarding_params), - uncapped_saturation: mixnode - .mixnode_details - .rewarding_details - .uncapped_bond_saturation(&rewarding_params), - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) - } + // Recompute the stake saturation just so that we can confidently state that the `as_at` + // field is consistent and correct. Luckily this is very cheap. + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let rewarding_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(StakeSaturationResponse { + saturation: mixnode + .mixnode_details + .rewarding_details + .bond_saturation(&rewarding_params), + uncapped_saturation: mixnode + .mixnode_details + .rewarding_details + .uncapped_bond_saturation(&rewarding_params), + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _get_mixnode_inclusion_probability( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { cache .inclusion_probabilities() @@ -295,7 +359,7 @@ pub(crate) async fn _get_mixnode_inclusion_probability( pub(crate) async fn _get_mixnode_avg_uptime( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; @@ -310,7 +374,7 @@ pub(crate) async fn _get_gateway_avg_uptime( cache: &NodeStatusCache, identity: &str, ) -> AxumResult { - let gateway = get_gateway_bond_annotated(cache, identity).await?; + let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; Ok(GatewayUptimeResponse { identity: identity.to_string(), @@ -338,7 +402,9 @@ pub(crate) async fn _get_mixnode_inclusion_probabilities( } } -pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { +pub(crate) async fn _get_legacy_mixnodes_detailed( + cache: &NodeStatusCache, +) -> Vec { cache .mixnodes_annotated_filtered() .await @@ -351,32 +417,50 @@ pub(crate) async fn _get_mixnodes_detailed_unfiltered( cache.mixnodes_annotated_full().await.unwrap_or_default() } -pub(crate) async fn _get_rewarded_set_detailed( - cache: &NodeStatusCache, +pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, ) -> Vec { - cache - .rewarded_set_annotated() - .await - .unwrap_or_default() - .into_inner() + let Some(rewarded_set) = contract_cache.rewarded_set().await else { + return Vec::new(); + }; + let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { + return Vec::new(); + }; + mixnodes + .into_iter() + .filter(|m| { + rewarded_set.is_active_mixnode(&m.mix_id()) || rewarded_set.is_standby(&m.mix_id()) + }) + .collect() } -pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { - cache - .active_set_annotated() - .await - .unwrap_or_default() - .into_inner() +pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, +) -> Vec { + let Some(rewarded_set) = contract_cache.rewarded_set().await else { + return Vec::new(); + }; + let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { + return Vec::new(); + }; + mixnodes + .into_iter() + .filter(|m| rewarded_set.is_active_mixnode(&m.mix_id())) + .collect() } -pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec { +pub(crate) async fn _get_legacy_gateways_detailed( + cache: &NodeStatusCache, +) -> Vec { cache .gateways_annotated_filtered() .await .unwrap_or_default() } -pub(crate) async fn _get_gateways_detailed_unfiltered( +pub(crate) async fn _get_legacy_gateways_detailed_unfiltered( cache: &NodeStatusCache, ) -> Vec { cache.gateways_annotated_full().await.unwrap_or_default() diff --git a/nym-api/src/node_status_api/helpers_deprecated.rs b/nym-api/src/node_status_api/helpers_deprecated.rs deleted file mode 100644 index 0788cd2cb3..0000000000 --- a/nym-api/src/node_status_api/helpers_deprecated.rs +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::models::RocketErrorResponse; -use crate::storage::NymApiStorage; -use crate::support::caching::Cache; -use crate::{NodeStatusCache, NymContractCache}; -use cosmwasm_std::Decimal; -use nym_api_requests::models::{ - AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, - GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, - GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, - MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, - UptimeResponse, -}; -use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; -use rocket::http::Status; -use rocket::State; - -use super::reward_estimate::compute_reward_estimate; - -async fn get_gateway_bond_annotated( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - cache - .gateway_annotated(identity) - .await - .ok_or(RocketErrorResponse::new( - "gateway bond not found", - Status::NotFound, - )) -} - -async fn get_mixnode_bond_annotated( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - cache - .mixnode_annotated(mix_id) - .await - .ok_or(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) -} - -pub(crate) async fn _gateway_report( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - let gateway = get_gateway_bond_annotated(cache, identity).await?; - - Ok(GatewayStatusReportResponse { - identity: gateway.identity().to_owned(), - owner: gateway.owner().to_string(), - most_recent: gateway.node_performance.most_recent.round_to_integer(), - last_hour: gateway.node_performance.last_hour.round_to_integer(), - last_day: gateway.node_performance.last_24h.round_to_integer(), - }) -} - -pub(crate) async fn _gateway_uptime_history( - storage: &NymApiStorage, - identity: &str, -) -> Result { - storage - .get_gateway_uptime_history(identity) - .await - .map(GatewayUptimeHistoryResponse::from) - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) -} - -pub(crate) async fn _gateway_core_status_count( - storage: &State, - identity: &str, - since: Option, -) -> Result { - let count = storage - .get_core_gateway_status_count(identity, since) - .await - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; - - Ok(GatewayCoreStatusResponse { - identity: identity.to_string(), - count, - }) -} - -pub(crate) async fn _mixnode_report( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(MixnodeStatusReportResponse { - mix_id, - identity: mixnode.identity_key().to_owned(), - owner: mixnode.owner().to_string(), - most_recent: mixnode.node_performance.most_recent.round_to_integer(), - last_hour: mixnode.node_performance.last_hour.round_to_integer(), - last_day: mixnode.node_performance.last_24h.round_to_integer(), - }) -} - -pub(crate) async fn _mixnode_uptime_history( - storage: &NymApiStorage, - mix_id: MixId, -) -> Result { - storage - .get_mixnode_uptime_history(mix_id) - .await - .map(MixnodeUptimeHistoryResponse::from) - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) -} - -pub(crate) async fn _mixnode_core_status_count( - storage: &State, - mix_id: MixId, - since: Option, -) -> Result { - let count = storage - .get_core_mixnode_status_count(mix_id, since) - .await - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; - - Ok(MixnodeCoreStatusResponse { mix_id, count }) -} - -pub(crate) async fn _get_mixnode_status( - cache: &NymContractCache, - mix_id: MixId, -) -> MixnodeStatusResponse { - MixnodeStatusResponse { - status: cache.mixnode_status(mix_id).await, - } -} - -pub(crate) async fn _get_mixnode_reward_estimation( - cache: &State, - validator_cache: &State, - mix_id: MixId, -) -> Result { - let (mixnode, status) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: ComputeRewardEstParam, - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, -) -> Result { - let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; - if let Some(mut mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => Some(RewardedSetNodeStatus::Active), - Some(false) => Some(RewardedSetNodeStatus::Standby), - None => actual_status.into(), - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } - - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost; - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(RocketErrorResponse::new( - "Pledge plus delegation too large", - Status::UnprocessableEntity, - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _get_mixnode_stake_saturation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, -) -> Result { - let (mixnode, _) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - // Recompute the stake saturation just so that we can confidently state that the `as_at` - // field is consistent and correct. Luckily this is very cheap. - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let rewarding_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - Ok(StakeSaturationResponse { - saturation: mixnode - .mixnode_details - .rewarding_details - .bond_saturation(&rewarding_params), - uncapped_saturation: mixnode - .mixnode_details - .rewarding_details - .uncapped_bond_saturation(&rewarding_params), - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _get_mixnode_inclusion_probability( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - cache - .inclusion_probabilities() - .await - .map(Cache::into_inner) - .and_then(|p| p.node(mix_id).cloned()) - .map(|p| InclusionProbabilityResponse { - in_active: p.in_active.into(), - in_reserve: p.in_reserve.into(), - }) - .ok_or_else(|| RocketErrorResponse::new("mixnode bond not found", Status::NotFound)) -} - -pub(crate) async fn _get_mixnode_avg_uptime( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(UptimeResponse { - mix_id, - avg_uptime: mixnode.node_performance.last_24h.round_to_integer(), - node_performance: mixnode.node_performance, - }) -} - -pub(crate) async fn _get_gateway_avg_uptime( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - let gateway = get_gateway_bond_annotated(cache, identity).await?; - - Ok(GatewayUptimeResponse { - identity: identity.to_string(), - avg_uptime: gateway.node_performance.last_24h.round_to_integer(), - node_performance: gateway.node_performance, - }) -} - -pub(crate) async fn _get_mixnode_inclusion_probabilities( - cache: &NodeStatusCache, -) -> Result { - if let Some(prob) = cache.inclusion_probabilities().await { - let as_at = prob.timestamp(); - let prob = prob.into_inner(); - Ok(AllInclusionProbabilitiesResponse { - inclusion_probabilities: prob.inclusion_probabilities, - samples: prob.samples, - elapsed: prob.elapsed, - delta_max: prob.delta_max, - delta_l2: prob.delta_l2, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "No data available", - Status::ServiceUnavailable, - )) - } -} - -pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { - cache - .mixnodes_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_mixnodes_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.mixnodes_annotated_full().await.unwrap_or_default() -} - -pub(crate) async fn _get_rewarded_set_detailed( - cache: &NodeStatusCache, -) -> Vec { - cache - .rewarded_set_annotated() - .await - .unwrap_or_default() - .into_inner() -} - -pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { - cache - .active_set_annotated() - .await - .unwrap_or_default() - .into_inner() -} - -pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec { - cache - .gateways_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_gateways_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.gateways_annotated_full().await.unwrap_or_default() -} diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 5f50e62964..3f7ed14b16 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -9,20 +9,14 @@ use crate::{ }; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; use std::time::Duration; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -#[cfg(feature = "axum")] pub(crate) mod helpers; -pub(crate) mod helpers_deprecated; pub(crate) mod models; pub(crate) mod reward_estimate; -pub(crate) mod routes_deprecated; +// pub(crate) mod routes_deprecated; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -30,51 +24,51 @@ pub(crate) const FIFTEEN_MINUTES: Duration = Duration::from_secs(900); pub(crate) const ONE_HOUR: Duration = Duration::from_secs(3600); pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); -pub(crate) fn node_status_routes( - settings: &OpenApiSettings, - enabled: bool, -) -> (Vec, OpenApi) { - if enabled { - openapi_get_routes_spec![ - settings: routes_deprecated::gateway_report, - routes_deprecated::gateway_uptime_history, - routes_deprecated::gateway_core_status_count, - routes_deprecated::mixnode_report, - routes_deprecated::mixnode_uptime_history, - routes_deprecated::mixnode_core_status_count, - routes_deprecated::get_mixnode_status, - routes_deprecated::get_mixnode_reward_estimation, - routes_deprecated::compute_mixnode_reward_estimation, - routes_deprecated::get_mixnode_stake_saturation, - routes_deprecated::get_mixnode_inclusion_probability, - routes_deprecated::get_mixnode_avg_uptime, - routes_deprecated::get_gateway_avg_uptime, - routes_deprecated::get_mixnode_inclusion_probabilities, - routes_deprecated::get_mixnodes_detailed, - routes_deprecated::get_mixnodes_detailed_unfiltered, - routes_deprecated::get_rewarded_set_detailed, - routes_deprecated::get_active_set_detailed, - routes_deprecated::get_gateways_detailed, - routes_deprecated::get_gateways_detailed_unfiltered, - routes_deprecated::unstable::mixnode_test_results, - routes_deprecated::unstable::gateway_test_results, - routes_deprecated::submit_gateway_monitoring_results, - routes_deprecated::submit_node_monitoring_results, - ] - } else { - // in the minimal variant we would not have access to endpoints relying on existence - // of the network monitor and the associated storage - openapi_get_routes_spec![ - settings: routes_deprecated::get_mixnode_status, - routes_deprecated::get_mixnode_stake_saturation, - routes_deprecated::get_mixnode_inclusion_probability, - routes_deprecated::get_mixnode_inclusion_probabilities, - routes_deprecated::get_mixnodes_detailed, - routes_deprecated::get_rewarded_set_detailed, - routes_deprecated::get_active_set_detailed, - ] - } -} +// pub(crate) fn node_status_routes( +// settings: &OpenApiSettings, +// enabled: bool, +// ) -> (Vec, OpenApi) { +// if enabled { +// openapi_get_routes_spec![ +// settings: routes_deprecated::gateway_report, +// routes_deprecated::gateway_uptime_history, +// routes_deprecated::gateway_core_status_count, +// routes_deprecated::mixnode_report, +// routes_deprecated::mixnode_uptime_history, +// routes_deprecated::mixnode_core_status_count, +// routes_deprecated::get_mixnode_status, +// routes_deprecated::get_mixnode_reward_estimation, +// routes_deprecated::compute_mixnode_reward_estimation, +// routes_deprecated::get_mixnode_stake_saturation, +// routes_deprecated::get_mixnode_inclusion_probability, +// routes_deprecated::get_mixnode_avg_uptime, +// routes_deprecated::get_gateway_avg_uptime, +// routes_deprecated::get_mixnode_inclusion_probabilities, +// routes_deprecated::get_mixnodes_detailed, +// routes_deprecated::get_mixnodes_detailed_unfiltered, +// routes_deprecated::get_rewarded_set_detailed, +// routes_deprecated::get_active_set_detailed, +// routes_deprecated::get_gateways_detailed, +// routes_deprecated::get_gateways_detailed_unfiltered, +// routes_deprecated::unstable::mixnode_test_results, +// routes_deprecated::unstable::gateway_test_results, +// routes_deprecated::submit_gateway_monitoring_results, +// routes_deprecated::submit_node_monitoring_results, +// ] +// } else { +// // in the minimal variant we would not have access to endpoints relying on existence +// // of the network monitor and the associated storage +// openapi_get_routes_spec![ +// settings: routes_deprecated::get_mixnode_status, +// routes_deprecated::get_mixnode_stake_saturation, +// routes_deprecated::get_mixnode_inclusion_probability, +// routes_deprecated::get_mixnode_inclusion_probabilities, +// routes_deprecated::get_mixnodes_detailed, +// routes_deprecated::get_rewarded_set_detailed, +// routes_deprecated::get_active_set_detailed, +// ] +// } +// } /// Spawn the node status cache refresher. /// diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 480fae65d2..ce4c0eb1b1 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -1,29 +1,25 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::ecash::error::{EcashError, RedemptionError}; use crate::node_status_api::utils::NodeUptimes; use crate::storage::models::NodeStatus; +use crate::support::caching::cache::UninitialisedCache; use nym_api_requests::models::{ - GatewayStatusReportResponse, GatewayUptimeHistoryResponse, HistoricalUptimeResponse, - MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, NodePerformance, RequestError, + HistoricalPerformanceResponse, HistoricalUptimeResponse, NodePerformance, + OldHistoricalUptimeResponse, RequestError, }; +use nym_contracts_common::NaiveFloat; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{IdentityKey, MixId}; -use okapi::openapi3::{Responses, SchemaObject}; -use rocket::http::Status; -use rocket::response::{self, Responder, Response}; -use rocket::serde::json::Json; -use rocket::Request; -use rocket_okapi::gen::OpenApiGenerator; -use rocket_okapi::response::OpenApiResponderInner; -use rocket_okapi::util::ensure_status_code_exists; -use schemars::gen::SchemaGenerator; -use schemars::schema::{InstanceType, Schema}; +use nym_mixnet_contract_common::{IdentityKey, NodeId}; +use nym_serde_helpers::date::DATE_FORMAT; +use reqwest::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; - +use std::fmt::Display; use thiserror::Error; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; +use tracing::error; #[derive(Error, Debug)] #[error("Received uptime value was within 0-100 range (got {received})")] @@ -40,6 +36,10 @@ impl Uptime { Uptime(0) } + pub fn is_zero(&self) -> bool { + self.0 == 0 + } + pub fn new(uptime: f32) -> Self { if uptime > 100f32 { error!("Got uptime {}, max is 100, returning 0", uptime); @@ -128,9 +128,8 @@ impl From for Performance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeStatusReport { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: IdentityKey, - pub(crate) owner: String, pub(crate) most_recent: Uptime, @@ -141,9 +140,8 @@ pub struct MixnodeStatusReport { impl MixnodeStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, - mix_id: MixId, + mix_id: NodeId, identity: IdentityKey, - owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, @@ -158,7 +156,6 @@ impl MixnodeStatusReport { MixnodeStatusReport { mix_id, identity, - owner, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, @@ -166,19 +163,6 @@ impl MixnodeStatusReport { } } -impl From for MixnodeStatusReportResponse { - fn from(status: MixnodeStatusReport) -> Self { - MixnodeStatusReportResponse { - mix_id: status.mix_id, - identity: status.identity, - owner: status.owner, - most_recent: status.most_recent.0, - last_hour: status.last_hour.0, - last_day: status.last_day.0, - } - } -} - impl From for NodePerformance { fn from(report: MixnodeStatusReport) -> Self { NodePerformance { @@ -191,8 +175,8 @@ impl From for NodePerformance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayStatusReport { + pub(crate) node_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) most_recent: Uptime, @@ -203,8 +187,8 @@ pub struct GatewayStatusReport { impl GatewayStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, + node_id: NodeId, identity: String, - owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, @@ -218,7 +202,7 @@ impl GatewayStatusReport { GatewayStatusReport { identity, - owner, + node_id, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, @@ -226,18 +210,6 @@ impl GatewayStatusReport { } } -impl From for GatewayStatusReportResponse { - fn from(status: GatewayStatusReport) -> Self { - GatewayStatusReportResponse { - identity: status.identity, - owner: status.owner, - most_recent: status.most_recent.0, - last_hour: status.last_hour.0, - last_day: status.last_day.0, - } - } -} - impl From for NodePerformance { fn from(report: GatewayStatusReport) -> Self { NodePerformance { @@ -250,68 +222,44 @@ impl From for NodePerformance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeUptimeHistory { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) history: Vec, } impl MixnodeUptimeHistory { - pub(crate) fn new( - mix_id: MixId, - identity: String, - owner: String, - history: Vec, - ) -> Self { + pub(crate) fn new(mix_id: NodeId, identity: String, history: Vec) -> Self { MixnodeUptimeHistory { mix_id, identity, - owner, history, } } } -impl From for MixnodeUptimeHistoryResponse { - fn from(history: MixnodeUptimeHistory) -> Self { - MixnodeUptimeHistoryResponse { - mix_id: history.mix_id, - identity: history.identity, - owner: history.owner, - history: history.history.into_iter().map(Into::into).collect(), - } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayUptimeHistory { pub(crate) identity: String, - pub(crate) owner: String, + pub(crate) node_id: NodeId, pub(crate) history: Vec, } impl GatewayUptimeHistory { - pub(crate) fn new(identity: String, owner: String, history: Vec) -> Self { + pub(crate) fn new( + node_id: NodeId, + identity: impl Into, + history: Vec, + ) -> Self { GatewayUptimeHistory { - identity, - owner, + node_id, + identity: identity.into(), history, } } } -impl From for GatewayUptimeHistoryResponse { - fn from(history: GatewayUptimeHistory) -> Self { - GatewayUptimeHistoryResponse { - identity: history.identity, - owner: history.owner, - history: history.history.into_iter().map(Into::into).collect(), - } - } -} - #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct HistoricalUptime { // ISO 8601 date string @@ -321,188 +269,172 @@ pub struct HistoricalUptime { pub(crate) uptime: Uptime, } -impl From for HistoricalUptimeResponse { +#[derive(Error, Debug)] +pub enum InvalidHistoricalPerformance { + #[error("the provided date could not be parsed")] + UnparsableDate, + + #[error("the provided uptime could not be parsed")] + MalformedPerformance, +} + +impl TryFrom for HistoricalPerformanceResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalPerformanceResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + performance: Performance::from_percentage_value(value.uptime.u8() as u64) + .map_err(|_| InvalidHistoricalPerformance::MalformedPerformance)? + .naive_to_f64(), + }) + } +} + +impl TryFrom for HistoricalUptimeResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalUptimeResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + uptime: value.uptime.u8(), + }) + } +} + +impl From for OldHistoricalUptimeResponse { fn from(uptime: HistoricalUptime) -> Self { - HistoricalUptimeResponse { + OldHistoricalUptimeResponse { date: uptime.date, uptime: uptime.uptime.0, } } } -#[deprecated(note = "TODO rocket remove once Rocket is phased out")] -pub(crate) struct RocketErrorResponse { - error_message: RequestError, - status: Status, +// TODO rocket remove smurf name after eliminating `rocket` +pub(crate) type AxumResult = Result; +pub(crate) struct AxumErrorResponse { + message: RequestError, + status: StatusCode, } -impl RocketErrorResponse { - pub(crate) fn new(error_message: impl Into, status: Status) -> Self { - RocketErrorResponse { - error_message: RequestError::new(error_message), - status, +impl AxumErrorResponse { + pub(crate) fn internal_msg(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn internal() -> Self { + Self { + message: RequestError::new("Internal server error"), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn not_implemented() -> Self { + Self { + message: RequestError::empty(), + status: StatusCode::NOT_IMPLEMENTED, + } + } + + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::NOT_FOUND, + } + } + + pub(crate) fn service_unavailable() -> Self { + Self { + message: RequestError::empty(), + status: StatusCode::SERVICE_UNAVAILABLE, + } + } + + pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::UNPROCESSABLE_ENTITY, + } + } + + pub(crate) fn forbidden(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::FORBIDDEN, + } + } + + pub(crate) fn bad_request(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::BAD_REQUEST, } } } -impl<'r, 'o: 'r> Responder<'r, 'o> for RocketErrorResponse { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - // piggyback on the existing implementation - // also prefer json over plain for ease of use in frontend - Response::build() - .merge(Json(self.error_message).respond_to(req)?) - .status(self.status) - .ok() - } -} - -impl JsonSchema for RocketErrorResponse { - fn schema_name() -> String { - "ErrorResponse".to_owned() - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema_object = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..SchemaObject::default() - }; - - let object_validation = schema_object.object(); - object_validation - .properties - .insert("error_message".to_owned(), gen.subschema_for::()); - object_validation - .required - .insert("error_message".to_owned()); - - // Status does not implement JsonSchema so we just explicitly specify the inner type. - object_validation - .properties - .insert("status".to_owned(), gen.subschema_for::()); - object_validation.required.insert("status".to_owned()); - - Schema::Object(schema_object) - } -} - -impl OpenApiResponderInner for RocketErrorResponse { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 404); - Ok(responses) - } -} - -#[cfg(feature = "axum")] -pub(crate) use axum_error::{AxumErrorResponse, AxumResult}; - -#[cfg(feature = "axum")] -/// TODO rocket: extract types from this module when axum becomes the only server in Nym API -mod axum_error { - pub use super::*; - use crate::ecash::error::{EcashError, RedemptionError}; - use std::fmt::Display; - - // TODO rocket remove smurf name after eliminating `rocket` - pub(crate) type AxumResult = Result; - pub(crate) struct AxumErrorResponse { - message: RequestError, - status: axum::http::StatusCode, - } - - impl AxumErrorResponse { - pub(crate) fn internal_msg(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } - } - - pub(crate) fn internal() -> Self { - Self { - message: RequestError::new("Internal server error"), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } - } - - pub(crate) fn not_implemented() -> Self { - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::NOT_IMPLEMENTED, - } - } - - pub(crate) fn not_found(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::NOT_FOUND, - } - } - - pub(crate) fn service_unavailable() -> Self { - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::SERVICE_UNAVAILABLE, - } - } - - pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::UNPROCESSABLE_ENTITY, - } - } - } - - impl axum::response::IntoResponse for AxumErrorResponse { - fn into_response(self) -> axum::response::Response { - (self.status, self.message.message().to_string()).into_response() - } - } - - impl From for AxumErrorResponse { - fn from(value: NymApiStorageError) -> Self { - error!("{value}"); - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } - } - } - - impl From for AxumErrorResponse { - fn from(value: EcashError) -> Self { - Self { - message: RequestError::new(value.to_string()), - status: axum::http::StatusCode::BAD_REQUEST, - } - } - } - - #[cfg(feature = "axum")] - impl From for AxumErrorResponse { - fn from(value: RedemptionError) -> Self { - Self { - message: RequestError::new(value.to_string()), - status: axum::http::StatusCode::BAD_REQUEST, - } +impl From for AxumErrorResponse { + fn from(_: UninitialisedCache) -> Self { + AxumErrorResponse { + message: RequestError::new("relevant cache hasn't been initialised yet"), + status: StatusCode::SERVICE_UNAVAILABLE, } } } -#[derive(Debug, thiserror::Error)] +impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message().to_string()).into_response() + } +} + +impl From for AxumErrorResponse { + fn from(value: NymApiStorageError) -> Self { + error!("{value}"); + Self { + message: RequestError::empty(), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for AxumErrorResponse { + fn from(value: EcashError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: StatusCode::BAD_REQUEST, + } + } +} + +impl From for AxumErrorResponse { + fn from(value: RedemptionError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: StatusCode::BAD_REQUEST, + } + } +} + +#[derive(Debug, Error)] pub enum NymApiStorageError { #[error("could not find status report associated with mixnode {mix_id}")] - MixnodeReportNotFound { mix_id: MixId }, + MixnodeReportNotFound { mix_id: NodeId }, - #[error("Could not find status report associated with gateway {identity}")] - GatewayReportNotFound { identity: IdentityKey }, + #[error("Could not find status report associated with gateway {node_id}")] + GatewayReportNotFound { node_id: NodeId }, #[error("could not find uptime history associated with mixnode {mix_id}")] - MixnodeUptimeHistoryNotFound { mix_id: MixId }, + MixnodeUptimeHistoryNotFound { mix_id: NodeId }, - #[error("could not find uptime history associated with gateway {identity}")] - GatewayUptimeHistoryNotFound { identity: IdentityKey }, + #[error("could not find uptime history associated with gateway {node_id}")] + GatewayUptimeHistoryNotFound { node_id: NodeId }, + + #[error("could not find gateway {identity} in the storage")] + GatewayNotFound { identity: String }, // I don't think we want to expose errors to the user about what really happened #[error("experienced internal database error")] @@ -524,3 +456,11 @@ impl NymApiStorageError { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uptime_response_conversion() {} +} diff --git a/nym-api/src/node_status_api/reward_estimate.rs b/nym-api/src/node_status_api/reward_estimate.rs index e9bd60ba09..6b1798227d 100644 --- a/nym-api/src/node_status_api/reward_estimate.rs +++ b/nym-api/src/node_status_api/reward_estimate.rs @@ -1,11 +1,14 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::helpers::RewardedSetStatus; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; +use nym_mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardingParams, +}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{Interval, RewardedSetNodeStatus}; +use nym_mixnet_contract_common::Interval; fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { if pledge_amount.is_zero() { @@ -17,9 +20,9 @@ fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) } pub fn compute_reward_estimate( - mixnode: &MixNodeDetails, + mixnode: &LegacyMixNodeDetailsWithLayer, performance: Performance, - rewarded_set_status: Option, + rewarded_set_status: RewardedSetStatus, rewarding_params: RewardingParams, interval: Interval, ) -> RewardEstimate { @@ -31,13 +34,22 @@ pub fn compute_reward_estimate( return Default::default(); } - let node_status = match rewarded_set_status { - Some(status) => status, - // if node is not in the rewarded set, it's not going to get anything - None => return Default::default(), + let is_active = match rewarded_set_status { + RewardedSetStatus::Active => true, + RewardedSetStatus::Standby => false, + RewardedSetStatus::Inactive => return Default::default(), }; - let node_reward_params = NodeRewardParams::new(performance, node_status.is_active()); + let work_factor = if is_active { + rewarding_params.active_node_work() + } else { + rewarding_params.standby_node_work() + }; + + let node_reward_params = NodeRewardingParameters { + performance, + work_factor, + }; let node_reward = mixnode .rewarding_details .node_reward(&rewarding_params, node_reward_params); @@ -63,7 +75,7 @@ pub fn compute_reward_estimate( } pub fn compute_apy_from_reward( - mixnode: &MixNodeDetails, + mixnode: &LegacyMixNodeDetailsWithLayer, reward_estimate: RewardEstimate, interval: Interval, ) -> (Decimal, Decimal) { diff --git a/nym-api/src/node_status_api/routes_deprecated.rs b/nym-api/src/node_status_api/routes_deprecated.rs index 29ab89fb4d..fa49ad692a 100644 --- a/nym-api/src/node_status_api/routes_deprecated.rs +++ b/nym-api/src/node_status_api/routes_deprecated.rs @@ -1,6 +1,18 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use super::NodeStatusCache; +use crate::node_status_api::helpers_deprecated::{ + _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, + _gateway_uptime_history, _get_gateway_avg_uptime, _get_mixnode_avg_uptime, + _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, + _get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status, + _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report, + _mixnode_uptime_history, +}; +use crate::node_status_api::models::RocketErrorResponse; +use crate::storage::NymApiStorage; +use crate::NymContractCache; use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, @@ -9,68 +21,54 @@ use nym_api_requests::models::{ MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::monitoring::MonitorMessage; use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; -use super::helpers_deprecated::_get_gateways_detailed; -use super::NodeStatusCache; -use crate::node_status_api::helpers_deprecated::{ - _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, - _gateway_uptime_history, _get_active_set_detailed, _get_gateway_avg_uptime, - _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, - _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, - _get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status, - _get_mixnodes_detailed, _get_mixnodes_detailed_unfiltered, _get_rewarded_set_detailed, - _mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history, -}; -use crate::node_status_api::models::RocketErrorResponse; -use crate::storage::NymApiStorage; -use crate::NymContractCache; - -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[post("/submit-gateway-monitoring-results", data = "")] pub(crate) async fn submit_gateway_monitoring_results( message: Json, storage: &State, ) -> Result<(), RocketErrorResponse> { - if !message.from_allowed() { - return Err(RocketErrorResponse::new( - "Monitor not registered to submit results".to_string(), - rocket::http::Status::Forbidden, - )); - } - - if !message.timely() { - return Err(RocketErrorResponse::new( - "Message is too old".to_string(), - rocket::http::Status::BadRequest, - )); - } - - if !message.verify() { - return Err(RocketErrorResponse::new( - "Invalid signature".to_string(), - rocket::http::Status::BadRequest, - )); - } - - match storage - .manager - .submit_gateway_statuses_v2(message.results()) - .await - { - Ok(_) => Ok(()), - Err(err) => { - error!("failed to submit gateway monitoring results: {}", err); - Err(RocketErrorResponse::new( - "failed to submit gateway monitoring results".to_string(), - rocket::http::Status::InternalServerError, - )) - } - } + todo!("rebasing"); + // if !message.from_allowed() { + // return Err(RocketErrorResponse::new( + // "Monitor not registered to submit results".to_string(), + // rocket::http::Status::Forbidden, + // )); + // } + // + // if !message.timely() { + // return Err(RocketErrorResponse::new( + // "Message is too old".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // if !message.verify() { + // return Err(RocketErrorResponse::new( + // "Invalid signature".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // match storage + // .manager + // .submit_gateway_statuses_v2(message.results()) + // .await + // { + // Ok(_) => Ok(()), + // Err(err) => { + // error!("failed to submit gateway monitoring results: {}", err); + // Err(RocketErrorResponse::new( + // "failed to submit gateway monitoring results".to_string(), + // rocket::http::Status::InternalServerError, + // )) + // } + // } } #[openapi(tag = "status")] @@ -79,62 +77,71 @@ pub(crate) async fn submit_node_monitoring_results( message: Json, storage: &State, ) -> Result<(), RocketErrorResponse> { - if !message.from_allowed() { - return Err(RocketErrorResponse::new( - "Monitor not registered to submit results".to_string(), - rocket::http::Status::Forbidden, - )); - } + todo!("rebasing"); - if !message.timely() { - return Err(RocketErrorResponse::new( - "Message is too old".to_string(), - rocket::http::Status::BadRequest, - )); - } - - if !message.verify() { - return Err(RocketErrorResponse::new( - "Invalid signature".to_string(), - rocket::http::Status::BadRequest, - )); - } - - match storage - .manager - .submit_mixnode_statuses_v2(message.results()) - .await - { - Ok(_) => Ok(()), - Err(err) => { - error!("failed to submit node monitoring results: {}", err); - Err(RocketErrorResponse::new( - "failed to submit node monitoring results".to_string(), - rocket::http::Status::InternalServerError, - )) - } - } + // if !message.from_allowed() { + // return Err(RocketErrorResponse::new( + // "Monitor not registered to submit results".to_string(), + // rocket::http::Status::Forbidden, + // )); + // } + // + // if !message.timely() { + // return Err(RocketErrorResponse::new( + // "Message is too old".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // if !message.verify() { + // return Err(RocketErrorResponse::new( + // "Invalid signature".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // match storage + // .manager + // .submit_mixnode_statuses_v2(message.results()) + // .await + // { + // Ok(_) => Ok(()), + // Err(err) => { + // error!("failed to submit node monitoring results: {}", err); + // Err(RocketErrorResponse::new( + // "failed to submit node monitoring results".to_string(), + // rocket::http::Status::InternalServerError, + // )) + // } + // } } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//report")] pub(crate) async fn gateway_report( cache: &State, identity: &str, ) -> Result, RocketErrorResponse> { - Ok(Json(_gateway_report(cache, identity).await?)) + todo!("rebasing"); + + // Ok(Json(_gateway_report(cache, identity).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//history")] pub(crate) async fn gateway_uptime_history( storage: &State, + nym_contract_cache: &State, identity: &str, ) -> Result, RocketErrorResponse> { - Ok(Json(_gateway_uptime_history(storage, identity).await?)) + todo!("rebasing"); + + // Ok(Json( + // _gateway_uptime_history(storage, nym_contract_cache, identity).await?, + // )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//core-status-count?")] pub(crate) async fn gateway_core_status_count( storage: &State, @@ -146,29 +153,34 @@ pub(crate) async fn gateway_core_status_count( )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//report")] pub(crate) async fn mixnode_report( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json(_mixnode_report(cache, mix_id).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//history")] pub(crate) async fn mixnode_uptime_history( storage: &State, - mix_id: MixId, + nym_contract_cache: &State, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { - Ok(Json(_mixnode_uptime_history(storage, mix_id).await?)) + todo!("rebasing"); + + // Ok(Json( + // _mixnode_uptime_history(storage, nym_contract_cache, mix_id).await?, + // )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//core-status-count?")] pub(crate) async fn mixnode_core_status_count( storage: &State, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result, RocketErrorResponse> { Ok(Json( @@ -176,28 +188,28 @@ pub(crate) async fn mixnode_core_status_count( )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//status")] pub(crate) async fn get_mixnode_status( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Json { Json(_get_mixnode_status(cache, mix_id).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//reward-estimation")] pub(crate) async fn get_mixnode_reward_estimation( cache: &State, validator_cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?, )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[post( "/mixnode//compute-reward-estimation", data = "" @@ -206,7 +218,7 @@ pub(crate) async fn compute_mixnode_reward_estimation( user_reward_param: Json, cache: &State, validator_cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _compute_mixnode_reward_estimation( @@ -219,39 +231,39 @@ pub(crate) async fn compute_mixnode_reward_estimation( )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//stake-saturation")] pub(crate) async fn get_mixnode_stake_saturation( cache: &State, validator_cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?, )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//inclusion-probability")] pub(crate) async fn get_mixnode_inclusion_probability( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_inclusion_probability(cache, mix_id).await?, )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//avg_uptime")] pub(crate) async fn get_mixnode_avg_uptime( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//avg_uptime")] pub(crate) async fn get_gateway_avg_uptime( cache: &State, @@ -260,7 +272,7 @@ pub(crate) async fn get_gateway_avg_uptime( Ok(Json(_get_gateway_avg_uptime(cache, identity).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/inclusion_probability")] pub(crate) async fn get_mixnode_inclusion_probabilities( cache: &State, @@ -268,15 +280,17 @@ pub(crate) async fn get_mixnode_inclusion_probabilities( Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/detailed")] pub async fn get_mixnodes_detailed( cache: &State, ) -> Json> { - Json(_get_mixnodes_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_legacy_mixnodes_detailed(cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/detailed-unfiltered")] pub async fn get_mixnodes_detailed_unfiltered( cache: &State, @@ -284,36 +298,45 @@ pub async fn get_mixnodes_detailed_unfiltered( Json(_get_mixnodes_detailed_unfiltered(cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/rewarded/detailed")] pub async fn get_rewarded_set_detailed( - cache: &State, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_rewarded_set_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/active/detailed")] pub async fn get_active_set_detailed( - cache: &State, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_active_set_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateways/detailed")] pub async fn get_gateways_detailed( cache: &State, ) -> Json> { - Json(_get_gateways_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_legacy_gateways_detailed(cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateways/detailed-unfiltered")] pub async fn get_gateways_detailed_unfiltered( cache: &State, ) -> Json> { - Json(_get_gateways_detailed_unfiltered(cache).await) + todo!("rebasing"); + // Json(_get_legacy_gateways_detailed_unfiltered(cache).await) } pub mod unstable { @@ -325,7 +348,7 @@ pub mod unstable { TestRoute, }; use nym_api_requests::pagination::Pagination; - use nym_mixnet_contract_common::MixId; + use nym_mixnet_contract_common::NodeId; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; @@ -421,7 +444,7 @@ pub mod unstable { const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; async fn _mixnode_test_results( - mix_id: MixId, + mix_id: NodeId, page: u32, per_page: u32, info_cache: &State, @@ -482,7 +505,7 @@ pub mod unstable { #[openapi(tag = "UNSTABLE - DO **NOT** USE")] #[get("/mixnodes/unstable//test-results?")] pub async fn mixnode_test_results( - mix_id: MixId, + mix_id: NodeId, pagination: PaginationRequest, info_cache: &State, storage: &State, diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index 2a33a0118f..67de44839b 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -6,11 +6,12 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::ONE_DAY; use crate::storage::NymApiStorage; -use log::error; use nym_task::{TaskClient, TaskManager}; use std::time::Duration; use time::{OffsetDateTime, PrimitiveDateTime, Time}; use tokio::time::{interval, sleep}; +use tracing::error; +use tracing::{info, trace, warn}; pub(crate) struct HistoricalUptimeUpdater { storage: NymApiStorage, @@ -88,7 +89,7 @@ impl HistoricalUptimeUpdater { // resultant Duration is positive let time_left: Duration = (update_datetime - now).try_into().unwrap(); - log::info!( + info!( "waiting until {update_datetime} to update the historical uptimes for the first time ({} seconds left)", time_left.as_secs() ); diff --git a/nym-api/src/node_status_api/utils.rs b/nym-api/src/node_status_api/utils.rs index 0682cf62ba..4d86be5faa 100644 --- a/nym-api/src/node_status_api/utils.rs +++ b/nym-api/src/node_status_api/utils.rs @@ -4,23 +4,23 @@ use crate::node_status_api::models::Uptime; use crate::node_status_api::{FIFTEEN_MINUTES, ONE_HOUR}; use crate::storage::models::NodeStatus; -use log::warn; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use tracing::warn; use time::OffsetDateTime; // A temporary helper structs used to produce reports for active nodes. pub(crate) struct ActiveMixnodeStatuses { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) statuses: Vec, } pub(crate) struct ActiveGatewayStatuses { + pub(crate) node_id: NodeId, + pub(crate) identity: String, - pub(crate) owner: String, pub(crate) statuses: Vec, } diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index c04404f2df..2ced76b8fe 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -2,44 +2,41 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::caching::Cache; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::{ - families::FamilyHead, GatewayBond, IdentityKey, Interval, MixId, MixNodeDetails, - RewardingParams, -}; +use nym_mixnet_contract_common::{Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams}; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; pub(crate) struct ValidatorCacheData { - pub(crate) mixnodes: Cache>, - pub(crate) gateways: Cache>, + pub(crate) legacy_mixnodes: Cache>, + pub(crate) legacy_gateways: Cache>, + pub(crate) nym_nodes: Cache>, + pub(crate) rewarded_set: Cache, - pub(crate) mixnodes_blacklist: Cache>, - pub(crate) gateways_blacklist: Cache>, - - pub(crate) rewarded_set: Cache>, - pub(crate) active_set: Cache>, + // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. + // instead clients are meant to be filtering out them themselves based on the provided scores. + pub(crate) legacy_mixnodes_blacklist: Cache>, + pub(crate) legacy_gateways_blacklist: Cache>, pub(crate) current_reward_params: Cache>, pub(crate) current_interval: Cache>, - pub(crate) mix_to_family: Cache>, - pub(crate) contracts_info: Cache, } impl ValidatorCacheData { pub(crate) fn new() -> Self { ValidatorCacheData { - mixnodes: Cache::default(), - gateways: Cache::default(), + legacy_mixnodes: Cache::default(), + legacy_gateways: Cache::default(), + nym_nodes: Default::default(), rewarded_set: Cache::default(), - active_set: Cache::default(), - mixnodes_blacklist: Cache::default(), - gateways_blacklist: Cache::default(), + + legacy_mixnodes_blacklist: Cache::default(), + legacy_gateways_blacklist: Cache::default(), current_interval: Cache::default(), current_reward_params: Cache::default(), - mix_to_family: Cache::default(), contracts_info: Cache::default(), } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index efaeac74a6..8f7e26caa5 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -4,12 +4,11 @@ use crate::nym_contract_cache::cache::data::CachedContractsInfo; use crate::support::caching::Cache; use data::ValidatorCacheData; -use nym_api_requests::models::MixnodeStatus; -use nym_mixnet_contract_common::{ - families::FamilyHead, GatewayBond, IdentityKey, Interval, MixId, MixNodeBond, MixNodeDetails, - RewardingParams, +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use rocket::fairing::AdHoc; +use nym_api_requests::models::MixnodeStatus; +use nym_mixnet_contract_common::{Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams}; use std::{ collections::HashSet, sync::{ @@ -18,12 +17,15 @@ use std::{ }, time::Duration, }; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::time; +use tracing::{debug, error}; mod data; pub(crate) mod refresher; +const CACHE_TIMEOUT_MS: u64 = 100; + #[derive(Clone)] pub struct NymContractCache { pub(crate) initialised: Arc, @@ -38,38 +40,55 @@ impl NymContractCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub fn stage() -> AdHoc { - AdHoc::on_ignite("Validator Cache Stage", |rocket| async { - rocket.manage(Self::new()) - }) + /// Returns a copy of the current cache data. + async fn get_owned( + &self, + fn_arg: impl FnOnce(RwLockReadGuard<'_, ValidatorCacheData>) -> Cache, + ) -> Option> { + match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { + Ok(cache) => Some(fn_arg(cache)), + Err(e) => { + error!("{e}"); + None + } + } + } + + async fn get<'a, T: 'a>( + &'a self, + fn_arg: impl FnOnce(&ValidatorCacheData) -> &Cache, + ) -> Option>> { + match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { + Ok(cache) => Some(RwLockReadGuard::map(cache, |item| fn_arg(item))), + Err(e) => { + error!("{e}"); + None + } + } } - #[allow(clippy::too_many_arguments)] pub(crate) async fn update( &self, - mixnodes: Vec, - gateways: Vec, - rewarded_set: Vec, - active_set: Vec, + mixnodes: Vec, + gateways: Vec, + nym_nodes: Vec, + rewarded_set: RewardedSet, rewarding_params: RewardingParams, current_interval: Interval, - mix_to_family: Vec<(IdentityKey, FamilyHead)>, nym_contracts_info: CachedContractsInfo, ) { match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes.unchecked_update(mixnodes); - cache.gateways.unchecked_update(gateways); + cache.legacy_mixnodes.unchecked_update(mixnodes); + cache.legacy_gateways.unchecked_update(gateways); + cache.nym_nodes.unchecked_update(nym_nodes); cache.rewarded_set.unchecked_update(rewarded_set); - cache.active_set.unchecked_update(active_set); cache .current_reward_params .unchecked_update(Some(rewarding_params)); cache .current_interval .unchecked_update(Some(current_interval)); - cache.mix_to_family.unchecked_update(mix_to_family); cache.contracts_info.unchecked_update(nym_contracts_info) } Err(err) => { @@ -78,39 +97,31 @@ impl NymContractCache { } } - pub async fn mixnodes_blacklist(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes_blacklist.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(HashSet::new()) - } - } + pub async fn mixnodes_blacklist(&self) -> Cache> { + self.get_owned(|cache| cache.legacy_mixnodes_blacklist.clone_cache()) + .await + .unwrap_or_default() } - pub async fn gateways_blacklist(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways_blacklist.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(HashSet::new()) - } - } + pub async fn gateways_blacklist(&self) -> Cache> { + self.get_owned(|cache| cache.legacy_gateways_blacklist.clone_cache()) + .await + .unwrap_or_default() } - pub async fn update_mixnodes_blacklist(&self, add: HashSet, remove: HashSet) { + pub async fn update_mixnodes_blacklist(&self, add: HashSet, remove: HashSet) { let blacklist = self.mixnodes_blacklist().await; - let mut blacklist = blacklist.union(&add).cloned().collect::>(); + let mut blacklist = blacklist.union(&add).cloned().collect::>(); let to_remove = blacklist .intersection(&remove) .cloned() - .collect::>(); + .collect::>(); for key in to_remove { blacklist.remove(&key); } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes_blacklist.unchecked_update(blacklist); + cache.legacy_mixnodes_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update mixnodes blacklist: {err}"); @@ -118,26 +129,19 @@ impl NymContractCache { } } - pub async fn update_gateways_blacklist( - &self, - add: HashSet, - remove: HashSet, - ) { + pub async fn update_gateways_blacklist(&self, add: HashSet, remove: HashSet) { let blacklist = self.gateways_blacklist().await; - let mut blacklist = blacklist - .union(&add) - .cloned() - .collect::>(); + let mut blacklist = blacklist.union(&add).cloned().collect::>(); let to_remove = blacklist .intersection(&remove) .cloned() - .collect::>(); + .collect::>(); for key in to_remove { blacklist.remove(&key); } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.gateways_blacklist.unchecked_update(blacklist); + cache.legacy_gateways_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update gateways blacklist: {err}"); @@ -145,8 +149,8 @@ impl NymContractCache { } } - pub async fn mixnodes_filtered(&self) -> Vec { - let mixnodes = self.mixnodes_all().await; + pub async fn legacy_mixnodes_filtered(&self) -> Vec { + let mixnodes = self.legacy_mixnodes_all().await; if mixnodes.is_empty() { return Vec::new(); } @@ -162,34 +166,65 @@ impl NymContractCache { } } - pub async fn mixnodes_all(&self) -> Vec { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes.clone(), - Err(err) => { - error!("{err}"); - Vec::new() - } - } + pub async fn all_cached_legacy_mixnodes( + &self, + ) -> Option>>> { + self.get(|c| &c.legacy_mixnodes).await } - pub async fn mixnodes_filtered_basic(&self) -> Vec { - self.mixnodes_filtered() + pub async fn legacy_gateway_owner(&self, node_id: NodeId) -> Option { + self.get(|c| &c.legacy_gateways) + .await? + .iter() + .find(|g| g.node_id == node_id) + .map(|g| g.owner.to_string()) + } + + pub async fn legacy_mixnode_owner(&self, node_id: NodeId) -> Option { + self.get(|c| &c.legacy_mixnodes) + .await? + .iter() + .find(|m| m.mix_id() == node_id) + .map(|m| m.bond_information.owner.to_string()) + } + + pub async fn all_cached_legacy_gateways( + &self, + ) -> Option>>> { + self.get(|c| &c.legacy_gateways).await + } + + pub async fn all_cached_nym_nodes( + &self, + ) -> Option>>> { + self.get(|c| &c.nym_nodes).await + } + + pub async fn legacy_mixnodes_all(&self) -> Vec { + self.get_owned(|cache| cache.legacy_mixnodes.clone_cache()) + .await + .unwrap_or_default() + .into_inner() + } + + pub async fn legacy_mixnodes_filtered_basic(&self) -> Vec { + self.legacy_mixnodes_filtered() .await .into_iter() .map(|bond| bond.bond_information) .collect() } - pub async fn mixnodes_all_basic(&self) -> Vec { - self.mixnodes_all() + pub async fn legacy_mixnodes_all_basic(&self) -> Vec { + self.legacy_mixnodes_all() .await .into_iter() .map(|bond| bond.bond_information) .collect() } - pub async fn gateways_filtered(&self) -> Vec { - let gateways = self.gateways_all().await; + pub async fn legacy_gateways_filtered(&self) -> Vec { + let gateways = self.legacy_gateways_all().await; if gateways.is_empty() { return Vec::new(); } @@ -199,107 +234,122 @@ impl NymContractCache { if !blacklist.is_empty() { gateways .into_iter() - .filter(|mix| !blacklist.contains(mix.identity())) + .filter(|gw| !blacklist.contains(&gw.node_id)) .collect() } else { gateways } } - pub async fn gateways_all(&self) -> Vec { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways.clone(), - Err(err) => { - error!("{err}"); - Vec::new() - } - } + pub async fn legacy_gateways_all(&self) -> Vec { + self.get_owned(|cache| cache.legacy_gateways.clone_cache()) + .await + .unwrap_or_default() + .into_inner() } - pub async fn rewarded_set(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.rewarded_set.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } - } + pub async fn nym_nodes(&self) -> Vec { + self.get_owned(|cache| cache.nym_nodes.clone_cache()) + .await + .unwrap_or_default() + .into_inner() } - pub async fn active_set(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.active_set.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } - } + pub async fn nym_nodes_filtered(&self) -> Vec { + todo!() } - pub async fn mix_to_family(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mix_to_family.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } + pub async fn rewarded_set(&self) -> Option>> { + self.get(|cache| &cache.rewarded_set).await + } + + pub async fn rewarded_set_owned(&self) -> Cache { + self.get_owned(|cache| cache.rewarded_set.clone_cache()) + .await + .unwrap_or_default() + } + + pub async fn legacy_v1_rewarded_set_mixnodes(&self) -> Vec { + let Some(rewarded_set) = self.rewarded_set().await else { + return Vec::new(); + }; + + let mut rewarded_nodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + // rewarded mixnode = active or standby + for standby in &rewarded_set.standby { + rewarded_nodes.insert(*standby); } + + self.legacy_mixnodes_all() + .await + .into_iter() + .filter(|m| rewarded_nodes.contains(&m.mix_id())) + .collect() + } + + pub async fn legacy_v1_active_set_mixnodes(&self) -> Vec { + let Some(rewarded_set) = self.rewarded_set().await else { + return Vec::new(); + }; + + let active_nodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + self.legacy_mixnodes_all() + .await + .into_iter() + .filter(|m| active_nodes.contains(&m.mix_id())) + .collect() } pub(crate) async fn interval_reward_params(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_reward_params.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(None) - } - } + self.get_owned(|cache| cache.current_reward_params.clone_cache()) + .await + .unwrap_or_default() } pub(crate) async fn current_interval(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_interval.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(None) - } - } + self.get_owned(|cache| cache.current_interval.clone_cache()) + .await + .unwrap_or_default() } pub(crate) async fn contract_details(&self) -> Cache { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.contracts_info.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::default() - } - } + self.get_owned(|cache| cache.contracts_info.clone_cache()) + .await + .unwrap_or_default() } - pub async fn mixnode_details(&self, mix_id: MixId) -> (Option, MixnodeStatus) { - // it might not be the most optimal to possibly iterate the entire vector to find (or not) - // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) + pub async fn legacy_mixnode_details( + &self, + mix_id: NodeId, + ) -> (Option, MixnodeStatus) { + // the old behaviour was to get the nodes from the filtered list, so let's not change it here + let rewarded_set = self.rewarded_set_owned().await; + let all_bonded = &self.legacy_mixnodes_filtered().await; + let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) else { + return (None, MixnodeStatus::NotFound); + }; - let active_set = &self.active_set().await; - if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { + if rewarded_set.is_active_mixnode(&mix_id) { return (Some(bond.clone()), MixnodeStatus::Active); } - let rewarded_set = &self.rewarded_set().await; - if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { + if rewarded_set.is_standby(&mix_id) { return (Some(bond.clone()), MixnodeStatus::Standby); } - let all_bonded = &self.mixnodes_filtered().await; - if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { - (Some(bond.clone()), MixnodeStatus::Inactive) - } else { - (None, MixnodeStatus::NotFound) - } + (Some(bond.clone()), MixnodeStatus::Inactive) } - pub async fn mixnode_status(&self, mix_id: MixId) -> MixnodeStatus { - self.mixnode_details(mix_id).await.1 + pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { + self.legacy_mixnode_details(mix_id).await.1 } pub fn initialised(&self) -> bool { diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index d200a3445d..a46ef92afd 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -6,14 +6,21 @@ use crate::nym_contract_cache::cache::data::{CachedContractInfo, CachedContracts use crate::nyxd::Client; use crate::support::caching::CacheNotification; use anyhow::Result; -use nym_mixnet_contract_common::{MixId, MixNodeDetails, RewardedSetNodeStatus}; +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use nym_mixnet_contract_common::{LegacyMixLayer, RewardedSet}; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, NymContractsProvider, VestingQueryClient, }; +use rand::prelude::SliceRandom; +use rand::rngs::OsRng; +use std::collections::HashSet; use std::{collections::HashMap, sync::atomic::Ordering, time::Duration}; use tokio::sync::watch; use tokio::time; +use tracing::{error, info, trace, warn}; pub struct NymContractCacheRefresher { nyxd_client: Client, @@ -109,15 +116,66 @@ impl NymContractCacheRefresher { let rewarding_params = self.nyxd_client.get_current_rewarding_parameters().await?; let current_interval = self.nyxd_client.get_current_interval().await?.interval; - let mixnodes = self.nyxd_client.get_mixnodes().await?; - let gateways = self.nyxd_client.get_gateways().await?; + let nym_nodes = self.nyxd_client.get_nymnodes().await?; + let mixnode_details = self.nyxd_client.get_mixnodes().await?; + let gateway_bonds = self.nyxd_client.get_gateways().await?; + let gateway_ids: HashMap<_, _> = self + .nyxd_client + .get_gateway_ids() + .await? + .into_iter() + .map(|id| (id.identity, id.node_id)) + .collect(); - let mix_to_family = self.nyxd_client.get_all_family_members().await?; + let mut gateways = Vec::with_capacity(gateway_bonds.len()); + for bond in gateway_bonds { + // we explicitly panic here because that value MUST exist. + // if it doesn't, we messed up the migration and we have big problems + let node_id = *gateway_ids.get(bond.identity()).unwrap_or_else(|| { + panic!( + "CONTRACT DATA INCONSISTENCY: MISSING GATEWAY ID FOR: {}", + bond.identity() + ) + }); + gateways.push(LegacyGatewayBondWithId { bond, node_id }) + } - let rewarded_set_map = self.get_rewarded_set_map().await; + let rewarded_set = self.get_rewarded_set().await; + let layer1 = rewarded_set.layer1.iter().collect::>(); + let layer2 = rewarded_set.layer2.iter().collect::>(); + let layer3 = rewarded_set.layer3.iter().collect::>(); - let (rewarded_set, active_set) = - Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set_map); + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + let mut mixnodes = Vec::with_capacity(mixnode_details.len()); + for detail in mixnode_details { + // if node is not in the rewarded set, well. + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + let layer = if layer1.contains(&detail.mix_id()) { + LegacyMixLayer::One + } else if layer2.contains(&detail.mix_id()) { + LegacyMixLayer::Two + } else if layer3.contains(&detail.mix_id()) { + LegacyMixLayer::Three + } else { + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(&mut rng).copied().unwrap() + }; + + mixnodes.push(LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: detail.bond_information, + layer, + }, + rewarding_details: detail.rewarding_details, + pending_changes: detail.pending_changes, + }) + } let contract_info = self.get_nym_contracts_info().await?; @@ -131,11 +189,10 @@ impl NymContractCacheRefresher { .update( mixnodes, gateways, + nym_nodes, rewarded_set, - active_set, rewarding_params, current_interval, - mix_to_family, contract_info, ) .await; @@ -147,32 +204,31 @@ impl NymContractCacheRefresher { Ok(()) } - async fn get_rewarded_set_map(&self) -> HashMap { + async fn get_rewarded_set(&self) -> RewardedSet { self.nyxd_client - .get_rewarded_set_mixnodes() + .get_rewarded_set_nodes() .await - .map(|nodes| nodes.into_iter().collect()) .unwrap_or_default() } - fn collect_rewarded_and_active_set_details( - all_mixnodes: &[MixNodeDetails], - rewarded_set_nodes: &HashMap, - ) -> (Vec, Vec) { - let mut active_set = Vec::new(); - let mut rewarded_set = Vec::new(); - - for mix in all_mixnodes { - if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { - rewarded_set.push(mix.clone()); - if status.is_active() { - active_set.push(mix.clone()) - } - } - } - - (rewarded_set, active_set) - } + // fn collect_rewarded_and_active_set_details( + // all_mixnodes: &[MixNodeDetails], + // rewarded_set_nodes: RewardedSet, + // ) -> (Vec, Vec) { + // let mut active_set = Vec::new(); + // let mut rewarded_set = Vec::new(); + // + // for mix in all_mixnodes { + // if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { + // rewarded_set.push(mix.clone()); + // if status.is_active() { + // active_set.push(mix.clone()) + // } + // } + // } + // + // (rewarded_set, active_set) + // } pub(crate) async fn run(&self, mut shutdown: TaskClient) { let mut interval = time::interval(self.caching_interval); diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs index 84f2c70fd2..d57f1317c4 100644 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -1,20 +1,21 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::helpers_deprecated::{ - _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, - }, - v2::AxumAppState, +use crate::node_status_api::helpers::{ + _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, + _get_rewarded_set_legacy_mixnodes_detailed, }; -use axum::{extract, Router}; +use crate::support::http::state::AppState; +use axum::extract::State; +use axum::{Json, Router}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, -}; +use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; use std::collections::HashSet; -pub(crate) fn nym_contract_cache_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn nym_contract_cache_routes() -> Router { Router::new() .route("/mixnodes", axum::routing::get(get_mixnodes)) .route( @@ -52,13 +53,16 @@ pub(crate) fn nym_contract_cache_routes() -> Router { get, path = "/v1/mixnodes", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] -async fn get_mixnodes( - extract::State(state): extract::State, -) -> axum::Json> { - state.nym_contract_cache().mixnodes_filtered().await.into() +#[deprecated] +async fn get_mixnodes(State(state): State) -> Json> { + state + .nym_contract_cache() + .legacy_mixnodes_filtered() + .await + .into() } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -76,10 +80,9 @@ async fn get_mixnodes( (status = 200, body = Vec) ) )] -async fn get_mixnodes_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_mixnodes_detailed(state.node_status_cache()) +#[deprecated] +async fn get_mixnodes_detailed(State(state): State) -> Json> { + _get_legacy_mixnodes_detailed(state.node_status_cache()) .await .into() } @@ -92,10 +95,17 @@ async fn get_mixnodes_detailed( (status = 200, body = Vec) ) )] -async fn get_gateways( - extract::State(state): extract::State, -) -> axum::Json> { - state.nym_contract_cache().gateways_filtered().await.into() +#[deprecated] +async fn get_gateways(State(state): State) -> Json> { + Json( + state + .nym_contract_cache() + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(), + ) } #[utoipa::path( @@ -103,18 +113,20 @@ async fn get_gateways( get, path = "/v1/mixnodes/rewarded", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] +#[deprecated] async fn get_rewarded_set( - extract::State(state): extract::State, -) -> axum::Json> { - state - .nym_contract_cache() - .rewarded_set() - .await - .to_owned() - .into() + State(state): State, +) -> Json> { + Json( + state + .nym_contract_cache() + .legacy_v1_rewarded_set_mixnodes() + .await + .clone(), + ) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -132,12 +144,16 @@ async fn get_rewarded_set( (status = 200, body = Vec) ) )] +#[deprecated] async fn get_rewarded_set_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_rewarded_set_detailed(state.node_status_cache()) - .await - .into() + State(state): State, +) -> Json> { + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await + .into() } #[utoipa::path( @@ -145,17 +161,16 @@ async fn get_rewarded_set_detailed( get, path = "/v1/mixnodes/active", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] -async fn get_active_set( - extract::State(state): extract::State, -) -> axum::Json> { +#[deprecated] +async fn get_active_set(State(state): State) -> Json> { state .nym_contract_cache() - .active_set() + .legacy_v1_active_set_mixnodes() .await - .to_owned() + .clone() .into() } @@ -175,10 +190,9 @@ async fn get_active_set( (status = 200, body = Vec) ) )] -async fn get_active_set_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_active_set_detailed(state.node_status_cache()) +#[deprecated] +async fn get_active_set_detailed(State(state): State) -> Json> { + _get_active_set_legacy_mixnodes_detailed(state.node_status_cache(), state.nym_contract_cache()) .await .into() } @@ -188,12 +202,11 @@ async fn get_active_set_detailed( get, path = "/v1/mixnodes/blacklisted", responses( - (status = 200, body = Option>) + (status = 200, body = Option>) ) )] -async fn get_blacklisted_mixnodes( - extract::State(state): extract::State, -) -> axum::Json>> { +#[deprecated] +async fn get_blacklisted_mixnodes(State(state): State) -> Json>> { let blacklist = state .nym_contract_cache() .mixnodes_blacklist() @@ -215,20 +228,22 @@ async fn get_blacklisted_mixnodes( (status = 200, body = Option>) ) )] -async fn get_blacklisted_gateways( - extract::State(state): extract::State, -) -> axum::Json>> { - let blacklist = state - .nym_contract_cache() - .gateways_blacklist() - .await - .to_owned(); +#[deprecated] +async fn get_blacklisted_gateways(State(state): State) -> Json>> { + let cache = state.nym_contract_cache(); + let blacklist = cache.gateways_blacklist().await.clone(); if blacklist.is_empty() { - None + Json(None) } else { - Some(blacklist) + let gateways = cache.legacy_gateways_all().await; + Json(Some( + gateways + .into_iter() + .filter(|g| blacklist.contains(&g.node_id)) + .map(|g| g.gateway.identity_key.clone()) + .collect(), + )) } - .into() } #[utoipa::path( @@ -240,8 +255,8 @@ async fn get_blacklisted_gateways( ) )] async fn get_interval_reward_params( - extract::State(state): extract::State, -) -> axum::Json> { + State(state): State, +) -> Json> { state .nym_contract_cache() .interval_reward_params() @@ -258,9 +273,7 @@ async fn get_interval_reward_params( (status = 200, body = Option) ) )] -async fn get_current_epoch( - extract::State(state): extract::State, -) -> axum::Json> { +async fn get_current_epoch(State(state): State) -> Json> { state .nym_contract_cache() .current_interval() diff --git a/nym-api/src/nym_contract_cache/legacy_helpers.rs b/nym-api/src/nym_contract_cache/legacy_helpers.rs new file mode 100644 index 0000000000..939f19b3a9 --- /dev/null +++ b/nym-api/src/nym_contract_cache/legacy_helpers.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/nym_contract_cache/mod.rs index 9b1f48d9ad..49f4147a69 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/nym_contract_cache/mod.rs @@ -4,33 +4,30 @@ use crate::nym_contract_cache::cache::NymContractCache; use crate::support::{self, config, nyxd}; use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; use self::cache::refresher::NymContractCacheRefresher; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; - -pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_mixnodes, - routes::get_mixnodes_detailed, - routes::get_gateways, - routes::get_active_set, - routes::get_active_set_detailed, - routes::get_rewarded_set, - routes::get_rewarded_set_detailed, - routes::get_blacklisted_mixnodes, - routes::get_blacklisted_gateways, - routes::get_interval_reward_params, - routes::get_current_epoch, - ] -} +pub(crate) mod legacy_helpers; +// pub(crate) mod routes; +// +// pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: routes::get_mixnodes, +// routes::get_mixnodes_detailed, +// routes::get_gateways, +// routes::get_active_set, +// routes::get_active_set_detailed, +// routes::get_rewarded_set, +// routes::get_rewarded_set_detailed, +// routes::get_blacklisted_mixnodes, +// routes::get_blacklisted_gateways, +// routes::get_blacklisted_gateways_v2, +// routes::get_interval_reward_params, +// routes::get_current_epoch, +// ] +// } pub(crate) fn start_refresher( config: &config::NodeStatusAPI, diff --git a/nym-api/src/nym_contract_cache/routes.rs b/nym-api/src/nym_contract_cache/routes.rs index cef7f04c9c..52a9dd70e0 100644 --- a/nym-api/src/nym_contract_cache/routes.rs +++ b/nym-api/src/nym_contract_cache/routes.rs @@ -1,28 +1,21 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::{ - helpers_deprecated::{ - _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, - }, - NodeStatusCache, - }, - nym_contract_cache::cache::NymContractCache, -}; +use crate::{node_status_api::NodeStatusCache, nym_contract_cache::cache::NymContractCache}; use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, -}; +use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use rocket::{serde::json::Json, State}; use rocket_okapi::openapi; use std::collections::HashSet; -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes")] -pub async fn get_mixnodes(cache: &State) -> Json> { - Json(cache.mixnodes_filtered().await) +pub async fn get_mixnodes( + cache: &State, +) -> Json> { + Json(cache.legacy_mixnodes_filtered().await) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -32,24 +25,34 @@ pub async fn get_mixnodes(cache: &State) -> Json, ) -> Json> { - Json(_get_mixnodes_detailed(cache).await) + todo!("rebasing") + // Json(_get_legacy_mixnodes_detailed(cache).await) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/gateways")] pub async fn get_gateways(cache: &State) -> Json> { - Json(cache.gateways_filtered().await) + Json( + cache + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(), + ) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes/rewarded")] -pub async fn get_rewarded_set(cache: &State) -> Json> { - Json(cache.rewarded_set().await.clone()) +pub async fn get_rewarded_set( + cache: &State, +) -> Json> { + Json(cache.legacy_v1_rewarded_set_mixnodes().await.clone()) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -59,18 +62,22 @@ pub async fn get_rewarded_set(cache: &State) -> Json, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_rewarded_set_detailed(cache).await) + todo!("rebasing") + // Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes/active")] -pub async fn get_active_set(cache: &State) -> Json> { - Json(cache.active_set().await.clone()) +pub async fn get_active_set( + cache: &State, +) -> Json> { + Json(cache.legacy_v1_active_set_mixnodes().await.clone()) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -80,19 +87,21 @@ pub async fn get_active_set(cache: &State) -> Json, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_active_set_detailed(cache).await) + todo!("rebasing") + // Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes/blacklisted")] pub async fn get_blacklisted_mixnodes( cache: &State, -) -> Json>> { +) -> Json>> { let blacklist = cache.mixnodes_blacklist().await.clone(); if blacklist.is_empty() { Json(None) @@ -101,11 +110,31 @@ pub async fn get_blacklisted_mixnodes( } } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/gateways/blacklisted")] pub async fn get_blacklisted_gateways( cache: &State, ) -> Json>> { + let blacklist = cache.gateways_blacklist().await.clone(); + if blacklist.is_empty() { + Json(None) + } else { + let gateways = cache.legacy_gateways_all().await; + Json(Some( + gateways + .into_iter() + .filter(|g| blacklist.contains(&g.node_id)) + .map(|g| g.gateway.identity_key.clone()) + .collect(), + )) + } +} + +#[openapi(tag = "contract-cache", deprecated = true)] +#[get("/gateways/blacklisted_v2")] +pub async fn get_blacklisted_gateways_v2( + cache: &State, +) -> Json>> { let blacklist = cache.gateways_blacklist().await.clone(); if blacklist.is_empty() { Json(None) diff --git a/nym-api/src/nym_nodes/handlers.rs b/nym-api/src/nym_nodes/handlers.rs deleted file mode 100644 index 7f775d8de4..0000000000 --- a/nym-api/src/nym_nodes/handlers.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::v2::AxumAppState; -use axum::{extract::State, Json, Router}; -use nym_api_requests::models::{DescribedGateway, DescribedMixNode}; -use nym_mixnet_contract_common::MixNodeBond; -use std::ops::Deref; - -// obviously this should get refactored later on because gateways will go away. -// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD - -pub(crate) fn nym_node_routes() -> axum::Router { - Router::new() - .route( - "/gateways/described", - axum::routing::get(get_gateways_described), - ) - .route( - "/mixnodes/described", - axum::routing::get(get_mixnodes_described), - ) -} - -#[utoipa::path( - tag = "Nym Nodes", - get, - path = "/v1/gateways/described", - responses( - (status = 200, body = Vec) - ) -)] -async fn get_gateways_described(State(state): State) -> Json> { - let gateways = state.nym_contract_cache().gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = state.described_nodes_state().get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - gateways - .into_iter() - .map(|bond| DescribedGateway { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} - -#[utoipa::path( - tag = "Nym Nodes", - get, - path = "/v1/mixnodes/described", - responses( - (status = 200, body = Vec) - ) -)] -async fn get_mixnodes_described(State(state): State) -> Json> { - let mixnodes = state - .nym_contract_cache() - .mixnodes_filtered() - .await - .into_iter() - .map(|m| m.bond_information) - .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = state.described_nodes_state().get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - mixnodes - .into_iter() - .map(|bond| DescribedMixNode { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs new file mode 100644 index 0000000000..1ebd9260c7 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -0,0 +1,97 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::state::AppState; +use axum::extract::State; +use axum::{Json, Router}; +use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; + +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn legacy_nym_node_routes() -> Router { + Router::new() + .route( + "/gateways/described", + axum::routing::get(get_gateways_described), + ) + .route( + "/mixnodes/described", + axum::routing::get(get_mixnodes_described), + ) +} + +#[utoipa::path( + tag = "Legacy gateways", + get, + path = "/v1/gateways/described", + responses( + (status = 200, body = Vec) + ) +)] +#[deprecated] +async fn get_gateways_described( + State(state): State, +) -> Json> { + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + let gateways = contract_cache.legacy_gateways_filtered().await; + if gateways.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(gateways.into_iter().map(Into::into).collect()); + }; + + Json( + gateways + .into_iter() + .map(|bond| LegacyDescribedGateway { + self_described: self_descriptions.get_description(&bond.node_id).cloned(), + bond, + }) + .collect(), + ) +} + +#[utoipa::path( + tag = "Legacy Mixnodes", + get, + path = "/v1/mixnodes/described", + responses( + (status = 200, body = Vec) + ) +)] +#[deprecated] +async fn get_mixnodes_described( + State(state): State, +) -> Json> { + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + + let mixnodes = contract_cache + .legacy_mixnodes_filtered() + .await + .into_iter() + .map(|m| m.bond_information) + .collect::>(); + if mixnodes.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(mixnodes.into_iter().map(Into::into).collect()); + }; + + Json( + mixnodes + .into_iter() + .map(|bond| LegacyDescribedMixNode { + self_described: self_descriptions.get_description(&bond.mix_id).cloned(), + bond, + }) + .collect(), + ) +} diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs new file mode 100644 index 0000000000..7655584625 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -0,0 +1,281 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; +use crate::support::http::state::AppState; +use axum::extract::{Path, Query, State}; +use axum::routing::get; +use axum::{Json, Router}; +use nym_api_requests::models::{ + AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NymNodeData, + PerformanceHistoryResponse, UptimeHistoryResponse, +}; +use nym_api_requests::pagination::{PaginatedResponse, Pagination}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::NymNodeDetails; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use time::Date; +use utoipa::{IntoParams, ToSchema}; + +pub(crate) mod legacy; +pub(crate) mod unstable; + +pub(crate) fn nym_node_routes() -> Router { + Router::new() + .route("/bonded", get(get_bonded_nodes)) + .route("/described", get(get_described_nodes)) + .route("/annotation/:node_id", get(get_node_annotation)) + .route("/performance/:node_id", get(get_current_node_performance)) + .route( + "/historical-performance/:node_id", + get(get_historical_performance), + ) + .route( + "/performance-history/:node_id", + get(get_node_performance_history), + ) + // to make it compatible with all the explorers that were used to using 0-100 values + .route("/uptime-history/:node_id", get(get_node_uptime_history)) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/bonded", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn get_bonded_nodes( + State(state): State, + Query(pagination): Query, +) -> Json> { + // TODO: implement it + let _ = pagination; + + let details = state.nym_contract_cache().nym_nodes().await; + let total = details.len(); + + Json(PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: details, + }) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/described", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn get_described_nodes( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // TODO: implement it + let _ = pagination; + + let cache = state.described_nodes_cache.get().await?; + let descriptions = cache.all_nodes(); + + let data = descriptions + .map(|n| &n.description) + .cloned() + .collect::>(); + + Ok(Json(PaginatedResponse { + pagination: Pagination { + total: data.len(), + page: 0, + size: data.len(), + }, + data, + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/annotation/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = AnnotationResponse) + ), + params(NodeIdParam), +)] +async fn get_node_annotation( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(AnnotationResponse { + node_id, + annotation: annotations.get(&node_id).cloned(), + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/performance/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = NodePerformanceResponse) + ), + params(NodeIdParam), +)] +async fn get_current_node_performance( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(NodePerformanceResponse { + node_id, + performance: annotations + .get(&node_id) + .map(|n| n.last_24h_performance.naive_to_f64()), + })) +} + +// todo; probably extract it to requests crate +#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema, JsonSchema)] +#[into_params(parameter_in = Query)] +pub(crate) struct DateQuery { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub(crate) date: Date, +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/historical-performance/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = NodeDatePerformanceResponse) + ), + params(DateQuery, NodeIdParam) +)] +async fn get_historical_performance( + Path(NodeIdParam { node_id }): Path, + Query(DateQuery { date }): Query, + State(state): State, +) -> AxumResult> { + let uptime = state + .storage() + .get_historical_node_uptime_on(node_id, date) + .await?; + + Ok(Json(NodeDatePerformanceResponse { + node_id, + date, + performance: uptime.and_then(|u| { + Performance::from_percentage_value(u.uptime as u64) + .map(|p| p.naive_to_f64()) + .ok() + }), + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/performance-history/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PerformanceHistoryResponse) + ), + params(PaginationRequest, NodeIdParam) +)] +async fn get_node_performance_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(PerformanceHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/uptime-history/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PerformanceHistoryResponse) + ), + params(PaginationRequest, NodeIdParam) +)] +async fn get_node_uptime_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(UptimeHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} diff --git a/nym-api/src/nym_nodes/handlers_unstable.rs b/nym-api/src/nym_nodes/handlers/unstable.rs similarity index 52% rename from nym-api/src/nym_nodes/handlers_unstable.rs rename to nym-api/src/nym_nodes/handlers/unstable.rs index f0a7d8cc8b..2fab1eef94 100644 --- a/nym-api/src/nym_nodes/handlers_unstable.rs +++ b/nym-api/src/nym_nodes/handlers/unstable.rs @@ -21,19 +21,18 @@ //! - `/gateway/` => only returns (entry) gateway role data use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Query; use axum::extract::State; use axum::{Json, Router}; use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, + CachedNodesResponse, FullFatNode, NodeRole, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, }; use nym_bin_common::version_checker; use serde::Deserialize; -use std::cmp::min; -use std::ops::Deref; +use std::collections::HashSet; -pub(crate) fn nym_node_routes_unstable() -> axum::Router { +pub(crate) fn nym_node_routes_unstable() -> axum::Router { Router::new() .route("/skimmed", axum::routing::get(nodes_basic)) .route("/semi-skimmed", axum::routing::get(nodes_expanded)) @@ -84,7 +83,7 @@ impl SemverCompatibilityQueryParam { ) )] async fn nodes_basic( - state: State, + state: State, Query(NodesParams { role, semver_compatibility, @@ -123,7 +122,7 @@ async fn nodes_basic( ) )] async fn nodes_expanded( - state: State, + state: State, Query(NodesParams { role, semver_compatibility, @@ -162,7 +161,7 @@ async fn nodes_expanded( ) )] async fn nodes_detailed( - state: State, + state: State, Query(NodesParams { role, semver_compatibility, @@ -201,61 +200,113 @@ async fn nodes_detailed( ) )] async fn gateways_basic( - state: State, + state: State, Query(SemverCompatibilityQueryParam { semver_compatibility, }): Query, ) -> AxumResult>> { let status_cache = state.node_status_cache(); - let describe_cache = state.described_nodes_state(); - let gateways_cache = - status_cache - .gateways_cache() - .await - .ok_or(AxumErrorResponse::internal_msg( - "could not obtain gateways cache", - ))?; + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); - if gateways_cache.is_empty() { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: vec![], - })); + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic + let active_gateways = rewarded_set.gateways().into_iter().collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 3. grab all legacy gateways + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_gateways = status_cache + .annotated_legacy_gateways() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); + + // 5. only return nodes that are present in the active set + let mut active_skimmed_gateways = Vec::new(); + + for (node_id, legacy) in annotated_legacy_gateways.iter() { + if !active_gateways.contains(node_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + active_skimmed_gateways.push( + described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), + ) + } else { + active_skimmed_gateways.push(legacy.into()); + } } - // if the self describe cache is unavailable don't try to use self-describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: gateways_cache.values().map(Into::into).collect(), - })); - }; + for nym_node in gateway_capable_nym_nodes { + // if this node is not an active gateway, ignore it + if !active_gateways.contains(&nym_node.node_id) { + continue; + } - let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp()); + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit + let role = if rewarded_set.is_exit(&nym_node.node_id) { + NodeRole::ExitGateway + } else { + NodeRole::EntryGateway + }; + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_gateways + .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_gateways.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); - // the same comment holds as with `get_gateways_described`. - // this is inefficient and will have to get refactored with directory v3 Ok(Json(CachedNodesResponse { - refreshed_at: refreshed_at.into(), - nodes: gateways_cache - .values() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond.gateway_bond.gateway.version, - semver_compatibility, - ) - } else { - true - } - }) - .map(|annotated_bond| { - SkimmedNode::from_described_gateway( - annotated_bond, - self_descriptions.deref().get(annotated_bond.identity()), - ) - }) - .collect(), + refreshed_at, + nodes: active_skimmed_gateways, })) } @@ -269,7 +320,7 @@ async fn gateways_basic( ) )] async fn gateways_expanded( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, @@ -287,7 +338,7 @@ async fn gateways_expanded( ) )] async fn gateways_detailed( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, @@ -305,38 +356,120 @@ async fn gateways_detailed( ) )] async fn mixnodes_basic( - state: State, + state: State, Query(SemverCompatibilityQueryParam { semver_compatibility, }): Query, ) -> AxumResult>> { - let mixnodes_cache = state - .node_status_cache() - .active_mixnodes_cache() + let status_cache = state.node_status_cache(); + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() .await - .ok_or(AxumErrorResponse::internal_msg( - "could not obtain mixnodes cache", - ))?; + .ok_or_else(AxumErrorResponse::internal)?; + + // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic + let active_mixnodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 3. grab all legacy mixnodes + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_mixnodes = status_cache + .annotated_legacy_mixnodes() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + + // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it + + // 5. only return nodes that are present in the active set + let mut active_skimmed_mixnodes = Vec::new(); + + for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { + if !active_mixnodes.contains(mix_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(mix_id) { + active_skimmed_mixnodes.push(described.to_skimmed_node( + NodeRole::Mixnode { + layer: legacy.mixnode_details.bond_information.layer.into(), + }, + legacy.node_performance.last_24h, + )) + } else { + active_skimmed_mixnodes.push(legacy.into()); + } + } + + for nym_node in mixing_nym_nodes { + // if this node is not an active mixnode, ignore it + if !active_mixnodes.contains(&nym_node.node_id) { + continue; + } + + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // SAFETY: if we determined our node IS active, it MUST have a layer + // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data + #[allow(clippy::unwrap_used)] + let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_mixnodes.push( + nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), + ); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_mixnodes.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); + Ok(Json(CachedNodesResponse { - refreshed_at: mixnodes_cache.timestamp().into(), - nodes: mixnodes_cache - .iter() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond - .mixnode_details - .bond_information - .mix_node - .version, - semver_compatibility, - ) - } else { - true - } - }) - .map(Into::into) - .collect(), + refreshed_at, + nodes: active_skimmed_mixnodes, })) } @@ -350,7 +483,7 @@ async fn mixnodes_basic( ) )] async fn mixnodes_expanded( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, @@ -368,7 +501,7 @@ async fn mixnodes_expanded( ) )] async fn mixnodes_detailed( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs index d7d5b98a7e..33a7ff2fd8 100644 --- a/nym-api/src/nym_nodes/mod.rs +++ b/nym-api/src/nym_nodes/mod.rs @@ -1,37 +1,35 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -#[cfg(feature = "axum")] pub(crate) mod handlers; -#[cfg(feature = "axum")] -pub(crate) mod handlers_unstable; -pub(crate) mod routes; -mod unstable_routes; +// pub(crate) mod routes; +// mod unstable_routes; -/// Merges the routes with http information and returns it to Rocket for serving -pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_gateways_described, routes::get_mixnodes_described - ] -} - -pub(crate) fn nym_node_routes_next(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: - unstable_routes::nodes_basic, - unstable_routes::nodes_expanded, - unstable_routes::nodes_detailed, - unstable_routes::gateways_basic, - unstable_routes::gateways_expanded, - unstable_routes::gateways_detailed, - unstable_routes::mixnodes_basic, - unstable_routes::mixnodes_expanded, - unstable_routes::mixnodes_detailed, - ] -} +// /// Merges the routes with http information and returns it to Rocket for serving +// pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: +// routes::get_gateways_described, +// routes::get_mixnodes_described, +// ] +// } +// +// pub(crate) fn nym_node_routes_next(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: +// unstable_routes::nodes_basic, +// unstable_routes::nodes_expanded, +// unstable_routes::nodes_detailed, +// unstable_routes::gateways_basic, +// unstable_routes::gateways_expanded, +// unstable_routes::gateways_detailed, +// unstable_routes::mixnodes_basic, +// unstable_routes::mixnodes_expanded, +// unstable_routes::mixnodes_detailed, +// routes::all_described_nodes, +// routes::node_description, +// routes::node_annotation_by_identity, +// routes::node_annotation +// ] +// } diff --git a/nym-api/src/nym_nodes/routes.rs b/nym-api/src/nym_nodes/routes.rs index 061cc8cf9c..f5574418cc 100644 --- a/nym-api/src/nym_nodes/routes.rs +++ b/nym-api/src/nym_nodes/routes.rs @@ -2,60 +2,115 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; -use nym_api_requests::models::{DescribedGateway, DescribedMixNode}; -use nym_mixnet_contract_common::MixNodeBond; +use nym_api_requests::models::{ + AnnotationResponse, LegacyDescribedGateway, LegacyDescribedMixNode, NymNodeDescription, +}; +use nym_mixnet_contract_common::NodeId; use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; use std::ops::Deref; -// obviously this should get refactored later on because gateways will go away. -// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD +#[openapi(tag = "Nym Nodes")] +#[get("/all/described")] +pub async fn all_described_nodes( + describe_cache: &State>, +) -> Json> { + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(Vec::new()); + }; + + Json(self_descriptions.all_nodes().cloned().collect()) +} #[openapi(tag = "Nym Nodes")] +#[get("/all//described")] +pub async fn node_description( + node_id: NodeId, + describe_cache: &State>, +) -> Json> { + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(None); + }; + + Json(self_descriptions.get_node(&node_id).cloned()) +} + +#[openapi(tag = "Nym Nodes")] +#[get("/annotation-by-identity/")] +pub async fn node_annotation_by_identity( + identity: String, + status_cache: &State, +) -> Json { + let Some(node_id) = status_cache.map_identity_to_node_id(&identity).await else { + return Json(Default::default()); + }; + node_annotation(node_id, status_cache).await +} + +#[openapi(tag = "Nym Nodes")] +#[get("/annotation/")] +pub async fn node_annotation( + node_id: NodeId, + status_cache: &State, +) -> Json { + let Some(annotation) = status_cache.node_annotations().await else { + return Json(Default::default()); + }; + + Json(AnnotationResponse { + node_id: Some(node_id), + annotation: annotation.get(&node_id).cloned(), + }) +} + +/// This only returns descriptions of **legacy** gateways +#[openapi(tag = "Nym Nodes", deprecated = true)] #[get("/gateways/described")] pub async fn get_gateways_described( contract_cache: &State, describe_cache: &State>, -) -> Json> { - let gateways = contract_cache.gateways_filtered().await; +) -> Json> { + let gateways = contract_cache.legacy_gateways_filtered().await; if gateways.is_empty() { return Json(Vec::new()); } - // if the self describe cache is unavailable, well, don't attach describe data + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways let Ok(self_descriptions) = describe_cache.get().await else { return Json(gateways.into_iter().map(Into::into).collect()); }; - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored Json( gateways .into_iter() - .map(|bond| DescribedGateway { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), + .map(|bond| LegacyDescribedGateway { + self_described: self_descriptions + .deref() + .get_description(&bond.node_id) + .cloned(), bond, }) .collect(), ) } -#[openapi(tag = "Nym Nodes")] +/// This only returns descriptions of **legacy** mixnodes +#[openapi(tag = "Nym Nodes", deprecated = true)] #[get("/mixnodes/described")] pub async fn get_mixnodes_described( contract_cache: &State, describe_cache: &State>, -) -> Json> { +) -> Json> { let mixnodes = contract_cache - .mixnodes_filtered() + .legacy_mixnodes_filtered() .await .into_iter() .map(|m| m.bond_information) - .collect::>(); + .collect::>(); if mixnodes.is_empty() { return Json(Vec::new()); } @@ -65,14 +120,14 @@ pub async fn get_mixnodes_described( return Json(mixnodes.into_iter().map(Into::into).collect()); }; - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored Json( mixnodes .into_iter() - .map(|bond| DescribedMixNode { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), + .map(|bond| LegacyDescribedMixNode { + self_described: self_descriptions + .deref() + .get_description(&bond.mix_id) + .cloned(), bond, }) .collect(), diff --git a/nym-api/src/nym_nodes/unstable_routes.rs b/nym-api/src/nym_nodes/unstable_routes.rs index aa12e5b7f3..0e178da389 100644 --- a/nym-api/src/nym_nodes/unstable_routes.rs +++ b/nym-api/src/nym_nodes/unstable_routes.rs @@ -4,17 +4,17 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::models::RocketErrorResponse; use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, + CachedNodesResponse, FullFatNode, NodeRole, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, }; use nym_bin_common::version_checker; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; -use std::cmp::min; -use std::ops::Deref; +use std::collections::HashSet; /* routes: @@ -36,6 +36,7 @@ use std::ops::Deref; #[get("/skimmed?&")] pub async fn nodes_basic( status_cache: &State, + contract_cache: &State, describe_cache: &State>, role: Option, semver_compatibility: Option, @@ -43,10 +44,22 @@ pub async fn nodes_basic( if let Some(role) = role { match role { NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_basic(status_cache, semver_compatibility).await + return mixnodes_basic( + status_cache, + contract_cache, + describe_cache, + semver_compatibility, + ) + .await } NodeRoleQueryParam::EntryGateway => { - return gateways_basic(status_cache, describe_cache, semver_compatibility).await + return gateways_basic( + status_cache, + contract_cache, + describe_cache, + semver_compatibility, + ) + .await } _ => {} } @@ -112,57 +125,108 @@ pub async fn nodes_detailed( #[get("/gateways/skimmed?")] pub async fn gateways_basic( status_cache: &State, + contract_cache: &State, describe_cache: &State>, semver_compatibility: Option, ) -> Result>, RocketErrorResponse> { - let gateways_cache = status_cache - .gateways_cache() + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() .await - .ok_or(RocketErrorResponse::new( - "could not obtain gateways cache", - Status::InternalServerError, - ))?; + .ok_or_else(RocketErrorResponse::internal_server_error)?; - if gateways_cache.is_empty() { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: vec![], - })); + // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic + let active_gateways = rewarded_set.gateways().into_iter().collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 3. grab all legacy gateways + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_gateways = status_cache + .annotated_legacy_gateways() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); + + // 5. only return nodes that are present in the active set + let mut active_skimmed_gateways = Vec::new(); + + for (node_id, legacy) in annotated_legacy_gateways.iter() { + if !active_gateways.contains(node_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + active_skimmed_gateways.push( + described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), + ) + } else { + active_skimmed_gateways.push(legacy.into()); + } } - // if the self describe cache is unavailable don't try to use self-describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: gateways_cache.values().map(Into::into).collect(), - })); - }; + for nym_node in gateway_capable_nym_nodes { + // if this node is not an active gateway, ignore it + if !active_gateways.contains(&nym_node.node_id) { + continue; + } - let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp()); + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit + let role = if rewarded_set.is_exit(&nym_node.node_id) { + NodeRole::ExitGateway + } else { + NodeRole::EntryGateway + }; + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_gateways + .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_gateways.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); - // the same comment holds as with `get_gateways_described`. - // this is inefficient and will have to get refactored with directory v3 Ok(Json(CachedNodesResponse { - refreshed_at: refreshed_at.into(), - nodes: gateways_cache - .values() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond.gateway_bond.gateway.version, - semver_compatibility, - ) - } else { - true - } - }) - .map(|annotated_bond| { - SkimmedNode::from_described_gateway( - annotated_bond, - self_descriptions.deref().get(annotated_bond.identity()), - ) - }) - .collect(), + refreshed_at, + nodes: active_skimmed_gateways, })) } @@ -197,36 +261,116 @@ pub async fn gateways_detailed( #[openapi(tag = "Unstable Nym Nodes")] #[get("/mixnodes/skimmed?")] pub async fn mixnodes_basic( - cache: &State, + status_cache: &State, + contract_cache: &State, + describe_cache: &State>, semver_compatibility: Option, ) -> Result>, RocketErrorResponse> { - let mixnodes_cache = cache - .active_mixnodes_cache() + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() .await - .ok_or(RocketErrorResponse::new( - "could not obtain mixnodes cache", - Status::InternalServerError, - ))?; + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic + let active_mixnodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 3. grab all legacy mixnodes + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_mixnodes = status_cache + .annotated_legacy_mixnodes() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + + // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it + + // 5. only return nodes that are present in the active set + let mut active_skimmed_mixnodes = Vec::new(); + + for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { + if !active_mixnodes.contains(mix_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(mix_id) { + active_skimmed_mixnodes.push(described.to_skimmed_node( + NodeRole::Mixnode { + layer: legacy.mixnode_details.bond_information.layer.into(), + }, + legacy.node_performance.last_24h, + )) + } else { + active_skimmed_mixnodes.push(legacy.into()); + } + } + + for nym_node in mixing_nym_nodes { + // if this node is not an active mixnode, ignore it + if !active_mixnodes.contains(&nym_node.node_id) { + continue; + } + + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // SAFETY: if we determined our node IS active, it MUST have a layer + // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data + #[allow(clippy::unwrap_used)] + let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_mixnodes.push( + nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), + ); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_mixnodes.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); + Ok(Json(CachedNodesResponse { - refreshed_at: mixnodes_cache.timestamp().into(), - nodes: mixnodes_cache - .iter() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond - .mixnode_details - .bond_information - .mix_node - .version, - semver_compatibility, - ) - } else { - true - } - }) - .map(Into::into) - .collect(), + refreshed_at, + nodes: active_skimmed_mixnodes, })) } diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index ea34727f9a..7dc316aac2 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -3,7 +3,7 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::status::ApiStatusState; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Json; use axum::Router; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; @@ -11,7 +11,7 @@ use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_compact_ecash::Base58; use std::sync::Arc; -pub(crate) fn api_status_routes() -> Router { +pub(crate) fn api_status_routes() -> Router { let api_status_state = Arc::new(ApiStatusState::new()); Router::new() @@ -84,7 +84,7 @@ async fn signer_information( identity: signer_state.identity.clone(), announce_address: signer_state.announce_address.clone(), verification_key: signer_state - .coconut_keypair + .ecash_keypair .verification_key() .await .map(|maybe_vk| maybe_vk.to_bs58()), diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 1d09b341c8..5e74eeffb6 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -4,15 +4,11 @@ use crate::ecash; use nym_bin_common::bin_info; use nym_bin_common::build_information::BinaryBuildInformation; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; + use tokio::time::Instant; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; +// pub(crate) mod routes; pub(crate) struct ApiStatusState { startup_time: Instant, @@ -28,7 +24,7 @@ pub(crate) struct SignerState { pub announce_address: String, - pub(crate) coconut_keypair: ecash::keys::KeyPair, + pub(crate) ecash_keypair: ecash::keys::KeyPair, } impl ApiStatusState { @@ -45,11 +41,11 @@ impl ApiStatusState { } } -pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: - routes::health, - routes::build_information, - routes::signer_information - ] -} +// pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: +// routes::health, +// routes::build_information, +// routes::signer_information +// ] +// } diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs index 804744df18..911e9e2a9b 100644 --- a/nym-api/src/status/routes.rs +++ b/nym-api/src/status/routes.rs @@ -44,7 +44,7 @@ pub(crate) async fn signer_information( identity: signer_state.identity.clone(), announce_address: signer_state.announce_address.clone(), verification_key: signer_state - .coconut_keypair + .ecash_keypair .verification_key() .await .map(|maybe_vk| maybe_vk.to_bs58()), diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 4df805728a..706d2b342b 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -31,12 +31,12 @@ impl SharedCache { SharedCache::default() } - pub(crate) async fn update(&self, value: T) { + pub(crate) async fn update(&self, value: impl Into) { let mut guard = self.0.write().await; if let Some(ref mut existing) = guard.inner { existing.unchecked_update(value) } else { - guard.inner = Some(Cache::new(value)) + guard.inner = Some(Cache::new(value.into())) } } @@ -52,6 +52,17 @@ impl SharedCache { ) -> Result, UninitialisedCache> { Ok(RwLockReadGuard::map(self.get().await?, |a| &a.value)) } + + pub(crate) async fn naive_wait_for_initial_values(&self) { + let initialisation_backoff = Duration::from_secs(5); + loop { + if self.get().await.is_ok() { + break; + } else { + tokio::time::sleep(initialisation_backoff).await; + } + } + } } impl From> for SharedCache { @@ -118,8 +129,8 @@ impl Cache { } // ugh. I hate to expose it, but it'd have broken pre-existing code - pub(crate) fn unchecked_update(&mut self, value: T) { - self.value = value; + pub(crate) fn unchecked_update(&mut self, value: impl Into) { + self.value = value.into(); self.as_at = OffsetDateTime::now_utc() } @@ -135,7 +146,6 @@ impl Cache { self.as_at } - #[allow(dead_code)] pub fn into_inner(self) -> T { self.value } diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 6d32bb4f5a..b301220a0e 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::caching::cache::SharedCache; +use async_trait::async_trait; use nym_task::TaskClient; use std::time::Duration; use tokio::time::interval; +use tracing::{error, info, trace}; pub struct CacheRefresher { name: String, @@ -88,6 +90,8 @@ where self.shared_cache.clone() } + // TODO: in the future offer 2 options of refreshing cache. either provide `T` directly + // or via `FnMut(&mut T)` closure async fn do_refresh_cache(&self) { match self.provider.try_refresh().await { Ok(updated_items) => { @@ -100,12 +104,12 @@ where } pub async fn refresh(&self, task_client: &mut TaskClient) { - log::info!("{}: refreshing cache state", self.name); + info!("{}: refreshing cache state", self.name); tokio::select! { biased; _ = task_client.recv() => { - log::trace!("{}: Received shutdown while refreshing cache", self.name) + trace!("{}: Received shutdown while refreshing cache", self.name) } _ = self.do_refresh_cache() => (), } @@ -119,7 +123,7 @@ where tokio::select! { biased; _ = task_client.recv() => { - log::trace!("{}: Received shutdown", self.name) + trace!("{}: Received shutdown", self.name) } _ = refresh_interval.tick() => self.refresh(&mut task_client).await, } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index a19d2c1c29..08e8d49f94 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -1,9 +1,43 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::start_nym_api_tasks; +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::ecash::api_routes::handlers::ecash_routes; +use crate::ecash::client::Client; +use crate::ecash::comm::QueryCommunicationChannel; +use crate::ecash::dkg::controller::keys::{ + can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, +}; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::state::EcashState; +use crate::epoch_operations::EpochAdvancer; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::status::{ApiStatusState, SignerState}; +use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; -use cfg_if::cfg_if; +use crate::support::config::Config; +use crate::support::http::state::{AppState, ShutdownHandles, TASK_MANAGER_TIMEOUT_S}; +use crate::support::http::RouterBuilder; +use crate::support::nyxd; +use crate::support::storage::NymApiStorage; +use crate::v3_migration::migrate_v3_database; +use crate::{ + circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache, + node_status_api, nym_contract_cache, +}; +use anyhow::{bail, Context}; +use nym_config::defaults::NymNetworkDetails; +use nym_sphinx::receiver::SphinxMessageReceiver; +use nym_task::TaskManager; +use nym_validator_client::nyxd::Coin; +use std::sync::Arc; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; #[derive(clap::Args, Debug)] pub(crate) struct Args { @@ -54,6 +88,196 @@ pub(crate) struct Args { pub(crate) monitor_credentials_mode: Option, } +async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result { + let nyxd_client = nyxd::Client::new(config); + let connected_nyxd = config.get_nyxd_url(); + let nym_network_details = NymNetworkDetails::new_from_env(); + let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); + + let ecash_keypair_wrapper = ecash::keys::KeyPair::new(); + + // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen + if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.ecash_signer)? { + let issued_for = loaded_keys.issued_for_epoch; + ecash_keypair_wrapper.set(loaded_keys).await; + + if can_validate_coconut_keys(&nyxd_client, issued_for).await? { + ecash_keypair_wrapper.validate() + } + } + + let storage = NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; + + // try to perform any needed migrations of the storage + migrate_v3_database(&storage, &nyxd_client).await?; + + let identity_keypair = config.base.storage_paths.load_identity()?; + let identity_public_key = *identity_keypair.public_key(); + + let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); + + let nym_contract_cache_state = NymContractCache::new(); + let node_status_cache_state = NodeStatusCache::new(); + let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); + let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); + let described_nodes_cache = SharedCache::::new(); + let node_info_cache = unstable::NodeInfoCache::default(); + + let mut status_state = ApiStatusState::new(); + + // if ecash signer is enabled, add /ecash to server + let router = if config.ecash_signer.enabled { + let cosmos_address = nyxd_client.address().await; + + // make sure we have some tokens to cover multisig fees + let balance = nyxd_client.balance(&mix_denom).await?; + if balance.amount < ecash::MINIMUM_BALANCE { + let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); + bail!("the account ({cosmos_address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") + } + + let announce_address = config + .ecash_signer + .announce_address + .clone() + .map(|u| u.to_string()) + .unwrap_or_default(); + status_state.add_zk_nym_signer(SignerState { + cosmos_address: cosmos_address.to_string(), + identity: identity_keypair.public_key().to_base58_string(), + announce_address, + ecash_keypair: ecash_keypair_wrapper.clone(), + }); + + let ecash_contract = nyxd_client + .get_ecash_contract_address() + .await + .context("e-cash contract address is required to setup the zk-nym signer")?; + + let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); + + let ecash_state = EcashState::new( + ecash_contract, + nyxd_client.clone(), + identity_keypair, + ecash_keypair_wrapper.clone(), + comm_channel, + storage.clone(), + ) + .await?; + + router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) + } else { + router + }; + + let router = router.with_state(AppState { + nym_contract_cache: nym_contract_cache_state.clone(), + node_status_cache: node_status_cache_state.clone(), + circulating_supply_cache: circulating_supply_cache.clone(), + storage: storage.clone(), + described_nodes_cache: described_nodes_cache.clone(), + network_details, + node_info_cache, + }); + + let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); + + // start note describe cache refresher + // we should be doing the below, but can't due to our current startup structure + // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); + // let cache = refresher.get_shared_cache(); + node_describe_cache::new_refresher_with_initial_value( + &config.topology_cacher, + nym_contract_cache_state.clone(), + described_nodes_cache.clone(), + ) + .named("node-self-described-data-refresher") + .start(task_manager.subscribe_named("node-self-described-data-refresher")); + + // start all the caches first + let nym_contract_cache_listener = nym_contract_cache::start_refresher( + &config.node_status_api, + &nym_contract_cache_state, + nyxd_client.clone(), + &task_manager, + ); + node_status_api::start_cache_refresh( + &config.node_status_api, + &nym_contract_cache_state, + &node_status_cache_state, + storage.clone(), + nym_contract_cache_listener, + &task_manager, + ); + circulating_supply_api::start_cache_refresh( + &config.circulating_supply_cacher, + nyxd_client.clone(), + &circulating_supply_cache, + &task_manager, + ); + + // start dkg task + if config.ecash_signer.enabled { + let dkg_bte_keypair = load_bte_keypair(&config.ecash_signer)?; + + DkgController::start( + &config.ecash_signer, + nyxd_client.clone(), + ecash_keypair_wrapper, + dkg_bte_keypair, + identity_public_key, + rand::rngs::OsRng, + &task_manager, + )?; + } + + // and then only start the uptime updater (and the monitor itself, duh) + // if the monitoring is enabled + if config.network_monitor.enabled { + network_monitor::start::( + &config.network_monitor, + &nym_contract_cache_state, + described_nodes_cache.clone(), + &storage, + nyxd_client.clone(), + &task_manager, + ) + .await; + + HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + + // start 'rewarding' if its enabled + if config.rewarding.enabled { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + EpochAdvancer::start( + nyxd_client, + &nym_contract_cache_state, + described_nodes_cache.clone(), + &storage, + &task_manager, + ); + } + } + + let bind_address = config.base.bind_address.to_owned(); + let server = router.build_server(&bind_address).await?; + + let cancellation_token = CancellationToken::new(); + let shutdown_button = cancellation_token.clone(); + let axum_shutdown_receiver = cancellation_token.cancelled_owned(); + let server_handle = tokio::spawn(async move { + { + info!("Started Axum HTTP V2 server on {bind_address}"); + server.run(axum_shutdown_receiver).await + } + }); + + let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); + + Ok(shutdown) +} + pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { // args take precedence over env let config = try_load_current_config(&args.id)? @@ -62,41 +286,32 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - #[cfg(feature = "axum")] - let mut axum_shutdown = crate::v2::start_nym_api_tasks_v2(&config).await?; - let mut rocket_shutdown = start_nym_api_tasks(config).await?; + let mut axum_shutdown = start_nym_api_tasks_axum(&config).await?; // it doesn't matter which server catches the interrupt: it needs only be caught once - if let Err(err) = rocket_shutdown.task_manager_handle.catch_interrupt().await { + if let Err(err) = axum_shutdown.task_manager_mut().catch_interrupt().await { error!("Error stopping Rocket tasks: {err}"); } - log::info!("Stopping nym API"); - rocket_shutdown.rocket_handle.notify(); + info!("Stopping nym API"); - // because Rocket caught the interrupt, it had already signalled its - // background tasks to retire. Now do that for axum - cfg_if! { - if #[cfg(feature = "axum")] { - axum_shutdown.task_manager_mut().signal_shutdown().ok(); - axum_shutdown.task_manager_mut().wait_for_shutdown().await; + axum_shutdown.task_manager_mut().signal_shutdown().ok(); + axum_shutdown.task_manager_mut().wait_for_shutdown().await; - let running_server = axum_shutdown.shutdown_axum(); + let running_server = axum_shutdown.shutdown_axum(); - match running_server.await { - Ok(Ok(_)) => { - info!("Axum HTTP server shut down without errors"); - }, - Ok(Err(err)) => { - error!("Axum HTTP server terminated with: {err}"); - anyhow::bail!(err) - }, - Err(err) => { - error!("Server task panicked: {err}"); - } - }; + match running_server.await { + Ok(Ok(_)) => { + info!("Axum HTTP server shut down without errors"); } - } + Ok(Err(err)) => { + error!("Axum HTTP server terminated with: {err}"); + anyhow::bail!(err) + } + Err(err) => { + error!("Server task panicked: {err}"); + } + }; Ok(()) } diff --git a/nym-api/src/support/config/helpers.rs b/nym-api/src/support/config/helpers.rs index 6521c8f7f4..8a6652e4f8 100644 --- a/nym-api/src/support/config/helpers.rs +++ b/nym-api/src/support/config/helpers.rs @@ -42,7 +42,7 @@ pub(crate) fn initialise_new(id: &str) -> Result { // create DKG BTE keys let mut rng = OsRng; - init_bte_keypair(&mut rng, &config.coconut_signer)?; + init_bte_keypair(&mut rng, &config.ecash_signer)?; Ok(config) } diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index a26384652f..235464dc0a 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::config::persistence::{ - CoconutSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths, + EcashSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths, }; use crate::support::config::r#override::OverrideConfig; use crate::support::config::template::CONFIG_TEMPLATE; @@ -19,6 +19,7 @@ use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; +use tracing::debug; use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -106,7 +107,8 @@ pub struct Config { pub rewarding: Rewarding, - pub coconut_signer: CoconutSigner, + #[serde(alias = "coconut_signer")] + pub ecash_signer: EcashSigner, } impl NymConfigTemplate for Config { @@ -125,7 +127,7 @@ impl Config { topology_cacher: Default::default(), circulating_supply_cacher: Default::default(), rewarding: Default::default(), - coconut_signer: CoconutSigner::new_default(id.as_ref()), + ecash_signer: EcashSigner::new_default(id.as_ref()), } } @@ -136,7 +138,7 @@ impl Config { bail!("can't enable rewarding without providing a mnemonic") } - if !can_sign && self.coconut_signer.enabled { + if !can_sign && self.ecash_signer.enabled { bail!("can't enable coconut signer without providing a mnemonic") } @@ -159,10 +161,10 @@ impl Config { self.base.mnemonic = Some(mnemonic) } if let Some(enable_zk_nym) = args.enable_zk_nym { - self.coconut_signer.enabled = enable_zk_nym + self.ecash_signer.enabled = enable_zk_nym } if let Some(announce_address) = args.announce_address { - self.coconut_signer.announce_address = Some(announce_address) + self.ecash_signer.announce_address = Some(announce_address) } if let Some(monitor_credentials_mode) = args.monitor_credentials_mode { self.network_monitor.debug.disabled_credentials_mode = !monitor_credentials_mode @@ -226,13 +228,12 @@ impl Config { } } -// TODO rocket: when axum becomes the main server, change its bind addr default here fn default_http_socket_addr() -> SocketAddr { cfg_if::cfg_if! { if #[cfg(debug_assertions)] { - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081) + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080) } else { - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8081) + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080) } } } @@ -518,25 +519,25 @@ impl Default for RewardingDebug { } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct CoconutSigner { +pub struct EcashSigner { /// Specifies whether rewarding service is enabled in this process. pub enabled: bool, #[serde(deserialize_with = "de_maybe_stringified")] pub announce_address: Option, - pub storage_paths: CoconutSignerPaths, + pub storage_paths: EcashSignerPaths, #[serde(default)] - pub debug: CoconutSignerDebug, + pub debug: EcashSignerDebug, } -impl CoconutSigner { +impl EcashSigner { pub fn new_default>(id: P) -> Self { - CoconutSigner { + EcashSigner { enabled: false, announce_address: None, - storage_paths: CoconutSignerPaths::new_default(id), + storage_paths: EcashSignerPaths::new_default(id), debug: Default::default(), } } @@ -544,15 +545,15 @@ impl CoconutSigner { #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct CoconutSignerDebug { +pub struct EcashSignerDebug { /// Duration of the interval for polling the dkg contract. #[serde(with = "humantime_serde")] pub dkg_contract_polling_rate: Duration, } -impl Default for CoconutSignerDebug { +impl Default for EcashSignerDebug { fn default() -> Self { - CoconutSignerDebug { + EcashSignerDebug { dkg_contract_polling_rate: DEFAULT_DKG_CONTRACT_POLLING_RATE, } } diff --git a/nym-api/src/support/config/persistence.rs b/nym-api/src/support/config/persistence.rs index d14996b743..f2c98c038f 100644 --- a/nym-api/src/support/config/persistence.rs +++ b/nym-api/src/support/config/persistence.rs @@ -14,7 +14,9 @@ pub const DEFAULT_NODE_STATUS_API_DATABASE_FILENAME: &str = "db.sqlite"; pub const DEFAULT_DKG_PERSISTENT_STATE_FILENAME: &str = "dkg_persistent_state.json"; pub const DEFAULT_DKG_DECRYPTION_KEY_FILENAME: &str = "dkg_decryption_key.pem"; pub const DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME: &str = "dkg_public_key_with_proof.pem"; -pub const DEFAULT_COCONUT_KEY_FILENAME: &str = "coconut.pem"; + +// don't want to be changing the defaults in case something breaks..., but it should be called ecash.pem instead +pub const DEFAULT_ECASH_KEY_FILENAME: &str = "coconut.pem"; pub const DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME: &str = "private_identity.pem"; pub const DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME: &str = "public_identity.pem"; @@ -73,12 +75,13 @@ impl NodeStatusAPIPaths { } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct CoconutSignerPaths { +pub struct EcashSignerPaths { /// Path to a JSON file where state is persisted between different stages of DKG. pub dkg_persistent_state_path: PathBuf, /// Path to the coconut key. - pub coconut_key_path: PathBuf, + #[serde(alias = "coconut_key_path")] + pub ecash_key_path: PathBuf, /// Path to the dkg dealer decryption key. pub decryption_key_path: PathBuf, @@ -87,13 +90,13 @@ pub struct CoconutSignerPaths { pub public_key_with_proof_path: PathBuf, } -impl CoconutSignerPaths { +impl EcashSignerPaths { pub fn new_default>(id: P) -> Self { let data_dir = default_data_directory(id); - CoconutSignerPaths { + EcashSignerPaths { dkg_persistent_state_path: data_dir.join(DEFAULT_DKG_PERSISTENT_STATE_FILENAME), - coconut_key_path: data_dir.join(DEFAULT_COCONUT_KEY_FILENAME), + ecash_key_path: data_dir.join(DEFAULT_ECASH_KEY_FILENAME), decryption_key_path: data_dir.join(DEFAULT_DKG_DECRYPTION_KEY_FILENAME), public_key_with_proof_path: data_dir.join(DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME), } diff --git a/nym-api/src/support/config/template.rs b/nym-api/src/support/config/template.rs index 108cc0f426..5ee155e016 100644 --- a/nym-api/src/support/config/template.rs +++ b/nym-api/src/support/config/template.rs @@ -16,7 +16,6 @@ id = '{{ base.id }}' local_validator = '{{ base.local_validator }}' # Socket address this api will use for binding its http API. -# Note: only used if `axum` feature is enabled. bind_address = '{{ base.bind_address }}' # Mnemonic used for rewarding and validator interaction @@ -109,26 +108,26 @@ enabled = {{ rewarding.enabled }} # Note, only values in range 0-100 are valid minimum_interval_monitor_threshold = {{ rewarding.debug.minimum_interval_monitor_threshold }} -[coconut_signer] +[ecash_signer] -# Specifies whether coconut signing protocol is enabled in this process. -enabled = {{ coconut_signer.enabled }} +# Specifies whether ecash signing protocol is enabled in this process. +enabled = {{ ecash_signer.enabled }} # address of this nym-api as announced to other instances for the purposes of performing the DKG. -announce_address = '{{ coconut_signer.announce_address }}' +announce_address = '{{ ecash_signer.announce_address }}' -[coconut_signer.storage_paths] +[ecash_signer.storage_paths] # Path to a JSON file where state is persisted between different stages of DKG. -dkg_persistent_state_path = '{{ coconut_signer.storage_paths.dkg_persistent_state_path }}' +dkg_persistent_state_path = '{{ ecash_signer.storage_paths.dkg_persistent_state_path }}' -# Path to the coconut key. -coconut_key_path = '{{ coconut_signer.storage_paths.coconut_key_path }}' +# Path to the ecash key. +ecash_key_path = '{{ ecash_signer.storage_paths.ecash_key_path }}' # Path to the dkg dealer decryption key -decryption_key_path = '{{ coconut_signer.storage_paths.decryption_key_path }}' +decryption_key_path = '{{ ecash_signer.storage_paths.decryption_key_path }}' # Path to the dkg dealer public key with proof -public_key_with_proof_path = '{{ coconut_signer.storage_paths.public_key_with_proof_path }}' +public_key_with_proof_path = '{{ ecash_signer.storage_paths.public_key_with_proof_path }}' "#; diff --git a/nym-api/src/support/http/helpers.rs b/nym-api/src/support/http/helpers.rs index 45efa8f586..4ea95d4cd2 100644 --- a/nym-api/src/support/http/helpers.rs +++ b/nym-api/src/support/http/helpers.rs @@ -1,11 +1,21 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; -#[derive(Serialize, Deserialize, FromForm, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, ToSchema, IntoParams)] +#[into_params(parameter_in = Query)] pub struct PaginationRequest { pub page: Option, pub per_page: Option, } + +#[derive(Deserialize, IntoParams, ToSchema)] +#[into_params(parameter_in = Path)] +pub(crate) struct NodeIdParam { + #[schema(value_type = u32)] + pub(crate) node_id: NodeId, +} diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 612dcb9333..9446891443 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -1,141 +1,120 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::client::Client; -use crate::ecash::state::EcashState; -use crate::ecash::{self, comm::QueryCommunicationChannel}; -use crate::network::models::NetworkDetails; -use crate::network::network_routes; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::routes_deprecated::unstable; -use crate::node_status_api::{self, NodeStatusCache}; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::nym_nodes::{nym_node_routes_deprecated, nym_node_routes_next}; -use crate::status::{api_status_routes, ApiStatusState, SignerState}; -use crate::support::caching::cache::SharedCache; -use crate::support::config::Config; -use crate::support::{nyxd, storage}; -use crate::{circulating_supply_api, nym_contract_cache}; -use anyhow::{bail, Context, Result}; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::Coin; -use rocket::{Ignite, Rocket}; -use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; -use rocket_okapi::mount_endpoints_and_merged_docs; -use rocket_okapi::swagger_ui::make_swagger_ui; - pub(crate) mod helpers; pub(crate) mod openapi; +pub(crate) mod router; +pub(crate) mod state; +mod unstable_routes; -pub(crate) async fn setup_rocket( - config: &Config, - network_details: NetworkDetails, - nyxd_client: nyxd::Client, - identity_keypair: identity::KeyPair, - coconut_keypair: ecash::keys::KeyPair, -) -> anyhow::Result> { - let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); - let mut rocket = rocket::build(); +pub(crate) use router::RouterBuilder; - let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - - mount_endpoints_and_merged_docs! { - rocket, - "/v1".to_owned(), - openapi_settings, - "/" => (vec![], openapi::custom_openapi_spec()), - "" => circulating_supply_api::circulating_supply_routes(&openapi_settings), - "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), - "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), - "/network" => network_routes(&openapi_settings), - "/api-status" => api_status_routes(&openapi_settings), - "/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled), - "" => nym_node_routes_deprecated(&openapi_settings), - - // => when we move those routes, we'll need to add a redirection for backwards compatibility - "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) - } - - let storage = - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; - - let rocket = rocket - .manage(network_details) - .manage(SharedCache::::new()) - .mount("/swagger", make_swagger_ui(&openapi::get_docs())) - .attach(setup_rocket_cors()?) - .attach(NymContractCache::stage()) - .attach(NodeStatusCache::stage()) - .attach(CirculatingSupplyCache::stage(mix_denom.clone())) - .attach(storage::NymApiStorage::stage(storage.clone())) - .manage(unstable::NodeInfoCache::default()); - - let mut status_state = ApiStatusState::new(); - - let rocket = if config.coconut_signer.enabled { - // make sure we have some tokens to cover multisig fees - let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < ecash::MINIMUM_BALANCE { - let address = nyxd_client.address().await; - let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); - bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") - } - - let cosmos_address = nyxd_client.address().await.to_string(); - let announce_address = config - .coconut_signer - .announce_address - .clone() - .map(|u| u.to_string()) - .unwrap_or_default(); - status_state.add_zk_nym_signer(SignerState { - cosmos_address, - identity: identity_keypair.public_key().to_base58_string(), - announce_address, - coconut_keypair: coconut_keypair.clone(), - }); - - let ecash_contract = nyxd_client - .get_ecash_contract_address() - .await - .context("e-cash contract address is required to setup the zk-nym signer")?; - - let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - - let ecash_state = EcashState::new( - ecash_contract, - nyxd_client.clone(), - identity_keypair, - coconut_keypair, - comm_channel, - storage.clone(), - ) - .await?; - - rocket.manage(ecash_state) - } else { - rocket - }; - - Ok(rocket.manage(status_state).ignite().await?) -} - -fn setup_rocket_cors() -> Result { - let allowed_origins = AllowedOrigins::all(); - - // You can also deserialize this - let cors = rocket_cors::CorsOptions { - allowed_origins, - allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] - .into_iter() - .map(From::from) - .collect(), - allowed_headers: AllowedHeaders::all(), - allow_credentials: true, - ..Default::default() - } - .to_cors()?; - - Ok(cors) -} +// pub(crate) async fn setup_rocket( +// config: &Config, +// network_details: NetworkDetails, +// nyxd_client: nyxd::Client, +// identity_keypair: identity::KeyPair, +// coconut_keypair: ecash::keys::KeyPair, +// storage: NymApiStorage, +// ) -> anyhow::Result> { +// let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); +// let mut rocket = rocket::build(); +// +// let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); +// +// mount_endpoints_and_merged_docs! { +// rocket, +// "/v1".to_owned(), +// openapi_settings, +// "/" => (vec![], openapi::custom_openapi_spec()), +// "" => circulating_supply_api::circulating_supply_routes(&openapi_settings), +// "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), +// "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), +// "/network" => network_routes(&openapi_settings), +// "/api-status" => api_status_routes(&openapi_settings), +// "/ecash" => ecash::routes_open_api(&openapi_settings, config.ecash_signer.enabled), +// "" => nym_node_routes_deprecated(&openapi_settings), +// +// // => when we move those routes, we'll need to add a redirection for backwards compatibility +// "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) +// } +// +// let rocket = rocket +// .manage(network_details) +// .manage(SharedCache::::new()) +// .mount("/swagger", make_swagger_ui(&openapi::get_docs())) +// .attach(setup_rocket_cors()?) +// .attach(NymContractCache::stage()) +// .attach(NodeStatusCache::stage()) +// .attach(CirculatingSupplyCache::stage(mix_denom.clone())) +// .manage(unstable::NodeInfoCache::default()) +// .manage(storage.clone()); +// +// let mut status_state = ApiStatusState::new(); +// +// let rocket = if config.ecash_signer.enabled { +// // make sure we have some tokens to cover multisig fees +// let balance = nyxd_client.balance(&mix_denom).await?; +// if balance.amount < ecash::MINIMUM_BALANCE { +// let address = nyxd_client.address().await; +// let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); +// bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") +// } +// +// let cosmos_address = nyxd_client.address().await.to_string(); +// let announce_address = config +// .ecash_signer +// .announce_address +// .clone() +// .map(|u| u.to_string()) +// .unwrap_or_default(); +// status_state.add_zk_nym_signer(SignerState { +// cosmos_address, +// identity: identity_keypair.public_key().to_base58_string(), +// announce_address, +// ecash_keypair: coconut_keypair.clone(), +// }); +// +// let ecash_contract = nyxd_client +// .get_ecash_contract_address() +// .await +// .context("e-cash contract address is required to setup the zk-nym signer")?; +// +// let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); +// +// let ecash_state = EcashState::new( +// ecash_contract, +// nyxd_client.clone(), +// identity_keypair, +// coconut_keypair, +// comm_channel, +// storage.clone(), +// ) +// .await?; +// +// rocket.manage(ecash_state) +// } else { +// rocket +// }; +// +// Ok(rocket.manage(status_state).ignite().await?) +// } +// +// fn setup_rocket_cors() -> Result { +// let allowed_origins = AllowedOrigins::all(); +// +// // You can also deserialize this +// let cors = rocket_cors::CorsOptions { +// allowed_origins, +// allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] +// .into_iter() +// .map(From::from) +// .collect(), +// allowed_headers: AllowedHeaders::all(), +// allow_credentials: true, +// ..Default::default() +// } +// .to_cors()?; +// +// Ok(cors) +// } diff --git a/nym-api/src/support/http/openapi.rs b/nym-api/src/support/http/openapi.rs index 858d201dc7..4fc89422d9 100644 --- a/nym-api/src/support/http/openapi.rs +++ b/nym-api/src/support/http/openapi.rs @@ -1,41 +1,89 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket_okapi::swagger_ui::SwaggerUIConfig; +use crate::network::handlers::ContractVersionSchemaResponse; +use nym_api_requests::models; +use utoipa::OpenApi; +use utoipauto::utoipauto; -pub fn custom_openapi_spec() -> OpenApi { - use rocket_okapi::okapi::openapi3::*; - OpenApi { - openapi: OpenApi::default_version(), - info: Info { - title: "Nym API".to_owned(), - description: None, - terms_of_service: None, - contact: None, - license: None, - version: env!("CARGO_PKG_VERSION").to_owned(), - ..Default::default() - }, - servers: get_servers(), - ..Default::default() - } -} +// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released: +// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below) +// for automatic model discovery based on ToSchema / IntoParams implementation. +// Then you can remove `components(schemas)` manual imports below -fn get_servers() -> Vec { - if std::env::var_os("CARGO").is_some() { - return vec![]; - } - vec![rocket_okapi::okapi::openapi3::Server { - url: std::env::var("OPEN_API_BASE").unwrap_or_else(|_| "/api/v1/".to_owned()), - description: Some("API".to_owned()), - ..Default::default() - }] -} - -pub(crate) fn get_docs() -> SwaggerUIConfig { - SwaggerUIConfig { - url: "../v1/openapi.json".to_owned(), - ..SwaggerUIConfig::default() - } -} +#[utoipauto(paths = "./nym-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Nym API"), + tags(), + components(schemas( + models::CirculatingSupplyResponse, + models::CoinSchema, + nym_mixnet_contract_common::Interval, + nym_api_requests::models::GatewayStatusReportResponse, + nym_api_requests::models::GatewayUptimeHistoryResponse, + nym_api_requests::models::GatewayCoreStatusResponse, + nym_api_requests::models::GatewayUptimeResponse, + nym_api_requests::models::RewardEstimationResponse, + nym_api_requests::models::UptimeResponse, + nym_api_requests::models::ComputeRewardEstParam, + nym_api_requests::models::MixNodeBondAnnotated, + nym_api_requests::models::GatewayBondAnnotated, + nym_api_requests::models::MixnodeTestResultResponse, + nym_api_requests::models::StakeSaturationResponse, + nym_api_requests::models::InclusionProbabilityResponse, + nym_api_requests::models::AllInclusionProbabilitiesResponse, + nym_api_requests::models::InclusionProbability, + nym_api_requests::models::SelectionChance, + crate::network::models::NetworkDetails, + nym_config::defaults::NymNetworkDetails, + nym_config::defaults::ChainDetails, + nym_config::defaults::DenomDetailsOwned, + nym_config::defaults::ValidatorDetails, + nym_config::defaults::NymContracts, + ContractVersionSchemaResponse, + crate::network::models::ContractInformation, + nym_api_requests::models::ApiHealthResponse, + nym_api_requests::models::ApiStatus, + nym_bin_common::build_information::BinaryBuildInformationOwned, + nym_api_requests::models::SignerInformationResponse, + nym_api_requests::models::LegacyDescribedGateway, + nym_mixnet_contract_common::Gateway, + nym_mixnet_contract_common::GatewayBond, + nym_api_requests::models::NymNodeDescription, + nym_api_requests::models::HostInformation, + nym_api_requests::models::HostKeys, + nym_node_requests::api::v1::node::models::AuxiliaryDetails, + nym_api_requests::models::NetworkRequesterDetails, + nym_api_requests::models::IpPacketRouterDetails, + nym_api_requests::models::AuthenticatorDetails, + nym_api_requests::models::WebSockets, + nym_api_requests::nym_nodes::NodeRole, + nym_api_requests::models::LegacyDescribedMixNode, + nym_api_requests::ecash::VerificationKeyResponse, + nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse, + nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EpochCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialResponse, + nym_api_requests::ecash::models::IssuedTicketbookBody, + nym_api_requests::ecash::models::BlindedSignatureResponse, + nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse, + nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EcashTicketVerificationResponse, + nym_api_requests::ecash::models::EcashTicketVerificationRejection, + nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse, + nym_api_requests::ecash::models::SpentCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialsResponse, + nym_api_requests::nym_nodes::SkimmedNode, + nym_api_requests::nym_nodes::SemiSkimmedNode, + nym_api_requests::nym_nodes::FullFatNode, + nym_api_requests::nym_nodes::BasicEntryInformation, + nym_api_requests::nym_nodes::NodeRoleQueryParam, + nym_api_requests::models::AnnotationResponse, + nym_api_requests::models::NodePerformanceResponse, + nym_api_requests::models::NodeDatePerformanceResponse, + nym_api_requests::models::PerformanceHistoryResponse, + nym_api_requests::models::UptimeHistoryResponse, + )) +)] +pub(crate) struct ApiDoc; diff --git a/nym-api/src/v2/router.rs b/nym-api/src/support/http/router.rs similarity index 81% rename from nym-api/src/v2/router.rs rename to nym-api/src/support/http/router.rs index 6ece0b7323..38eec8d76e 100644 --- a/nym-api/src/v2/router.rs +++ b/nym-api/src/support/http/router.rs @@ -5,11 +5,15 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::node_status_routes; use crate::nym_contract_cache::handlers::nym_contract_cache_routes; +use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; -use crate::nym_nodes::handlers_unstable::nym_node_routes_unstable; use crate::status; -use crate::v2::AxumAppState; +use crate::support::http::openapi::ApiDoc; +use crate::support::http::state::AppState; +use crate::support::http::unstable_routes::unstable_routes; use anyhow::anyhow; +use axum::response::Redirect; +use axum::routing::get; use axum::Router; use core::net::SocketAddr; use nym_http_api_common::logging::logger; @@ -27,7 +31,7 @@ use utoipa_swagger_ui::SwaggerUi; /// /// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering pub(crate) struct RouterBuilder { - unfinished_router: Router, + unfinished_router: Router, } impl RouterBuilder { @@ -43,23 +47,22 @@ impl RouterBuilder { // .on_request(DefaultOnRequest::new()) // .on_response(DefaultOnResponse::new().latency_unit(tower_http::LatencyUnit::Micros)), // ) - // .route("/", axum::routing::get(|| async {axum::response::Redirect::permanent("/swagger")})) // .route("/swagger", axum::routing::get(hello)) let default_routes = Router::new() - .merge( - SwaggerUi::new("/swagger") - .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), - ) + .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/", get(|| async { Redirect::to("/swagger") })) .nest( "/v1", Router::new() - .nest("/circulating-supply", circulating_supply_routes()) + // unfortunately some routes didn't use correct prefix and were attached to the root .merge(nym_contract_cache_routes()) + .merge(legacy_nym_node_routes()) + .nest("/circulating-supply", circulating_supply_routes()) .nest("/status", node_status_routes(network_monitor)) .nest("/network", nym_network_routes()) .nest("/api-status", status::handlers::api_status_routes()) - .merge(nym_node_routes()) - .nest("/unstable/nym-nodes", nym_node_routes_unstable()), // CORS layer needs to be "outside" of routes + .nest("/nym-nodes", nym_node_routes()) + .nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes ); Self { @@ -67,7 +70,7 @@ impl RouterBuilder { } } - pub(crate) fn nest(self, path: &str, router: Router) -> Self { + pub(crate) fn nest(self, path: &str, router: Router) -> Self { Self { unfinished_router: self.unfinished_router.nest(path, router), } @@ -75,14 +78,14 @@ impl RouterBuilder { /// Invoke this as late as possible before constructing HTTP server /// (after all routes were added). - pub(crate) fn with_state(self, state: AxumAppState) -> RouterWithState { + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { RouterWithState { router: self.finalize_routes().with_state(state), } } /// Middleware added here intercepts the request before it gets to other routes. - fn finalize_routes(self) -> Router { + fn finalize_routes(self) -> Router { self.unfinished_router .layer(setup_cors()) .layer(axum::middleware::from_fn(logger)) diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs new file mode 100644 index 0000000000..a0a640333b --- /dev/null +++ b/nym-api/src/support/http/state.rs @@ -0,0 +1,104 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; +use crate::support::storage; +use nym_task::TaskManager; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +pub(crate) const TASK_MANAGER_TIMEOUT_S: u64 = 10; + +/// Shutdown goes 2 directions: +/// 1. signal background tasks to gracefully finish +/// 2. signal server itself +/// +/// These are done through separate shutdown handles. Of course, shut down server +/// AFTER you have shut down BG tasks (or past their grace period). +pub(crate) struct ShutdownHandles { + task_manager: TaskManager, + axum_shutdown_button: ShutdownAxum, + /// Tokio JoinHandle for axum server's task + axum_join_handle: AxumJoinHandle, +} + +impl ShutdownHandles { + /// Cancellation token is given to Axum server constructor. When the token + /// receives a shutdown signal, Axum server will shut down gracefully. + pub(crate) fn new( + task_manager: TaskManager, + axum_server_handle: AxumJoinHandle, + shutdown_button: CancellationToken, + ) -> Self { + Self { + task_manager, + axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), + axum_join_handle: axum_server_handle, + } + } + + pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { + &mut self.task_manager + } + + /// Signal server to shut down, then return join handle to its + /// `tokio` task + /// + /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html + #[must_use] + pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { + self.axum_shutdown_button.0.cancel(); + self.axum_join_handle + } +} + +struct ShutdownAxum(CancellationToken); + +type AxumJoinHandle = JoinHandle>; + +#[derive(Clone)] +pub(crate) struct AppState { + pub(crate) nym_contract_cache: NymContractCache, + pub(crate) node_status_cache: NodeStatusCache, + pub(crate) circulating_supply_cache: CirculatingSupplyCache, + pub(crate) storage: storage::NymApiStorage, + pub(crate) described_nodes_cache: SharedCache, + pub(crate) network_details: NetworkDetails, + pub(crate) node_info_cache: unstable::NodeInfoCache, +} + +impl AppState { + pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { + &self.nym_contract_cache + } + + pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { + &self.node_status_cache + } + + pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { + &self.circulating_supply_cache + } + + pub(crate) fn network_details(&self) -> &NetworkDetails { + &self.network_details + } + + pub(crate) fn described_nodes_cache(&self) -> &SharedCache { + &self.described_nodes_cache + } + + pub(crate) fn storage(&self) -> &storage::NymApiStorage { + &self.storage + } + + pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { + &self.node_info_cache + } +} diff --git a/nym-api/src/support/http/unstable_routes.rs b/nym-api/src/support/http/unstable_routes.rs new file mode 100644 index 0000000000..7bc1a9644c --- /dev/null +++ b/nym-api/src/support/http/unstable_routes.rs @@ -0,0 +1,11 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::nym_nodes::handlers::unstable::nym_node_routes_unstable; +use crate::support::http::state::AppState; +use axum::Router; + +// as those get stabilised, they should get deprecated and use a redirection instead +pub(crate) fn unstable_routes() -> Router { + Router::new().nest("/nym-nodes", nym_node_routes_unstable()) +} diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 69f9a49360..46ae0c67dc 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::error::EcashError; -use crate::epoch_operations::MixnodeWithPerformance; +use crate::epoch_operations::RewardedNodeWithParams; use crate::support::config::Config; use anyhow::Result; use async_trait::async_trait; @@ -24,14 +24,16 @@ use nym_config::defaults::{ChainDetails, NymNetworkDetails}; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; -use nym_mixnet_contract_common::families::FamilyHead; +use nym_mixnet_contract_common::gateway::PreassignedId; use nym_mixnet_contract_common::mixnode::MixNodeDetails; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment, - MixId, RewardedSetNodeStatus, + CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, NymNodeDetails, + RewardedSet, RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ @@ -213,6 +215,10 @@ impl Client { Ok(hash) } + pub(crate) async fn get_nymnodes(&self) -> Result, NyxdError> { + nyxd_query!(self, get_all_nymnodes_detailed().await) + } + pub(crate) async fn get_mixnodes(&self) -> Result, NyxdError> { nyxd_query!(self, get_all_mixnodes_detailed().await) } @@ -221,6 +227,10 @@ impl Client { nyxd_query!(self, get_all_gateways().await) } + pub(crate) async fn get_gateway_ids(&self) -> Result, NyxdError> { + nyxd_query!(self, get_all_preassigned_gateway_ids().await) + } + pub(crate) async fn get_current_interval(&self) -> Result { nyxd_query!(self, get_current_interval_details().await) } @@ -235,10 +245,8 @@ impl Client { nyxd_query!(self, get_rewarding_parameters().await) } - pub(crate) async fn get_rewarded_set_mixnodes( - &self, - ) -> Result, NyxdError> { - nyxd_query!(self, get_all_rewarded_set_mixnodes().await) + pub(crate) async fn get_rewarded_set_nodes(&self) -> Result { + nyxd_query!(self, get_rewarded_set().await) } pub(crate) async fn get_current_vesting_account_storage_key(&self) -> Result { @@ -266,13 +274,6 @@ impl Client { ) -> Result, NyxdError> { nyxd_query!(self, get_all_accounts_vesting_coins().await) } - - pub(crate) async fn get_all_family_members( - &self, - ) -> Result, NyxdError> { - nyxd_query!(self, get_all_family_members().await) - } - pub(crate) async fn get_pending_events_count(&self) -> Result { let pending = nyxd_query!(self, get_number_of_pending_events().await?); Ok(pending.epoch_events + pending.interval_events) @@ -283,29 +284,21 @@ impl Client { Ok(()) } + fn generate_reward_messages( + &self, + rewarded_set: &[RewardedNodeWithParams], + ) -> Vec<(ExecuteMsg, Vec)> { + rewarded_set + .iter() + .map(|node| (*node).into()) + .zip(std::iter::repeat(Vec::new())) + .collect() + } + pub(crate) async fn send_rewarding_messages( &self, - nodes: &[MixnodeWithPerformance], + rewarded_set: &[RewardedNodeWithParams], ) -> Result<(), NyxdError> { - // for some reason, compiler complains if this is explicitly inline in code ¯\_(ツ)_/¯ - #[inline] - #[allow(unused_variables)] - fn generate_reward_messages( - eligible_mixnodes: &[MixnodeWithPerformance], - ) -> Vec<(ExecuteMsg, Vec)> { - cfg_if::cfg_if! { - if #[cfg(feature = "no-reward")] { - vec![] - } else { - eligible_mixnodes - .iter() - .map(|node| (*node).into()) - .zip(std::iter::repeat(Vec::new())) - .collect() - } - } - } - // the expect is fine as we always construct the client with the mixnet contract explicitly set let mixnet_contract = nyxd_query!( self, @@ -314,7 +307,7 @@ impl Client { .clone() ); - let msgs = generate_reward_messages(nodes); + let msgs = self.generate_reward_messages(rewarded_set); // "technically" we don't need a write access to the client, // but we REALLY don't want to accidentally send any transactions while we're sending rewarding messages @@ -325,21 +318,65 @@ impl Client { &mixnet_contract, msgs, Default::default(), - format!("rewarding {} mixnodes", nodes.len()), + format!("rewarding {} nodes", rewarded_set.len()), ) .await? ); Ok(()) } - pub(crate) async fn advance_current_epoch( + fn generate_role_assignment_messages( &self, - new_rewarded_set: Vec, - expected_active_set_size: u32, + rewarded_set: RewardedSet, + ) -> Vec<(ExecuteMsg, Vec)> { + // currently we just assign all of them together, + // but the contract is ready to handle them separately should we need it + // if the tx is too big + let mut msgs = Vec::new(); + for (role, nodes) in [ + (Role::ExitGateway, rewarded_set.exit_gateways), + (Role::EntryGateway, rewarded_set.entry_gateways), + (Role::Layer1, rewarded_set.layer1), + (Role::Layer2, rewarded_set.layer2), + (Role::Layer3, rewarded_set.layer3), + (Role::Standby, rewarded_set.standby), + ] { + msgs.push(( + ExecuteMsg::AssignRoles { + assignment: RoleAssignment { role, nodes }, + }, + Vec::new(), + )); + } + msgs + } + + pub(crate) async fn send_role_assignment_messages( + &self, + rewarded_set: RewardedSet, ) -> Result<(), NyxdError> { + // the expect is fine as we always construct the client with the mixnet contract explicitly set + let mixnet_contract = nyxd_query!( + self, + mixnet_contract_address() + .expect("mixnet contract address is not available") + .clone() + ); + + let msgs = self.generate_role_assignment_messages(rewarded_set); + + // "technically" we don't need a write access to the client, + // but we REALLY don't want to accidentally send any transactions while we're sending rewarding messages + // as that would have messed up sequence numbers nyxd_signing!( self, - advance_current_epoch(new_rewarded_set, expected_active_set_size, None).await? + execute_multiple( + &mixnet_contract, + msgs, + Default::default(), + "assigning all the rewarded set roles", + ) + .await? ); Ok(()) } diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index a7950e3252..392efa6af4 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -1,14 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::{HistoricalUptime, Uptime}; + +use crate::node_status_api::models::{HistoricalUptime as ApiHistoricalUptime, Uptime}; use crate::node_status_api::utils::{ActiveGatewayStatuses, ActiveMixnodeStatuses}; use crate::support::storage::models::{ - ActiveGateway, ActiveMixnode, GatewayDetails, MixnodeDetails, NodeStatus, RewardingReport, - TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, + ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, NodeStatus, + RewardingReport, TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, }; -use nym_mixnet_contract_common::{EpochId, IdentityKey, MixId}; -use nym_types::monitoring::{GatewayResult, MixnodeResult, NodeResult}; -use time::OffsetDateTime; +use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; +use nym_types::monitoring::NodeResult; +use sqlx::FromRow; +use time::Date; +use tracing::info; #[derive(Clone)] pub(crate) struct StorageManager { @@ -16,12 +19,12 @@ pub(crate) struct StorageManager { } pub struct AvgMixnodeReliability { - mix_id: MixId, + mix_id: NodeId, value: Option, } impl AvgMixnodeReliability { - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.mix_id } @@ -30,14 +33,15 @@ impl AvgMixnodeReliability { } } +#[derive(FromRow)] pub struct AvgGatewayReliability { - identity: String, + node_id: NodeId, value: Option, } impl AvgGatewayReliability { - pub fn identity(&self) -> &str { - &self.identity + pub fn node_id(&self) -> NodeId { + self.node_id } pub fn value(&self) -> f32 { @@ -50,9 +54,9 @@ impl StorageManager { pub(crate) async fn get_mixnode_mix_ids_by_identity( &self, identity: &str, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { let ids = sqlx::query!( - r#"SELECT mix_id as "mix_id: MixId" FROM mixnode_details WHERE identity_key = ?"#, + r#"SELECT mix_id as "mix_id: NodeId" FROM mixnode_details WHERE identity_key = ?"#, identity ) .fetch_all(&self.connection_pool) @@ -91,7 +95,7 @@ impl StorageManager { AvgMixnodeReliability, r#" SELECT - d.mix_id as "mix_id: MixId", + d.mix_id as "mix_id: NodeId", AVG(s.reliability) as "value: f32" FROM mixnode_details d @@ -115,11 +119,12 @@ impl StorageManager { start_ts_secs: i64, end_ts_secs: i64, ) -> Result, sqlx::Error> { - let result = sqlx::query_as!( - AvgGatewayReliability, + // we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations. + // some (like v3 directory) happens at runtime + let result = sqlx::query_as( r#" SELECT - d.identity as "identity: String", + d.node_id as "node_id: NodeId", CASE WHEN count(*) > 3 THEN AVG(reliability) ELSE 100 END as "value: f32" FROM gateway_details d @@ -130,9 +135,9 @@ impl StorageManager { timestamp <= ? GROUP BY 1 "#, - start_ts_secs, - end_ts_secs ) + .bind(start_ts_secs) + .bind(end_ts_secs) .fetch_all(&self.connection_pool) .await?; Ok(result) @@ -145,7 +150,7 @@ impl StorageManager { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn get_mixnode_database_id( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, sqlx::Error> { let id = sqlx::query!("SELECT id FROM mixnode_details WHERE mix_id = ?", mix_id) .fetch_optional(&self.connection_pool) @@ -155,12 +160,23 @@ impl StorageManager { Ok(id) } + pub(crate) async fn get_gateway_database_id( + &self, + node_id: NodeId, + ) -> Result, sqlx::Error> { + let id = sqlx::query!("SELECT id FROM gateway_details WHERE node_id = ?", node_id) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.id); + + Ok(id) + } + /// Tries to obtain row id of given gateway given its identity - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_id(&self, identity: &str) -> Result, sqlx::Error> { + pub(crate) async fn get_gateway_database_id_by_identity( + &self, + identity: &str, + ) -> Result, sqlx::Error> { let id = sqlx::query!( "SELECT id FROM gateway_details WHERE identity = ?", identity @@ -172,21 +188,34 @@ impl StorageManager { Ok(id) } - /// Tries to obtain owner value of given mixnode given its mix_id - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_owner( + pub(crate) async fn get_gateway_node_id_from_identity_key( &self, - mix_id: MixId, - ) -> Result, sqlx::Error> { - let owner = sqlx::query!("SELECT owner FROM mixnode_details WHERE mix_id = ?", mix_id) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.owner); + identity: &str, + ) -> Result, sqlx::Error> { + let node_id = sqlx::query!( + r#"SELECT node_id as "node_id: NodeId" FROM gateway_details WHERE identity = ?"#, + identity + ) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.node_id); - Ok(owner) + Ok(node_id) + } + + pub(crate) async fn get_gateway_identity_key( + &self, + node_id: NodeId, + ) -> Result, sqlx::Error> { + let identity_key = sqlx::query!( + "SELECT identity FROM gateway_details WHERE node_id = ?", + node_id + ) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.identity); + + Ok(identity_key) } /// Tries to obtain identity value of given mixnode given its mix_id @@ -196,7 +225,7 @@ impl StorageManager { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn get_mixnode_identity_key( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, sqlx::Error> { let identity_key = sqlx::query!( "SELECT identity_key FROM mixnode_details WHERE mix_id = ?", @@ -209,26 +238,6 @@ impl StorageManager { Ok(identity_key) } - /// Tries to obtain owner value of given gateway given its identity - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_owner( - &self, - identity: &str, - ) -> Result, sqlx::Error> { - let owner = sqlx::query!( - "SELECT owner FROM gateway_details WHERE identity = ?", - identity - ) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.owner); - - Ok(owner) - } - /// Gets all reliability statuses for mixnode with particular identity that were inserted /// into the database after the specified unix timestamp. /// @@ -238,7 +247,7 @@ impl StorageManager { /// * `timestamp`: unix timestamp of the lower bound of the selection. pub(crate) async fn get_mixnode_statuses_since( &self, - mix_id: MixId, + mix_id: NodeId, timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( @@ -266,7 +275,7 @@ impl StorageManager { /// * `timestamp`: unix timestamp of the lower bound of the selection. pub(crate) async fn get_gateway_statuses_since( &self, - identity: &str, + node_id: NodeId, timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( @@ -276,9 +285,9 @@ impl StorageManager { FROM gateway_status JOIN gateway_details ON gateway_status.gateway_details_id = gateway_details.id - WHERE gateway_details.identity=? AND gateway_status.timestamp > ?; + WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?; "#, - identity, + node_id, timestamp, ) .fetch_all(&self.connection_pool) @@ -292,16 +301,16 @@ impl StorageManager { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn get_mixnode_historical_uptimes( &self, - mix_id: MixId, - ) -> Result, sqlx::Error> { + mix_id: NodeId, + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM mixnode_historical_uptime - JOIN mixnode_details - ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id = ? - ORDER BY date ASC + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE mixnode_details.mix_id = ? + ORDER BY date ASC "#, mix_id ) @@ -312,7 +321,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -330,18 +339,18 @@ impl StorageManager { /// * `identity`: identity (base58-encoded public key) of the gateway. pub(crate) async fn get_gateway_historical_uptimes( &self, - identity: &str, - ) -> Result, sqlx::Error> { + node_id: NodeId, + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM gateway_historical_uptime - JOIN gateway_details - ON gateway_historical_uptime.gateway_details_id = gateway_details.id - WHERE gateway_details.identity = ? - ORDER BY date ASC + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE gateway_details.node_id = ? + ORDER BY date ASC "#, - identity + node_id ) .fetch_all(&self.connection_pool) .await? @@ -350,7 +359,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -361,6 +370,54 @@ impl StorageManager { Ok(uptimes) } + pub(crate) async fn get_historical_mix_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE + mixnode_details.mix_id = ? + AND + mixnode_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn get_historical_gateway_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE + gateway_details.node_id = ? + AND + gateway_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + /// Gets all reliability statuses for mixnode with particular id that were inserted /// into the database within the specified time interval. /// @@ -451,7 +508,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_gateway_statuses_by_id( + pub(crate) async fn get_gateway_statuses_by_database_id( &self, id: i64, since: i64, @@ -481,7 +538,7 @@ impl StorageManager { pub(crate) async fn submit_mixnode_statuses( &self, timestamp: i64, - mixnode_results: Vec, + mixnode_results: Vec, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) @@ -489,13 +546,12 @@ impl StorageManager { for mixnode_result in mixnode_results { let mixnode_id = sqlx::query!( r#" - INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key, owner) VALUES (?, ?, ?); + INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key) VALUES (?, ?); SELECT id FROM mixnode_details WHERE mix_id = ?; "#, - mixnode_result.mix_id, + mixnode_result.node_id, mixnode_result.identity, - mixnode_result.owner, - mixnode_result.mix_id, + mixnode_result.node_id, ) .fetch_one(&mut tx) .await? @@ -524,39 +580,41 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { info!("Inserting {} mixnode statuses", mixnode_results.len()); - let timestamp = OffsetDateTime::now_utc().unix_timestamp(); - // insert it all in a transaction to make sure all nodes are updated at the same time - // (plus it's a nice guard against new nodes) - let mut tx = self.connection_pool.begin().await?; - for mixnode_result in mixnode_results { - let mixnode_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO mixnode_details_v2(node_id, identity_key) VALUES (?, ?); - SELECT id FROM mixnode_details_v2 WHERE node_id = ?; - "#, - mixnode_result.node_id, - mixnode_result.identity, - mixnode_result.node_id, - ) - .fetch_one(&mut tx) - .await? - .id; - - // insert the actual status - sqlx::query!( - r#" - INSERT INTO mixnode_status_v2 (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); - "#, - mixnode_id, - mixnode_result.reliability, - timestamp - ) - .execute(&mut tx) - .await?; - } - - // finally commit the transaction - tx.commit().await + todo!("look at what broke during rebasing"); + // + // let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + // // insert it all in a transaction to make sure all nodes are updated at the same time + // // (plus it's a nice guard against new nodes) + // let mut tx = self.connection_pool.begin().await?; + // for mixnode_result in mixnode_results { + // let mixnode_id = sqlx::query!( + // r#" + // INSERT OR IGNORE INTO mixnode_details_v2(node_id, identity_key) VALUES (?, ?); + // SELECT id FROM mixnode_details_v2 WHERE node_id = ?; + // "#, + // mixnode_result.node_id, + // mixnode_result.identity, + // mixnode_result.node_id, + // ) + // .fetch_one(&mut tx) + // .await? + // .id; + // + // // insert the actual status + // sqlx::query!( + // r#" + // INSERT INTO mixnode_status_v2 (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); + // "#, + // mixnode_id, + // mixnode_result.reliability, + // timestamp + // ) + // .execute(&mut tx) + // .await?; + // } + // + // // finally commit the transaction + // tx.commit().await } /// Tries to submit gateway [`NodeResult`] from the network monitor to the database. @@ -568,7 +626,7 @@ impl StorageManager { pub(crate) async fn submit_gateway_statuses( &self, timestamp: i64, - gateway_results: Vec, + gateway_results: Vec, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) @@ -580,11 +638,11 @@ impl StorageManager { // same ID "problem" as described for mixnode insertion let gateway_id = sqlx::query!( r#" - INSERT OR IGNORE INTO gateway_details(identity, owner) VALUES (?, ?); + INSERT OR IGNORE INTO gateway_details(node_id, identity) VALUES (?, ?); SELECT id FROM gateway_details WHERE identity = ?; "#, + gateway_result.node_id, gateway_result.identity, - gateway_result.owner, gateway_result.identity, ) .fetch_one(&mut tx) @@ -614,43 +672,45 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { info!("Inserting {} gateway statuses", gateway_results.len()); - let timestamp = OffsetDateTime::now_utc().unix_timestamp(); - // insert it all in a transaction to make sure all nodes are updated at the same time - // (plus it's a nice guard against new nodes) - let mut tx = self.connection_pool.begin().await?; + todo!("look at what broke during rebasing"); - for gateway_result in gateway_results { - // if gateway info doesn't exist, insert it and get its id - - // same ID "problem" as described for mixnode insertion - let gateway_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO gateway_details_v2(identity, node_id) VALUES (?, ?); - SELECT id FROM gateway_details_v2 WHERE identity = ?; - "#, - gateway_result.identity, - gateway_result.node_id, - gateway_result.identity, - ) - .fetch_one(&mut tx) - .await? - .id; - - // insert the actual status - sqlx::query!( - r#" - INSERT INTO gateway_status_v2 (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); - "#, - gateway_id, - gateway_result.reliability, - timestamp - ) - .execute(&mut tx) - .await?; - } - - // finally commit the transaction - tx.commit().await + // let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + // // insert it all in a transaction to make sure all nodes are updated at the same time + // // (plus it's a nice guard against new nodes) + // let mut tx = self.connection_pool.begin().await?; + // + // for gateway_result in gateway_results { + // // if gateway info doesn't exist, insert it and get its id + // + // // same ID "problem" as described for mixnode insertion + // let gateway_id = sqlx::query!( + // r#" + // INSERT OR IGNORE INTO gateway_details_v2(identity, node_id) VALUES (?, ?); + // SELECT id FROM gateway_details_v2 WHERE identity = ?; + // "#, + // gateway_result.identity, + // gateway_result.node_id, + // gateway_result.identity, + // ) + // .fetch_one(&mut tx) + // .await? + // .id; + // + // // insert the actual status + // sqlx::query!( + // r#" + // INSERT INTO gateway_status_v2 (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); + // "#, + // gateway_id, + // gateway_result.reliability, + // timestamp + // ) + // .execute(&mut tx) + // .await?; + // } + // + // // finally commit the transaction + // tx.commit().await } /// Saves the information about which nodes were used as core nodes during this particular @@ -802,13 +862,13 @@ impl StorageManager { /// * `uptime`: the actual uptime of the node during the specified day. pub(crate) async fn insert_gateway_historical_uptime( &self, - mix_id: i64, + db_id: i64, date: &str, uptime: u8, ) -> Result<(), sqlx::Error> { sqlx::query!( "INSERT INTO gateway_historical_uptime(gateway_details_id, date, uptime) VALUES (?, ?, ?)", - mix_id, + db_id, date, uptime, ).execute(&self.connection_pool).await?; @@ -901,7 +961,7 @@ impl StorageManager { sqlx::query_as!( ActiveMixnode, r#" - SELECT DISTINCT identity_key, mix_id as "mix_id: MixId", owner, id + SELECT DISTINCT identity_key, mix_id as "mix_id: NodeId", id FROM mixnode_details JOIN mixnode_status ON mixnode_details.id = mixnode_status.mixnode_details_id @@ -928,10 +988,9 @@ impl StorageManager { since: i64, until: i64, ) -> Result, sqlx::Error> { - sqlx::query_as!( - ActiveGateway, + sqlx::query_as( r#" - SELECT DISTINCT identity, owner, id + SELECT DISTINCT identity, node_id as "node_id: NodeId", id FROM gateway_details JOIN gateway_status ON gateway_details.id = gateway_status.gateway_details_id @@ -939,9 +998,9 @@ impl StorageManager { SELECT 1 FROM gateway_status WHERE timestamp > ? AND timestamp < ? ) "#, - since, - until, ) + .bind(since) + .bind(until) .fetch_all(&self.connection_pool) .await } @@ -1032,7 +1091,6 @@ impl StorageManager { let statuses = ActiveMixnodeStatuses { mix_id: active_node.mix_id, identity: active_node.identity_key, - owner: active_node.owner, statuses, }; @@ -1060,12 +1118,12 @@ impl StorageManager { let mut active_day_statuses = Vec::with_capacity(active_nodes.len()); for active_node in active_nodes.into_iter() { let statuses = self - .get_gateway_statuses_by_id(active_node.id, since, until) + .get_gateway_statuses_by_database_id(active_node.id, since, until) .await?; let statuses = ActiveGatewayStatuses { + node_id: active_node.node_id, identity: active_node.identity, - owner: active_node.owner, statuses, }; @@ -1092,13 +1150,12 @@ impl StorageManager { &self, id: i64, ) -> Result, sqlx::Error> { - sqlx::query_as!( - GatewayDetails, - "SELECT * FROM gateway_details WHERE id = ?", - id - ) - .fetch_optional(&self.connection_pool) - .await + // we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations. + // some (like v3 directory) happens at runtime + sqlx::query_as("SELECT * FROM gateway_details WHERE id = ?") + .bind(id) + .fetch_optional(&self.connection_pool) + .await } pub(crate) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result { @@ -1119,7 +1176,7 @@ impl StorageManager { pub(crate) async fn get_mixnode_statuses( &self, - mix_id: MixId, + mix_id: NodeId, limit: u32, offset: u32, ) -> Result, sqlx::Error> { @@ -1204,3 +1261,86 @@ impl StorageManager { .await } } + +pub(crate) mod v3_migration { + use crate::support::storage::manager::StorageManager; + use crate::support::storage::models::GatewayDetailsBeforeMigration; + use nym_mixnet_contract_common::NodeId; + + impl StorageManager { + pub(crate) async fn check_v3_migration(&self) -> Result { + sqlx::query!("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'",) + .fetch_one(&self.connection_pool) + .await + .map(|result| result.exists == 1) + } + + pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), sqlx::Error> { + sqlx::query!("INSERT INTO v3_migration_info(id) VALUES (0)") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_all_known_gateways( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM gateway_details") + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn set_gateway_node_id( + &self, + identity: &str, + node_id: NodeId, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE gateway_details SET node_id = ? WHERE identity = ?", + node_id, + identity + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM gateway_historical_uptime WHERE gateway_details_id = ?; + DELETE FROM gateway_status WHERE gateway_details_id = ?; + DELETE FROM testing_route WHERE gateway_id = ?; + DELETE FROM gateway_details WHERE id = ?; + "#, + db_id, + db_id, + db_id, + db_id, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn make_node_id_not_null(&self) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + CREATE TABLE gateway_details_temp + ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + node_id INTEGER NOT NULL UNIQUE, + identity VARCHAR NOT NULL UNIQUE + ); + + INSERT INTO gateway_details_temp SELECT * FROM gateway_details; + DROP TABLE gateway_details; + ALTER TABLE gateway_details_temp RENAME TO gateway_details; + "#, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + } +} diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index d4cdc64c02..e647adf7d0 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -3,21 +3,21 @@ use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ - GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, - NymApiStorageError, Uptime, + GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime as ApiHistoricalUptime, + MixnodeStatusReport, MixnodeUptimeHistory, NymApiStorageError, Uptime, }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; use crate::storage::models::{NodeStatus, TestingRoute}; use crate::support::storage::models::{ - GatewayDetails, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, + GatewayDetails, HistoricalUptime, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, }; -use nym_mixnet_contract_common::MixId; -use nym_types::monitoring::{GatewayResult, MixnodeResult}; -use rocket::fairing::AdHoc; +use nym_mixnet_contract_common::NodeId; +use nym_types::monitoring::NodeResult; use sqlx::ConnectOptions; use std::path::Path; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; +use tracing::{error, info, warn}; use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; @@ -64,18 +64,11 @@ impl NymApiStorage { Ok(storage) } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub(crate) fn stage(storage: NymApiStorage) -> AdHoc { - AdHoc::try_on_ignite("SQLx Database", |rocket| async { - Ok(rocket.manage(storage)) - }) - } - #[allow(unused)] pub(crate) async fn mix_identity_to_mix_ids( &self, identity: &str, - ) -> Result, NymApiStorageError> { + ) -> Result, NymApiStorageError> { Ok(self .manager .get_mixnode_mix_ids_by_identity(identity) @@ -86,7 +79,7 @@ impl NymApiStorage { pub(crate) async fn mix_identity_to_latest_mix_id( &self, identity: &str, - ) -> Result, NymApiStorageError> { + ) -> Result, NymApiStorageError> { Ok(self .mix_identity_to_mix_ids(identity) .await? @@ -127,7 +120,7 @@ impl NymApiStorage { /// * `since`: unix timestamp indicating the lower bound interval of the selection. async fn get_mixnode_statuses( &self, - mix_id: MixId, + mix_id: NodeId, since: i64, ) -> Result, NymApiStorageError> { let statuses = self @@ -143,16 +136,15 @@ impl NymApiStorage { /// /// # Arguments /// - /// * `identity`: identity key of the gateway to query. /// * `since`: unix timestamp indicating the lower bound interval of the selection. async fn get_gateway_statuses( &self, - identity: &str, + node_id: NodeId, since: i64, ) -> Result, NymApiStorageError> { let statuses = self .manager - .get_gateway_statuses_since(identity, since) + .get_gateway_statuses_since(node_id, since) .await?; Ok(statuses) @@ -165,7 +157,7 @@ impl NymApiStorage { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn construct_mixnode_report( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let now = OffsetDateTime::now_utc(); let day_ago = (now - ONE_DAY).unix_timestamp(); @@ -186,21 +178,14 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let mixnode_owner = - self.manager.get_mixnode_owner(mix_id).await?.expect( - "The node doesn't have an owner even though we have status information on it!", - ); - - let mixnode_identity = - self.manager.get_mixnode_identity_key(mix_id).await?.expect( - "The node doesn't have an owner even though we have status information on it!", - ); + let mixnode_identity = self.manager.get_mixnode_identity_key(mix_id).await?.expect( + "The node doesn't have an identity even though we have status information on it!", + ); Ok(MixnodeStatusReport::construct_from_last_day_reports( now, mix_id, mixnode_identity, - mixnode_owner, statuses, last_hour_runs_count, last_day_runs_count, @@ -209,19 +194,17 @@ impl NymApiStorage { pub(crate) async fn construct_gateway_report( &self, - identity: &str, + node_id: NodeId, ) -> Result { let now = OffsetDateTime::now_utc(); let day_ago = (now - ONE_DAY).unix_timestamp(); let hour_ago = (now - ONE_HOUR).unix_timestamp(); - let statuses = self.get_gateway_statuses(identity, day_ago).await?; + let statuses = self.get_gateway_statuses(node_id, day_ago).await?; // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report if statuses.is_empty() { - return Err(NymApiStorageError::GatewayReportNotFound { - identity: identity.to_owned(), - }); + return Err(NymApiStorageError::GatewayReportNotFound { node_id }); } // determine the number of runs the gateway should have been online for @@ -232,14 +215,18 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let gateway_owner = self.manager.get_gateway_owner(identity).await?.expect( - "The gateway doesn't have an owner even though we have status information on it!", - ); + let gateway_identity = self + .manager + .get_gateway_identity_key(node_id) + .await? + .expect( + "The node doesn't have an identity even though we have status information on it!", + ); Ok(GatewayStatusReport::construct_from_last_day_reports( now, - identity.to_owned(), - gateway_owner, + node_id, + gateway_identity, statuses, last_hour_runs_count, last_day_runs_count, @@ -248,7 +235,7 @@ impl NymApiStorage { pub(crate) async fn get_mixnode_uptime_history( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let history = self.manager.get_mixnode_historical_uptimes(mix_id).await?; @@ -256,69 +243,123 @@ impl NymApiStorage { return Err(NymApiStorageError::MixnodeUptimeHistoryNotFound { mix_id }); } - let mixnode_owner = - self.manager.get_mixnode_owner(mix_id).await?.expect( - "The node doesn't have an owner even though we have uptime history for it!", - ); - let mixnode_identity = self.manager.get_mixnode_identity_key(mix_id).await?.expect( "The node doesn't have an identity even though we have uptime history for it!", ); - Ok(MixnodeUptimeHistory::new( - mix_id, - mixnode_identity, - mixnode_owner, + Ok(MixnodeUptimeHistory::new(mix_id, mixnode_identity, history)) + } + + pub(crate) async fn get_gateway_uptime_history_by_identity( + &self, + gateway_identity: &str, + ) -> Result { + let Some(node_id) = self + .manager + .get_gateway_node_id_from_identity_key(gateway_identity) + .await? + else { + return Err(NymApiStorageError::GatewayNotFound { + identity: gateway_identity.to_string(), + }); + }; + + let history = self.manager.get_gateway_historical_uptimes(node_id).await?; + + if history.is_empty() { + return Err(NymApiStorageError::GatewayUptimeHistoryNotFound { node_id }); + } + + Ok(GatewayUptimeHistory::new( + node_id, + gateway_identity, history, )) } - pub(crate) async fn get_gateway_uptime_history( + pub(crate) async fn get_node_uptime_history( &self, - identity: &str, - ) -> Result { - let history = self - .manager - .get_gateway_historical_uptimes(identity) - .await?; + node_id: NodeId, + ) -> Result, NymApiStorageError> { + let history = self.manager.get_mixnode_historical_uptimes(node_id).await?; - if history.is_empty() { - return Err(NymApiStorageError::GatewayUptimeHistoryNotFound { - identity: identity.to_owned(), - }); + if !history.is_empty() { + return Ok(history); } - let gateway_owner = - self.manager.get_gateway_owner(identity).await?.expect( - "The gateway doesn't have an owner even though we have uptime history for it!", - ); - - Ok(GatewayUptimeHistory::new( - identity.to_owned(), - gateway_owner, - history, - )) + Ok(self.manager.get_gateway_historical_uptimes(node_id).await?) } pub(crate) async fn get_average_mixnode_uptime_in_the_last_24hrs( &self, - mix_id: MixId, + node_id: NodeId, end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_mixnode_uptime_in_time_interval(mix_id, start, end_ts_secs) - .await + let reliability = self + .get_average_mixnode_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) } pub(crate) async fn get_average_gateway_uptime_in_the_last_24hrs( &self, - identity: &str, + node_id: NodeId, end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_gateway_uptime_in_time_interval(identity, start, end_ts_secs) + let reliability = self + .get_average_gateway_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) + } + + pub(crate) async fn get_average_node_uptime_in_the_last_24hrs( + &self, + node_id: NodeId, + end_ts_secs: i64, + ) -> Result { + let start = end_ts_secs - 86400; + self.get_average_node_reliability_in_time_interval(node_id, start, end_ts_secs) .await + .map(Uptime::new) + } + + pub(crate) async fn get_historical_mix_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_mix_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_gateway_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_gateway_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_node_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + if let Ok(result_as_mix) = self.get_historical_mix_uptime_on(node_id, date).await { + if result_as_mix.is_some() { + return Ok(result_as_mix); + } + } + + self.get_historical_gateway_uptime_on(node_id, date).await } /// Based on the data available in the validator API, determines the average uptime of particular @@ -329,15 +370,16 @@ impl NymApiStorage { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_mixnode_uptime_in_time_interval( + pub(crate) async fn get_average_mixnode_reliability_in_time_interval( &self, - mix_id: MixId, + mix_id: NodeId, start: i64, end: i64, - ) -> Result { + ) -> Result { + // those two should have been a single sql query /shrug let mixnode_database_id = match self.manager.get_mixnode_database_id(mix_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -345,11 +387,7 @@ impl NymApiStorage { .get_mixnode_average_reliability_in_interval(mixnode_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) - } + Ok(reliability.unwrap_or_default()) } /// Based on the data available in the validator API, determines the average uptime of particular @@ -360,15 +398,16 @@ impl NymApiStorage { /// * `identity`: base58-encoded identity of the gateway. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_gateway_uptime_in_time_interval( + pub(crate) async fn get_average_gateway_reliability_in_time_interval( &self, - identity: &str, + node_id: NodeId, start: i64, end: i64, - ) -> Result { - let gateway_database_id = match self.manager.get_gateway_id(identity).await? { + ) -> Result { + // those two should have been a single sql query /shrug + let gateway_database_id = match self.manager.get_gateway_database_id(node_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -376,11 +415,26 @@ impl NymApiStorage { .get_gateway_average_reliability_in_interval(gateway_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) + Ok(reliability.unwrap_or_default()) + } + + pub(crate) async fn get_average_node_reliability_in_time_interval( + &self, + node_id: NodeId, + start: i64, + end: i64, + ) -> Result { + if let Ok(result_as_mix) = self + .get_average_mixnode_reliability_in_time_interval(node_id, start, end) + .await + { + if result_as_mix != 0. { + return Ok(result_as_mix); + } } + + self.get_average_gateway_reliability_in_time_interval(node_id, start, end) + .await } /// Obtain status reports of mixnodes that were active in the specified time interval. @@ -416,7 +470,6 @@ impl NymApiStorage { OffsetDateTime::from_unix_timestamp(end).unwrap(), statuses.mix_id, statuses.identity, - statuses.owner, statuses.statuses, last_hour_runs_count, last_day_runs_count, @@ -458,8 +511,8 @@ impl NymApiStorage { .map(|statuses| { GatewayStatusReport::construct_from_last_day_reports( OffsetDateTime::from_unix_timestamp(end).unwrap(), + statuses.node_id, statuses.identity, - statuses.owner, statuses.statuses, last_hour_runs_count, last_day_runs_count, @@ -509,7 +562,7 @@ impl NymApiStorage { let gateway_db_id = self .manager - .get_gateway_id(&test_route.gateway().identity_key.to_base58_string()) + .get_gateway_database_id(test_route.gateway().node_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { reason: format!( @@ -539,7 +592,7 @@ impl NymApiStorage { /// * `since`: optional unix timestamp indicating the lower bound interval of the selection. pub(crate) async fn get_core_mixnode_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { let db_id = self.manager.get_mixnode_database_id(mix_id).await?; @@ -565,12 +618,15 @@ impl NymApiStorage { /// /// * `identity`: identity (base58-encoded public key) of the gateway. /// * `since`: optional unix timestamp indicating the lower bound interval of the selection. - pub(crate) async fn get_core_gateway_status_count( + pub(crate) async fn get_core_gateway_status_count_by_identity( &self, identity: &str, since: Option, ) -> Result { - let node_id = self.manager.get_gateway_id(identity).await?; + let node_id = self + .manager + .get_gateway_database_id_by_identity(identity) + .await?; if let Some(node_id) = node_id { let since = since @@ -595,8 +651,8 @@ impl NymApiStorage { /// * `route_results`: pub(crate) async fn insert_monitor_run_results( &self, - mixnode_results: Vec, - gateway_results: Vec, + mixnode_results: Vec, + gateway_results: Vec, test_routes: Vec, ) -> Result<(), NymApiStorageError> { info!("Submitting new node results to the database. There are {} mixnode results and {} gateway results", mixnode_results.len(), gateway_results.len()); @@ -678,8 +734,8 @@ impl NymApiStorage { for report in gateway_reports { // if this ever fails, we have a super weird error because we just constructed report for that node // and we never delete node data! - let node_id = match self.manager.get_gateway_id(&report.identity).await? { - Some(node_id) => node_id, + let db_id = match self.manager.get_gateway_database_id(report.node_id).await? { + Some(db_id) => db_id, None => { error!( "Somehow we failed to grab id of gateway {} from the database!", @@ -690,7 +746,7 @@ impl NymApiStorage { }; self.manager - .insert_gateway_historical_uptime(node_id, today_iso_8601, report.last_day.u8()) + .insert_gateway_historical_uptime(db_id, today_iso_8601, report.last_day.u8()) .await?; } @@ -749,7 +805,7 @@ impl NymApiStorage { pub(crate) async fn get_mixnode_detailed_statuses( &self, - mix_id: MixId, + mix_id: NodeId, limit: u32, offset: u32, ) -> Result, NymApiStorageError> { @@ -783,3 +839,42 @@ impl NymApiStorage { .await?) } } + +pub(crate) mod v3_migration { + use crate::node_status_api::models::NymApiStorageError; + use crate::support::storage::models::GatewayDetailsBeforeMigration; + use crate::support::storage::NymApiStorage; + use nym_mixnet_contract_common::NodeId; + + impl NymApiStorage { + pub(crate) async fn check_v3_migration(&self) -> Result { + Ok(self.manager.check_v3_migration().await?) + } + + pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), NymApiStorageError> { + Ok(self.manager.set_v3_migration_completion().await?) + } + + pub(crate) async fn get_all_known_gateways( + &self, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_all_known_gateways().await?) + } + + pub(crate) async fn set_gateway_node_id( + &self, + identity: &str, + node_id: NodeId, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.set_gateway_node_id(identity, node_id).await?) + } + + pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), NymApiStorageError> { + Ok(self.manager.purge_gateway(db_id).await?) + } + + pub(crate) async fn make_node_id_not_null(&self) -> Result<(), NymApiStorageError> { + Ok(self.manager.make_node_id_not_null().await?) + } + } +} diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index f48f2187f1..6a7b120e66 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use nym_api_requests::models::TestNode; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use sqlx::FromRow; +use time::Date; // Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway pub(crate) struct NodeStatus { @@ -23,15 +25,15 @@ impl NodeStatus { // Internally used structs to catch results from the database to find active mixnodes pub(crate) struct ActiveMixnode { pub(crate) id: i64, - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity_key: String, - pub(crate) owner: String, } +#[derive(FromRow)] pub(crate) struct ActiveGateway { pub(crate) id: i64, + pub(crate) node_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, } pub(crate) struct TestingRoute { @@ -54,7 +56,6 @@ pub(crate) struct RewardingReport { pub struct MixnodeDetails { pub id: i64, pub mix_id: i64, - pub owner: String, pub identity_key: String, } @@ -67,9 +68,19 @@ impl From for TestNode { } } +#[derive(FromRow)] +pub struct GatewayDetailsBeforeMigration { + pub id: i64, + #[sqlx(default)] + #[allow(dead_code)] + pub node_id: Option, + pub identity: String, +} + +#[derive(FromRow)] pub struct GatewayDetails { pub id: i64, - pub owner: String, + pub node_id: NodeId, pub identity: String, } @@ -111,3 +122,10 @@ pub struct TestedGatewayStatus { pub layer3_mix_id: i64, pub monitor_run_id: i64, } + +#[derive(FromRow)] +pub struct HistoricalUptime { + #[allow(dead_code)] + pub date: Date, + pub uptime: i64, +} diff --git a/nym-api/src/v2/api_docs.rs b/nym-api/src/v2/api_docs.rs deleted file mode 100644 index 9b248bb803..0000000000 --- a/nym-api/src/v2/api_docs.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::network::handlers::ContractVersionSchemaResponse; -use nym_api_requests::models; -use utoipa::OpenApi; -use utoipauto::utoipauto; - -// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released: -// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below) -// for automatic model discovery based on ToSchema / IntoParams implementation. -// Then you can remove `components(schemas)` manual imports below - -#[utoipauto(paths = "./nym-api/src")] -#[derive(OpenApi)] -#[openapi( - info(title = "Nym API"), - tags(), - components(schemas( - models::CirculatingSupplyResponse, - models::CoinSchema, - nym_mixnet_contract_common::Interval, - nym_api_requests::models::GatewayStatusReportResponse, - nym_api_requests::models::GatewayUptimeHistoryResponse, - nym_api_requests::models::HistoricalUptimeResponse, - nym_api_requests::models::GatewayCoreStatusResponse, - nym_api_requests::models::GatewayUptimeResponse, - nym_api_requests::models::RewardEstimationResponse, - nym_api_requests::models::UptimeResponse, - nym_api_requests::models::ComputeRewardEstParam, - nym_api_requests::models::MixNodeBondAnnotated, - nym_api_requests::models::GatewayBondAnnotated, - nym_api_requests::models::MixnodeTestResultResponse, - nym_api_requests::models::StakeSaturationResponse, - nym_api_requests::models::InclusionProbabilityResponse, - nym_api_requests::models::AllInclusionProbabilitiesResponse, - nym_api_requests::models::InclusionProbability, - nym_api_requests::models::SelectionChance, - crate::network::models::NetworkDetails, - nym_config::defaults::NymNetworkDetails, - nym_config::defaults::ChainDetails, - nym_config::defaults::DenomDetailsOwned, - nym_config::defaults::ValidatorDetails, - nym_config::defaults::NymContracts, - ContractVersionSchemaResponse, - crate::network::models::ContractInformation, - nym_api_requests::models::ApiHealthResponse, - nym_api_requests::models::ApiStatus, - nym_bin_common::build_information::BinaryBuildInformationOwned, - nym_api_requests::models::SignerInformationResponse, - nym_api_requests::models::DescribedGateway, - nym_api_requests::models::MixNodeDetailsSchema, - nym_mixnet_contract_common::Gateway, - nym_mixnet_contract_common::GatewayBond, - nym_api_requests::models::NymNodeDescription, - nym_api_requests::models::HostInformation, - nym_api_requests::models::HostKeys, - nym_node_requests::api::v1::node::models::AuxiliaryDetails, - nym_api_requests::models::NetworkRequesterDetails, - nym_api_requests::models::IpPacketRouterDetails, - nym_api_requests::models::AuthenticatorDetails, - nym_api_requests::models::WebSockets, - nym_api_requests::nym_nodes::NodeRole, - nym_api_requests::models::DescribedMixNode, - nym_api_requests::ecash::VerificationKeyResponse, - nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse, - nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse, - nym_api_requests::ecash::models::EpochCredentialsResponse, - nym_api_requests::ecash::models::IssuedCredentialResponse, - nym_api_requests::ecash::models::IssuedTicketbookBody, - nym_api_requests::ecash::models::BlindedSignatureResponse, - nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse, - nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse, - nym_api_requests::ecash::models::EcashTicketVerificationResponse, - nym_api_requests::ecash::models::EcashTicketVerificationRejection, - nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse, - nym_api_requests::ecash::models::SpentCredentialsResponse, - nym_api_requests::ecash::models::IssuedCredentialsResponse, - nym_api_requests::nym_nodes::SkimmedNode, - nym_api_requests::nym_nodes::BasicEntryInformation, - nym_api_requests::nym_nodes::SemiSkimmedNode, - nym_api_requests::nym_nodes::NodeRoleQueryParam, - )) -)] -pub(super) struct ApiDoc; diff --git a/nym-api/src/v2/mod.rs b/nym-api/src/v2/mod.rs deleted file mode 100644 index 59109d16b7..0000000000 --- a/nym-api/src/v2/mod.rs +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::support::nyxd; -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::api_routes::handlers::ecash_routes; -use crate::ecash::client::Client; -use crate::ecash::comm::QueryCommunicationChannel; -use crate::ecash::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, -}; -use crate::ecash::dkg::controller::DkgController; -use crate::ecash::state::EcashState; -use crate::epoch_operations::{self, RewardedSetUpdater}; -use crate::network::models::NetworkDetails; -use crate::node_describe_cache::{self, DescribedNodes}; -use crate::node_status_api::handlers::unstable; -use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; -use crate::node_status_api::{self, NodeStatusCache}; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::status::{ApiStatusState, SignerState}; -use crate::support::caching::cache::SharedCache; -use crate::support::config::Config; -use crate::support::storage; -use crate::{circulating_supply_api, ecash, network_monitor, nym_contract_cache}; -use anyhow::{bail, Context}; -use nym_config::defaults::NymNetworkDetails; -use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; -use nym_validator_client::nyxd::Coin; -use router::RouterBuilder; -use std::sync::Arc; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -pub(crate) mod api_docs; -pub(crate) mod router; - -const TASK_MANAGER_TIMEOUT_S: u64 = 10; - -/// Shutdown goes 2 directions: -/// 1. signal background tasks to gracefully finish -/// 2. signal server itself -/// -/// These are done through separate shutdown handles. Ofcourse, shut down server -/// AFTER you have shut down BG tasks (or past their grace period). -pub(crate) struct ShutdownHandles { - task_manager: TaskManager, - axum_shutdown_button: ShutdownAxum, - /// Tokio JoinHandle for axum server's task - axum_join_handle: AxumJoinHandle, -} - -impl ShutdownHandles { - /// Cancellation token is given to Axum server constructor. When the token - /// receives a shutdown signal, Axum server will shut down gracefully. - pub(crate) fn new( - task_manager: TaskManager, - axum_server_handle: AxumJoinHandle, - shutdown_button: CancellationToken, - ) -> Self { - Self { - task_manager, - axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), - axum_join_handle: axum_server_handle, - } - } - - pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { - &mut self.task_manager - } - - /// Signal server to shut down, then return join handle to its - /// `tokio` task - /// - /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html - #[must_use] - pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { - self.axum_shutdown_button.0.cancel(); - self.axum_join_handle - } -} - -struct ShutdownAxum(CancellationToken); - -type AxumJoinHandle = JoinHandle>; - -#[derive(Clone)] -// TODO rocket remove smurf name after eliminating rocket -pub(crate) struct AxumAppState { - nym_contract_cache: NymContractCache, - node_status_cache: NodeStatusCache, - circulating_supply_cache: CirculatingSupplyCache, - storage: storage::NymApiStorage, - described_nodes_state: SharedCache, - network_details: NetworkDetails, - node_info_cache: unstable::NodeInfoCache, -} - -impl AxumAppState { - pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { - &self.nym_contract_cache - } - - pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { - &self.node_status_cache - } - - pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { - &self.circulating_supply_cache - } - - pub(crate) fn network_details(&self) -> &NetworkDetails { - &self.network_details - } - - pub(crate) fn described_nodes_state(&self) -> &SharedCache { - &self.described_nodes_state - } - - pub(crate) fn storage(&self) -> &storage::NymApiStorage { - &self.storage - } - - pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { - &self.node_info_cache - } -} - -pub(crate) async fn start_nym_api_tasks_v2(config: &Config) -> anyhow::Result { - let nyxd_client = nyxd::Client::new(config); - let connected_nyxd = config.get_nyxd_url(); - let nym_network_details = NymNetworkDetails::new_from_env(); - let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - - let coconut_keypair = ecash::keys::KeyPair::new(); - - // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { - let issued_for = loaded_keys.issued_for_epoch; - coconut_keypair.set(loaded_keys).await; - - if can_validate_coconut_keys(&nyxd_client, issued_for).await? { - coconut_keypair.validate() - } - } - - let identity_keypair = config.base.storage_paths.load_identity()?; - let identity_public_key = *identity_keypair.public_key(); - - let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - - let nym_contract_cache_state = NymContractCache::new(); - let node_status_cache_state = NodeStatusCache::new(); - let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); - let described_nodes_state = SharedCache::::new(); - let storage = - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; - let node_info_cache = unstable::NodeInfoCache::default(); - - let mut status_state = ApiStatusState::new(); - - // if coconut signer is enabled, add /coconut to server - let router = if config.coconut_signer.enabled { - // make sure we have some tokens to cover multisig fees - let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < ecash::MINIMUM_BALANCE { - let address = nyxd_client.address().await; - let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); - bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") - } - - let cosmos_address = nyxd_client.address().await.to_string(); - let announce_address = config - .coconut_signer - .announce_address - .clone() - .map(|u| u.to_string()) - .unwrap_or_default(); - status_state.add_zk_nym_signer(SignerState { - cosmos_address, - identity: identity_keypair.public_key().to_base58_string(), - announce_address, - coconut_keypair: coconut_keypair.clone(), - }); - - let ecash_contract = nyxd_client - .get_ecash_contract_address() - .await - .context("e-cash contract address is required to setup the zk-nym signer")?; - - let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - - let ecash_state = EcashState::new( - ecash_contract, - nyxd_client.clone(), - identity_keypair, - coconut_keypair.clone(), - comm_channel, - storage.clone(), - ) - .await?; - - router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) - } else { - router - }; - - let router = router.with_state(AxumAppState { - nym_contract_cache: nym_contract_cache_state.clone(), - node_status_cache: node_status_cache_state.clone(), - circulating_supply_cache: circulating_supply_cache.clone(), - storage: storage.clone(), - described_nodes_state: described_nodes_state.clone(), - network_details, - node_info_cache, - }); - - let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); - - // start note describe cache refresher - // we should be doing the below, but can't due to our current startup structure - // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); - // let cache = refresher.get_shared_cache(); - node_describe_cache::new_refresher_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), - described_nodes_state, - ) - .named("node-self-described-data-refresher") - .start(task_manager.subscribe_named("node-self-described-data-refresher")); - - // start all the caches first - let nym_contract_cache_listener = nym_contract_cache::start_refresher( - &config.node_status_api, - &nym_contract_cache_state, - nyxd_client.clone(), - &task_manager, - ); - node_status_api::start_cache_refresh( - &config.node_status_api, - &nym_contract_cache_state, - &node_status_cache_state, - storage.clone(), - nym_contract_cache_listener, - &task_manager, - ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - &circulating_supply_cache, - &task_manager, - ); - - // start dkg task - if config.coconut_signer.enabled { - let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?; - - DkgController::start( - &config.coconut_signer, - nyxd_client.clone(), - coconut_keypair, - dkg_bte_keypair, - identity_public_key, - rand::rngs::OsRng, - &task_manager, - )?; - } - - // and then only start the uptime updater (and the monitor itself, duh) - // if the monitoring is enabled - if config.network_monitor.enabled { - network_monitor::start::( - &config.network_monitor, - &nym_contract_cache_state, - &storage, - nyxd_client.clone(), - &task_manager, - ) - .await; - - HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); - - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - RewardedSetUpdater::start( - nyxd_client, - &nym_contract_cache_state, - storage, - &task_manager, - ); - } - } - - let bind_address = config.base.bind_address.to_owned(); - let server = router.build_server(&bind_address).await?; - - let cancellation_token = CancellationToken::new(); - let shutdown_button = cancellation_token.clone(); - let axum_shutdown_receiver = cancellation_token.cancelled_owned(); - let server_handle = tokio::spawn(async move { - { - info!("Started Axum HTTP V2 server on {bind_address}"); - server.run(axum_shutdown_receiver).await - } - }); - - let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); - - Ok(shutdown) -} diff --git a/nym-api/src/v3_migration.rs b/nym-api/src/v3_migration.rs new file mode 100644 index 0000000000..c4e920247b --- /dev/null +++ b/nym-api/src/v3_migration.rs @@ -0,0 +1,82 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::nyxd::Client; +use crate::support::storage::NymApiStorage; +use anyhow::bail; +use std::collections::HashMap; +use tracing::{debug, info, warn}; + +pub async fn migrate_v3_database( + storage: &NymApiStorage, + nyxd_client: &Client, +) -> anyhow::Result<()> { + if storage.check_v3_migration().await? { + // we have already run the migration + return Ok(()); + } + + info!( + "migrating the database to be compatible with the v3 directory. this might take a while..." + ); + + // get the ids of all the gateways + let preassigned_ids = nyxd_client + .get_gateway_ids() + .await? + .into_iter() + .map(|id| (id.identity, id.node_id)) + .collect::>(); + let contract_gateways = nyxd_client.get_gateways().await?; + let nym_nodes = nyxd_client.get_nymnodes().await?; + + if preassigned_ids.len() != contract_gateways.len() { + bail!("CONTRACT DATA CORRUPTION: THE NUMBER OF PREASSIGNED GATEWAY IDS IS DIFFERENT THAN THE NUMBER OF GATEWAYS") + } + + // assign node_id to every gateway + let all_known = storage.get_all_known_gateways().await?; + for gateway in all_known { + let identity = &gateway.identity; + debug!("migrating gateway {identity}"); + if let Some(assigned) = preassigned_ids.get(identity) { + storage + .set_gateway_node_id(&gateway.identity, *assigned) + .await?; + continue; + }; + + // no pre-assigned id, perhaps the operator has already migrated into a nym-node? + if let Some(nym_node) = nym_nodes + .iter() + .find(|n| &n.bond_information.node.identity_key == identity) + { + storage + .set_gateway_node_id(identity, nym_node.node_id()) + .await?; + continue; + } + + // check if that gateway is even still bonded + let bonded = contract_gateways + .iter() + .any(|g| &g.gateway.identity_key == identity); + + if !bonded { + warn!("could not migrate gateway {identity}, as it does not appear to be bonded. all of its data is going to get purged."); + storage.purge_gateway(gateway.id).await?; + } else { + // this is critical issue because it should have never happened + warn!("could not migrate gateway {identity} even though it's still bonded. something bad has happened!"); + bail!("could not migrate gateway {identity}") + } + } + + debug!("making the column not nullable"); + storage.make_node_id_not_null().await?; + + debug!("marking v3 migration as complete"); + storage.set_v3_migration_completion().await?; + + Ok(()) +} diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 87257e2835..6255a5644a 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::api::v1::gateway::models::WebSockets; -use crate::api::v1::node::models::{AuxiliaryDetails, SignedHostInformation}; +use crate::api::v1::node::models::{AuxiliaryDetails, NodeRoles, SignedHostInformation}; use crate::api::ErrorResponse; use crate::routes; use async_trait::async_trait; @@ -39,6 +39,10 @@ pub trait NymNodeApiClientExt: ApiClient { .await } + async fn get_roles(&self) -> Result { + self.get_json_from(routes::api::v1::roles_absolute()).await + } + async fn get_auxiliary_details(&self) -> Result { self.get_json_from(routes::api::v1::auxiliary_absolute()) .await diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index b3c4a03931..eb2a4f4ce0 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -18,6 +18,27 @@ pub struct NodeRoles { pub ip_packet_router_enabled: bool, } +impl NodeRoles { + pub fn can_operate_mixnode(&self) -> bool { + self.mixnode_enabled + } + + pub fn can_operate_entry_gateway(&self) -> bool { + self.gateway_enabled + } + + pub fn can_operate_exit_gateway(&self) -> bool { + self.gateway_enabled && self.network_requester_enabled && self.ip_packet_router_enabled + } +} + +#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AnnouncePorts { + pub verloc_port: Option, + pub mix_port: Option, +} + #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostInformation { @@ -175,6 +196,9 @@ pub struct AuxiliaryDetails { #[schemars(length(equal = 2))] pub location: Option, + #[serde(default)] + pub announce_ports: AnnouncePorts, + /// Specifies whether this node operator has agreed to the terms and conditions /// as defined at // make sure to include the default deserialisation as this field hasn't existed when the struct was first created diff --git a/nym-node/src/cli/commands/migrate.rs b/nym-node/src/cli/commands/migrate.rs index 08d9d0b2e0..ec8cdaeeea 100644 --- a/nym-node/src/cli/commands/migrate.rs +++ b/nym-node/src/cli/commands/migrate.rs @@ -235,6 +235,7 @@ async fn migrate_mixnode(mut args: Args) -> Result<(), NymNodeError> { .with_mixnode(args.mixnode.override_config_section(config::MixnodeConfig { verloc: config::mixnode::Verloc { bind_address: SocketAddr::new(ip, cfg.mixnode.verloc_port), + announce_port: None, debug: config::mixnode::VerlocDebug { packets_per_node: cfg.verloc.packets_per_node, connection_timeout: cfg.verloc.connection_timeout, @@ -386,6 +387,7 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> { })) .with_mixnet(args.mixnet.override_config_section(config::Mixnet { bind_address: SocketAddr::new(ip, cfg.gateway.mix_port), + announce_port: None, nym_api_urls: cfg.gateway.nym_api_urls.clone(), nyxd_urls: cfg.gateway.nyxd_urls.clone(), debug: config::MixnetDebug { diff --git a/nym-node/src/config/mixnode.rs b/nym-node/src/config/mixnode.rs index 684ea7129b..f5a56e128a 100644 --- a/nym-node/src/config/mixnode.rs +++ b/nym-node/src/config/mixnode.rs @@ -7,6 +7,7 @@ use crate::error::MixnodeError; use clap::crate_version; use nym_config::defaults::DEFAULT_VERLOC_LISTENING_PORT; use nym_config::helpers::inaddr_any; +use nym_config::serde_helpers::de_maybe_port; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::time::Duration; @@ -41,6 +42,12 @@ pub struct Verloc { /// default: `0.0.0.0:1790` pub bind_address: SocketAddr, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + pub announce_port: Option, + #[serde(default)] pub debug: VerlocDebug, } @@ -49,6 +56,7 @@ impl Default for Verloc { fn default() -> Self { Verloc { bind_address: SocketAddr::new(inaddr_any(), DEFAULT_VERLOC_PORT), + announce_port: None, debug: Default::default(), } } diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 5da49419cf..60ed79ab20 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -12,6 +12,7 @@ use nym_config::defaults::{ mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, WG_PORT, }; use nym_config::helpers::inaddr_any; +use nym_config::serde_helpers::de_maybe_port; use nym_config::serde_helpers::de_maybe_stringified; use nym_config::{ must_get_home, parse_urls, read_config_from_toml_file, save_formatted_config_to_file, @@ -416,6 +417,12 @@ pub struct Mixnet { /// default: `0.0.0.0:1789` pub bind_address: SocketAddr, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + pub announce_port: Option, + /// Addresses to nym APIs from which the node gets the view of the network. pub nym_api_urls: Vec, @@ -492,6 +499,7 @@ impl Default for Mixnet { Mixnet { bind_address: SocketAddr::new(inaddr_any(), DEFAULT_MIXNET_PORT), + announce_port: None, nym_api_urls, nyxd_urls, debug: Default::default(), diff --git a/nym-node/src/config/old_configs/old_config_v3.rs b/nym-node/src/config/old_configs/old_config_v3.rs index 809955e5ee..f13cfdf4a0 100644 --- a/nym-node/src/config/old_configs/old_config_v3.rs +++ b/nym-node/src/config/old_configs/old_config_v3.rs @@ -992,6 +992,7 @@ pub async fn try_upgrade_config_v3>( }, mixnet: Mixnet { bind_address: old_cfg.mixnet.bind_address, + announce_port: None, nym_api_urls: old_cfg.mixnet.nym_api_urls, nyxd_urls: old_cfg.mixnet.nyxd_urls, debug: MixnetDebug { @@ -1066,6 +1067,7 @@ pub async fn try_upgrade_config_v3>( storage_paths: MixnodePaths {}, verloc: Verloc { bind_address: old_cfg.mixnode.verloc.bind_address, + announce_port: None, debug: VerlocDebug { packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node, connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout, diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index f1576f2fe2..83588a0639 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -41,6 +41,12 @@ location = '{{ host.location }}' # default: `0.0.0.0:1789` bind_address = '{{ mixnet.bind_address }}' +# If applicable, custom port announced in the self-described API that other clients and nodes +# will use. +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_port ={{#if mixnet.announce_port }} {{ mixnet.announce_port }} {{else}} 0 {{/if}} + # Addresses to nym APIs from which the node gets the view of the network. nym_api_urls = [ {{#each mixnet.nym_api_urls }}'{{this}}',{{/each}} @@ -144,6 +150,12 @@ public_diffie_hellman_key_file = '{{ wireguard.storage_paths.public_diffie_hellm # default: `0.0.0.0:1790` bind_address = '{{ mixnode.verloc.bind_address }}' +# If applicable, custom port announced in the self-described API that other clients and nodes +# will use. +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_port ={{#if mixnode.verloc.announce_port }} {{ mixnode.verloc.announce_port }} {{else}} 0 {{/if}} + [mixnode.storage_paths] # currently empty diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 3bb5fe83ca..d3e756c6e0 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -25,7 +25,7 @@ use nym_node::config::{ }; use nym_node::error::{EntryGatewayError, ExitGatewayError, MixnodeError, NymNodeError}; use nym_node_http_api::api::api_requests; -use nym_node_http_api::api::api_requests::v1::node::models::NodeDescription; +use nym_node_http_api::api::api_requests::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_node_http_api::state::metrics::{SharedMixingStats, SharedVerlocStats}; use nym_node_http_api::state::AppState; use nym_node_http_api::{NymNodeHTTPServer, NymNodeRouter}; @@ -632,6 +632,10 @@ impl NymNode { let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { location: self.config.host.location, + announce_ports: AnnouncePorts { + verloc_port: self.config.mixnode.verloc.announce_port, + mix_port: self.config.mixnet.announce_port, + }, accepted_operator_terms_and_conditions: self.accepted_operator_terms_and_conditions, }; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 951f240165..eeef135c89 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -173,9 +173,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", @@ -1637,9 +1637,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -3096,10 +3096,12 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", "schemars", "serde", + "serde_json", "sha2 0.10.8", "tendermint 0.37.0", "thiserror", @@ -3289,6 +3291,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "humantime-serde", "log", "nym-contracts-common", @@ -3361,7 +3364,6 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", - "hex", "serde", "time", ] @@ -3450,7 +3452,6 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", - "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.4", @@ -4878,9 +4879,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -5762,18 +5763,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -5977,7 +5978,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.20", + "winnow 0.6.19", ] [[package]] @@ -6853,9 +6854,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" dependencies = [ "memchr", ] diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 0aac089ee2..0e76cba619 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -17,9 +17,8 @@ use serde::{Deserialize, Serialize}; )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriContractStateParams { - minimum_mixnode_pledge: DecCoin, - minimum_gateway_pledge: DecCoin, - minimum_mixnode_delegation: Option, + minimum_pledge: DecCoin, + minimum_delegation: Option, operating_cost: TauriOperatingCostRange, profit_margin: TauriProfitMarginRange, @@ -52,7 +51,7 @@ impl TauriContractStateParams { state_params: ContractStateParams, reg: &RegisteredCoins, ) -> Result { - let rewarding_denom = &state_params.minimum_mixnode_pledge.denom; + let rewarding_denom = &state_params.minimum_pledge.denom; let min_operating_cost_c = Coin { denom: rewarding_denom.into(), amount: state_params.interval_operating_cost.minimum, @@ -63,12 +62,10 @@ impl TauriContractStateParams { }; Ok(TauriContractStateParams { - minimum_mixnode_pledge: reg - .attempt_convert_to_display_dec_coin(state_params.minimum_mixnode_pledge.into())?, - minimum_gateway_pledge: reg - .attempt_convert_to_display_dec_coin(state_params.minimum_gateway_pledge.into())?, - minimum_mixnode_delegation: state_params - .minimum_mixnode_delegation + minimum_pledge: reg + .attempt_convert_to_display_dec_coin(state_params.minimum_pledge.into())?, + minimum_delegation: state_params + .minimum_delegation .map(|min_del| reg.attempt_convert_to_display_dec_coin(min_del.into())) .transpose()?, @@ -96,16 +93,13 @@ impl TauriContractStateParams { let max_operating_cost_c = reg.attempt_convert_to_base_coin(self.operating_cost.maximum)?; Ok(ContractStateParams { - minimum_mixnode_delegation: self - .minimum_mixnode_delegation + minimum_delegation: self + .minimum_delegation .map(|min_del| reg.attempt_convert_to_base_coin(min_del)) .transpose()? .map(Into::into), - minimum_mixnode_pledge: reg - .attempt_convert_to_base_coin(self.minimum_mixnode_pledge)? - .into(), - minimum_gateway_pledge: reg - .attempt_convert_to_base_coin(self.minimum_gateway_pledge)? + minimum_pledge: reg + .attempt_convert_to_base_coin(self.minimum_pledge)? .into(), profit_margin: ContractProfitMarginRange { diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 9d879c706e..cefac100e7 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -158,6 +158,9 @@ pub enum BackendError { #[error("there aren't any vesting delegations to migrate")] NoVestingDelegations, + + #[error("this operation is no longer allowed to be performed with vesting tokens. please move them to your liquid balance and try again")] + DisabledVestingOperation, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index da0093e293..fc8212d48e 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -10,7 +10,7 @@ use nym_contracts_common::signing::{ use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::{ construct_legacy_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, - MixNodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, + NodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, }; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; @@ -39,7 +39,7 @@ impl AddressAndNonceProvider for DirectSigningHttpRpcValidatorClient { pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: Coin, vesting: bool, ) -> Result { @@ -61,7 +61,7 @@ pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, mix_node: &MixNode, - cost_params: &MixNodeCostParams, + cost_params: &NodeCostParams, pledge: &Coin, vesting: bool, msg_signature: &MessageSignature, @@ -148,6 +148,7 @@ mod tests { use super::*; use cosmwasm_std::coin; use nym_contracts_common::Percent; + use nym_mixnet_contract_common::NodeCostParams; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -186,7 +187,7 @@ mod tests { identity_key: identity_keypair.public_key().to_base58_string(), version: "v1.2.3".to_string(), }; - let dummy_cost_params = MixNodeCostParams { + let dummy_cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(1111111, "unym"), }; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 1cf3a7bd69..a942321f9b 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -9,10 +9,11 @@ use crate::state::WalletState; use crate::{nyxd_client, Gateway, MixNode}; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::{MixId, MixNodeConfigUpdate}; +use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId}; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; -use nym_types::mixnode::{MixNodeCostParams, MixNodeDetails}; +use nym_types::mixnode::{MixNodeDetails, NodeCostParams}; +use nym_types::nym_node::NymNodeDetails; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; @@ -89,7 +90,7 @@ pub async fn unbond_gateway( #[tauri::command] pub async fn bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, fee: Option, @@ -256,7 +257,7 @@ pub async fn unbond_mixnode( #[tauri::command] pub async fn update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -272,7 +273,7 @@ pub async fn update_mixnode_cost_params( let res = guard .current_client()? .nyxd - .update_mixnode_cost_params(cost_params, fee) + .update_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -420,6 +421,37 @@ pub async fn gateway_bond_details( Ok(res) } +#[tauri::command] +pub async fn nym_node_bond_details( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get nym-node bond details"); + let guard = state.read().await; + let client = guard.current_client()?; + let res = client + .nyxd + .get_owned_nymnode(&client.nyxd.address()) + .await?; + let details = res + .details + .map(|details| { + guard + .registered_coins() + .map(|reg| NymNodeDetails::from_mixnet_contract_nym_node_details(details, reg)) + }) + .transpose()? + .transpose()?; + log::info!( + "<<< node_id/identity_key = {:?}", + details.as_ref().map(|r| ( + r.bond_information.node_id, + &r.bond_information.node.identity_key + )) + ); + log::trace!("<<< {:?}", details); + Ok(details) +} + #[tauri::command] pub async fn get_pending_operator_rewards( address: String, @@ -459,7 +491,7 @@ pub async fn get_pending_operator_rewards( #[tauri::command] pub async fn get_number_of_mixnode_delegators( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(nyxd_client!(state) @@ -487,7 +519,7 @@ pub async fn get_mix_node_description( #[tauri::command] pub async fn get_mixnode_uptime( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Get mixnode uptime"); diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 7a705f6d03..31af28d199 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -4,14 +4,14 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::delegate::vesting_undelegate_from_mixnode; -use nym_mixnet_contract_common::mixnode::StakeSaturationResponse; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::mixnode::MixStakeSaturationResponse; +use nym_mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; use nym_types::deprecated::{ convert_to_delegation_events, DelegationEvent, WrappedDelegationEvent, }; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_types::pending_events::PendingEpochEvent; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::client::NymApiClientExt; @@ -69,7 +69,7 @@ pub async fn get_pending_delegation_events( #[tauri::command] pub async fn delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -86,10 +86,7 @@ pub async fn delegate_to_mixnode( delegation_base, fee, ); - let res = client - .nyxd - .delegate_to_mixnode(mix_id, delegation_base, fee) - .await?; + let res = client.nyxd.delegate(mix_id, delegation_base, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( @@ -99,7 +96,7 @@ pub async fn delegate_to_mixnode( #[tauri::command] pub async fn undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -111,11 +108,7 @@ pub async fn undelegate_from_mixnode( mix_id, fee ); - let res = guard - .current_client()? - .nyxd - .undelegate_from_mixnode(mix_id, fee) - .await?; + let res = guard.current_client()?.nyxd.undelegate(mix_id, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( @@ -125,7 +118,7 @@ pub async fn undelegate_from_mixnode( #[tauri::command] pub async fn undelegate_all_from_mixnode( - mix_id: MixId, + mix_id: NodeId, uses_vesting_contract_tokens: bool, fee_liquid: Option, fee_vesting: Option, @@ -195,8 +188,8 @@ pub async fn get_all_mix_delegations( let d = Delegation::from_mixnet_contract(delegation.clone(), reg).tap_err(|err| { log::error!( - " <<< Failed to get delegation for mix id {} from contract. Error: {}", - delegation.mix_id, + " <<< Failed to get delegation for node id {} from contract. Error: {}", + delegation.node_id, err ); })?; @@ -262,7 +255,7 @@ pub async fn get_all_mix_delegations( let cost_params = mixnode .as_ref() .map(|m| { - MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + NodeCostParams::from_mixnet_contract_mixnode_cost_params( m.rewarding_details.cost_params.clone(), reg, ) @@ -329,7 +322,7 @@ pub async fn get_all_mix_delegations( log::error!(" <<< {}", str_err); error_strings.push(str_err); }) - .unwrap_or(StakeSaturationResponse { + .unwrap_or(MixStakeSaturationResponse { mix_id: d.mix_id, uncapped_saturation: None, current_saturation: None, @@ -420,7 +413,7 @@ pub async fn get_all_mix_delegations( } fn filter_pending_events( - mix_id: MixId, + mix_id: NodeId, pending_events: &[WrappedDelegationEvent], ) -> Vec { pending_events @@ -434,7 +427,7 @@ fn filter_pending_events( #[tauri::command] pub async fn get_pending_delegator_rewards( address: String, - mix_id: MixId, + mix_id: NodeId, proxy: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 266295c6e1..4563005b88 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -1,7 +1,7 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::rewards::vesting_claim_delegator_reward; -use nym_mixnet_contract_common::{MixId, RewardingParams}; +use nym_mixnet_contract_common::{NodeId, RewardingParams}; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, @@ -31,17 +31,17 @@ pub async fn claim_operator_reward( #[tauri::command] pub async fn claim_delegator_reward( - mix_id: MixId, + node_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!(">>> Withdraw delegator reward: mix_id = {}", mix_id); + log::info!(">>> Withdraw delegator reward: node_id = {node_id}"); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nyxd - .withdraw_delegator_reward(mix_id, fee) + .withdraw_delegator_reward(node_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -52,19 +52,16 @@ pub async fn claim_delegator_reward( #[tauri::command] pub async fn claim_locked_and_unlocked_delegator_reward( - mix_id: MixId, + node_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { - log::info!( - ">>> Claim delegator reward (locked and unlocked): mix_id = {}", - mix_id - ); + log::info!(">>> Claim delegator reward (locked and unlocked): node_id = {node_id}",); let guard = state.read().await; let client = guard.current_client()?; - log::trace!(">>> Get delegations: mix_id = {}", mix_id); + log::trace!(">>> Get delegations: node_id = {node_id}"); let address = client.nyxd.address(); let delegations = client.nyxd.get_all_delegator_delegations(&address).await?; log::trace!("<<< {} delegations", delegations.len()); @@ -76,11 +73,11 @@ pub async fn claim_locked_and_unlocked_delegator_reward( .to_string(); let liquid_delegation = client .nyxd - .get_delegation_details(mix_id, &address, None) + .get_delegation_details(node_id, &address, None) .await?; let vesting_delegation = client .nyxd - .get_delegation_details(mix_id, &address, Some(vesting_contract)) + .get_delegation_details(node_id, &address, Some(vesting_contract)) .await?; drop(guard); @@ -97,10 +94,10 @@ pub async fn claim_locked_and_unlocked_delegator_reward( let mut res: Vec = vec![]; if did_delegate_with_mixnet_contract { - res.push(claim_delegator_reward(mix_id, fee.clone(), state.clone()).await?); + res.push(claim_delegator_reward(node_id, fee.clone(), state.clone()).await?); } if did_delegate_with_vesting_contract { - res.push(vesting_claim_delegator_reward(mix_id, fee, state).await?); + res.push(vesting_claim_delegator_reward(node_id, fee, state).await?); } log::trace!("<<< {:?}", res); Ok(res) diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 096615b3b5..916dac0cea 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -5,7 +5,7 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; use nym_mixnet_contract_common::{ - reward_params::Performance, Coin, IdentityKeyRef, MixId, Percent, + reward_params::Performance, Coin, IdentityKeyRef, NodeId, Percent, }; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{ @@ -16,7 +16,7 @@ use nym_validator_client::models::{ #[tauri::command] pub async fn mixnode_core_node_status( - mix_id: MixId, + mix_id: NodeId, since: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -46,7 +46,7 @@ pub async fn gateway_report( #[tauri::command] pub async fn mixnode_status( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state).get_mixnode_status(mix_id).await?) @@ -54,7 +54,7 @@ pub async fn mixnode_status( #[tauri::command] pub async fn mixnode_reward_estimation( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -87,7 +87,7 @@ pub async fn compute_mixnode_reward_estimation( #[tauri::command] pub async fn mixnode_stake_saturation( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -97,7 +97,7 @@ pub async fn mixnode_stake_saturation( #[tauri::command] pub async fn mixnode_inclusion_probability( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) diff --git a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs index d539cf5eb3..c0c3dd373a 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs @@ -8,11 +8,11 @@ use crate::operations::helpers::{ use crate::state::WalletState; use nym_mixnet_contract_common::{Gateway, MixNode}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; async fn mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, vesting: bool, state: tauri::State<'_, WalletState>, @@ -64,7 +64,7 @@ async fn gateway_bonding_msg_payload( #[tauri::command] pub async fn generate_mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -74,7 +74,7 @@ pub async fn generate_mixnode_bonding_msg_payload( #[tauri::command] pub async fn vesting_generate_mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index c64fcc53af..afcb0a512f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -1,17 +1,16 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; - use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{ExecuteMsg, Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::{ExecuteMsg, Gateway, MixNode, NodeId}; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::NymContractsProvider; +use std::cmp::Ordering; async fn simulate_mixnet_operation( msg: ExecuteMsg, @@ -70,7 +69,7 @@ pub async fn simulate_unbond_gateway( #[tauri::command] pub async fn simulate_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, @@ -148,19 +147,14 @@ pub async fn simulate_unbond_mixnode( #[tauri::command] pub async fn simulate_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let reg = guard.registered_coins()?; let new_costs = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; - simulate_mixnet_operation( - ExecuteMsg::UpdateMixnodeCostParams { new_costs }, - None, - &state, - ) - .await + simulate_mixnet_operation(ExecuteMsg::UpdateCostParams { new_costs }, None, &state).await } #[tauri::command] @@ -191,24 +185,19 @@ pub async fn simulate_update_gateway_config( #[tauri::command] pub async fn simulate_delegate_to_mixnode( - mix_id: MixId, + node_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation( - ExecuteMsg::DelegateToMixnode { mix_id }, - Some(amount), - &state, - ) - .await + simulate_mixnet_operation(ExecuteMsg::Delegate { node_id }, Some(amount), &state).await } #[tauri::command] pub async fn simulate_undelegate_from_mixnode( - mix_id: MixId, + node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await + simulate_mixnet_operation(ExecuteMsg::Undelegate { node_id }, None, &state).await } #[tauri::command] @@ -220,8 +209,13 @@ pub async fn simulate_claim_operator_reward( #[tauri::command] pub async fn simulate_claim_delegator_reward( - mix_id: MixId, + node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation(ExecuteMsg::WithdrawDelegatorReward { mix_id }, None, &state).await + simulate_mixnet_operation( + ExecuteMsg::WithdrawDelegatorReward { node_id }, + None, + &state, + ) + .await } diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index 9a33e2a3ed..9578575c3f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -1,18 +1,17 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; - use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::{Gateway, MixNode, NodeId}; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::NymContractsProvider; use nym_vesting_contract_common::ExecuteMsg; +use std::cmp::Ordering; async fn simulate_vesting_operation( msg: ExecuteMsg, @@ -75,7 +74,7 @@ pub async fn simulate_vesting_unbond_gateway( #[tauri::command] pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, @@ -173,7 +172,7 @@ pub async fn simulate_vesting_unbond_mixnode( #[tauri::command] pub async fn simulate_vesting_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -216,7 +215,7 @@ pub async fn simulate_vesting_update_gateway_config( #[tauri::command] pub async fn simulate_vesting_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -237,7 +236,7 @@ pub async fn simulate_vesting_delegate_to_mixnode( #[tauri::command] pub async fn simulate_vesting_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation( @@ -270,7 +269,7 @@ pub async fn simulate_vesting_claim_operator_reward( #[tauri::command] pub async fn simulate_vesting_claim_delegator_reward( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation(ExecuteMsg::ClaimDelegatorReward { mix_id }, None, &state).await diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 8e47953109..dfa7aec1f9 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -8,7 +8,7 @@ use crate::{Gateway, MixNode}; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::{contract_traits::VestingSigningClient, Fee}; use std::cmp::Ordering; @@ -76,7 +76,7 @@ pub async fn vesting_unbond_gateway( #[tauri::command] pub async fn vesting_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, fee: Option, @@ -284,7 +284,7 @@ pub async fn withdraw_vested_coins( #[tauri::command] pub async fn vesting_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 64ac3ed736..bbb1d8df11 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -3,14 +3,14 @@ use crate::error::BackendError; use crate::state::WalletState; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::{contract_traits::VestingSigningClient, Fee}; #[tauri::command] pub async fn vesting_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -40,7 +40,7 @@ pub async fn vesting_delegate_to_mixnode( #[tauri::command] pub async fn vesting_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs index e5624cfbf4..6d5fbadf16 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -68,7 +68,7 @@ pub async fn migrate_vested_delegations( for delegation in &vesting_delegations { migrate_msgs.push(( ExecuteMsg::MigrateVestedDelegation { - mix_id: delegation.mix_id, + mix_id: delegation.node_id, }, Vec::new(), )); diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index d990b2c1c5..7cf2155cdb 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -3,7 +3,7 @@ use crate::error::BackendError; use crate::state::WalletState; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; use nym_validator_client::nyxd::Fee; @@ -30,7 +30,7 @@ pub async fn vesting_claim_operator_reward( #[tauri::command] pub async fn vesting_claim_delegator_reward( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 2a340cc5dd..f294d4c09e 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -5,9 +5,9 @@ use crate::error::BackendError; use crate::nyxd_client; use crate::state::WalletState; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{IdentityKey, MixId, Percent}; +use nym_mixnet_contract_common::{IdentityKey, NodeId, Percent}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_wallet_types::app::AppEnv; @@ -45,11 +45,20 @@ pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result) -> Result { + Ok(nyxd_client!(state) + .get_owned_nym_node(&nyxd_client!(state).address()) + .await? + .details + .is_some()) +} + #[tauri::command] pub async fn try_convert_pubkey_to_mix_id( state: tauri::State<'_, WalletState>, mix_identity: IdentityKey, -) -> Result, BackendError> { +) -> Result, BackendError> { let res = nyxd_client!(state) .get_mixnode_details_by_identity(mix_identity) .await?; @@ -62,7 +71,7 @@ pub async fn try_convert_pubkey_to_mix_id( pub async fn default_mixnode_cost_params( state: tauri::State<'_, WalletState>, profit_margin_percent: Percent, -) -> Result { +) -> Result { // attaches the old pre-update default operating cost of 40 nym per interval let guard = state.read().await; @@ -71,7 +80,7 @@ pub async fn default_mixnode_cost_params( let current_network = guard.current_network(); let denom = current_network.mix_denom().display; - Ok(MixNodeCostParams { + Ok(NodeCostParams { profit_margin_percent, interval_operating_cost: DecCoin { denom: denom.into(), diff --git a/nym-wallet/src/requests/queries.ts b/nym-wallet/src/requests/queries.ts index be74041f33..dbb7bb6e18 100644 --- a/nym-wallet/src/requests/queries.ts +++ b/nym-wallet/src/requests/queries.ts @@ -1,63 +1,65 @@ import { - DecCoin, - GatewayBond, - InclusionProbabilityResponse, - MixNodeDetails, - MixnodeStatusResponse, - PendingIntervalEvent, - RewardEstimationResponse, - StakeSaturationResponse, - WrappedDelegationEvent, + DecCoin, + GatewayBond, + InclusionProbabilityResponse, + MixNodeDetails, + MixnodeStatusResponse, + PendingIntervalEvent, + RewardEstimationResponse, + StakeSaturationResponse, + WrappedDelegationEvent, + NymNodeDetails, } from '@nymproject/types'; -import { Interval, TGatewayReport, TNodeDescription } from 'src/types'; -import { invokeWrapper } from './wrapper'; +import {Interval, TGatewayReport, TNodeDescription} from 'src/types'; +import {invokeWrapper} from './wrapper'; export const getAllPendingDelegations = async () => - invokeWrapper('get_pending_delegation_events'); + invokeWrapper('get_pending_delegation_events'); export const getMixnodeBondDetails = async () => invokeWrapper('mixnode_bond_details'); export const getGatewayBondDetails = async () => invokeWrapper('gateway_bond_details'); +export const getNymNodeBondDetails = async () => invokeWrapper('nym_node_bond_details'); export const getMixnodeAvgUptime = async () => invokeWrapper('get_mixnode_avg_uptime'); export const getPendingOperatorRewards = async (address: string) => - invokeWrapper('get_pending_operator_rewards', { address }); + invokeWrapper('get_pending_operator_rewards', {address}); export const getMixnodeStakeSaturation = async (mixId: number) => - invokeWrapper('mixnode_stake_saturation', { mixId }); + invokeWrapper('mixnode_stake_saturation', {mixId}); export const getMixnodeRewardEstimation = async (mixId: number) => - invokeWrapper('mixnode_reward_estimation', { mixId }); + invokeWrapper('mixnode_reward_estimation', {mixId}); export const getMixnodeStatus = async (mixId: number) => - invokeWrapper('mixnode_status', { mixId }); + invokeWrapper('mixnode_status', {mixId}); export const checkMixnodeOwnership = async () => invokeWrapper('owns_mixnode'); export const checkGatewayOwnership = async () => invokeWrapper('owns_gateway'); export const getInclusionProbability = async (mixId: number) => - invokeWrapper('mixnode_inclusion_probability', { mixId }); + invokeWrapper('mixnode_inclusion_probability', {mixId}); export const getCurrentInterval = async () => invokeWrapper('get_current_interval'); export const getNumberOfMixnodeDelegators = async (mixId: number) => - invokeWrapper('get_number_of_mixnode_delegators', { mixId }); + invokeWrapper('get_number_of_mixnode_delegators', {mixId}); export const getNodeDescription = async (host: string, port: number) => - invokeWrapper('get_mix_node_description', { host, port }); + invokeWrapper('get_mix_node_description', {host, port}); export const getPendingIntervalEvents = async () => - invokeWrapper('get_pending_interval_events'); + invokeWrapper('get_pending_interval_events'); export const getGatewayReport = async (identity: string) => - invokeWrapper('gateway_report', { identity }); + invokeWrapper('gateway_report', {identity}); export const computeMixnodeRewardEstimation = async (args: { - mixId: number; - performance: string; - pledgeAmount: number; - totalDelegation: number; - profitMarginPercent: string; - intervalOperatingCost: { denom: 'unym'; amount: string }; + mixId: number; + performance: string; + pledgeAmount: number; + totalDelegation: number; + profitMarginPercent: string; + intervalOperatingCost: { denom: 'unym'; amount: string }; }) => invokeWrapper('compute_mixnode_reward_estimation', args); -export const getMixnodeUptime = async (mixId: number) => invokeWrapper('get_mixnode_uptime', { mixId }); +export const getMixnodeUptime = async (mixId: number) => invokeWrapper('get_mixnode_uptime', {mixId}); diff --git a/nym-wallet/src/types/rust/AppEnv.ts b/nym-wallet/src/types/rust/AppEnv.ts index 861ecd9ef5..49ebfc27d1 100644 --- a/nym-wallet/src/types/rust/AppEnv.ts +++ b/nym-wallet/src/types/rust/AppEnv.ts @@ -1,5 +1,3 @@ -export interface AppEnv { - ADMIN_ADDRESS: string | null; - SHOW_TERMINAL: string | null; - ENABLE_QA_MODE: string | null; -} +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface AppEnv { ADMIN_ADDRESS: string | null, SHOW_TERMINAL: string | null, ENABLE_QA_MODE: string | null, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/AppVersion.ts b/nym-wallet/src/types/rust/AppVersion.ts index 82b94c5596..b0624301c1 100644 --- a/nym-wallet/src/types/rust/AppVersion.ts +++ b/nym-wallet/src/types/rust/AppVersion.ts @@ -1,7 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface AppVersion { - current_version: string; - latest_version: string; - is_update_available: boolean; -} +export interface AppVersion { current_version: string, latest_version: string, is_update_available: boolean, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/Interval.ts b/nym-wallet/src/types/rust/Interval.ts index c1526be658..ab2c4c4bc9 100644 --- a/nym-wallet/src/types/rust/Interval.ts +++ b/nym-wallet/src/types/rust/Interval.ts @@ -1,8 +1,3 @@ -export interface Interval { - id: number; - epochs_in_interval: number; - current_epoch_start_unix: bigint; - current_epoch_id: number; - epoch_length_seconds: bigint; - total_elapsed_epochs: number; -} +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface Interval { id: number, epochs_in_interval: number, current_epoch_start_unix: bigint, current_epoch_id: number, epoch_length_seconds: bigint, total_elapsed_epochs: number, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/Network.ts b/nym-wallet/src/types/rust/Network.ts index d55f0bce67..03c73643d6 100644 --- a/nym-wallet/src/types/rust/Network.ts +++ b/nym-wallet/src/types/rust/Network.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; +export type Network = "QA" | "SANDBOX" | "MAINNET"; \ No newline at end of file diff --git a/nym-wallet/src/types/rust/OperatingCostRange.ts b/nym-wallet/src/types/rust/OperatingCostRange.ts index b3877c8347..34ea2f65a4 100644 --- a/nym-wallet/src/types/rust/OperatingCostRange.ts +++ b/nym-wallet/src/types/rust/OperatingCostRange.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; +import type { DecCoin } from "../../../../ts-packages/types/src/types/rust/DecCoin"; -export interface TauriOperatingCostRange { - minimum: DecCoin; - maximum: DecCoin; -} +export interface TauriOperatingCostRange { minimum: DecCoin, maximum: DecCoin, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/ProfitMarginRange.ts b/nym-wallet/src/types/rust/ProfitMarginRange.ts index c12d49d5d9..631a4e1097 100644 --- a/nym-wallet/src/types/rust/ProfitMarginRange.ts +++ b/nym-wallet/src/types/rust/ProfitMarginRange.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface TauriProfitMarginRange { - minimum: string; - maximum: string; -} +export interface TauriProfitMarginRange { minimum: string, maximum: string, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/StateParams.ts b/nym-wallet/src/types/rust/StateParams.ts index b909e4c734..aa6eb0366a 100644 --- a/nym-wallet/src/types/rust/StateParams.ts +++ b/nym-wallet/src/types/rust/StateParams.ts @@ -1,12 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; -import type { TauriOperatingCostRange } from './OperatingCostRange'; -import type { TauriProfitMarginRange } from './ProfitMarginRange'; +import type { DecCoin } from "../../../../ts-packages/types/src/types/rust/DecCoin"; +import type { TauriOperatingCostRange } from "./OperatingCostRange"; +import type { TauriProfitMarginRange } from "./ProfitMarginRange"; -export interface TauriContractStateParams { - minimum_mixnode_pledge: DecCoin; - minimum_gateway_pledge: DecCoin; - minimum_mixnode_delegation: DecCoin | null; - operating_cost: TauriOperatingCostRange; - profit_margin: TauriProfitMarginRange; -} +export interface TauriContractStateParams { minimum_pledge: DecCoin, minimum_delegation: DecCoin | null, operating_cost: TauriOperatingCostRange, profit_margin: TauriProfitMarginRange, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/ValidatorUrl.ts b/nym-wallet/src/types/rust/ValidatorUrl.ts index 00291a57cd..7e94537f97 100644 --- a/nym-wallet/src/types/rust/ValidatorUrl.ts +++ b/nym-wallet/src/types/rust/ValidatorUrl.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface ValidatorUrl { - url: string; - name: string | null; -} +export interface ValidatorUrl { url: string, name: string | null, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/ValidatorUrls.ts b/nym-wallet/src/types/rust/ValidatorUrls.ts index 57118d6c3f..9a579b2f41 100644 --- a/nym-wallet/src/types/rust/ValidatorUrls.ts +++ b/nym-wallet/src/types/rust/ValidatorUrls.ts @@ -1,6 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ValidatorUrl } from './ValidatorUrl'; +import type { ValidatorUrl } from "./ValidatorUrl"; -export interface ValidatorUrls { - urls: Array; -} +export interface ValidatorUrls { urls: Array, } \ No newline at end of file diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index d21916e6c9..6737c2dcae 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -4,7 +4,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; use nym_topology::provider_trait::{async_trait, TopologyProvider}; -use nym_topology::{nym_topology_from_detailed, NymTopology}; +use nym_topology::{nym_topology_from_basic_info, NymTopology}; use url::Url; struct MyTopologyProvider { @@ -21,23 +21,25 @@ impl MyTopologyProvider { async fn get_topology(&self) -> NymTopology { let mixnodes = self .validator_client - .get_cached_active_mixnodes() + .get_basic_mixnodes(None) .await .unwrap(); - // in our topology provider only use mixnodes that have mix_id divisible by 3 - // and have more than 100k nym (i.e. 100'000'000'000 unym) in stake + // in our topology provider only use mixnodes that have node_id divisible by 3 + // and has exactly 100 performance score // why? because this is just an example to showcase arbitrary uses and capabilities of this trait let filtered_mixnodes = mixnodes .into_iter() - .filter(|mix| { - mix.mix_id() % 3 == 0 && mix.total_stake() > "100000000000".parse().unwrap() - }) + .filter(|mix| mix.node_id % 3 == 0 && mix.performance.is_hundred()) .collect::>(); - let gateways = self.validator_client.get_cached_gateways().await.unwrap(); + let gateways = self + .validator_client + .get_basic_gateways(None) + .await + .unwrap(); - nym_topology_from_detailed(filtered_mixnodes, gateways) + nym_topology_from_basic_info(&filtered_mixnodes, &gateways) } } diff --git a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs index cf47bba50c..bfd62d1a8f 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -3,7 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::mix::Layer; +use nym_topology::mix::LegacyMixLayer; use nym_topology::{mix, NymTopology}; use std::collections::BTreeMap; @@ -19,7 +19,7 @@ async fn main() { let mut mixnodes = BTreeMap::new(); mixnodes.insert( 1, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 63, owner: None, host: "172.105.92.48".parse().unwrap(), @@ -30,13 +30,13 @@ async fn main() { sphinx_key: "CBmYewWf43iarBq349KhbfYMc9ys2ebXWd4Vp4CLQ5Rq" .parse() .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "1.1.0".into(), }], ); mixnodes.insert( 2, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 23, owner: None, host: "178.79.143.65".parse().unwrap(), @@ -47,13 +47,13 @@ async fn main() { sphinx_key: "8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG" .parse() .unwrap(), - layer: Layer::Two, + layer: LegacyMixLayer::Two, version: "1.1.0".into(), }], ); mixnodes.insert( 3, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 66, owner: None, host: "139.162.247.97".parse().unwrap(), @@ -64,7 +64,7 @@ async fn main() { sphinx_key: "7KyZh8Z8KxuVunqytAJ2eXFuZkCS7BLTZSzujHJZsGa2" .parse() .unwrap(), - layer: Layer::Three, + layer: LegacyMixLayer::Three, version: "1.1.0".into(), }], ); diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile index 2aa57e5ef9..126fde637f 100644 --- a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile @@ -1,5 +1,5 @@ all: build build: - RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown + RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown --no-default-features wasm-opt --signext-lowering -O ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm -o ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm \ No newline at end of file diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs index 9fc92d17d0..0351ce4d2b 100644 --- a/tools/internal/testnet-manager/src/manager/local_apis.rs +++ b/tools/internal/testnet-manager/src/manager/local_apis.rs @@ -119,11 +119,11 @@ impl NetworkManager { let pub_id = &storage_paths["public_identity_key_file"] .as_str() .expect("nym-api config serialisation has changed"); - let ecash = &parsed_config["coconut_signer"] + let ecash = &parsed_config["ecash_signer"] .as_table() .expect("nym-api config serialisation has changed")["storage_paths"] .as_table() - .expect("nym-api config serialisation has changed")["coconut_key_path"] + .expect("nym-api config serialisation has changed")["ecash_key_path"] .as_str() .expect("nym-api config serialisation has changed"); diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index c8c9cf1fd6..d5ea4be530 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -7,7 +7,8 @@ use crate::manager::network::LoadedNetwork; use crate::manager::node::NymNode; use crate::manager::NetworkManager; use console::style; -use nym_mixnet_contract_common::{Layer, LayerAssignment}; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::RoleAssignment; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::DirectSigningHttpRpcNyxdClient; use serde::{Deserialize, Serialize}; @@ -385,17 +386,92 @@ impl NetworkManager { let fut = rewarder.reconcile_epoch_events(None, None); ctx.async_with_progress(fut).await?; - ctx.set_pb_message("finally assigning the active set..."); - let fut = rewarder.get_rewarding_parameters(); - let rewarding_params = ctx.async_with_progress(fut).await?; - let active_set_size = rewarding_params.active_set_size; + ctx.set_pb_message("[BROKEN] finally assigning the active set..."); + // let fut = rewarder.get_rewarding_parameters(); + // let rewarding_params = ctx.async_with_progress(fut).await?; + // let active_set_size = rewarding_params.active_set_size; - let layer_assignment = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; - let fut = rewarder.advance_current_epoch(layer_assignment, active_set_size, None); + let unused_variable = "this has to be fixed up and refactored...."; + /* + fn generate_role_assignment_messages( + &self, + rewarded_set: RewardedSet, + ) -> Vec<(ExecuteMsg, Vec)> { + // currently we just assign all of them together, + // but the contract is ready to handle them separately should we need it + // if the tx is too big + let mut msgs = Vec::new(); + for (role, nodes) in [ + (Role::ExitGateway, rewarded_set.exit_gateways), + (Role::EntryGateway, rewarded_set.entry_gateways), + (Role::Layer1, rewarded_set.layer1), + (Role::Layer2, rewarded_set.layer2), + (Role::Layer3, rewarded_set.layer3), + (Role::Standby, rewarded_set.standby), + ] { + msgs.push(( + ExecuteMsg::AssignRoles { + assignment: RoleAssignment { role, nodes }, + }, + Vec::new(), + )); + } + msgs + } + */ + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![4], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Layer1, + nodes: vec![1], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Layer2, + nodes: vec![2], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Layer3, + nodes: vec![3], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Standby, + nodes: vec![], + }, + None, + ); ctx.async_with_progress(fut).await?; Ok(()) diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index 568632f734..bdbf39cf32 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -11,6 +11,7 @@ use cw_utils::Threshold; use indicatif::HumanDuration; use nym_coconut_dkg_common::types::TimeConfiguration; use nym_config::defaults::NymNetworkDetails; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; use nym_mixnet_contract_common::{Decimal, InitialRewardingParams, Percent}; use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; use nym_validator_client::nyxd::Config; @@ -151,8 +152,12 @@ impl NetworkManager { sybil_resistance: Percent::from_percentage_value(30).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_size: 240, - active_set_size: 240, + rewarded_set_params: RewardedSetParams { + entry_gateways: 70, + exit_gateways: 50, + mixnodes: 120, + standby: 0, + }, }, profit_margin: Default::default(), interval_operating_cost: Default::default(), diff --git a/tools/internal/testnet-manager/src/manager/node.rs b/tools/internal/testnet-manager/src/manager/node.rs index e0c6d6981f..3cf3443c24 100644 --- a/tools/internal/testnet-manager/src/manager/node.rs +++ b/tools/internal/testnet-manager/src/manager/node.rs @@ -7,7 +7,7 @@ use nym_contracts_common::signing::MessageSignature; use nym_contracts_common::Percent; use nym_mixnet_contract_common::{ construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Gateway, - MixNode, MixNodeCostParams, + MixNode, NodeCostParams, }; use nym_validator_client::nyxd::CosmWasmCoin; @@ -68,8 +68,8 @@ impl NymNode { } } - pub(crate) fn cost_params(&self) -> MixNodeCostParams { - MixNodeCostParams { + pub(crate) fn cost_params(&self) -> NodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: CosmWasmCoin::new(40_000000, "unym"), } diff --git a/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs index 630ebccb00..d5a525faba 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs @@ -1,36 +1,26 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use anyhow::bail; use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::gateway::{ + MixnetOperatorsGateway, MixnetOperatorsGatewayCommands, +}; use nym_network_defaults::NymNetworkDetails; pub(crate) mod settings; pub(crate) async fn execute( global_args: ClientArgs, - gateway: nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGateway, + gateway: MixnetOperatorsGateway, network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match gateway.command { - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Bond(args) => { - nym_cli_commands::validator::mixnet::operators::gateway::bond_gateway::bond_gateway(args, create_signing_client(global_args, network_details)?).await - }, - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Unbond(_args) => { + MixnetOperatorsGatewayCommands::Unbond(_args) => { nym_cli_commands::validator::mixnet::operators::gateway::unbond_gateway::unbond_gateway(create_signing_client(global_args, network_details)?).await }, - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::CreateGatewayBondingSignPayload(args) => { - nym_cli_commands::validator::mixnet::operators::gateway::gateway_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Settings(settings) => { - settings::execute(global_args, settings, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::VestingBond(args) => { - nym_cli_commands::validator::mixnet::operators::gateway::vesting_bond_gateway::vesting_bond_gateway(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::VestingUnbond(_args) => { - nym_cli_commands::validator::mixnet::operators::gateway::vesting_unbond_gateway::vesting_unbond_gateway(create_signing_client(global_args, network_details)?).await - - } + MixnetOperatorsGatewayCommands::MigrateToNymnode(args) => nym_cli_commands::validator::mixnet::operators::gateway::nymnode_migration::migrate_to_nymnode(args, create_signing_client(global_args, network_details)?).await, + _ => bail!("this command is no longer available. please migrate your mixnode into a Nym-Node via `migrate-to-nymnode` command") } Ok(()) } diff --git a/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs index b909063125..d87ee0d37a 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs @@ -4,6 +4,7 @@ use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; +#[allow(dead_code)] pub(crate) async fn execute( global_args: ClientArgs, settings: nym_cli_commands::validator::mixnet::operators::gateway::settings::MixnetOperatorsGatewaySettings, diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs deleted file mode 100644 index 0f10e79fa3..0000000000 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; -use nym_cli_commands::validator::mixnet::operators::mixnode::families::MixnetOperatorsMixnodeFamiliesCommands; -use nym_network_defaults::NymNetworkDetails; - -pub(crate) async fn execute( - global_args: ClientArgs, - families: nym_cli_commands::validator::mixnet::operators::mixnode::families::MixnetOperatorsMixnodeFamilies, - network_details: &NymNetworkDetails, -) -> anyhow::Result<()> { - match families.command { - MixnetOperatorsMixnodeFamiliesCommands::CreateFamily(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::create_family::create_family( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::JoinFamily(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::join_family::join_family( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::LeaveFamily(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::leave_family::leave_family( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::KickFamilyMember(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::kick_family_member::kick_family_member( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::CreateFamilyJoinPermitSignPayload(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::create_family_join_permit_sign_payload::create_family_join_permit_sign_payload(args, create_query_client(network_details)?).await - } - } - Ok(()) -} diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs index 3923093dbb..c881e4b9c4 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#[allow(dead_code)] pub(crate) async fn execute( keys: nym_cli_commands::validator::mixnet::operators::mixnode::keys::MixnetOperatorsMixnodeKeys, ) -> anyhow::Result<()> { diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs index ac2d301f4f..4ebe8e94d8 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs @@ -1,10 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use anyhow::bail; use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands; use nym_network_defaults::NymNetworkDetails; -pub(crate) mod families; pub(crate) mod keys; pub(crate) mod rewards; pub(crate) mod settings; @@ -15,43 +16,14 @@ pub(crate) async fn execute( network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match mixnode.command { - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Keys(keys) => { - keys::execute(keys).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Rewards(rewards) => { - rewards::execute(global_args, rewards, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Settings(settings) => { - settings::execute(global_args, settings, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Families(families) => { - families::execute(global_args, families, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Bond(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::bond_mixnode::bond_mixnode(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Unbond(args) => { + MixnetOperatorsMixnodeCommands::Unbond(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::unbond_mixnode::unbond_mixnode(args, create_signing_client(global_args, network_details)?).await } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::CreateMixnodeBondingSignPayload(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::mixnode_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::PledgeMore(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::pledge_more::pledge_more(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::PledgeMoreVesting(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::vesting_pledge_more::vesting_pledge_more(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledge(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::decrease_pledge::decrease_pledge(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledgeVesting(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::vesting_decrease_pledge::vesting_decrease_pledge(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::MigrateVestedNode(args) => { + MixnetOperatorsMixnodeCommands::MigrateVestedNode(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::migrate_vested_mixnode::migrate_vested_mixnode(args, create_signing_client(global_args, network_details)?).await } - _ => unreachable!() + MixnetOperatorsMixnodeCommands::MigrateToNymnode(args) => nym_cli_commands::validator::mixnet::operators::mixnode::nymnode_migration::migrate_to_nymnode(args, create_signing_client(global_args, network_details)?).await, + _ => bail!("this command is no longer available. please migrate your mixnode into a Nym-Node via `migrate-to-nymnode` command") } Ok(()) } diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs index d485a5131b..b93f981c5b 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs @@ -4,6 +4,7 @@ use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; +#[allow(dead_code)] pub(crate) async fn execute( global_args: ClientArgs, rewards: nym_cli_commands::validator::mixnet::operators::mixnode::rewards::MixnetOperatorsMixnodeRewards, diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs index a5d5c02883..a431c19ab0 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs @@ -4,6 +4,7 @@ use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; +#[allow(dead_code)] pub(crate) async fn execute( global_args: ClientArgs, settings: nym_cli_commands::validator::mixnet::operators::mixnode::settings::MixnetOperatorsMixnodeSettings, @@ -14,7 +15,7 @@ pub(crate) async fn execute( nym_cli_commands::validator::mixnet::operators::mixnode::settings::update_config::update_config(args, create_signing_client(global_args, network_details)?).await } nym_cli_commands::validator::mixnet::operators::mixnode::settings::MixnetOperatorsMixnodeSettingsCommands::UpdateCostParameters(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::settings::update_cost_params::update_cost_params(args, create_signing_client(global_args, network_details)?).await + nym_cli_commands::validator::mixnet::operators::mixnode::settings::update_cost_params::update_cost_params(args, create_signing_client(global_args, network_details)?).await? } _ => unreachable!(), } diff --git a/tools/nym-cli/src/validator/mixnet/operators/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mod.rs index 0e14e36a21..9739f2f428 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mod.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use nym_cli_commands::context::ClientArgs; +use nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands; use nym_network_defaults::NymNetworkDetails; pub(crate) mod gateways; pub(crate) mod identity_key; pub(crate) mod mixnodes; +mod nymnodes; pub(crate) async fn execute( global_args: ClientArgs, @@ -14,14 +16,17 @@ pub(crate) async fn execute( network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match operators.command { - nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands::Gateway( - gateway, - ) => gateways::execute(global_args, gateway, network_details).await, - nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands::Mixnode( - mixnode, - ) => mixnodes::execute(global_args, mixnode, network_details).await, - nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands::IdentityKey( - identity_key, - ) => identity_key::execute(global_args, identity_key, network_details).await, + MixnetOperatorsCommands::Nymnode(nymnode) => { + nymnodes::execute(global_args, nymnode, network_details).await + } + MixnetOperatorsCommands::Gateway(gateway) => { + gateways::execute(global_args, gateway, network_details).await + } + MixnetOperatorsCommands::Mixnode(mixnode) => { + mixnodes::execute(global_args, mixnode, network_details).await + } + MixnetOperatorsCommands::IdentityKey(identity_key) => { + identity_key::execute(global_args, identity_key, network_details).await + } } } diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/keys.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/keys.rs new file mode 100644 index 0000000000..b1a0554d42 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/keys.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::validator::mixnet::operators::nymnode::keys::MixnetOperatorsNymNodeKeysCommands; + +pub(crate) async fn execute( + keys: nym_cli_commands::validator::mixnet::operators::nymnode::keys::MixnetOperatorsNymNodeKeys, +) -> anyhow::Result<()> { + match keys.command { + MixnetOperatorsNymNodeKeysCommands::DecodeNodeKey(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::keys::decode_node_key::decode_node_key(args) + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/mod.rs new file mode 100644 index 0000000000..24bd998aa5 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/mod.rs @@ -0,0 +1,49 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::bond_nymnode::bond_nymnode; +use nym_cli_commands::validator::mixnet::operators::nymnode::unbond_nymnode::unbond_nymnode; +use nym_cli_commands::validator::mixnet::operators::nymnode::{ + nymnode_bonding_sign_payload, MixnetOperatorsNymNodeCommands, +}; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) mod keys; +pub(crate) mod pledge; +pub(crate) mod rewards; +pub(crate) mod settings; + +pub(crate) async fn execute( + global_args: ClientArgs, + nymnode: nym_cli_commands::validator::mixnet::operators::nymnode::MixnetOperatorsNymNode, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match nymnode.command { + MixnetOperatorsNymNodeCommands::CreateNodeBondingSignPayload(args) => { + nymnode_bonding_sign_payload::create_payload( + args, + create_signing_client(global_args, network_details)?, + ) + .await + } + MixnetOperatorsNymNodeCommands::Keys(keys) => keys::execute(keys).await?, + MixnetOperatorsNymNodeCommands::Rewards(rewards) => { + rewards::execute(global_args, rewards, network_details).await? + } + MixnetOperatorsNymNodeCommands::Settings(settings) => { + settings::execute(global_args, settings, network_details).await? + } + MixnetOperatorsNymNodeCommands::Pledge(pledge) => { + pledge::execute(global_args, pledge, network_details).await? + } + MixnetOperatorsNymNodeCommands::Bond(args) => { + bond_nymnode(args, create_signing_client(global_args, network_details)?).await + } + MixnetOperatorsNymNodeCommands::Unbond(args) => { + unbond_nymnode(args, create_signing_client(global_args, network_details)?).await + } + } + + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/pledge.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/pledge.rs new file mode 100644 index 0000000000..d27a91b422 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/pledge.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::pledge::MixnetOperatorsNymNodePledgeCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + pledge: nym_cli_commands::validator::mixnet::operators::nymnode::pledge::MixnetOperatorsNymNodePledge, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match pledge.command { + MixnetOperatorsNymNodePledgeCommands::Increase(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::pledge::increase_pledge::increase_pledge(args, create_signing_client(global_args, network_details)?).await + }, + MixnetOperatorsNymNodePledgeCommands::Decrease(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::pledge::decrease_pledge::decrease_pledge(args, create_signing_client(global_args, network_details)?).await + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/rewards.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/rewards.rs new file mode 100644 index 0000000000..261449d41b --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/rewards.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::rewards::MixnetOperatorsNymNodeRewardsCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + rewards: nym_cli_commands::validator::mixnet::operators::nymnode::rewards::MixnetOperatorsNymNodeRewards, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match rewards.command { + MixnetOperatorsNymNodeRewardsCommands::Claim(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::rewards::claim_operator_reward::claim_operator_reward(args, create_signing_client(global_args, network_details)?).await + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/settings.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/settings.rs new file mode 100644 index 0000000000..babb8e4244 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/settings.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::settings::MixnetOperatorsNymNodeSettingsCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + settings: nym_cli_commands::validator::mixnet::operators::nymnode::settings::MixnetOperatorsNymNodeSettings, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match settings.command { + MixnetOperatorsNymNodeSettingsCommands::UpdateConfig(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::settings::update_config::update_config(args, create_signing_client(global_args, network_details)?).await + }, + MixnetOperatorsNymNodeSettingsCommands::UpdateCostParameters(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::settings::update_cost_params::update_cost_params(args, create_signing_client(global_args, network_details)?).await? + } + } + Ok(()) +} diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index e3728bd234..8a7720664f 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -6,7 +6,7 @@ use nym_api_requests::models::{ use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::{ GatewayConfigUpdate, Interval as ContractInterval, IntervalRewardParams, - IntervalRewardingParamsUpdate, MixNode, MixNodeConfigUpdate, RewardedSetNodeStatus, + IntervalRewardingParamsUpdate, MixNode, MixNodeConfigUpdate, NymNode, PendingNodeChanges, RewardingParams, UnbondedMixnode, }; use nym_types::account::{Account, AccountEntry, AccountWithMnemonic, Balance}; @@ -18,7 +18,8 @@ use nym_types::deprecated::{DelegationEvent, DelegationEventKind, WrappedDelegat use nym_types::fees::{self, FeeDetails}; use nym_types::gas::{Gas, GasInfo}; use nym_types::gateway::{Gateway, GatewayBond}; -use nym_types::mixnode::{MixNodeBond, MixNodeCostParams, MixNodeDetails, MixNodeRewarding}; +use nym_types::mixnode::{MixNodeBond, MixNodeDetails, NodeCostParams, NodeRewarding}; +use nym_types::nym_node::{NymNodeBond, NymNodeDetails}; use nym_types::pending_events::{ PendingEpochEvent, PendingEpochEventData, PendingIntervalEvent, PendingIntervalEventData, }; @@ -69,10 +70,11 @@ fn main() { do_export!(MixNode); do_export!(MixNodeConfigUpdate); do_export!(RewardingParams); - do_export!(RewardedSetNodeStatus); do_export!(UnbondedMixnode); do_export!(RewardEstimate); do_export!(ContractInterval); + do_export!(NymNode); + do_export!(PendingNodeChanges); // common/types/src do_export!(Account); @@ -99,9 +101,14 @@ fn main() { do_export!(CurrencyDenom); do_export!(DecCoin); do_export!(MixNodeBond); - do_export!(MixNodeCostParams); + do_export!(NodeCostParams); do_export!(MixNodeDetails); - do_export!(MixNodeRewarding); + + // for nym-node: + do_export!(NymNodeDetails); + do_export!(NymNodeBond); + + do_export!(NodeRewarding); do_export!(OriginalVestingResponse); do_export!(PendingEpochEvent); do_export!(PendingEpochEventData); @@ -155,6 +162,7 @@ fn main() { && !path.starts_with("./Cargo.toml") && !path.starts_with("./.gitignore") && f.file_type().is_file() + && f.path().extension() == Some("ts".as_ref()) }) { // construct the source and destination paths that can be used to replace the output file diff --git a/ts-packages/types/src/types/rust/NodeRewarding.ts b/ts-packages/types/src/types/rust/NodeRewarding.ts new file mode 100644 index 0000000000..3fa37c30bd --- /dev/null +++ b/ts-packages/types/src/types/rust/NodeRewarding.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NodeCostParams } from "./MixNodeCostParams"; + +export interface NodeRewarding { cost_params: NodeCostParams, operator: string, delegates: string, total_unit_reward: string, unit_delegation: string, last_rewarded_epoch: number, unique_delegations: number, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/NymNode.ts b/ts-packages/types/src/types/rust/NymNode.ts new file mode 100644 index 0000000000..78bddc8007 --- /dev/null +++ b/ts-packages/types/src/types/rust/NymNode.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface NymNode { host: string, custom_http_port: number | null, identity_key: string, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/NymNodeBond.ts b/ts-packages/types/src/types/rust/NymNodeBond.ts new file mode 100644 index 0000000000..b8f608f2c7 --- /dev/null +++ b/ts-packages/types/src/types/rust/NymNodeBond.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DecCoin } from "./DecCoin"; + +export interface NymNodeBond { node_id: number, owner: string, original_pledge: DecCoin, bonding_height: bigint, is_unbonding: boolean, host: string, custom_http_port: number | null, identity_key: string, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/NymNodeDetails.ts b/ts-packages/types/src/types/rust/NymNodeDetails.ts new file mode 100644 index 0000000000..7b6c0b5f2a --- /dev/null +++ b/ts-packages/types/src/types/rust/NymNodeDetails.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NodeRewarding } from "./NodeRewarding"; +import type { NymNodeBond } from "./NymNodeBond"; +import type { PendingNodeChanges } from "./PendingNodeChanges"; + +export interface NymNodeDetails { bond_information: NymNodeBond, rewarding_details: NodeRewarding, pending_changes: PendingNodeChanges, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/PendingNodeChanges.ts b/ts-packages/types/src/types/rust/PendingNodeChanges.ts new file mode 100644 index 0000000000..3018f1e041 --- /dev/null +++ b/ts-packages/types/src/types/rust/PendingNodeChanges.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface PendingNodeChanges { pledge_change: number | null, cost_params_change: number | null, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/index.ts b/ts-packages/types/src/types/rust/index.ts index af9d8b5c99..02f817b113 100644 --- a/ts-packages/types/src/types/rust/index.ts +++ b/ts-packages/types/src/types/rust/index.ts @@ -54,3 +54,7 @@ export * from './UnbondedMixnode'; export * from './VestingAccountInfo'; export * from './VestingPeriod'; export * from './WrappedDelegationEvent'; +export * from './NymNode'; +export * from './NymNodeBond'; +export * from './NymNodeDetails'; +export * from './NodeRewarding'; From 1339e204200e6489a74c6fc386a4232b2044b736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 12 Sep 2024 12:07:33 +0100 Subject: [PATCH 041/207] typo --- contracts/mixnet/src/mixnet_contract_settings/transactions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 16750eb44c..7f6ec4b6c7 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -84,7 +84,7 @@ pub mod tests { use cw_controllers::AdminError::NotAdmin; #[test] - fn update_contract_rewarding_validtor_address() { + fn update_contract_rewarding_validator_address() { let mut deps = test_helpers::init_contract(); let info = mock_info("not-the-creator", &[]); From fd88776998bd15e5359ed214b53448c5c68ef98b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 19 Sep 2024 16:27:32 +0100 Subject: [PATCH 042/207] fix legacy test --- contracts/mixnet/src/mixnodes/transactions.rs | 4 ++-- nym-wallet/Cargo.lock | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index fea26bc24e..7b8ed90d04 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -362,10 +362,10 @@ pub mod tests { .unwrap(); assert_eq!(nym_node.bond_information.owner, info.sender); - // make sure we got assigned the next id (note: we have already bonded a mixnode before in this test) + // make sure we got assigned the next id (note: we have already bonded a mixnode and a gateway before in this test) let bond = must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); - assert_eq!(2, bond.node_id); + assert_eq!(3, bond.node_id); } #[test] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index eeef135c89..0d8ffbc8a7 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -5943,7 +5943,11 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", +<<<<<<< HEAD "toml_edit 0.22.22", +======= + "toml_edit 0.22.21", +>>>>>>> 2485d63d0 (fix legacy test) ] [[package]] @@ -5970,15 +5974,25 @@ dependencies = [ [[package]] name = "toml_edit" +<<<<<<< HEAD version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +======= +version = "0.22.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +>>>>>>> 2485d63d0 (fix legacy test) dependencies = [ "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", +<<<<<<< HEAD "winnow 0.6.19", +======= + "winnow 0.6.18", +>>>>>>> 2485d63d0 (fix legacy test) ] [[package]] @@ -6854,9 +6868,15 @@ dependencies = [ [[package]] name = "winnow" +<<<<<<< HEAD version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" +======= +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +>>>>>>> 2485d63d0 (fix legacy test) dependencies = [ "memchr", ] From 5c08a89be35cfcae44b8c9d3b7a9d24d555db9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 19 Sep 2024 17:51:32 +0100 Subject: [PATCH 043/207] exposed nym-node related operations in the wallet backend --- .../validator-client/src/nym_api/mod.rs | 10 +- .../validator-client/src/nym_api/routes.rs | 2 + nym-node/nym-node-requests/src/api/client.rs | 9 +- nym-wallet/Cargo.lock | 3 + nym-wallet/src-tauri/Cargo.toml | 1 + nym-wallet/src-tauri/src/error.rs | 28 +-- nym-wallet/src-tauri/src/main.rs | 10 + .../src-tauri/src/operations/helpers.rs | 49 ++++- .../src-tauri/src/operations/mixnet/bond.rs | 175 +++++++++++++++++- .../signatures/ed25519_signing_payload.rs | 36 +++- .../src/operations/vesting/migrate.rs | 7 +- nym-wallet/src-tauri/src/utils.rs | 2 +- 12 files changed, 311 insertions(+), 21 deletions(-) diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 6c5337afaf..7723ab89a8 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -11,7 +11,7 @@ use nym_api_requests::ecash::models::{ }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::legacy::LegacyGatewayBondWithId; -use nym_api_requests::models::LegacyDescribedMixNode; +use nym_api_requests::models::{LegacyDescribedMixNode, NodePerformanceResponse}; pub use nym_api_requests::{ ecash::{ models::{ @@ -427,6 +427,14 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_current_node_performance( + &self, + node_id: NodeId, + ) -> Result { + self.get_json_from(format!("/v1/nym-nodes/performance/{node_id}")) + .await + } + async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index dc87026701..e2a1540dda 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -38,6 +38,7 @@ pub mod ecash { pub const STATUS_ROUTES: &str = "status"; pub const MIXNODE: &str = "mixnode"; pub const GATEWAY: &str = "gateway"; +pub const NYM_NODES: &str = "nym-nodes"; pub const CORE_STATUS_COUNT: &str = "core-status-count"; pub const SINCE_ARG: &str = "since"; @@ -52,5 +53,6 @@ pub const STAKE_SATURATION: &str = "stake-saturation"; pub const INCLUSION_CHANCE: &str = "inclusion-probability"; pub const SUBMIT_GATEWAY: &str = "submit-gateway-monitoring-results"; pub const SUBMIT_NODE: &str = "submit-node-monitoring-results"; +pub const PERFORMANCE: &str = "performance"; pub const SERVICE_PROVIDERS: &str = "services"; diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 6255a5644a..11ec66a4b9 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::api::v1::gateway::models::WebSockets; -use crate::api::v1::node::models::{AuxiliaryDetails, NodeRoles, SignedHostInformation}; +use crate::api::v1::node::models::{ + AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation, +}; use crate::api::ErrorResponse; use crate::routes; use async_trait::async_trait; @@ -32,6 +34,11 @@ pub trait NymNodeApiClientExt: ApiClient { .await } + async fn get_description(&self) -> Result { + self.get_json_from(routes::api::v1::description_absolute()) + .await + } + async fn get_build_information( &self, ) -> Result { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 0d8ffbc8a7..895eb5fe65 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3335,6 +3335,7 @@ dependencies = [ name = "nym-node-requests" version = "0.1.0" dependencies = [ + "async-trait", "base64 0.22.1", "celes", "humantime 2.1.0", @@ -3342,6 +3343,7 @@ dependencies = [ "nym-bin-common", "nym-crypto", "nym-exit-policy", + "nym-http-api-client", "nym-wireguard-types", "schemars", "serde", @@ -3549,6 +3551,7 @@ dependencies = [ "nym-contracts-common", "nym-crypto", "nym-mixnet-contract-common", + "nym-node-requests", "nym-store-cipher", "nym-types", "nym-validator-client", diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 8e995a784f..61eee10d85 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -52,6 +52,7 @@ zeroize = { version = "1.5", features = ["zeroize_derive", "serde"] } cosmwasm-std = "1.3.0" cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } +nym-node-requests = { path = "../../nym-node/nym-node-requests" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index cefac100e7..573e931b6b 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,5 +1,6 @@ use nym_contracts_common::signing::SigningAlgorithm; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; +use nym_node_requests::api::client::NymNodeApiClientError; use nym_types::error::TypesError; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError; @@ -12,17 +13,17 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum BackendError { - #[error("{source}")] + #[error(transparent)] TypesError { #[from] source: TypesError, }, - #[error("{source}")] + #[error(transparent)] Bip39Error { #[from] source: bip39::Error, }, - #[error("{source}")] + #[error(transparent)] TendermintError { #[from] source: cosmrs::rpc::Error, @@ -33,42 +34,47 @@ pub enum BackendError { #[source] source: NyxdError, }, - #[error("{source}")] + #[error(transparent)] CosmwasmStd { #[from] source: cosmwasm_std::StdError, }, - #[error("{source}")] + #[error(transparent)] ErrorReport { #[from] source: eyre::Report, }, - #[error("{source}")] + #[error(transparent)] NymApiError { #[from] source: NymAPIError, }, - #[error("{source}")] + #[error(transparent)] + NymNodeApiError { + #[from] + source: NymNodeApiClientError, + }, + #[error(transparent)] IOError { #[from] source: io::Error, }, - #[error("{source}")] + #[error(transparent)] SerdeJsonError { #[from] source: serde_json::Error, }, - #[error("{source}")] + #[error(transparent)] MalformedUrlProvided { #[from] source: url::ParseError, }, - #[error("{source}")] + #[error(transparent)] ReqwestError { #[from] source: reqwest::Error, }, - #[error("{source}")] + #[error(transparent)] K256Error { #[from] source: k256::ecdsa::Error, diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 623ab63e45..a570bfb00b 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -73,6 +73,14 @@ fn main() { mixnet::bond::get_number_of_mixnode_delegators, mixnet::bond::get_mix_node_description, mixnet::bond::get_mixnode_avg_uptime, + mixnet::bond::bond_nymnode, + mixnet::bond::unbond_nymnode, + mixnet::bond::nym_node_bond_details, + mixnet::bond::get_nym_node_description, + mixnet::bond::migrate_legacy_mixnode, + mixnet::bond::migrate_legacy_gateway, + mixnet::bond::update_nymnode_config, + mixnet::bond::get_nymnode_performance, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, @@ -103,6 +111,7 @@ fn main() { state::save_config_to_files, utils::owns_gateway, utils::owns_mixnode, + utils::owns_nym_node, utils::get_env, utils::try_convert_pubkey_to_mix_id, utils::default_mixnode_cost_params, @@ -188,6 +197,7 @@ fn main() { signatures::ed25519_signing_payload::generate_mixnode_bonding_msg_payload, signatures::ed25519_signing_payload::vesting_generate_mixnode_bonding_msg_payload, signatures::ed25519_signing_payload::generate_gateway_bonding_msg_payload, + signatures::ed25519_signing_payload::generate_nym_node_bonding_msg_payload, signatures::ed25519_signing_payload::vesting_generate_gateway_bonding_msg_payload, help::log::help_log_toggle_window, app::window::create_main_window, diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index fc8212d48e..c2267ccb20 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -10,7 +10,8 @@ use nym_contracts_common::signing::{ use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::{ construct_legacy_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, - NodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, + NodeCostParams, NymNode, NymNodeBondingPayload, SignableGatewayBondingMsg, + SignableLegacyMixNodeBondingMsg, SignableNymNodeBondingMsg, }; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; @@ -143,6 +144,52 @@ pub(crate) async fn verify_gateway_bonding_sign_payload( + client: &P, + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: Coin, +) -> Result { + let payload = NymNodeBondingPayload::new(nym_node, cost_params); + let sender = client.cw_address(); + let content = ContractMessageContent::new(sender, vec![pledge.into()], payload); + let nonce = client.get_signing_nonce().await?; + + Ok(SignableMessage::new(nonce, content)) +} + +pub(crate) async fn verify_nym_node_bonding_sign_payload( + client: &P, + nym_node: &NymNode, + cost_params: &NodeCostParams, + pledge: &Coin, + msg_signature: &MessageSignature, +) -> Result<(), BackendError> { + let identity_key = identity::PublicKey::from_base58_string(&nym_node.identity_key)?; + let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; + + // recreate the plaintext + let msg = create_nym_node_bonding_sign_payload( + client, + nym_node.clone(), + cost_params.clone(), + pledge.clone(), + ) + .await?; + let plaintext = msg.to_plaintext()?; + + if !msg.algorithm.is_ed25519() { + return Err(BackendError::UnexpectedSigningAlgorithm { + received: msg.algorithm, + expected: SigningAlgorithm::Ed25519, + }); + } + + // TODO: possibly provide better error message if this check fails + identity_key.verify(plaintext, &signature)?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index a942321f9b..ba3d03b6ba 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -4,12 +4,18 @@ use crate::error::BackendError; use crate::operations::helpers::{ verify_gateway_bonding_sign_payload, verify_mixnode_bonding_sign_payload, + verify_nym_node_bonding_sign_payload, }; use crate::state::WalletState; use crate::{nyxd_client, Gateway, MixNode}; +use log::info; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId}; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId, NymNode}; +use nym_node_requests::api::client::NymNodeApiClientExt; +use nym_node_requests::api::v1::node::models::NodeDescription; +use nym_node_requests::api::ErrorResponse; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; use nym_types::mixnode::{MixNodeDetails, NodeCostParams}; @@ -23,7 +29,7 @@ use std::cmp::Ordering; use std::time::Duration; #[derive(Debug, Serialize, Deserialize)] -pub struct NodeDescription { +pub struct LegacyNodeDescription { name: String, description: String, link: String, @@ -136,6 +142,54 @@ pub async fn bond_mixnode( )?) } +#[tauri::command] +pub async fn bond_nymnode( + nymnode: NymNode, + cost_params: NodeCostParams, + msg_signature: MessageSignature, + pledge: DecCoin, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let reg = guard.registered_coins()?; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Bond NymNode: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", + nymnode.identity_key, + pledge, + pledge_base, + fee, + ); + + let client = guard.current_client()?; + // check the signature to make sure the user copied it correctly + if let Err(err) = verify_nym_node_bonding_sign_payload( + client, + &nymnode, + &cost_params, + &pledge_base, + &msg_signature, + ) + .await + { + log::warn!("failed to verify provided nymnode bonding signature: {err}"); + return Err(err); + } + + let res = client + .nyxd + .bond_nymnode(nymnode, cost_params, msg_signature, pledge_base, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn update_pledge( current_pledge: DecCoin, @@ -255,6 +309,23 @@ pub async fn unbond_mixnode( )?) } +#[tauri::command] +pub async fn unbond_nymnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> Unbond NymNode, fee = {fee:?}"); + let res = guard.current_client()?.nyxd.unbond_nymnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn update_mixnode_cost_params( new_costs: NodeCostParams, @@ -506,7 +577,7 @@ pub async fn get_number_of_mixnode_delegators( pub async fn get_mix_node_description( host: &str, port: u16, -) -> Result { +) -> Result { Ok(reqwest::Client::builder() .timeout(Duration::from_millis(1000)) .build()? @@ -517,6 +588,23 @@ pub async fn get_mix_node_description( .await?) } +#[tauri::command] +pub async fn get_nym_node_description( + host: &str, + port: u16, +) -> Result { + Ok( + nym_node_requests::api::Client::builder::<_, ErrorResponse>(format!( + "http://{host}:{port}" + ))? + .with_timeout(Duration::from_millis(1000)) + .with_user_agent(format!("nym-wallet/{}", env!("CARGO_PKG_VERSION"))) + .build::()? + .get_description() + .await?, + ) +} + #[tauri::command] pub async fn get_mixnode_uptime( mix_id: NodeId, @@ -531,3 +619,84 @@ pub async fn get_mixnode_uptime( log::info!(">>> Uptime response: {}", uptime.avg_uptime); Ok(uptime.avg_uptime) } + +#[tauri::command] +pub async fn migrate_legacy_mixnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + info!(">>> migrate to NymNode, fee = {fee:?}"); + let client = guard.current_client()?; + + let res = client.nyxd.migrate_legacy_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn migrate_legacy_gateway( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + info!(">>> migrate to NymNode, fee = {fee:?}"); + let client = guard.current_client()?; + + let res = client.nyxd.migrate_legacy_gateway(None, fee).await?; + + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn update_nymnode_config( + update: NodeConfigUpdate, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> update nym node config: update = {update:?}, fee {fee:?}",); + let res = guard + .current_client()? + .nyxd + .update_nymnode_config(update, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn get_nymnode_performance( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + let Some(details) = nym_node_bond_details(state.clone()).await? else { + return Ok(None); + }; + let node_id = details.bond_information.node_id; + + log::trace!(" >>> Get node performance: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nym_api + .get_current_node_performance(node_id) + .await?; + log::trace!(" <<< {res:?}"); + + Ok(res.performance) +} diff --git a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs index c0c3dd373a..53658067aa 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs @@ -4,9 +4,10 @@ use crate::error::BackendError; use crate::operations::helpers::{ create_gateway_bonding_sign_payload, create_mixnode_bonding_sign_payload, + create_nym_node_bonding_sign_payload, }; use crate::state::WalletState; -use nym_mixnet_contract_common::{Gateway, MixNode}; +use nym_mixnet_contract_common::{Gateway, MixNode, NymNode}; use nym_types::currency::DecCoin; use nym_types::mixnode::NodeCostParams; @@ -61,6 +62,29 @@ async fn gateway_bonding_msg_payload( Ok(msg.to_base58_string()?) } +async fn nym_node_bonding_msg_payload( + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let reg = guard.registered_coins()?; + + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Bond nym_node bonding signature: identity_key = {}, pledge_display = {pledge}, pledge_base = {pledge_base}", + nym_node.identity_key, + ); + + let client = guard.current_client()?; + + let msg = + create_nym_node_bonding_sign_payload(client, nym_node, cost_params, pledge_base).await?; + Ok(msg.to_base58_string()?) +} + #[tauri::command] pub async fn generate_mixnode_bonding_msg_payload( mixnode: MixNode, @@ -90,6 +114,16 @@ pub async fn generate_gateway_bonding_msg_payload( gateway_bonding_msg_payload(gateway, pledge, false, state).await } +#[tauri::command] +pub async fn generate_nym_node_bonding_msg_payload( + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + nym_node_bonding_msg_payload(nym_node, cost_params, pledge, state).await +} + #[tauri::command] pub async fn vesting_generate_gateway_bonding_msg_payload( gateway: Gateway, diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs index 6d5fbadf16..00257464b1 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BackendError; -use crate::nyxd_client; use crate::state::WalletState; use nym_mixnet_contract_common::ExecuteMsg; use nym_types::transaction::TransactionExecuteResult; @@ -20,7 +19,11 @@ pub async fn migrate_vested_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!(">>> migrate vested mixnode, fee = {fee:?}"); - let res = nyxd_client!(state).migrate_vested_mixnode(fee).await?; + let res = guard + .current_client()? + .nyxd + .migrate_vested_mixnode(fee) + .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index f294d4c09e..9a90861059 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -48,7 +48,7 @@ pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result) -> Result { Ok(nyxd_client!(state) - .get_owned_nym_node(&nyxd_client!(state).address()) + .get_owned_nymnode(&nyxd_client!(state).address()) .await? .details .is_some()) From 9a9d91cb4f93c2802dccfb8db0a48ae802268414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 23 Sep 2024 11:53:48 +0100 Subject: [PATCH 044/207] additional unit tests for compatibility-based txs + bug fixes --- .../contract_traits/mixnet_query_client.rs | 32 +- .../mixnet-contract/src/msg.rs | 1 + .../mixnet-contract/src/types.rs | 4 + contracts/mixnet/src/compat/helpers.rs | 433 +++++++++++++ contracts/mixnet/src/compat/transactions.rs | 568 ++++++++++++++++++ contracts/mixnet/src/gateways/transactions.rs | 24 +- .../src/mixnet_contract_settings/storage.rs | 2 +- contracts/mixnet/src/mixnodes/transactions.rs | 339 ++++------- contracts/mixnet/src/rewards/transactions.rs | 16 +- contracts/mixnet/src/support/helpers.rs | 2 +- contracts/mixnet/src/support/tests/mod.rs | 76 ++- 11 files changed, 1255 insertions(+), 242 deletions(-) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 2f1c3cf0e6..a217f91663 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -886,50 +886,54 @@ mod tests { MixnetQueryMsg::GetSigningNonce { address } => { client.get_signing_nonce(&address.parse().unwrap()).ignore() } - QueryMsg::GetPreassignedGatewayIds { start_after, limit } => client + MixnetQueryMsg::GetPreassignedGatewayIds { start_after, limit } => client .get_preassigned_gateway_ids_paged(start_after, limit) .ignore(), - QueryMsg::GetNymNodeBondsPaged { limit, start_after } => { + MixnetQueryMsg::GetNymNodeBondsPaged { limit, start_after } => { client.get_nymnode_bonds_paged(limit, start_after).ignore() } - QueryMsg::GetNymNodesDetailedPaged { limit, start_after } => client + MixnetQueryMsg::GetNymNodesDetailedPaged { limit, start_after } => client .get_nymnodes_detailed_paged(limit, start_after) .ignore(), - QueryMsg::GetUnbondedNymNode { node_id } => { + MixnetQueryMsg::GetUnbondedNymNode { node_id } => { client.get_unbonded_nymnode_information(node_id).ignore() } - QueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => client + MixnetQueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => client .get_unbonded_nymnodes_paged(limit, start_after) .ignore(), - QueryMsg::GetUnbondedNymNodesByOwnerPaged { + MixnetQueryMsg::GetUnbondedNymNodesByOwnerPaged { owner, limit, start_after, } => client .get_unbonded_nymnodes_by_owner_paged(&owner.parse().unwrap(), limit, start_after) .ignore(), - QueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + MixnetQueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { identity_key, limit, start_after, } => client .get_unbonded_nymnodes_by_identity_paged(&identity_key, limit, start_after) .ignore(), - QueryMsg::GetOwnedNymNode { address } => { + MixnetQueryMsg::GetOwnedNymNode { address } => { client.get_owned_nymnode(&address.parse().unwrap()).ignore() } - QueryMsg::GetNymNodeDetails { node_id } => client.get_nymnode_details(node_id).ignore(), - QueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => client + MixnetQueryMsg::GetNymNodeDetails { node_id } => { + client.get_nymnode_details(node_id).ignore() + } + MixnetQueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => client .get_nymnode_details_by_identity(node_identity) .ignore(), - QueryMsg::GetNodeRewardingDetails { node_id } => { + MixnetQueryMsg::GetNodeRewardingDetails { node_id } => { client.get_nymnode_rewarding_details(node_id).ignore() } - QueryMsg::GetNodeStakeSaturation { node_id } => { + MixnetQueryMsg::GetNodeStakeSaturation { node_id } => { client.get_node_stake_saturation(node_id).ignore() } - QueryMsg::GetRoleAssignment { role } => client.get_role_assignment(role).ignore(), - QueryMsg::GetRewardedSetMetadata {} => client.get_rewarded_set_metadata().ignore(), + MixnetQueryMsg::GetRoleAssignment { role } => client.get_role_assignment(role).ignore(), + MixnetQueryMsg::GetRewardedSetMetadata {} => { + client.get_rewarded_set_metadata().ignore() + } } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 39a81b93c5..5e7570714d 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -672,6 +672,7 @@ pub enum QueryMsg { // delegation-related: /// Gets all delegations associated with particular node #[cfg_attr(feature = "schema", returns(PagedNodeDelegationsResponse))] + #[serde(alias = "GetMixnodeDelegations", alias = "get_mixnode_delegations")] GetNodeDelegations { /// Id of the node to query. #[serde(alias = "mix_id")] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 35aac81f53..f695bb22f7 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -166,6 +166,10 @@ impl RangedValue where T: Copy + PartialOrd + PartialEq, { + pub fn new(minimum: T, maximum: T) -> Self { + RangedValue { minimum, maximum } + } + pub fn normalise(&self, value: T) -> T { if value < self.minimum { self.minimum diff --git a/contracts/mixnet/src/compat/helpers.rs b/contracts/mixnet/src/compat/helpers.rs index 0f57650eaf..0664bed06b 100644 --- a/contracts/mixnet/src/compat/helpers.rs +++ b/contracts/mixnet/src/compat/helpers.rs @@ -142,3 +142,436 @@ pub fn may_get_bond( Ok(None) } } + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::bail; + + #[cfg(test)] + mod reward_withdrawing_permission { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + let details = test.mixnode_by_id(node_id).unwrap(); + + // node must not be in the process of unbonding + assert!(ensure_can_withdraw_rewards(&details).is_ok()); + + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_withdraw_rewards(&details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + let details = test.nymnode_by_id(node_id).unwrap(); + + // node must not be in the process of unbonding + assert!(ensure_can_withdraw_rewards(&details).is_ok()); + + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_withdraw_rewards(&details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + Ok(()) + } + } + + #[cfg(test)] + mod modifying_cost_params_permission { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!(ensure_can_modify_cost_params(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!(ensure_can_modify_cost_params(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + Ok(()) + } + } + + #[cfg(test)] + mod increasing_pledge_permission { + use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!(ensure_can_increase_pledge(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_legacy_mixnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_legacy_mixnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!(ensure_can_increase_pledge(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_dummy_nymnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_dummy_nymnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + Ok(()) + } + } + + #[cfg(test)] + mod decreasing_pledge_permission { + use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::coin; + use cosmwasm_std::testing::mock_info; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", Some(100_000_000_000u128.into())); + let valid_decrease = test.coin(100); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!( + ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease).is_ok() + ); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_legacy_mixnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_legacy_mixnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // denom must match + let node_id = test.add_legacy_mixnode("owner4", Some(100_000_000_000u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = coin(123, "weird-denom"); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!(res, MixnetContractError::WrongDenom { .. })); + + // value must be non-zero + let node_id = test.add_legacy_mixnode("owner5", Some(100_000_000_000u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(0); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::ZeroCoinAmount); + + // new pledge must be bigger than minimum + let node_id = test.add_legacy_mixnode("owner6", Some(100_000_100u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(101); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::InvalidPledgeReduction { .. } + )); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", Some(100_000_000_000u128.into())); + let valid_decrease = test.coin(100); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!( + ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease).is_ok() + ); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_dummy_nymnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_dummy_nymnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // denom must match + let node_id = test.add_dummy_nymnode("owner4", Some(100_000_000_000u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = coin(123, "weird-denom"); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!(res, MixnetContractError::WrongDenom { .. })); + + // value must be non-zero + let node_id = test.add_dummy_nymnode("owner5", Some(100_000_000_000u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(0); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::ZeroCoinAmount); + + // new pledge must be bigger than minimum + let node_id = test.add_dummy_nymnode("owner6", Some(100_000_100u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(101); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::InvalidPledgeReduction { .. } + )); + + Ok(()) + } + } +} diff --git a/contracts/mixnet/src/compat/transactions.rs b/contracts/mixnet/src/compat/transactions.rs index dbedab1325..77b20da946 100644 --- a/contracts/mixnet/src/compat/transactions.rs +++ b/contracts/mixnet/src/compat/transactions.rs @@ -93,3 +93,571 @@ pub(crate) fn try_withdraw_operator_reward( Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod increasing_pledge { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[test.coin(100000)]); + let err = test.execute_fn(try_increase_pledge, sender).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", Some(100_000_000u128.into())); + let sender = mock_info("owner", &[test.coin(100_000)]); + test.assert_simple_execution(try_increase_pledge, sender); + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 100_100_000u128 + ); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + let sender = mock_info("owner", &[test.coin(100000)]); + let err = test.execute_fn(try_increase_pledge, sender).unwrap_err(); + + // it's illegal to increase pledge for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", Some(100_000_000u128.into())); + let sender = mock_info("owner", &[test.coin(100_000)]); + test.assert_simple_execution(try_increase_pledge, sender); + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 100_100_000u128 + ); + + Ok(()) + } + } + + #[cfg(test)] + mod decreasing_pledge { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + let err = try_decrease_pledge(test.deps_mut(), env, sender, decrease_by).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", Some(120_000_000u128.into())); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + try_decrease_pledge(test.deps_mut(), env, sender, decrease_by)?; + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 119_999_000u128 + ); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + let err = try_decrease_pledge(test.deps_mut(), env, sender, decrease_by).unwrap_err(); + + // it's illegal to decrease pledge for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", Some(120_000_000u128.into())); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + + try_decrease_pledge(test.deps_mut(), env, sender, decrease_by)?; + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 119_999_000u128 + ); + + Ok(()) + } + } + + #[cfg(test)] + mod updating_cost_params { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{Addr, Uint128}; + use mixnet_contract_common::{OperatingCostRange, ProfitMarginRange}; + use nym_contracts_common::Percent; + + fn new_dummy_params() -> NodeCostParams { + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(69).unwrap(), + interval_operating_cost: Coin { + denom: TEST_COIN_DENOM.to_string(), + amount: 123456789u128.into(), + }, + } + } + + #[test] + fn profit_margin_must_be_within_range() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let minimum = Percent::from_percentage_value(10)?; + let maximum = Percent::from_percentage_value(80)?; + let range = ProfitMarginRange::new(minimum, maximum); + test.update_profit_margin_range(range); + + // below lower + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::from_percentage_value(9)?; + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // zero + test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::zero(); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // exactly at lower + test.add_dummy_nymnode("owner3", None); + let sender = mock_info("owner3", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = minimum; + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + // above upper + test.add_dummy_nymnode("owner4", None); + let sender = mock_info("owner4", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::from_percentage_value(81)?; + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // hundred + test.add_dummy_nymnode("owner5", None); + let sender = mock_info("owner5", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::hundred(); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // exactly at upper + test.add_dummy_nymnode("owner6", None); + let sender = mock_info("owner6", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = maximum; + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn operating_cost_must_be_within_range() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let minimum = Uint128::new(1000); + let maximum = Uint128::new(100_000_000); + let range = OperatingCostRange::new(minimum, maximum); + test.update_operating_cost_range(range); + + // below lower + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(999); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // zero + test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(0); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // exactly at lower + test.add_dummy_nymnode("owner3", None); + let sender = mock_info("owner3", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(minimum.u128()); + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + // above upper + test.add_dummy_nymnode("owner4", None); + let sender = mock_info("owner4", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(100_000_001); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // max + test.add_dummy_nymnode("owner5", None); + let sender = mock_info("owner5", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(u128::MAX); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // exactly at upper + test.add_dummy_nymnode("owner6", None); + let sender = mock_info("owner6", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(100_000_000); + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let err = try_update_cost_params(test.deps_mut(), env, sender, new_dummy_params()) + .unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + + let update = new_dummy_params(); + try_update_cost_params(test.deps_mut(), env, sender, update.clone())?; + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.cost_params_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!(update, after.rewarding_details.cost_params); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let err = try_update_cost_params(test.deps_mut(), env, sender, new_dummy_params()) + .unwrap_err(); + + // it's illegal to update cost parameters for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + + let update = new_dummy_params(); + try_update_cost_params(test.deps_mut(), env, sender, update.clone())?; + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.cost_params_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!(update, after.rewarding_details.cost_params); + + Ok(()) + } + } + + #[cfg(test)] + mod withdrawing_operator_reward { + use super::*; + use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::{ExtractBankMsg, TestSetup}; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let err = try_withdraw_operator_reward(test.deps_mut(), sender).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + // no rewards + test.add_legacy_mixnode("owner1", None); + let sender = mock_info("owner1", &[]); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_none()); + + let node_id = test.add_legacy_mixnode("owner2", None); + let sender = mock_info("owner2", &[]); + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, + test_helpers::performance(100.), + ); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_some()); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + + let sender = mock_info("owner", &[]); + let err = try_withdraw_operator_reward(test.deps_mut(), sender).unwrap_err(); + + // no rewards for legacy gateways... + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + // no rewards + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_none()); + + let node_id = test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, + test_helpers::performance(100.), + ); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_some()); + + Ok(()) + } + } +} diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 5720c48455..da03caaad3 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -143,7 +143,7 @@ pub mod tests { use crate::gateways::queries; use crate::interval::pending_events; use crate::mixnet_contract_settings::storage::minimum_node_pledge; - use crate::nodes::helpers::get_node_details_by_identity; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; use crate::signing::storage as signing_storage; use crate::support::tests; use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; @@ -153,7 +153,7 @@ pub mod tests { use mixnet_contract_common::ExecuteMsg; #[test] - fn gateway_add() { + fn gateway_add() -> anyhow::Result<()> { let mut test = TestSetup::new(); // if we fail validation (by say not sending enough funds @@ -218,10 +218,22 @@ pub mod tests { assert!(result.is_ok()); // and the node has been added as a nym-node - let nym_node = get_node_details_by_identity(test.deps().storage, gateway.identity_key) - .unwrap() - .unwrap(); - assert_eq!(nym_node.bond_information.owner, info.sender) + let nym_node = + get_node_details_by_identity(test.deps().storage, gateway.identity_key.clone()) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender); + + let maybe_legacy = + storage::gateways().may_load(test.deps().storage, &gateway.identity_key)?; + assert!(maybe_legacy.is_none()); + + // make sure we got assigned the next id (note: we have already bonded a mixnode and a gateway before in this test) + let bond = + must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); + assert_eq!(3, bond.node_id); + + Ok(()) } #[test] diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 3897585856..f9024673f2 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -34,7 +34,7 @@ pub(crate) fn profit_margin_range( .map(|state| state.params.profit_margin)?) } -pub(crate) fn interval_oprating_cost_range( +pub(crate) fn interval_operating_cost_range( storage: &dyn Storage, ) -> Result { Ok(CONTRACT_STATE diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 7b8ed90d04..52fe1a45ed 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -257,12 +257,10 @@ pub fn try_migrate_to_nymnode( #[cfg(test)] pub mod tests { use super::*; - use crate::compat::transactions::{ - try_decrease_pledge, try_increase_pledge, try_update_cost_params, - }; + use crate::compat::transactions::try_increase_pledge; use crate::contract::execute; use crate::mixnet_contract_settings::storage::minimum_node_pledge; - use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_identity}; use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; use crate::signing::storage as signing_storage; use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; @@ -275,7 +273,7 @@ pub mod tests { use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, Percent}; #[test] - fn mixnode_add() { + fn mixnode_add() -> anyhow::Result<()> { let mut test = TestSetup::new(); let env = test.env(); @@ -357,15 +355,22 @@ pub mod tests { assert!(result.is_ok()); // and the node has been added as a nym-node - let nym_node = get_node_details_by_identity(test.deps().storage, mixnode.identity_key) - .unwrap() - .unwrap(); + let nym_node = + get_node_details_by_identity(test.deps().storage, mixnode.identity_key.clone()) + .unwrap() + .unwrap(); assert_eq!(nym_node.bond_information.owner, info.sender); + let maybe_legacy = + get_mixnode_details_by_identity(test.deps().storage, mixnode.identity_key)?; + assert!(maybe_legacy.is_none()); + // make sure we got assigned the next id (note: we have already bonded a mixnode and a gateway before in this test) let bond = must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); assert_eq!(3, bond.node_id); + + Ok(()) } #[test] @@ -524,7 +529,9 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -538,10 +545,10 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); - let sender = mock_info(owner, &[]); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); + let details = test.mixnode_by_id(node_id).unwrap(); let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details).unwrap(); let sender = mock_info(owner, &[test.coin(1000)]); let res = try_remove_mixnode(test.deps_mut(), env.clone(), sender); @@ -640,15 +647,20 @@ pub mod tests { let mut test = TestSetup::new(); let env = test.env(); let owner = "alice"; - let info = mock_info(owner, &[]); - test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let res = try_update_cost_params(test.deps_mut(), env.clone(), info, update.clone()); + let res = try_update_mixnode_cost_params( + test.deps_mut(), + env.clone(), + update.clone(), + details, + ); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -668,21 +680,16 @@ pub mod tests { interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; - // try updating a non-existing mixnode bond - let res = - try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: Addr::unchecked(owner) - }) - ); - - let mix_id = test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); // "normal" update succeeds - let res = - try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); + let res = try_update_mixnode_cost_params( + test.deps_mut(), + env.clone(), + update.clone(), + details.clone(), + ); assert!(res.is_ok()); // see if the event has been pushed onto the queue @@ -695,7 +702,7 @@ pub mod tests { assert_eq!(1, event.0); assert_eq!( PendingIntervalEventKind::ChangeMixCostParams { - mix_id, + mix_id: node_id, new_costs: update.clone(), }, event.1.kind @@ -705,18 +712,16 @@ pub mod tests { test_helpers::execute_all_pending_events(test.deps_mut(), env.clone()); // and see if the config has actually been updated - let mix = get_mixnode_details_by_id(test.deps().storage, mix_id) + let mix = get_mixnode_details_by_id(test.deps().storage, node_id) .unwrap() .unwrap(); assert_eq!(mix.rewarding_details.cost_params, update); // but we cannot perform any updates whilst the mixnode is already unbonding try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()).unwrap(); - let res = try_update_cost_params(test.deps_mut(), env, info, update); - assert_eq!( - res, - Err(MixnetContractError::NodeIsUnbonding { node_id: mix_id }) - ) + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_update_mixnode_cost_params(test.deps_mut(), env, update, details); + assert_eq!(res, Err(MixnetContractError::NodeIsUnbonding { node_id })) } #[test] @@ -796,15 +801,15 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_legacy_mixnode(owner, None); - let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env, sender); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let increase = test.coins(1000); + let res = try_increase_mixnode_pledge(test.deps_mut(), env, increase, details); assert!(matches!( res, @@ -814,38 +819,21 @@ pub mod tests { } #[test] - fn is_not_allowed_if_account_doesnt_own_mixnode() { + fn is_not_allowed_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); let env = test.env(); - let sender = mock_info("not-mix-owner", &[]); - - let res = try_increase_pledge(test.deps_mut(), env, sender); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: Addr::unchecked("not-mix-owner") - }) - ) - } - - #[test] - fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { - let mut test = TestSetup::new(); - let env = test.env(); - - // TODO: I dislike this cross-test access, but it provides us with exactly what we need - // perhaps it should be refactored a bit? - let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); - let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); - let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); let ids = setup_mix_combinations(&mut test, None); let mix_id_unbonding = ids[1].mix_id; - let res = try_increase_pledge( + let increase = test.coins(1000); + let details = test.mixnode_by_id(mix_id_unbonding).unwrap(); + + let res = try_increase_mixnode_pledge( test.deps_mut(), env.clone(), - mock_info(owner_unbonding.as_str(), &[]), + increase.clone(), + details, ); assert_eq!( res, @@ -853,32 +841,6 @@ pub mod tests { node_id: mix_id_unbonding }) ); - - // if the nodes are gone we treat them as tey never existed in the first place - // (regardless of if there's some leftover data) - let res = try_increase_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner_unbonded_leftover.as_str(), &[]), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded_leftover - }) - ); - - let res = try_increase_pledge( - test.deps_mut(), - env, - mock_info(owner_unbonded.as_str(), &[]), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded - }) - ) } #[test] @@ -887,14 +849,21 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender_empty = mock_info(owner, &[]); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty); + let sender_empty = Vec::new(); + let res = try_increase_mixnode_pledge( + test.deps_mut(), + env.clone(), + sender_empty, + details.clone(), + ); assert_eq!(res, Err(MixnetContractError::NoBondFound)); - let sender_zero = mock_info(owner, &[test.coin(0)]); - let res = try_increase_pledge(test.deps_mut(), env, sender_zero); + let sender_zero = test.coins(0); + let res = + try_increase_mixnode_pledge(test.deps_mut(), env, sender_zero, details.clone()); assert_eq!( res, Err(MixnetContractError::InsufficientPledge { @@ -911,11 +880,19 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_legacy_mixnode(owner, None); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let sender = test.coins(1000); + try_increase_mixnode_pledge( + test.deps_mut(), + env.clone(), + sender.clone(), + details.clone(), + ) + .unwrap(); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -925,39 +902,22 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); - let sender = mock_info(owner, &[]); - let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender); + let amount = test.coin(1000); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details.clone()) + .unwrap(); + + let sender = test.coins(10000); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { pending_event_id: 2 }) ); - - // artificial event - let owner = "mix-owner3"; - let mix_id = test.add_legacy_mixnode(owner, None); - let pending_change = PendingMixNodeChanges { - pledge_change: Some(1234), - cost_params_change: None, - }; - storage::PENDING_MIXNODE_CHANGES - .save(test.deps_mut().storage, mix_id, &pending_change) - .unwrap(); - - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env, sender); - assert_eq!( - res, - Err(MixnetContractError::PendingPledgeChange { - pending_event_id: 1234 - }) - ); } #[test] @@ -966,12 +926,13 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; let mix_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(mix_id).unwrap(); let events = test.pending_epoch_events(); assert!(events.is_empty()); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env, sender).unwrap(); + let sender = test.coins(1000); + try_increase_mixnode_pledge(test.deps_mut(), env, sender, details).unwrap(); let events = test.pending_epoch_events(); @@ -1012,15 +973,15 @@ pub mod tests { let owner = "mix-owner"; let decrease = test.coin(1000); - test.add_legacy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + let details = test.mixnode_by_id(node_id).unwrap(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert!(matches!( res, @@ -1030,44 +991,21 @@ pub mod tests { } #[test] - fn is_not_allowed_if_account_doesnt_own_mixnode() { - let mut test = TestSetup::new(); - let env = test.env(); - let sender = mock_info("not-mix-owner", &[]); - let decrease = test.coin(1000); - - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: Addr::unchecked("not-mix-owner") - }) - ) - } - - #[test] - fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { + fn is_not_allowed_if_mixnode_is_unbonding() { let mut test = TestSetup::new(); let env = test.env(); - // just to make sure that after decrease the value would still be above the minimum - let stake = Uint128::new(100_000_000_000); - let decrease = test.coin(1000); - - // TODO: I dislike this cross-test access, but it provides us with exactly what we need - // perhaps it should be refactored a bit? - let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); - let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); - let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); - - let ids = setup_mix_combinations(&mut test, Some(stake)); + let ids = setup_mix_combinations(&mut test, None); let mix_id_unbonding = ids[1].mix_id; - let res = try_decrease_pledge( + let decrease = test.coin(1000); + let details = test.mixnode_by_id(mix_id_unbonding).unwrap(); + + let res = try_decrease_mixnode_pledge( test.deps_mut(), env.clone(), - mock_info(owner_unbonding.as_str(), &[]), decrease.clone(), + details, ); assert_eq!( res, @@ -1075,34 +1013,6 @@ pub mod tests { node_id: mix_id_unbonding }) ); - - // if the nodes are gone we treat them as tey never existed in the first place - // (regardless of if there's some leftover data) - let res = try_decrease_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner_unbonded_leftover.as_str(), &[]), - decrease.clone(), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded_leftover - }) - ); - - let res = try_decrease_pledge( - test.deps_mut(), - env, - mock_info(owner_unbonded.as_str(), &[]), - decrease, - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded - }) - ) } #[test] @@ -1114,17 +1024,17 @@ pub mod tests { let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let pledge_amount = minimum_pledge.amount + Uint128::new(100); let pledged = test.coin(pledge_amount.u128()); - test.add_legacy_mixnode(owner, Some(pledge_amount)); + let node_id = test.add_legacy_mixnode(owner, Some(pledge_amount)); + let details = test.mixnode_by_id(node_id).unwrap(); let invalid_decrease = test.coin(150); let valid_decrease = test.coin(50); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge( + let res = try_decrease_mixnode_pledge( test.deps_mut(), env.clone(), - sender.clone(), invalid_decrease.clone(), + details.clone(), ); assert_eq!( res, @@ -1136,7 +1046,7 @@ pub mod tests { }) ); - let res = try_decrease_pledge(test.deps_mut(), env, sender, valid_decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, valid_decrease, details); assert!(res.is_ok()) } @@ -1149,10 +1059,10 @@ pub mod tests { let decrease = test.coin(0); let owner = "mix-owner"; - test.add_legacy_mixnode(owner, Some(stake)); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert_eq!(res, Err(MixnetContractError::ZeroCoinAmount)) } @@ -1165,11 +1075,20 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_legacy_mixnode(owner, Some(stake)); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); - let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone()); + let sender = test.coins(1000); + try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender.clone(), details) + .unwrap(); + + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_decrease_mixnode_pledge( + test.deps_mut(), + env.clone(), + decrease.clone(), + details, + ); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1179,13 +1098,18 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_legacy_mixnode(owner, Some(stake)); - let sender = mock_info(owner, &[]); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details).unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone()); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_decrease_mixnode_pledge( + test.deps_mut(), + env.clone(), + decrease.clone(), + details, + ); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1204,8 +1128,8 @@ pub mod tests { .save(test.deps_mut().storage, mix_id, &pending_change) .unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let details = test.mixnode_by_id(mix_id).unwrap(); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1225,12 +1149,13 @@ pub mod tests { let owner = "mix-owner"; let mix_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(mix_id).unwrap(); let events = test.pending_epoch_events(); assert!(events.is_empty()); let sender = mock_info(owner, &[]); - try_decrease_pledge(test.deps_mut(), env, sender, decrease.clone()).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env, decrease.clone(), details).unwrap(); let events = test.pending_epoch_events(); diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index c5cc5c64d4..21ed0e7b33 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -13,7 +13,7 @@ use crate::rewards::helpers::update_and_save_last_rewarded; use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ ensure_any_node_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, - AttachSendTokens, + AttachSendTokens, }; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; @@ -149,14 +149,20 @@ pub(crate) fn try_withdraw_nym_node_operator_reward( deps: DepsMut<'_>, node_details: NymNodeDetails, ) -> Result { - ensure_can_withdraw_rewards(&node_details)?; - let node_id = node_details.node_id(); let owner = node_details.bond_information.owner.clone(); - let reward = helpers::withdraw_operator_reward(deps.storage, node_details)?; + ensure_can_withdraw_rewards(&node_details)?; - Ok(Response::new().add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) + let reward = helpers::withdraw_operator_reward(deps.storage, node_details)?; + let mut response = Response::new(); + + // if the reward is zero, don't track or send anything - there's no point + if !reward.amount.is_zero() { + response = response.send_tokens(&owner, reward.clone()) + } + + Ok(response.add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) } pub(crate) fn try_withdraw_mixnode_operator_reward( diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 9d8e158e76..8a19de6fa2 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -301,7 +301,7 @@ pub fn ensure_operating_cost_within_range( storage: &dyn Storage, operating_cost: &Coin, ) -> Result<(), MixnetContractError> { - let range = mixnet_params_storage::interval_oprating_cost_range(storage)?; + let range = mixnet_params_storage::interval_operating_cost_range(storage)?; if !range.within_range(operating_cost.amount) { return Err(MixnetContractError::OperatingCostOutsideRange { denom: operating_cost.denom.clone(), diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index de41b21d43..328c6ef56d 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -16,14 +16,18 @@ pub mod test_helpers { perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, }; use crate::interval::{pending_events, storage as interval_storage}; + use crate::mixnet_contract_settings::queries::query_contract_settings_params; use crate::mixnet_contract_settings::storage::{ self as mixnet_params_storage, minimum_node_pledge, }; use crate::mixnet_contract_settings::storage::{rewarding_denom, rewarding_validator_address}; + use crate::mixnodes::helpers::get_mixnode_details_by_id; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; use crate::mixnodes::transactions::try_remove_mixnode; - use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::nodes::helpers::{ + get_node_details_by_id, get_node_details_by_identity, must_get_node_bond_by_owner, + }; use crate::nodes::storage as nymnodes_storage; use crate::nodes::storage::helpers::RoleStorageBucket; use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; @@ -68,10 +72,11 @@ pub mod test_helpers { use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ - Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, Gateway, - GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg, - Interval, MixNode, MixNodeBond, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, - NymNodeBondingPayload, Percent, RoleAssignment, SignableGatewayBondingMsg, + ContractStateParams, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, + Gateway, GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, + InstantiateMsg, Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, + NodeId, NymNode, NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, + Percent, ProfitMarginRange, RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ @@ -87,6 +92,23 @@ pub mod test_helpers { use std::str::FromStr; use std::time::Duration; + pub(crate) trait ExtractBankMsg { + fn unwrap_bank_msg(self) -> Option; + } + + impl ExtractBankMsg for Response { + fn unwrap_bank_msg(self) -> Option { + for msg in self.messages { + match msg.msg { + CosmosMsg::Bank(bank_msg) => return Some(bank_msg), + _ => continue, + } + } + + None + } + } + pub enum NodeQueryType { ById(NodeId), ByIdentity(IdentityKey), @@ -168,7 +190,6 @@ pub mod test_helpers { self.env.clone() } - #[allow(unused)] pub fn execute( &mut self, info: MessageInfo, @@ -187,7 +208,6 @@ pub mod test_helpers { self.execute(self.mock_info(sender), msg) } - #[allow(unused)] pub fn execute_fn( &mut self, exec_fn: F, @@ -213,7 +233,6 @@ pub mod test_helpers { self.execute_fn(exec_fn, info) } - #[allow(unused)] #[track_caller] pub fn assert_simple_execution(&mut self, exec_fn: F, info: MessageInfo) -> Response where @@ -239,6 +258,36 @@ pub mod test_helpers { .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) } + pub fn update_profit_margin_range(&mut self, range: ProfitMarginRange) { + let current = query_contract_settings_params(self.deps()).unwrap(); + + self.execute( + self.owner(), + ExecuteMsg::UpdateContractStateParams { + updated_parameters: ContractStateParams { + profit_margin: range, + ..current + }, + }, + ) + .unwrap(); + } + + pub fn update_operating_cost_range(&mut self, range: OperatingCostRange) { + let current = query_contract_settings_params(self.deps()).unwrap(); + + self.execute( + self.owner(), + ExecuteMsg::UpdateContractStateParams { + updated_parameters: ContractStateParams { + interval_operating_cost: range, + ..current + }, + }, + ) + .unwrap(); + } + pub fn get_node_id(&self, query_type: impl Into) -> NodeId { match query_type.into() { NodeQueryType::ById(id) => id, @@ -452,6 +501,7 @@ pub mod test_helpers { (bond.node_id, keypair) } + #[track_caller] pub fn add_legacy_mixnode(&mut self, owner: &str, stake: Option) -> NodeId { let stake = self.make_mix_pledge(stake); let (mixnode, _, _) = self.mixnode_with_signature(owner, Some(stake.clone())); @@ -547,6 +597,14 @@ pub mod test_helpers { self.make_node_pledge(stake) } + pub fn mixnode_by_id(&self, node_id: NodeId) -> Option { + get_mixnode_details_by_id(self.deps().storage, node_id).unwrap() + } + + pub fn nymnode_by_id(&self, node_id: NodeId) -> Option { + get_node_details_by_id(self.deps().storage, node_id).unwrap() + } + pub fn mixnode_bonding_signature( &mut self, key: &identity::PrivateKey, @@ -657,6 +715,7 @@ pub mod test_helpers { (gateway, owner_signature) } + #[track_caller] pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) { let bond_details = mixnodes_storage::mixnode_bonds() .load(self.deps().storage, mix_id) @@ -671,6 +730,7 @@ pub mod test_helpers { .unwrap(); } + #[track_caller] pub fn start_unbonding_nymnode(&mut self, node_id: NodeId) { let bond_details = nymnodes_storage::nym_nodes() .load(self.deps().storage, node_id) From 49fd0dc9e1cb34937adec0a113118dca73c20658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 23 Sep 2024 17:11:28 +0100 Subject: [PATCH 045/207] wip: additional nym-nodes routes --- .../src/node_status_api/handlers/unstable.rs | 23 ++++++ .../nym_nodes/handlers/unstable/full_fat.rs | 2 + .../handlers/{unstable.rs => unstable/mod.rs} | 18 +++++ .../handlers/unstable/semi_skimmed.rs | 2 + .../nym_nodes/handlers/unstable/skimmed.rs | 71 +++++++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 nym-api/src/nym_nodes/handlers/unstable/full_fat.rs rename nym-api/src/nym_nodes/handlers/{unstable.rs => unstable/mod.rs} (96%) create mode 100644 nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs create mode 100644 nym-api/src/nym_nodes/handlers/unstable/skimmed.rs diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index 7e30a4ca31..2b72f72908 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -1,6 +1,7 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; @@ -162,6 +163,17 @@ async fn _mixnode_test_results( }) } +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + MixIdParam, PaginationRequest + ), + path = "/v1/status/mixnodes/unstable/{mix_id}/test-results", + responses( + (status = 200, body = MixnodeTestResultResponse) + ) +)] pub async fn mixnode_test_results( Path(mix_id): Path, Query(pagination): Query, @@ -250,6 +262,17 @@ async fn _gateway_test_results( }) } +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + PaginationRequest + ), + path = "/v1/status/gateways/unstable/{identity}/test-results", + responses( + (status = 200, body = GatewayTestResultResponse) + ) +)] pub async fn gateway_test_results( Path(gateway_identity): Path, Query(pagination): Query, diff --git a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs new file mode 100644 index 0000000000..939f19b3a9 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/nym_nodes/handlers/unstable.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs similarity index 96% rename from nym-api/src/nym_nodes/handlers/unstable.rs rename to nym-api/src/nym_nodes/handlers/unstable/mod.rs index 2fab1eef94..fc83cee6c9 100644 --- a/nym-api/src/nym_nodes/handlers/unstable.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -32,6 +32,10 @@ use nym_bin_common::version_checker; use serde::Deserialize; use std::collections::HashSet; +mod full_fat; +mod semi_skimmed; +mod skimmed; + pub(crate) fn nym_node_routes_unstable() -> axum::Router { Router::new() .route("/skimmed", axum::routing::get(nodes_basic)) @@ -73,6 +77,10 @@ impl SemverCompatibilityQueryParam { } } +// async fn nodes_noise() -> AxumResult { +// todo!() +// } + #[utoipa::path( tag = "Unstable Nym Nodes", get, @@ -109,6 +117,14 @@ async fn nodes_basic( } } + // TODO: + /* + - `/v1/unstable/nym-nodes/skimmed` - now works with `exit` parameter + - `/v1/unstable/nym-nodes/skimmed` - introduced `no-legacy` flag to ignore legacy mixnodes/gateways (where applicable) + - `/v1/unstable/nym-nodes/skimmed` - will now return **ALL** nodes if no query parameter is provided + + */ + Err(AxumErrorResponse::not_implemented()) } @@ -199,6 +215,7 @@ async fn nodes_detailed( (status = 200, body = CachedNodesResponse) ) )] +#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")] async fn gateways_basic( state: State, Query(SemverCompatibilityQueryParam { @@ -355,6 +372,7 @@ async fn gateways_detailed( (status = 200, body = CachedNodesResponse) ) )] +#[deprecated(note = "use '/v1/unstable/nym-nodes/mixnodes/skimmed/active' instead")] async fn mixnodes_basic( state: State, Query(SemverCompatibilityQueryParam { diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs new file mode 100644 index 0000000000..939f19b3a9 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs new file mode 100644 index 0000000000..079f58bf05 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumResult; +use crate::nym_nodes::handlers::unstable::SemverCompatibilityQueryParam; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; + +// - `/v1/unstable/nym-nodes/skimmed/active` - returns all Nym Nodes **AND** legacy mixnodes **AND** legacy gateways that are currently in the active set, unless `no-legacy` parameter is used +async fn nodes_basic_active( + state: State, +) -> AxumResult>> { + todo!() +} + +// - `/v1/unstable/nym-nodes/skimmed/standby` - returns all Nym Nodes **AND** legacy mixnodes **AND** legacy gateways that are currently in the standby set, unless `no-legacy` parameter is used +async fn nodes_basic_standby( + state: State, +) -> AxumResult>> { + todo!() +} + +// - `/v1/unstable/nym-nodes/mixnodes/skimmed/all` - returns all Nym Nodes **AND** legacy mixnodes that are currently bonded and support mixing role, unless `no-legacy` parameter is used +async fn mixnodes_basic_all( + state: State, + Query(SemverCompatibilityQueryParam { + semver_compatibility, + }): Query, +) -> AxumResult>> { + todo!() +} + +// - `/v1/unstable/nym-nodes/mixnodes/skimmed/active` - returns all Nym Nodes **AND** legacy mixnodes that are currently in the active set, unless `no-legacy` parameter is used +async fn mixnodes_basic_active( + state: State, + Query(SemverCompatibilityQueryParam { + semver_compatibility, + }): Query, +) -> AxumResult>> { + todo!() +} + +// - `/v1/unstable/nym-nodes/entry-gateways/skimmed/active` - returns all Nym Nodes **AND** legacy gateways that are currently in the active set and are assigned the entry role, unless `no-legacy` parameter is used +async fn entry_gateways_basic_active( + state: State, +) -> AxumResult>> { + todo!() +} + +// - `/v1/unstable/nym-nodes/exit-gateways/skimmed/active` - returns all Nym Nodes **AND** legacy gateways that are currently in the active set and are assigned the exit role, unless `no-legacy` parameter is used +async fn exit_gateways_basic_active( + state: State, +) -> AxumResult>> { + todo!() +} + +// - `/v1/unstable/nym-nodes/entry-gateways/skimmed/all` - returns all Nym Nodes **AND** legacy gateways that are currently bonded and support entry gateway role, unless `no-legacy` parameter is used +async fn entry_gateways_basic_all( + state: State, +) -> AxumResult>> { + todo!() +} + +// - `/v1/unstable/nym-nodes/exit-gateways/skimmed/all` - returns all Nym Nodes **AND** legacy gateways that are currently bonded and support exit gateway role, unless `no-legacy` parameter is used +async fn exit_gateways_basic_all( + state: State, +) -> AxumResult>> { + todo!() +} From 279b00d83394caf7a37800fa8f8ec500ea9b19a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 23 Sep 2024 17:51:32 +0100 Subject: [PATCH 046/207] exposing node role in nym node annotation --- .../validator-client/src/nym_api/mod.rs | 12 +- .../contract_traits/mixnet_query_client.rs | 5 +- .../mixnet-contract/src/types.rs | 56 -------- nym-api/nym-api-requests/src/models.rs | 2 + nym-api/src/epoch_operations/rewarding.rs | 1 + .../src/network_monitor/monitor/preparer.rs | 6 +- .../src/node_status_api/cache/node_sets.rs | 11 +- .../src/node_status_api/cache/refresher.rs | 1 + nym-api/src/nym_contract_cache/cache/data.rs | 123 +++++++++++++++++- nym-api/src/nym_contract_cache/cache/mod.rs | 6 +- nym-wallet/src-tauri/src/main.rs | 4 + .../src-tauri/src/operations/mixnet/bond.rs | 38 +++++- .../src/operations/nym_api/status.rs | 24 +++- 13 files changed, 211 insertions(+), 78 deletions(-) diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 7723ab89a8..2ecadd7641 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -11,7 +11,9 @@ use nym_api_requests::ecash::models::{ }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::legacy::LegacyGatewayBondWithId; -use nym_api_requests::models::{LegacyDescribedMixNode, NodePerformanceResponse}; +use nym_api_requests::models::{ + AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, +}; pub use nym_api_requests::{ ecash::{ models::{ @@ -435,6 +437,14 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_node_annotation( + &self, + node_id: NodeId, + ) -> Result { + self.get_json_from(format!("/v1/nym-nodes/annotation/{node_id}")) + .await + } + async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index a217f91663..ba90467f32 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -7,14 +7,13 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; -use nym_api_requests::models::StakeSaturationResponse; use nym_contracts_common::signing::Nonce; use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; use nym_mixnet_contract_common::nym_node::{ EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, PagedNymNodeDetailsResponse, - PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, UnbondedNodeResponse, - UnbondedNymNode, + PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, StakeSaturationResponse, + UnbondedNodeResponse, UnbondedNymNode, }; use nym_mixnet_contract_common::reward_params::WorkFactor; use nym_mixnet_contract_common::{ diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index f695bb22f7..ad71782c44 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -64,62 +64,6 @@ impl RewardedSet { pub fn rewarded_set_size(&self) -> usize { self.active_set_size() + self.standby.len() } - - pub fn try_get_mix_layer(&self, node_id: &NodeId) -> Option { - if self.layer1.contains(node_id) { - Some(1) - } else if self.layer2.contains(node_id) { - Some(2) - } else if self.layer3.contains(node_id) { - Some(3) - } else { - None - } - } - - pub fn is_entry(&self, node_id: &NodeId) -> bool { - self.entry_gateways.contains(node_id) - } - - pub fn is_exit(&self, node_id: &NodeId) -> bool { - self.exit_gateways.contains(node_id) - } - - pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { - self.layer1.contains(node_id) - || self.layer2.contains(node_id) - || self.layer3.contains(node_id) - } - - pub fn gateways(&self) -> Vec { - let mut gateways = Vec::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); - for entry in &self.entry_gateways { - gateways.push(*entry) - } - for exit in &self.exit_gateways { - gateways.push(*exit) - } - gateways - } - - pub fn active_mixnodes(&self) -> Vec { - let mut mixnodes = - Vec::with_capacity(self.layer1.len() + self.layer2.len() + self.layer3.len()); - for mix in &self.layer1 { - mixnodes.push(*mix) - } - for mix in &self.layer2 { - mixnodes.push(*mix) - } - for mix in &self.layer3 { - mixnodes.push(*mix) - } - mixnodes - } - - pub fn is_standby(&self, node_id: &NodeId) -> bool { - self.standby.contains(node_id) - } } #[cw_serde] diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 072a2aa549..196b351eeb 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -8,6 +8,7 @@ use crate::legacy::{ use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent}; @@ -125,6 +126,7 @@ pub struct NodePerformance { #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct NodeAnnotation { pub last_24h_performance: Performance, + pub current_role: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] diff --git a/nym-api/src/epoch_operations/rewarding.rs b/nym-api/src/epoch_operations/rewarding.rs index 624ec520b4..af4d29db4a 100644 --- a/nym-api/src/epoch_operations/rewarding.rs +++ b/nym-api/src/epoch_operations/rewarding.rs @@ -84,6 +84,7 @@ impl EpochAdvancer { .rewarded_set_owned() .await .into_inner() + .into() } }; diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 0f139b05d0..3567563e63 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -4,7 +4,7 @@ use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; -use crate::nym_contract_cache::cache::NymContractCache; +use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; use crate::support::caching::cache::SharedCache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; use nym_api_requests::models::NymNodeDescription; @@ -275,7 +275,7 @@ impl PacketPreparer { &self, rng: &mut R, blacklist: &mut HashSet, - rewarded_set: &RewardedSet, + rewarded_set: &CachedRewardedSet, legacy_mixnodes: Vec, mixing_nym_nodes: impl Iterator + 'a, ) -> HashMap> { @@ -477,7 +477,7 @@ impl PacketPreparer { fn nym_node_to_legacy_mix( &self, rng: &mut R, - rewarded_set: &RewardedSet, + rewarded_set: &CachedRewardedSet, mixing_nym_node: &NymNodeDescription, ) -> Option { let maybe_explicit_layer = rewarded_set diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index b60f4d34a6..d532308c68 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -3,12 +3,13 @@ use crate::node_status_api::helpers::RewardedSetStatus; use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; +use crate::nym_contract_cache::cache::CachedRewardedSet; use crate::support::storage::NymApiStorage; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::{ GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, NodePerformance, }; -use nym_mixnet_contract_common::{reward_params::Performance, Interval, NodeId, RewardedSet}; +use nym_mixnet_contract_common::{reward_params::Performance, Interval, NodeId}; use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; use nym_topology::NetworkAddress; use std::collections::{HashMap, HashSet}; @@ -46,7 +47,7 @@ pub(super) async fn get_gateway_performance_from_storage( } // TODO: this might have to be moved to a different file if other places also rely on this functionality -fn get_rewarded_set_status(rewarded_set: &RewardedSet, node_id: NodeId) -> RewardedSetStatus { +fn get_rewarded_set_status(rewarded_set: &CachedRewardedSet, node_id: NodeId) -> RewardedSetStatus { if rewarded_set.is_standby(&node_id) { RewardedSetStatus::Standby } else if rewarded_set.is_active_mixnode(&node_id) { @@ -61,7 +62,7 @@ pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( mixnodes: Vec, interval_reward_params: RewardingParams, current_interval: Interval, - rewarded_set: &RewardedSet, + rewarded_set: &CachedRewardedSet, blacklist: &HashSet, ) -> HashMap { let mut annotated = HashMap::new(); @@ -188,6 +189,7 @@ pub(crate) async fn produce_node_annotations( legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], + rewarded_set: &CachedRewardedSet, current_interval: Interval, ) -> HashMap { let mut annotations = HashMap::new(); @@ -207,6 +209,7 @@ pub(crate) async fn produce_node_annotations( legacy_mix.mix_id(), NodeAnnotation { last_24h_performance: perf, + current_role: rewarded_set.role(legacy_mix.mix_id()), }, ); } @@ -226,6 +229,7 @@ pub(crate) async fn produce_node_annotations( legacy_gateway.node_id, NodeAnnotation { last_24h_performance: perf, + current_role: rewarded_set.role(legacy_gateway.node_id), }, ); } @@ -245,6 +249,7 @@ pub(crate) async fn produce_node_annotations( nym_node.node_id(), NodeAnnotation { last_24h_performance: perf, + current_role: rewarded_set.role(nym_node.node_id()), }, ); } diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index aa664885a9..900051f911 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -149,6 +149,7 @@ impl NodeStatusCacheRefresher { &mixnode_details, &gateway_bonds, &nym_nodes, + &rewarded_set, current_interval, ) .await; diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index 2ced76b8fe..622286be24 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -4,15 +4,136 @@ use crate::support::caching::Cache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_contracts_common::ContractBuildInformation; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams}; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; +#[derive(Default, Clone)] +pub(crate) struct CachedRewardedSet { + pub(crate) entry_gateways: HashSet, + + pub(crate) exit_gateways: HashSet, + + pub(crate) layer1: HashSet, + + pub(crate) layer2: HashSet, + + pub(crate) layer3: HashSet, + + pub(crate) standby: HashSet, +} + +impl From for CachedRewardedSet { + fn from(value: RewardedSet) -> Self { + CachedRewardedSet { + entry_gateways: value.entry_gateways.into_iter().collect(), + exit_gateways: value.exit_gateways.into_iter().collect(), + layer1: value.layer1.into_iter().collect(), + layer2: value.layer2.into_iter().collect(), + layer3: value.layer3.into_iter().collect(), + standby: value.standby.into_iter().collect(), + } + } +} + +impl From for RewardedSet { + fn from(value: CachedRewardedSet) -> Self { + RewardedSet { + entry_gateways: value.entry_gateways.into_iter().collect(), + exit_gateways: value.exit_gateways.into_iter().collect(), + layer1: value.layer1.into_iter().collect(), + layer2: value.layer2.into_iter().collect(), + layer3: value.layer3.into_iter().collect(), + standby: value.standby.into_iter().collect(), + } + } +} + +impl CachedRewardedSet { + pub(crate) fn role(&self, node_id: NodeId) -> Option { + if self.entry_gateways.contains(&node_id) { + Some(Role::EntryGateway) + } else if self.exit_gateways.contains(&node_id) { + Some(Role::ExitGateway) + } else if self.layer1.contains(&node_id) { + Some(Role::Layer1) + } else if self.layer2.contains(&node_id) { + Some(Role::Layer2) + } else if self.layer3.contains(&node_id) { + Some(Role::Layer3) + } else if self.standby.contains(&node_id) { + Some(Role::Standby) + } else { + None + } + } + + pub fn active_set_size(&self) -> usize { + self.entry_gateways.len() + + self.exit_gateways.len() + + self.layer1.len() + + self.layer2.len() + + self.layer3.len() + } + + pub fn rewarded_set_size(&self) -> usize { + self.active_set_size() + self.standby.len() + } + + pub fn try_get_mix_layer(&self, node_id: &NodeId) -> Option { + if self.layer1.contains(node_id) { + Some(1) + } else if self.layer2.contains(node_id) { + Some(2) + } else if self.layer3.contains(node_id) { + Some(3) + } else { + None + } + } + + pub fn is_standby(&self, node_id: &NodeId) -> bool { + self.standby.contains(node_id) + } + + pub fn is_entry(&self, node_id: &NodeId) -> bool { + self.entry_gateways.contains(node_id) + } + + pub fn is_exit(&self, node_id: &NodeId) -> bool { + self.exit_gateways.contains(node_id) + } + + pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { + self.layer1.contains(node_id) + || self.layer2.contains(node_id) + || self.layer3.contains(node_id) + } + + pub(crate) fn gateways(&self) -> HashSet { + let mut gateways = + HashSet::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); + gateways.extend(&self.entry_gateways); + gateways.extend(&self.exit_gateways); + gateways + } + + pub(crate) fn active_mixnodes(&self) -> HashSet { + let mut mixnodes = + HashSet::with_capacity(self.layer1.len() + self.layer2.len() + self.layer3.len()); + mixnodes.extend(&self.layer1); + mixnodes.extend(&self.layer2); + mixnodes.extend(&self.layer3); + mixnodes + } +} + pub(crate) struct ValidatorCacheData { pub(crate) legacy_mixnodes: Cache>, pub(crate) legacy_gateways: Cache>, pub(crate) nym_nodes: Cache>, - pub(crate) rewarded_set: Cache, + pub(crate) rewarded_set: Cache, // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. // instead clients are meant to be filtering out them themselves based on the provided scores. diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index 8f7e26caa5..1f40768a9e 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -24,6 +24,8 @@ use tracing::{debug, error}; mod data; pub(crate) mod refresher; +pub(crate) use self::data::CachedRewardedSet; + const CACHE_TIMEOUT_MS: u64 = 100; #[derive(Clone)] @@ -259,11 +261,11 @@ impl NymContractCache { todo!() } - pub async fn rewarded_set(&self) -> Option>> { + pub async fn rewarded_set(&self) -> Option>> { self.get(|cache| &cache.rewarded_set).await } - pub async fn rewarded_set_owned(&self) -> Cache { + pub async fn rewarded_set_owned(&self) -> Cache { self.get_owned(|cache| cache.rewarded_set.clone_cache()) .await .unwrap_or_default() diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index a570bfb00b..acaaa6d995 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -81,6 +81,8 @@ fn main() { mixnet::bond::migrate_legacy_gateway, mixnet::bond::update_nymnode_config, mixnet::bond::get_nymnode_performance, + mixnet::bond::get_nymnode_uptime, + mixnet::bond::get_nymnode_stake_saturation, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, @@ -123,6 +125,8 @@ fn main() { nym_api::status::mixnode_stake_saturation, nym_api::status::mixnode_status, nym_api::status::gateway_report, + nym_api::status::get_nymnode_role, + nym_api::status::get_nymnode_annotation, vesting::rewards::vesting_claim_delegator_reward, vesting::rewards::vesting_claim_operator_reward, vesting::bond::vesting_bond_gateway, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index ba3d03b6ba..28075562ec 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -11,7 +11,7 @@ use crate::{nyxd_client, Gateway, MixNode}; use log::info; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_mixnet_contract_common::nym_node::{NodeConfigUpdate, StakeSaturationResponse}; use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId, NymNode}; use nym_node_requests::api::client::NymNodeApiClientExt; use nym_node_requests::api::v1::node::models::NodeDescription; @@ -682,13 +682,9 @@ pub async fn update_nymnode_config( #[tauri::command] pub async fn get_nymnode_performance( + node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { - let Some(details) = nym_node_bond_details(state.clone()).await? else { - return Ok(None); - }; - let node_id = details.bond_information.node_id; - log::trace!(" >>> Get node performance: node_id = {node_id}"); let guard = state.read().await; let res = guard @@ -700,3 +696,33 @@ pub async fn get_nymnode_performance( Ok(res.performance) } + +#[tauri::command] +pub async fn get_nymnode_uptime( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Get legacy nymnode uptime"); + + let performance = get_nymnode_performance(node_id, state).await?; + + // convert value in range 0.0 - 1.0 into 0-100 + Ok(performance + .map(|p| (p * 100.).floor() as u8) + .unwrap_or_default()) +} + +#[tauri::command] +pub async fn get_nymnode_stake_saturation( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::trace!(" >>> Get node stake saturation: node_id = {node_id}"); + + let res = nyxd_client!(state) + .get_node_stake_saturation(node_id) + .await?; + log::trace!(" <<< {res:?}"); + + Ok(res) +} diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 916dac0cea..1fa8dc70c4 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -4,14 +4,15 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{ reward_params::Performance, Coin, IdentityKeyRef, NodeId, Percent, }; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{ - ComputeRewardEstParam, GatewayCoreStatusResponse, GatewayStatusReportResponse, - InclusionProbabilityResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + AnnotationResponse, ComputeRewardEstParam, GatewayCoreStatusResponse, + GatewayStatusReportResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse, + MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; #[tauri::command] @@ -104,3 +105,20 @@ pub async fn mixnode_inclusion_probability( .get_mixnode_inclusion_probability(mix_id) .await?) } + +#[tauri::command] +pub async fn get_nymnode_role( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + let annotation = get_nymnode_annotation(node_id, state).await?; + Ok(annotation.annotation.and_then(|n| n.current_role)) +} + +#[tauri::command] +pub async fn get_nymnode_annotation( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + Ok(api_client!(state).get_node_annotation(node_id).await?) +} From 8c979e3bac58b11544553309e5651bb14114f08d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 24 Sep 2024 15:23:23 +0100 Subject: [PATCH 047/207] scaffolding additional skimmed endpoints + strenghten key types --- .../crypto/src/asymmetric/encryption/mod.rs | 3 + .../asymmetric/encryption/serde_helpers.rs | 38 ++ .../src/asymmetric/identity/serde_helpers.rs | 2 +- common/topology/src/gateway.rs | 6 +- common/topology/src/mix.rs | 8 +- gateway/src/http/mod.rs | 6 +- mixnode/src/node/http/mod.rs | 6 +- nym-api/nym-api-requests/src/models.rs | 56 +- nym-api/nym-api-requests/src/nym_nodes.rs | 170 +++--- nym-api/nym-api-requests/src/pagination.rs | 4 +- .../rewarded_set_assignment.rs | 4 +- nym-api/src/node_describe_cache/mod.rs | 20 +- .../src/node_describe_cache/query_helpers.rs | 12 +- nym-api/src/nym_nodes/handlers/mod.rs | 24 +- .../nym_nodes/handlers/unstable/full_fat.rs | 25 + .../src/nym_nodes/handlers/unstable/mod.rs | 552 +++--------------- .../handlers/unstable/semi_skimmed.rs | 25 + .../nym_nodes/handlers/unstable/skimmed.rs | 473 ++++++++++++++- nym-node/nym-node-http-api/Cargo.toml | 4 +- nym-node/nym-node-requests/src/api/mod.rs | 40 +- .../src/api/v1/metrics/models.rs | 4 +- .../src/api/v1/node/models.rs | 25 +- nym-node/src/node/http/mod.rs | 8 +- 23 files changed, 862 insertions(+), 653 deletions(-) create mode 100644 common/crypto/src/asymmetric/encryption/serde_helpers.rs diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index 1ea4b9d95a..fb841541fc 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -12,6 +12,9 @@ use rand::{CryptoRng, RngCore}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "serde")] +pub mod serde_helpers; + /// Size of a X25519 private key pub const PRIVATE_KEY_SIZE: usize = 32; diff --git a/common/crypto/src/asymmetric/encryption/serde_helpers.rs b/common/crypto/src/asymmetric/encryption/serde_helpers.rs new file mode 100644 index 0000000000..374afbd40d --- /dev/null +++ b/common/crypto/src/asymmetric/encryption/serde_helpers.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::PublicKey; + +pub mod bs58_x25519_pubkey { + use super::*; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(key: &PublicKey, serializer: S) -> Result { + serializer.serialize_str(&key.to_base58_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + PublicKey::from_base58_string(s).map_err(serde::de::Error::custom) + } +} + +pub mod option_bs58_x25519_pubkey { + use super::*; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize( + key: &Option, + serializer: S, + ) -> Result { + key.map(|key| key.to_base58_string()).serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + let s = Option::::deserialize(deserializer)?; + s.map(|s| PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom)) + .transpose() + } +} diff --git a/common/crypto/src/asymmetric/identity/serde_helpers.rs b/common/crypto/src/asymmetric/identity/serde_helpers.rs index 927ca77ec7..9d200df27a 100644 --- a/common/crypto/src/asymmetric/identity/serde_helpers.rs +++ b/common/crypto/src/asymmetric/identity/serde_helpers.rs @@ -3,7 +3,7 @@ use super::PublicKey; -pub mod bs58_pubkey { +pub mod bs58_ed25519_pubkey { use super::*; use serde::{Deserialize, Deserializer, Serializer}; diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index 3596b7e38f..e6f4981560 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -153,7 +153,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { if value.ip_addresses.is_empty() { return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: value.ed25519_identity_pubkey.clone(), + gateway: value.ed25519_identity_pubkey.to_base58_string(), }); } @@ -173,8 +173,8 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { mix_host: SocketAddr::new(*ip, value.mix_port), clients_ws_port: entry_details.ws_port, clients_wss_port: entry_details.wss_port, - identity_key: value.ed25519_identity_pubkey.parse()?, - sphinx_key: value.x25519_sphinx_pubkey.parse()?, + identity_key: value.ed25519_identity_pubkey, + sphinx_key: value.x25519_sphinx_pubkey, version: NodeVersion::Unknown, }) } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 6a0ff57ec3..edfbe8740f 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -112,11 +112,11 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { fn try_from(value: &'a SkimmedNode) -> Result { if value.ip_addresses.is_empty() { return Err(MixnodeConversionError::NoIpAddressesProvided { - mixnode: value.ed25519_identity_pubkey.clone(), + mixnode: value.ed25519_identity_pubkey.to_base58_string(), }); } - let layer = match value.role { + let layer = match value.epoch_role { NodeRole::Mixnode { layer } => layer .try_into() .map_err(|_| MixnodeConversionError::InvalidLayer)?, @@ -133,8 +133,8 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { mix_id: value.node_id, host, mix_host: SocketAddr::new(*ip, value.mix_port), - identity_key: value.ed25519_identity_pubkey.parse()?, - sphinx_key: value.x25519_sphinx_pubkey.parse()?, + identity_key: value.ed25519_identity_pubkey, + sphinx_key: value.x25519_sphinx_pubkey, layer, version: NodeVersion::Unknown, }) diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index 72d0aa499d..a3a697d56f 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -40,9 +40,9 @@ fn load_host_details( ip_address: config.host.public_ips.clone(), hostname: config.host.hostname.clone(), keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: identity_keypair.public_key().to_base58_string(), - x25519_sphinx: sphinx_key.to_base58_string(), - x25519_noise: "".to_string(), + ed25519_identity: *identity_keypair.public_key(), + x25519_sphinx: *sphinx_key, + x25519_noise: None, }, }; diff --git a/mixnode/src/node/http/mod.rs b/mixnode/src/node/http/mod.rs index 3784aab759..658c2ce980 100644 --- a/mixnode/src/node/http/mod.rs +++ b/mixnode/src/node/http/mod.rs @@ -24,9 +24,9 @@ fn load_host_details( ip_address: config.host.public_ips.clone(), hostname: config.host.hostname.clone(), keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: identity_keypair.public_key().to_base58_string(), - x25519_sphinx: sphinx_key.to_base58_string(), - x25519_noise: "".to_string(), + ed25519_identity: *identity_keypair.public_key(), + x25519_sphinx: *sphinx_key, + x25519_noise: None, }, }; diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 196b351eeb..4367a71ff2 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -8,6 +8,8 @@ use crate::legacy::{ use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; +use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; +use nym_crypto::asymmetric::x25519::{self, serde_helpers::bs58_x25519_pubkey}; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; @@ -508,8 +510,13 @@ impl From for HostInf #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HostKeys { - pub ed25519: String, - pub x25519: String, + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub ed25519: ed25519::PublicKey, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + pub x25519: x25519::PublicKey, } impl From for HostKeys { @@ -633,9 +640,9 @@ impl NymNodeDescription { } } - pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { + pub fn try_to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { let keys = &self.description.host_information.keys; - let entry = if self.description.declared_role.can_operate_entry_gateway() { + let entry = if self.description.declared_role.entry { Some(self.entry_information()) } else { None @@ -650,7 +657,8 @@ impl NymNodeDescription { // we can't use the declared roles, we have to take whatever was provided in the contract. // why? say this node COULD operate as an exit, but it might be the case the contract decided // to assign it an ENTRY role only. we have to use that one instead. - role, + epoch_role: role, + supported_roles: self.description.declared_role.into(), entry, performance, } @@ -665,6 +673,31 @@ pub enum DescribedNodeType { NymNode, } +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct DeclaredRoles { + pub mixnode: bool, + pub entry: bool, + pub exit_nr: bool, + pub exit_ipr: bool, +} + +impl DeclaredRoles { + pub fn can_operate_exit_gateway(&self) -> bool { + self.exit_ipr && self.exit_nr + } +} + +impl From for DeclaredRoles { + fn from(value: NodeRoles) -> Self { + DeclaredRoles { + mixnode: value.mixnode_enabled, + entry: value.gateway_enabled, + exit_nr: value.gateway_enabled && value.network_requester_enabled, + exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled, + } + } +} + // this struct is getting quite bloated... #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NymNodeData { @@ -674,7 +707,7 @@ pub struct NymNodeData { pub host_information: HostInformation, #[serde(default)] - pub declared_role: NodeRoles, + pub declared_role: DeclaredRoles, #[serde(default)] pub auxiliary_details: AuxiliaryDetails, @@ -861,6 +894,17 @@ pub struct PartialTestResult { pub type MixnodeTestResultResponse = PaginatedResponse; pub type GatewayTestResultResponse = PaginatedResponse; +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NoiseDetails { + #[schemars(with = "String")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25119_pubkey: x25519::PublicKey, + + pub mixnet_port: u16, + + pub ip_addresses: Vec, +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 7499c3f83b..75eaffd3ca 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -2,8 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::{ - GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeData, OffsetDateTimeJsonSchemaWrapper, + DeclaredRoles, GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeData, + OffsetDateTimeJsonSchemaWrapper, }; +use crate::pagination::PaginatedResponse; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; +use nym_crypto::asymmetric::{ed25519, x25519}; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; @@ -32,7 +37,13 @@ impl CachedNodesResponse { } } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaginatedCachedNodesResponse { + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + pub nodes: PaginatedResponse, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "kebab-case")] pub enum NodeRoleQueryParam { ActiveMixnode, @@ -79,14 +90,27 @@ pub struct SkimmedNode { #[schema(value_type = u32)] pub node_id: NodeId, - pub ed25519_identity_pubkey: String, + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub ed25519_identity_pubkey: ed25519::PublicKey, + #[schema(value_type = Vec)] pub ip_addresses: Vec, // TODO: to be deprecated in favour of well-known hardcoded port for everyone pub mix_port: u16, - pub x25519_sphinx_pubkey: String, - pub role: NodeRole, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + pub x25519_sphinx_pubkey: x25519::PublicKey, + + #[serde(alias = "role")] + pub epoch_role: NodeRole, + + // needed for the purposes of sending appropriate test packets + #[serde(default)] + pub supported_roles: DeclaredRoles, + pub entry: Option, /// Average node performance in last 24h period @@ -96,74 +120,90 @@ pub struct SkimmedNode { impl SkimmedNode { pub fn get_mix_layer(&self) -> Option { - match self.role { + match self.epoch_role { NodeRole::Mixnode { layer } => Some(layer), _ => None, } } - pub fn from_described_gateway( - annotated: &GatewayBondAnnotated, - description: Option<&NymNodeData>, - ) -> Self { - let mut base: SkimmedNode = annotated.into(); - let Some(description) = description else { - return base; - }; - - // safety: the conversion always set the entry field - let entry = base.entry.as_mut().unwrap(); - entry - .hostname - .clone_from(&description.host_information.hostname); - entry.ws_port = description.mixnet_websockets.ws_port; - entry.wss_port = description.mixnet_websockets.wss_port; - - // always prefer self-described data - if !description.host_information.ip_address.is_empty() { - base.ip_addresses - .clone_from(&description.host_information.ip_address) - } - - base - } + // pub fn from_described_gateway( + // annotated: &GatewayBondAnnotated, + // description: Option<&NymNodeData>, + // ) -> Self { + // let mut base: SkimmedNode = annotated.into(); + // let Some(description) = description else { + // return base; + // }; + // + // // safety: the conversion always sets the entry field + // let entry = base.entry.as_mut().unwrap(); + // entry + // .hostname + // .clone_from(&description.host_information.hostname); + // entry.ws_port = description.mixnet_websockets.ws_port; + // entry.wss_port = description.mixnet_websockets.wss_port; + // + // // always prefer self-described data + // if !description.host_information.ip_address.is_empty() { + // base.ip_addresses + // .clone_from(&description.host_information.ip_address) + // } + // + // base.supported_roles = description.declared_role; + // + // base + // } } -impl<'a> From<&'a MixNodeBondAnnotated> for SkimmedNode { - fn from(value: &'a MixNodeBondAnnotated) -> Self { - SkimmedNode { - node_id: value.mix_id(), - ed25519_identity_pubkey: value.identity_key().to_string(), - ip_addresses: value.ip_addresses.clone(), - mix_port: value.mix_node().mix_port, - x25519_sphinx_pubkey: value.mix_node().sphinx_key.clone(), - role: NodeRole::Mixnode { - layer: value.mixnode_details.bond_information.layer.into(), - }, - entry: None, - performance: value.node_performance.last_24h, - } - } -} +// impl<'a> From<&'a MixNodeBondAnnotated> for SkimmedNode { +// fn from(value: &'a MixNodeBondAnnotated) -> Self { +// todo!() +// // SkimmedNode { +// // node_id: value.mix_id(), +// // ed25519_identity_pubkey: value.identity_key().to_string(), +// // ip_addresses: value.ip_addresses.clone(), +// // mix_port: value.mix_node().mix_port, +// // x25519_sphinx_pubkey: value.mix_node().sphinx_key.clone(), +// // epoch_role: NodeRole::Mixnode { +// // layer: value.mixnode_details.bond_information.layer.into(), +// // }, +// // supported_roles: DeclaredRoles { +// // mixnode: true, +// // entry: false, +// // exit_nr: false, +// // exit_ipr: false, +// // }, +// // entry: None, +// // performance: value.node_performance.last_24h, +// // } +// } +// } -impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode { - fn from(value: &'a GatewayBondAnnotated) -> Self { - SkimmedNode { - node_id: value.gateway_bond.node_id, - ip_addresses: value.ip_addresses.clone(), - ed25519_identity_pubkey: value.gateway_bond.bond.identity().clone(), - mix_port: value.gateway_bond.bond.gateway.mix_port, - x25519_sphinx_pubkey: value.gateway_bond.bond.gateway.sphinx_key.clone(), - role: NodeRole::EntryGateway, - entry: Some(BasicEntryInformation { - hostname: None, - ws_port: value.gateway_bond.bond.gateway.clients_port, - wss_port: None, - }), - performance: value.node_performance.last_24h, - } - } -} +// impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode { +// fn from(value: &'a GatewayBondAnnotated) -> Self { +// todo!() +// // SkimmedNode { +// // node_id: value.gateway_bond.node_id, +// // ip_addresses: value.ip_addresses.clone(), +// // ed25519_identity_pubkey: value.gateway_bond.bond.identity().clone(), +// // mix_port: value.gateway_bond.bond.gateway.mix_port, +// // x25519_sphinx_pubkey: value.gateway_bond.bond.gateway.sphinx_key.clone(), +// // epoch_role: NodeRole::EntryGateway, +// // supported_roles: DeclaredRoles { +// // mixnode: false, +// // entry: true, +// // exit_nr: false, +// // exit_ipr: false, +// // }, +// // entry: Some(BasicEntryInformation { +// // hostname: None, +// // ws_port: value.gateway_bond.bond.gateway.clients_port, +// // wss_port: None, +// // }), +// // performance: value.node_performance.last_24h, +// // } +// } +// } // an intermediate variant that exposes additional data such as noise keys but without // the full fat of the self-described data diff --git a/nym-api/nym-api-requests/src/pagination.rs b/nym-api/nym-api-requests/src/pagination.rs index c17d5a7bee..2d82c826ae 100644 --- a/nym-api/nym-api-requests/src/pagination.rs +++ b/nym-api/nym-api-requests/src/pagination.rs @@ -5,14 +5,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct Pagination { pub total: usize, pub page: u32, pub size: usize, } -#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct PaginatedResponse { pub pagination: Pagination, pub data: Vec, diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index de5e3b0f92..9ca5b5fb00 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -262,10 +262,10 @@ impl EpochAdvancer { }; let mut available_roles = Vec::new(); - if self_described.declared_role.can_operate_mixnode() { + if self_described.declared_role.mixnode { available_roles.push(AvailableRole::Mix) } - if self_described.declared_role.can_operate_entry_gateway() { + if self_described.declared_role.entry { available_roles.push(AvailableRole::EntryGateway) } if self_described.declared_role.can_operate_exit_gateway() { diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 2b9b8d0564..3e66fd132e 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -80,7 +80,7 @@ impl NodeDescriptionTopologyExt for NymNodeDescription { let ips = &self.description.host_information.ip_address; if ips.is_empty() { return Err(MixnodeConversionError::NoIpAddressesProvided { - mixnode: keys.ed25519.clone(), + mixnode: keys.ed25519.to_base58_string(), }); } @@ -97,8 +97,8 @@ impl NodeDescriptionTopologyExt for NymNodeDescription { mix_id: self.node_id, host, mix_host, - identity_key: ed25519::PublicKey::from_base58_string(&keys.ed25519)?, - sphinx_key: x25519::PublicKey::from_base58_string(&keys.x25519)?, + identity_key: keys.ed25519, + sphinx_key: keys.x25519, layer, version: self .description @@ -115,7 +115,7 @@ impl NodeDescriptionTopologyExt for NymNodeDescription { let ips = &self.description.host_information.ip_address; if ips.is_empty() { return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: keys.ed25519.clone(), + gateway: keys.ed25519.to_base58_string(), }); } @@ -134,12 +134,8 @@ impl NodeDescriptionTopologyExt for NymNodeDescription { mix_host, clients_ws_port: self.description.mixnet_websockets.ws_port, clients_wss_port: self.description.mixnet_websockets.wss_port, - identity_key: identity::PublicKey::from_base58_string( - &self.description.host_information.keys.ed25519, - )?, - sphinx_key: encryption::PublicKey::from_base58_string( - &self.description.host_information.keys.x25519, - )?, + identity_key: self.description.host_information.keys.ed25519, + sphinx_key: self.description.host_information.keys.x25519, version: self .description .build_information @@ -171,7 +167,7 @@ impl DescribedNodes { self.nodes .values() .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) - .filter(|n| n.description.declared_role.can_operate_mixnode()) + .filter(|n| n.description.declared_role.mixnode) } pub fn gateway_capable_nym_nodes<'a>( @@ -180,7 +176,7 @@ impl DescribedNodes { self.nodes .values() .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) - .filter(|n| n.description.declared_role.can_operate_entry_gateway()) + .filter(|n| n.description.declared_role.entry) } } diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index cdb9e04d4e..3e6c5027b9 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -5,8 +5,8 @@ use crate::node_describe_cache::NodeDescribeCacheError; use futures::future::{maybe_done, MaybeDone}; use futures::{FutureExt, TryFutureExt}; use nym_api_requests::models::{ - AuthenticatorDetails, HostInformation, IpPacketRouterDetails, NetworkRequesterDetails, - NymNodeData, WebSockets, WireguardDetails, + AuthenticatorDetails, DeclaredRoles, HostInformation, IpPacketRouterDetails, + NetworkRequesterDetails, NymNodeData, WebSockets, WireguardDetails, }; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_config::defaults::mainnet; @@ -46,7 +46,7 @@ pub(crate) async fn query_for_described_data( // all of those should be happening concurrently. NodeDescribedInfoMegaFuture::new( client.get_build_information().map_err(map_query_err), - client.get_roles().map_err(map_query_err), + client.get_roles().ok_into().map_err(map_query_err), client.get_auxiliary_details() .inspect_err(|err| { // old nym-nodes will not have this field, so use the default instead @@ -102,7 +102,7 @@ impl Future for NodeDescribedInfoMegaFuture where F1: Future>, - F2: Future>, + F2: Future>, F3: Future, F4: Future>, F5: Future, NodeDescribeCacheError>>, @@ -185,7 +185,7 @@ where struct ResolvedNodeDescribedInfo { build_info: Result, - roles: Result, + roles: Result, // TODO: in the future make it return a Result as well. auxiliary_details: AuxiliaryDetails, websockets: Result, @@ -212,7 +212,7 @@ impl ResolvedNodeDescribedInfo { pub(crate) struct UnwrappedResolvedNodeDescribedInfo { pub(crate) build_info: BinaryBuildInformationOwned, - pub(crate) roles: NodeRoles, + pub(crate) roles: DeclaredRoles, pub(crate) auxiliary_details: AuxiliaryDetails, pub(crate) websockets: WebSockets, pub(crate) network_requester: Option, diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 7655584625..46e4b38d73 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -8,8 +8,8 @@ use axum::extract::{Path, Query, State}; use axum::routing::get; use axum::{Json, Router}; use nym_api_requests::models::{ - AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NymNodeData, - PerformanceHistoryResponse, UptimeHistoryResponse, + AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NoiseDetails, + NymNodeData, PerformanceHistoryResponse, UptimeHistoryResponse, }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; @@ -25,6 +25,7 @@ pub(crate) mod unstable; pub(crate) fn nym_node_routes() -> Router { Router::new() + .route("/noise", get(nodes_noise)) .route("/bonded", get(get_bonded_nodes)) .route("/described", get(get_described_nodes)) .route("/annotation/:node_id", get(get_node_annotation)) @@ -41,6 +42,25 @@ pub(crate) fn nym_node_routes() -> Router { .route("/uptime-history/:node_id", get(get_node_uptime_history)) } +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/noise", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn nodes_noise( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // TODO: implement it + let _ = pagination; + todo!() +} + #[utoipa::path( tag = "Nym Nodes", get, diff --git a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs index 939f19b3a9..d16d68e240 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs @@ -1,2 +1,27 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use nym_api_requests::nym_nodes::{CachedNodesResponse, FullFatNode}; + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/", + context_path = "/v1/unstable/nym-nodes/full-fat", + responses( + // (status = 200, body = CachedNodesResponse) + (status = 501) + ) +)] +pub(super) async fn nodes_detailed( + _state: State, + _query_params: Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index fc83cee6c9..1102fa8874 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -20,509 +20,97 @@ //! - `/mixnodes/` => only returns mixnode role data //! - `/gateway/` => only returns (entry) gateway role data -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::support::http::state::AppState; -use axum::extract::Query; -use axum::extract::State; -use axum::{Json, Router}; -use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRole, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, +use crate::nym_nodes::handlers::unstable::full_fat::nodes_detailed; +use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded; +use crate::nym_nodes::handlers::unstable::skimmed::{ + deprecated_gateways_basic, deprecated_mixnodes_basic, entry_gateways_basic_active, + entry_gateways_basic_all, exit_gateways_basic_active, exit_gateways_basic_all, + mixnodes_basic_active, mixnodes_basic_all, nodes_basic, nodes_basic_active, + nodes_basic_standby, }; -use nym_bin_common::version_checker; +use crate::support::http::helpers::PaginationRequest; +use crate::support::http::state::AppState; +use axum::routing::get; +use axum::Router; +use nym_api_requests::nym_nodes::NodeRoleQueryParam; use serde::Deserialize; -use std::collections::HashSet; -mod full_fat; -mod semi_skimmed; -mod skimmed; +pub(crate) mod full_fat; +pub(crate) mod semi_skimmed; +pub(crate) mod skimmed; -pub(crate) fn nym_node_routes_unstable() -> axum::Router { +#[allow(deprecated)] +pub(crate) fn nym_node_routes_unstable() -> Router { Router::new() - .route("/skimmed", axum::routing::get(nodes_basic)) - .route("/semi-skimmed", axum::routing::get(nodes_expanded)) - .route("/full-fat", axum::routing::get(nodes_detailed)) .nest( - "/gateways", + "/skimmed", Router::new() - .route("/skimmed", axum::routing::get(gateways_basic)) - .route("/semi-skimmed", axum::routing::get(gateways_expanded)) - .route("/full-fat", axum::routing::get(gateways_detailed)), + .route("/", get(nodes_basic)) + .route("/active", get(nodes_basic_active)) + .route("/standby", get(nodes_basic_standby)) + .nest( + "/mixnodes", + Router::new() + .route("/active", get(mixnodes_basic_active)) + .route("/all", get(mixnodes_basic_all)), + ) + .nest( + "/entry-gateways", + Router::new() + .route("/active", get(entry_gateways_basic_active)) + .route("/all", get(entry_gateways_basic_all)), + ) + .nest( + "/exit-gateways", + Router::new() + .route("/active", get(exit_gateways_basic_active)) + .route("/all", get(exit_gateways_basic_all)), + ), ) .nest( - "/mixnodes", - Router::new() - .route("/skimmed", axum::routing::get(mixnodes_basic)) - .route("/semi-skimmed", axum::routing::get(mixnodes_expanded)) - .route("/full-fat", axum::routing::get(mixnodes_detailed)), + "/semi-skimmed", + Router::new().route("/", get(nodes_expanded)), ) + .nest("/full-fat", Router::new().route("/", get(nodes_detailed))) + .route("/gateways/skimmed", get(deprecated_gateways_basic)) + .route("/mixnodes/skimmed", get(deprecated_mixnodes_basic)) +} + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +struct NodesParamsWithRole { + #[param(inline)] + role: Option, + + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, } #[derive(Debug, Deserialize, utoipa::IntoParams)] struct NodesParams { - #[param(inline)] - role: Option, semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, } -#[derive(Debug, Deserialize, utoipa::IntoParams)] -struct SemverCompatibilityQueryParam { - semver_compatibility: Option, -} - -impl SemverCompatibilityQueryParam { - pub fn new(semver_compatibility: Option) -> Self { - Self { - semver_compatibility, +impl From for NodesParams { + fn from(params: NodesParamsWithRole) -> Self { + NodesParams { + semver_compatibility: params.semver_compatibility, + no_legacy: params.no_legacy, + page: params.page, + per_page: params.per_page, } } } -// async fn nodes_noise() -> AxumResult { -// todo!() -// } - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn nodes_basic( - state: State, - Query(NodesParams { - role, - semver_compatibility, - }): Query, -) -> AxumResult>> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_basic( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_basic( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await; - } - _ => {} +impl<'a> From<&'a NodesParams> for PaginationRequest { + fn from(params: &'a NodesParams) -> Self { + PaginationRequest { + page: params.page, + per_page: params.per_page, } } - - // TODO: - /* - - `/v1/unstable/nym-nodes/skimmed` - now works with `exit` parameter - - `/v1/unstable/nym-nodes/skimmed` - introduced `no-legacy` flag to ignore legacy mixnodes/gateways (where applicable) - - `/v1/unstable/nym-nodes/skimmed` - will now return **ALL** nodes if no query parameter is provided - - */ - - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/v1/unstable/nym-nodes/semi-skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn nodes_expanded( - state: State, - Query(NodesParams { - role, - semver_compatibility, - }): Query, -) -> AxumResult>> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_expanded( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_expanded( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - _ => {} - } - } - - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/v1/unstable/nym-nodes/full-fat", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn nodes_detailed( - state: State, - Query(NodesParams { - role, - semver_compatibility, - }): Query, -) -> AxumResult>> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_detailed( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_detailed( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - _ => {} - } - } - - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/gateways/skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")] -async fn gateways_basic( - state: State, - Query(SemverCompatibilityQueryParam { - semver_compatibility, - }): Query, -) -> AxumResult>> { - let status_cache = state.node_status_cache(); - let contract_cache = state.nym_contract_cache(); - let describe_cache = state.described_nodes_cache(); - - // 1. get the rewarded set - let rewarded_set = contract_cache - .rewarded_set() - .await - .ok_or_else(AxumErrorResponse::internal)?; - - // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic - let active_gateways = rewarded_set.gateways().into_iter().collect::>(); - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = status_cache - .node_annotations() - .await - .ok_or_else(AxumErrorResponse::internal)?; - - // 3. grab all legacy gateways - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_gateways = status_cache - .annotated_legacy_gateways() - .await - .ok_or_else(AxumErrorResponse::internal)?; - - // 4. grab all relevant described nym-nodes - let describe_cache = describe_cache.get().await?; - let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); - - // 5. only return nodes that are present in the active set - let mut active_skimmed_gateways = Vec::new(); - - for (node_id, legacy) in annotated_legacy_gateways.iter() { - if !active_gateways.contains(node_id) { - continue; - } - - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = legacy.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - active_skimmed_gateways.push( - described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), - ) - } else { - active_skimmed_gateways.push(legacy.into()); - } - } - - for nym_node in gateway_capable_nym_nodes { - // if this node is not an active gateway, ignore it - if !active_gateways.contains(&nym_node.node_id) { - continue; - } - - // if we have wrong version, ignore - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = nym_node.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit - let role = if rewarded_set.is_exit(&nym_node.node_id) { - NodeRole::ExitGateway - } else { - NodeRole::EntryGateway - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations - .get(&nym_node.node_id) - .copied() - .unwrap_or_default(); - - active_skimmed_gateways - .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // min of all caches - let refreshed_at = [ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_gateways.timestamp(), - ] - .into_iter() - .min() - .unwrap() - .into(); - - Ok(Json(CachedNodesResponse { - refreshed_at, - nodes: active_skimmed_gateways, - })) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/gateways/semi-skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn gateways_expanded( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/gateways/full-fat", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn gateways_detailed( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/mixnodes/skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -#[deprecated(note = "use '/v1/unstable/nym-nodes/mixnodes/skimmed/active' instead")] -async fn mixnodes_basic( - state: State, - Query(SemverCompatibilityQueryParam { - semver_compatibility, - }): Query, -) -> AxumResult>> { - let status_cache = state.node_status_cache(); - let contract_cache = state.nym_contract_cache(); - let describe_cache = state.described_nodes_cache(); - - // 1. get the rewarded set - let rewarded_set = contract_cache - .rewarded_set() - .await - .ok_or_else(AxumErrorResponse::internal)?; - - // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic - let active_mixnodes = rewarded_set - .active_mixnodes() - .into_iter() - .collect::>(); - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = status_cache - .node_annotations() - .await - .ok_or_else(AxumErrorResponse::internal)?; - - // 3. grab all legacy mixnodes - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_mixnodes = status_cache - .annotated_legacy_mixnodes() - .await - .ok_or_else(AxumErrorResponse::internal)?; - - // 4. grab all relevant described nym-nodes - let describe_cache = describe_cache.get().await?; - let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); - - // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it - - // 5. only return nodes that are present in the active set - let mut active_skimmed_mixnodes = Vec::new(); - - for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { - if !active_mixnodes.contains(mix_id) { - continue; - } - - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = legacy.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(mix_id) { - active_skimmed_mixnodes.push(described.to_skimmed_node( - NodeRole::Mixnode { - layer: legacy.mixnode_details.bond_information.layer.into(), - }, - legacy.node_performance.last_24h, - )) - } else { - active_skimmed_mixnodes.push(legacy.into()); - } - } - - for nym_node in mixing_nym_nodes { - // if this node is not an active mixnode, ignore it - if !active_mixnodes.contains(&nym_node.node_id) { - continue; - } - - // if we have wrong version, ignore - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = nym_node.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // SAFETY: if we determined our node IS active, it MUST have a layer - // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data - #[allow(clippy::unwrap_used)] - let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations - .get(&nym_node.node_id) - .copied() - .unwrap_or_default(); - - active_skimmed_mixnodes.push( - nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), - ); - } - - // min of all caches - let refreshed_at = [ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_mixnodes.timestamp(), - ] - .into_iter() - .min() - .unwrap() - .into(); - - Ok(Json(CachedNodesResponse { - refreshed_at, - nodes: active_skimmed_mixnodes, - })) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/mixnodes/semi-skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn mixnodes_expanded( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/mixnodes/full-fat", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn mixnodes_detailed( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) } diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs index 939f19b3a9..294ec222e5 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs @@ -1,2 +1,27 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/", + context_path = "/v1/unstable/nym-nodes/semi-skimmed", + responses( + // (status = 200, body = CachedNodesResponse) + (status = 501) + ) +)] +pub(super) async fn nodes_expanded( + _state: State, + _query_params: Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index 079f58bf05..e547b7fc63 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -1,71 +1,480 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::AxumResult; -use crate::nym_nodes::handlers::unstable::SemverCompatibilityQueryParam; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; +use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; use axum::extract::{Query, State}; use axum::Json; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; +use nym_api_requests::nym_nodes::{ + CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode, +}; +use nym_bin_common::version_checker; +use std::collections::HashSet; + +pub type PaginatedSkimmedNodes = AxumResult>>; + +/// Deprecated query that gets ALL gateways +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/gateways/skimmed", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")] +pub(super) async fn deprecated_gateways_basic( + state: State, + query_params: Query, +) -> AxumResult>> { + // TODO: + // 1. call '/v1/unstable/entry-gateways/mixnodes/skimmed/all' + // 2. remove pagination + // 3. return result + + todo!() + + // let status_cache = state.node_status_cache(); + // let contract_cache = state.nym_contract_cache(); + // let describe_cache = state.described_nodes_cache(); + // + // // 1. get the rewarded set + // let rewarded_set = contract_cache + // .rewarded_set() + // .await + // .ok_or_else(AxumErrorResponse::internal)?; + // + // // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic + // let active_gateways = rewarded_set.gateways().into_iter().collect::>(); + // + // // 2. grab all annotations so that we could attach scores to the [nym] nodes + // let annotations = status_cache + // .node_annotations() + // .await + // .ok_or_else(AxumErrorResponse::internal)?; + // + // // 3. grab all legacy gateways + // // due to legacy endpoints we already have fully annotated data on them + // let annotated_legacy_gateways = status_cache + // .annotated_legacy_gateways() + // .await + // .ok_or_else(AxumErrorResponse::internal)?; + // + // // 4. grab all relevant described nym-nodes + // let describe_cache = describe_cache.get().await?; + // let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); + // + // // 5. only return nodes that are present in the active set + // let mut active_skimmed_gateways = Vec::new(); + // + // for (node_id, legacy) in annotated_legacy_gateways.iter() { + // if !active_gateways.contains(node_id) { + // continue; + // } + // + // if let Some(semver_compat) = semver_compatibility.as_ref() { + // let version = legacy.version(); + // if !version_checker::is_minor_version_compatible(version, semver_compat) { + // continue; + // } + // } + // + // // if we have self-described info, prefer it over contract data + // if let Some(described) = describe_cache.get_node(node_id) { + // active_skimmed_gateways.push( + // described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), + // ) + // } else { + // active_skimmed_gateways.push(legacy.into()); + // } + // } + // + // for nym_node in gateway_capable_nym_nodes { + // // if this node is not an active gateway, ignore it + // if !active_gateways.contains(&nym_node.node_id) { + // continue; + // } + // + // // if we have wrong version, ignore + // if let Some(semver_compat) = semver_compatibility.as_ref() { + // let version = nym_node.version(); + // if !version_checker::is_minor_version_compatible(version, semver_compat) { + // continue; + // } + // } + // + // // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit + // let role = if rewarded_set.is_exit(&nym_node.node_id) { + // NodeRole::ExitGateway + // } else { + // NodeRole::EntryGateway + // }; + // + // // honestly, not sure under what exact circumstances this value could be missing, + // // but in that case just use 0 performance + // let annotation = annotations + // .get(&nym_node.node_id) + // .copied() + // .unwrap_or_default(); + // + // active_skimmed_gateways + // .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + // } + // + // // min of all caches + // let refreshed_at = [ + // rewarded_set.timestamp(), + // annotations.timestamp(), + // describe_cache.timestamp(), + // annotated_legacy_gateways.timestamp(), + // ] + // .into_iter() + // .min() + // .unwrap() + // .into(); + // + // Ok(Json(CachedNodesResponse { + // refreshed_at, + // nodes: active_skimmed_gateways, + // })) +} + +/// Deprecated query that gets ACTIVE-ONLY mixnodes +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/skimmed", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +#[deprecated(note = "use '/v1/unstable/nym-nodes/mixnodes/skimmed/active' instead")] +pub(super) async fn deprecated_mixnodes_basic( + state: State, + query_params: Query, +) -> AxumResult>> { + // TODO: + // 1. call '/v1/unstable/nym-nodes/mixnodes/skimmed/active' + // 2. remove pagination + // 3. return result + + todo!() + // + // let status_cache = state.node_status_cache(); + // let contract_cache = state.nym_contract_cache(); + // let describe_cache = state.described_nodes_cache(); + // + // // 1. get the rewarded set + // let rewarded_set = contract_cache + // .rewarded_set() + // .await + // .ok_or_else(AxumErrorResponse::internal)?; + // + // // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic + // let active_mixnodes = rewarded_set + // .active_mixnodes() + // .into_iter() + // .collect::>(); + // + // // 2. grab all annotations so that we could attach scores to the [nym] nodes + // let annotations = status_cache + // .node_annotations() + // .await + // .ok_or_else(AxumErrorResponse::internal)?; + // + // // 3. grab all legacy mixnodes + // // due to legacy endpoints we already have fully annotated data on them + // let annotated_legacy_mixnodes = status_cache + // .annotated_legacy_mixnodes() + // .await + // .ok_or_else(AxumErrorResponse::internal)?; + // + // // 4. grab all relevant described nym-nodes + // let describe_cache = describe_cache.get().await?; + // let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + // + // // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it + // + // // 5. only return nodes that are present in the active set + // let mut active_skimmed_mixnodes = Vec::new(); + // + // for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { + // if !active_mixnodes.contains(mix_id) { + // continue; + // } + // + // if let Some(semver_compat) = semver_compatibility.as_ref() { + // let version = legacy.version(); + // if !version_checker::is_minor_version_compatible(version, semver_compat) { + // continue; + // } + // } + // + // // if we have self-described info, prefer it over contract data + // if let Some(described) = describe_cache.get_node(mix_id) { + // active_skimmed_mixnodes.push(described.to_skimmed_node( + // NodeRole::Mixnode { + // layer: legacy.mixnode_details.bond_information.layer.into(), + // }, + // legacy.node_performance.last_24h, + // )) + // } else { + // active_skimmed_mixnodes.push(legacy.into()); + // } + // } + // + // for nym_node in mixing_nym_nodes { + // // if this node is not an active mixnode, ignore it + // if !active_mixnodes.contains(&nym_node.node_id) { + // continue; + // } + // + // // if we have wrong version, ignore + // if let Some(semver_compat) = semver_compatibility.as_ref() { + // let version = nym_node.version(); + // if !version_checker::is_minor_version_compatible(version, semver_compat) { + // continue; + // } + // } + // + // // SAFETY: if we determined our node IS active, it MUST have a layer + // // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data + // #[allow(clippy::unwrap_used)] + // let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); + // + // // honestly, not sure under what exact circumstances this value could be missing, + // // but in that case just use 0 performance + // let annotation = annotations + // .get(&nym_node.node_id) + // .copied() + // .unwrap_or_default(); + // + // active_skimmed_mixnodes.push( + // nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), + // ); + // } + // + // // min of all caches + // let refreshed_at = [ + // rewarded_set.timestamp(), + // annotations.timestamp(), + // describe_cache.timestamp(), + // annotated_legacy_mixnodes.timestamp(), + // ] + // .into_iter() + // .min() + // .unwrap() + // .into(); + // + // Ok(Json(CachedNodesResponse { + // refreshed_at, + // nodes: active_skimmed_mixnodes, + // })) +} + +/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn nodes_basic( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + if let Some(role) = query_params.0.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_all(state, Query(query_params.0.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_all(state, Query(query_params.0.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_all(state, Query(query_params.0.into())).await + } + }; + } + + let query_params = query_params.0; + + // TODO: implement pagination + + let semver = query_params.semver_compatibility; + let no_legacy = query_params.no_legacy; + + // TODO: + /* + - `/v1/unstable/nym-nodes/skimmed` - now works with `exit` parameter + - `/v1/unstable/nym-nodes/skimmed` - introduced `no-legacy` flag to ignore legacy mixnodes/gateways (where applicable) + - `/v1/unstable/nym-nodes/skimmed` - will now return **ALL** nodes if no query parameter is provided + + */ + + Err(AxumErrorResponse::not_implemented()) +} // - `/v1/unstable/nym-nodes/skimmed/active` - returns all Nym Nodes **AND** legacy mixnodes **AND** legacy gateways that are currently in the active set, unless `no-legacy` parameter is used -async fn nodes_basic_active( +/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the **active set** +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn nodes_basic_active( state: State, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } -// - `/v1/unstable/nym-nodes/skimmed/standby` - returns all Nym Nodes **AND** legacy mixnodes **AND** legacy gateways that are currently in the standby set, unless `no-legacy` parameter is used -async fn nodes_basic_standby( +/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the **standby set** +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/skimmed/standby", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn nodes_basic_standby( state: State, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } -// - `/v1/unstable/nym-nodes/mixnodes/skimmed/all` - returns all Nym Nodes **AND** legacy mixnodes that are currently bonded and support mixing role, unless `no-legacy` parameter is used -async fn mixnodes_basic_all( +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and support mixing role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/mixnodes/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn mixnodes_basic_all( state: State, - Query(SemverCompatibilityQueryParam { - semver_compatibility, - }): Query, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } -// - `/v1/unstable/nym-nodes/mixnodes/skimmed/active` - returns all Nym Nodes **AND** legacy mixnodes that are currently in the active set, unless `no-legacy` parameter is used -async fn mixnodes_basic_active( +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with one of the mixing roles. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/mixnodes/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn mixnodes_basic_active( state: State, - Query(SemverCompatibilityQueryParam { - semver_compatibility, - }): Query, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } -// - `/v1/unstable/nym-nodes/entry-gateways/skimmed/active` - returns all Nym Nodes **AND** legacy gateways that are currently in the active set and are assigned the entry role, unless `no-legacy` parameter is used -async fn entry_gateways_basic_active( +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with the entry role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/entry-gateways/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn entry_gateways_basic_active( state: State, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } -// - `/v1/unstable/nym-nodes/exit-gateways/skimmed/active` - returns all Nym Nodes **AND** legacy gateways that are currently in the active set and are assigned the exit role, unless `no-legacy` parameter is used -async fn exit_gateways_basic_active( +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with the exit role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/exit-gateways/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn exit_gateways_basic_active( state: State, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } -// - `/v1/unstable/nym-nodes/entry-gateways/skimmed/all` - returns all Nym Nodes **AND** legacy gateways that are currently bonded and support entry gateway role, unless `no-legacy` parameter is used -async fn entry_gateways_basic_all( +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support entry gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/entry-gateways/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn entry_gateways_basic_all( state: State, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } -// - `/v1/unstable/nym-nodes/exit-gateways/skimmed/all` - returns all Nym Nodes **AND** legacy gateways that are currently bonded and support exit gateway role, unless `no-legacy` parameter is used -async fn exit_gateways_basic_all( +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support exit gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/exit-gateways/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn exit_gateways_basic_all( state: State, -) -> AxumResult>> { + query_params: Query, +) -> PaginatedSkimmedNodes { todo!() } diff --git a/nym-node/nym-node-http-api/Cargo.toml b/nym-node/nym-node-http-api/Cargo.toml index 54db63a1ae..27b3fb53d5 100644 --- a/nym-node/nym-node-http-api/Cargo.toml +++ b/nym-node/nym-node-http-api/Cargo.toml @@ -30,9 +30,7 @@ fastrand = { workspace = true } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } -nym-node-requests = { path = "../nym-node-requests", default-features = false, features = [ - "openapi", -] } +nym-node-requests = { path = "../nym-node-requests", default-features = false, features = ["openapi"] } nym-task = { path = "../../common/task" } nym-metrics = { path = "../../common/nym-metrics" } diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 7a6c61b436..4db9d49297 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -46,6 +46,7 @@ impl SignedData { let Ok(plaintext) = serde_json::to_string(&self.data) else { return false; }; + let Ok(signature) = identity::Signature::from_base58_string(&self.signature) else { return false; }; @@ -56,12 +57,7 @@ impl SignedData { impl SignedHostInformation { pub fn verify_host_information(&self) -> bool { - let Ok(pub_key) = identity::PublicKey::from_base58_string(&self.keys.ed25519_identity) - else { - return false; - }; - - if self.verify(&pub_key) { + if self.verify(&self.keys.ed25519_identity) { return true; } @@ -70,7 +66,7 @@ impl SignedHostInformation { data: LegacyHostInformation::from(self.data.clone()), signature: self.signature.clone(), } - .verify(&pub_key) + .verify(&self.keys.ed25519_identity) } } @@ -105,20 +101,36 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); let host_info = crate::api::v1::node::models::HostInformation { ip_address: vec!["1.1.1.1".parse().unwrap()], hostname: Some("foomp.com".to_string()), keys: crate::api::v1::node::models::HostKeys { - ed25519_identity: ed22519.public_key().to_base58_string(), - x25519_sphinx: x25519_sphinx.public_key().to_base58_string(), - x25519_noise: "".to_string(), + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: None, }, }; let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap(); assert!(signed_info.verify(ed22519.public_key())); - assert!(signed_info.verify_host_information()) + assert!(signed_info.verify_host_information()); + + let host_info_with_noise = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: Some(*x25519_noise.public_key()), + }, + }; + + let signed_info = + SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap(); + assert!(signed_info.verify(ed22519.public_key())); + assert!(signed_info.verify_host_information()); } #[test] @@ -140,9 +152,9 @@ mod tests { ip_address: legacy_info.ip_address.clone(), hostname: legacy_info.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { - ed25519_identity: legacy_info.keys.ed25519.clone(), - x25519_sphinx: legacy_info.keys.x25519.clone(), - x25519_noise: "".to_string(), + ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(), + x25519_sphinx: legacy_info.keys.x25519.parse().unwrap(), + x25519_noise: None, }, }; diff --git a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs index 3374b1b0c4..f0051e81e1 100644 --- a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_crypto::asymmetric::identity::{self, serde_helpers::bs58_pubkey}; +use nym_crypto::asymmetric::identity::{self, serde_helpers::bs58_ed25519_pubkey}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt; @@ -86,7 +86,7 @@ impl VerlocResultData { #[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct VerlocNodeResult { - #[serde(with = "bs58_pubkey")] + #[serde(with = "bs58_ed25519_pubkey")] pub node_identity: identity::PublicKey, pub latest_measurement: Option, diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index eb2a4f4ce0..dcaa12435f 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -2,6 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use celes::Country; +use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; +use nym_crypto::asymmetric::x25519::{ + self, + serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::IpAddr; @@ -39,7 +44,7 @@ pub struct AnnouncePorts { pub mix_port: Option, } -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostInformation { /// Ip address(es) of this host, such as `1.1.1.1`. @@ -71,28 +76,34 @@ impl From for LegacyHostInformation { } } -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostKeys { /// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity. #[serde(alias = "ed25519")] - pub ed25519_identity: String, + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub ed25519_identity: ed25519::PublicKey, /// Base58-encoded x25519 public key of this node used for sphinx/outfox packet creation. /// Currently, it corresponds to either mixnode's or gateway's key. #[serde(alias = "x25519")] - pub x25519_sphinx: String, + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + pub x25519_sphinx: x25519::PublicKey, /// Base58-encoded x25519 public key of this node used for the noise protocol. #[serde(default)] - pub x25519_noise: String, + #[serde(with = "option_bs58_x25519_pubkey")] + #[schemars(with = "Option")] + pub x25519_noise: Option, } impl From for LegacyHostKeys { fn from(value: HostKeys) -> Self { LegacyHostKeys { - ed25519: value.ed25519_identity, - x25519: value.x25519_sphinx, + ed25519: value.ed25519_identity.to_base58_string(), + x25519: value.x25519_sphinx.to_base58_string(), } } } diff --git a/nym-node/src/node/http/mod.rs b/nym-node/src/node/http/mod.rs index b90398d6be..defdd9628e 100644 --- a/nym-node/src/node/http/mod.rs +++ b/nym-node/src/node/http/mod.rs @@ -17,17 +17,17 @@ pub(crate) fn sign_host_details( ed22519_identity: &ed25519::KeyPair, ) -> Result { let x25519_noise = if config.mixnet.debug.unsafe_disable_noise { - String::new() + None } else { - x25519_noise.to_base58_string() + Some(*x25519_noise) }; let host_info = api_requests::v1::node::models::HostInformation { ip_address: config.host.public_ips.clone(), hostname: config.host.hostname.clone(), keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: ed22519_identity.public_key().to_base58_string(), - x25519_sphinx: x22519_sphinx.to_base58_string(), + ed25519_identity: *ed22519_identity.public_key(), + x25519_sphinx: *x22519_sphinx, x25519_noise, }, }; From 304b192f52b1d05cb629b90202d4ead024c9bf1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 24 Sep 2024 16:44:05 +0100 Subject: [PATCH 048/207] basic handlers with a lot of repeated code --- nym-api/nym-api-requests/src/models.rs | 77 ++- nym-api/nym-api-requests/src/nym_nodes.rs | 104 +-- nym-api/src/main.rs | 3 + .../src/network_monitor/monitor/preparer.rs | 6 +- nym-api/src/node_describe_cache/mod.rs | 16 +- .../nym_nodes/handlers/unstable/helpers.rs | 21 + .../src/nym_nodes/handlers/unstable/mod.rs | 1 + .../nym_nodes/handlers/unstable/skimmed.rs | 645 +++++++++++------- nym-api/src/support/http/state.rs | 55 +- 9 files changed, 572 insertions(+), 356 deletions(-) create mode 100644 nym-api/src/nym_nodes/handlers/unstable/helpers.rs diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 4367a71ff2..be88d48087 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -29,6 +29,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; +use thiserror::Error; use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToResponse, ToSchema}; @@ -192,6 +193,15 @@ pub struct MixNodeBondAnnotated { pub ip_addresses: Vec, } +#[derive(Debug, Error)] +pub enum MalformedNodeBond { + #[error("the associated ed25519 identity key is malformed")] + InvalidEd25519Key, + + #[error("the associated x25519 sphinx key is malformed")] + InvalidX25519Key, +} + impl MixNodeBondAnnotated { pub fn mix_node(&self) -> &MixNode { &self.mixnode_details.bond_information.mix_node @@ -212,6 +222,32 @@ impl MixNodeBondAnnotated { pub fn version(&self) -> &str { &self.mixnode_details.bond_information.mix_node.version } + + pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + Ok(SkimmedNode { + node_id: self.mix_id(), + ed25519_identity_pubkey: self + .identity_key() + .parse() + .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, + ip_addresses: self.ip_addresses.clone(), + mix_port: self.mix_node().mix_port, + x25519_sphinx_pubkey: self + .mix_node() + .sphinx_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, + epoch_role: role, + supported_roles: DeclaredRoles { + mixnode: true, + entry: false, + exit_nr: false, + exit_ipr: false, + }, + entry: None, + performance: self.node_performance.last_24h, + }) + } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] @@ -242,6 +278,39 @@ impl GatewayBondAnnotated { pub fn owner(&self) -> &Addr { self.gateway_bond.bond.owner() } + + pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + Ok(SkimmedNode { + node_id: self.gateway_bond.node_id, + ip_addresses: self.ip_addresses.clone(), + ed25519_identity_pubkey: self + .gateway_bond + .gateway + .identity_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, + mix_port: self.gateway_bond.bond.gateway.mix_port, + x25519_sphinx_pubkey: self + .gateway_bond + .gateway + .sphinx_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, + epoch_role: role, + supported_roles: DeclaredRoles { + mixnode: false, + entry: true, + exit_nr: false, + exit_ipr: false, + }, + entry: Some(BasicEntryInformation { + hostname: None, + ws_port: self.gateway_bond.bond.gateway.clients_port, + wss_port: None, + }), + performance: self.node_performance.last_24h, + }) + } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -640,7 +709,7 @@ impl NymNodeDescription { } } - pub fn try_to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { + pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { let keys = &self.description.host_information.keys; let entry = if self.description.declared_role.entry { Some(self.entry_information()) @@ -650,15 +719,15 @@ impl NymNodeDescription { SkimmedNode { node_id: self.node_id, - ed25519_identity_pubkey: keys.ed25519.clone(), + ed25519_identity_pubkey: keys.ed25519, ip_addresses: self.description.host_information.ip_address.clone(), mix_port: self.description.mix_port(), - x25519_sphinx_pubkey: keys.x25519.clone(), + x25519_sphinx_pubkey: keys.x25519, // we can't use the declared roles, we have to take whatever was provided in the contract. // why? say this node COULD operate as an exit, but it might be the case the contract decided // to assign it an ENTRY role only. we have to use that one instead. epoch_role: role, - supported_roles: self.description.declared_role.into(), + supported_roles: self.description.declared_role, entry, performance, } diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 75eaffd3ca..bb7e9abe6c 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -1,11 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{ - DeclaredRoles, GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeData, - OffsetDateTimeJsonSchemaWrapper, -}; -use crate::pagination::PaginatedResponse; +use crate::models::{DeclaredRoles, NymNodeData, OffsetDateTimeJsonSchemaWrapper}; +use crate::pagination::{PaginatedResponse, Pagination}; use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; @@ -43,6 +40,25 @@ pub struct PaginatedCachedNodesResponse { pub nodes: PaginatedResponse, } +impl PaginatedCachedNodesResponse { + pub fn new_full( + refreshed_at: impl Into, + nodes: Vec, + ) -> Self { + PaginatedCachedNodesResponse { + refreshed_at: refreshed_at.into(), + nodes: PaginatedResponse { + pagination: Pagination { + total: nodes.len(), + page: 0, + size: nodes.len(), + }, + data: nodes, + }, + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "kebab-case")] pub enum NodeRoleQueryParam { @@ -125,86 +141,8 @@ impl SkimmedNode { _ => None, } } - - // pub fn from_described_gateway( - // annotated: &GatewayBondAnnotated, - // description: Option<&NymNodeData>, - // ) -> Self { - // let mut base: SkimmedNode = annotated.into(); - // let Some(description) = description else { - // return base; - // }; - // - // // safety: the conversion always sets the entry field - // let entry = base.entry.as_mut().unwrap(); - // entry - // .hostname - // .clone_from(&description.host_information.hostname); - // entry.ws_port = description.mixnet_websockets.ws_port; - // entry.wss_port = description.mixnet_websockets.wss_port; - // - // // always prefer self-described data - // if !description.host_information.ip_address.is_empty() { - // base.ip_addresses - // .clone_from(&description.host_information.ip_address) - // } - // - // base.supported_roles = description.declared_role; - // - // base - // } } -// impl<'a> From<&'a MixNodeBondAnnotated> for SkimmedNode { -// fn from(value: &'a MixNodeBondAnnotated) -> Self { -// todo!() -// // SkimmedNode { -// // node_id: value.mix_id(), -// // ed25519_identity_pubkey: value.identity_key().to_string(), -// // ip_addresses: value.ip_addresses.clone(), -// // mix_port: value.mix_node().mix_port, -// // x25519_sphinx_pubkey: value.mix_node().sphinx_key.clone(), -// // epoch_role: NodeRole::Mixnode { -// // layer: value.mixnode_details.bond_information.layer.into(), -// // }, -// // supported_roles: DeclaredRoles { -// // mixnode: true, -// // entry: false, -// // exit_nr: false, -// // exit_ipr: false, -// // }, -// // entry: None, -// // performance: value.node_performance.last_24h, -// // } -// } -// } - -// impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode { -// fn from(value: &'a GatewayBondAnnotated) -> Self { -// todo!() -// // SkimmedNode { -// // node_id: value.gateway_bond.node_id, -// // ip_addresses: value.ip_addresses.clone(), -// // ed25519_identity_pubkey: value.gateway_bond.bond.identity().clone(), -// // mix_port: value.gateway_bond.bond.gateway.mix_port, -// // x25519_sphinx_pubkey: value.gateway_bond.bond.gateway.sphinx_key.clone(), -// // epoch_role: NodeRole::EntryGateway, -// // supported_roles: DeclaredRoles { -// // mixnode: false, -// // entry: true, -// // exit_nr: false, -// // exit_ipr: false, -// // }, -// // entry: Some(BasicEntryInformation { -// // hostname: None, -// // ws_port: value.gateway_bond.bond.gateway.clients_port, -// // wss_port: None, -// // }), -// // performance: value.node_performance.last_24h, -// // } -// } -// } - // an intermediate variant that exposes additional data such as noise keys but without // the full fat of the self-described data #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 8534879327..4ee4205ea2 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,6 +1,9 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + use crate::epoch_operations::EpochAdvancer; use crate::support::cli; use crate::support::storage; diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 3567563e63..94c8d116f5 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -9,7 +9,7 @@ use crate::support::caching::cache::SharedCache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; use nym_api_requests::models::NymNodeDescription; use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::{LegacyMixLayer, NodeId, RewardedSet}; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_tester_utils::node::TestableNode; use nym_node_tester_utils::NodeTester; use nym_sphinx::acknowledgements::AckKey; @@ -344,7 +344,7 @@ impl PacketPreparer { let mixing_nym_nodes = descriptions.mixing_nym_nodes(); // last I checked `gatewaying` wasn't a word : ) - let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes(); + let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes(); let mut rng = thread_rng(); @@ -527,7 +527,7 @@ impl PacketPreparer { .await .expect("the cache must have been initialised!"); let mixing_nym_nodes = descriptions.mixing_nym_nodes(); - let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes(); + let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes(); let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes); let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways); diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 3e66fd132e..28d231639a 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -11,7 +11,6 @@ use async_trait::async_trait; use futures::{stream, StreamExt}; use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_crypto::asymmetric::{ed25519, encryption, identity, x25519}; use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; use nym_topology::gateway::GatewayConversionError; @@ -159,25 +158,30 @@ impl DescribedNodes { self.nodes.get(node_id) } - pub fn all_nodes<'a>(&'a self) -> impl Iterator + 'a { + pub fn all_nodes(&self) -> impl Iterator { self.nodes.values() } - pub fn mixing_nym_nodes<'a>(&'a self) -> impl Iterator + 'a { + pub fn mixing_nym_nodes(&self) -> impl Iterator { self.nodes .values() .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) .filter(|n| n.description.declared_role.mixnode) } - pub fn gateway_capable_nym_nodes<'a>( - &'a self, - ) -> impl Iterator + 'a { + pub fn entry_capable_nym_nodes(&self) -> impl Iterator { self.nodes .values() .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) .filter(|n| n.description.declared_role.entry) } + + pub fn exit_capable_nym_nodes(&self) -> impl Iterator { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.can_operate_exit_gateway()) + } } pub struct NodeDescriptionProvider { diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs new file mode 100644 index 0000000000..60309114d5 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -0,0 +1,21 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper; +use nym_bin_common::version_checker; +use time::OffsetDateTime; + +pub(crate) fn refreshed_at( + iter: impl IntoIterator, +) -> OffsetDateTimeJsonSchemaWrapper { + iter.into_iter().min().unwrap().into() +} + +pub(crate) fn semver(requirement: &Option, declared: &str) -> bool { + if let Some(semver_compat) = requirement.as_ref() { + if !version_checker::is_minor_version_compatible(declared, semver_compat) { + return false; + } + } + true +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index 1102fa8874..a3f50283ac 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -36,6 +36,7 @@ use nym_api_requests::nym_nodes::NodeRoleQueryParam; use serde::Deserialize; pub(crate) mod full_fat; +mod helpers; pub(crate) mod semi_skimmed; pub(crate) mod skimmed; diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index e547b7fc63..6904d265cc 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -2,16 +2,15 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver}; use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; -use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; use axum::extract::{Query, State}; use axum::Json; use nym_api_requests::nym_nodes::{ CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode, }; -use nym_bin_common::version_checker; -use std::collections::HashSet; +use tracing::trace; pub type PaginatedSkimmedNodes = AxumResult>>; @@ -31,116 +30,15 @@ pub(super) async fn deprecated_gateways_basic( state: State, query_params: Query, ) -> AxumResult>> { - // TODO: - // 1. call '/v1/unstable/entry-gateways/mixnodes/skimmed/all' - // 2. remove pagination + // 1. call '/v1/unstable/skimmed/entry-gateways/all' + let all_gateways = entry_gateways_basic_all(state, query_params).await?; + // 3. return result - - todo!() - - // let status_cache = state.node_status_cache(); - // let contract_cache = state.nym_contract_cache(); - // let describe_cache = state.described_nodes_cache(); - // - // // 1. get the rewarded set - // let rewarded_set = contract_cache - // .rewarded_set() - // .await - // .ok_or_else(AxumErrorResponse::internal)?; - // - // // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic - // let active_gateways = rewarded_set.gateways().into_iter().collect::>(); - // - // // 2. grab all annotations so that we could attach scores to the [nym] nodes - // let annotations = status_cache - // .node_annotations() - // .await - // .ok_or_else(AxumErrorResponse::internal)?; - // - // // 3. grab all legacy gateways - // // due to legacy endpoints we already have fully annotated data on them - // let annotated_legacy_gateways = status_cache - // .annotated_legacy_gateways() - // .await - // .ok_or_else(AxumErrorResponse::internal)?; - // - // // 4. grab all relevant described nym-nodes - // let describe_cache = describe_cache.get().await?; - // let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); - // - // // 5. only return nodes that are present in the active set - // let mut active_skimmed_gateways = Vec::new(); - // - // for (node_id, legacy) in annotated_legacy_gateways.iter() { - // if !active_gateways.contains(node_id) { - // continue; - // } - // - // if let Some(semver_compat) = semver_compatibility.as_ref() { - // let version = legacy.version(); - // if !version_checker::is_minor_version_compatible(version, semver_compat) { - // continue; - // } - // } - // - // // if we have self-described info, prefer it over contract data - // if let Some(described) = describe_cache.get_node(node_id) { - // active_skimmed_gateways.push( - // described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), - // ) - // } else { - // active_skimmed_gateways.push(legacy.into()); - // } - // } - // - // for nym_node in gateway_capable_nym_nodes { - // // if this node is not an active gateway, ignore it - // if !active_gateways.contains(&nym_node.node_id) { - // continue; - // } - // - // // if we have wrong version, ignore - // if let Some(semver_compat) = semver_compatibility.as_ref() { - // let version = nym_node.version(); - // if !version_checker::is_minor_version_compatible(version, semver_compat) { - // continue; - // } - // } - // - // // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit - // let role = if rewarded_set.is_exit(&nym_node.node_id) { - // NodeRole::ExitGateway - // } else { - // NodeRole::EntryGateway - // }; - // - // // honestly, not sure under what exact circumstances this value could be missing, - // // but in that case just use 0 performance - // let annotation = annotations - // .get(&nym_node.node_id) - // .copied() - // .unwrap_or_default(); - // - // active_skimmed_gateways - // .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - // } - // - // // min of all caches - // let refreshed_at = [ - // rewarded_set.timestamp(), - // annotations.timestamp(), - // describe_cache.timestamp(), - // annotated_legacy_gateways.timestamp(), - // ] - // .into_iter() - // .min() - // .unwrap() - // .into(); - // - // Ok(Json(CachedNodesResponse { - // refreshed_at, - // nodes: active_skimmed_gateways, - // })) + Ok(Json(CachedNodesResponse { + refreshed_at: all_gateways.refreshed_at, + // 2. remove pagination + nodes: all_gateways.0.nodes.data, + })) } /// Deprecated query that gets ACTIVE-ONLY mixnodes @@ -159,123 +57,15 @@ pub(super) async fn deprecated_mixnodes_basic( state: State, query_params: Query, ) -> AxumResult>> { - // TODO: - // 1. call '/v1/unstable/nym-nodes/mixnodes/skimmed/active' - // 2. remove pagination - // 3. return result + // 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active' + let active_mixnodes = mixnodes_basic_active(state, query_params).await?; - todo!() - // - // let status_cache = state.node_status_cache(); - // let contract_cache = state.nym_contract_cache(); - // let describe_cache = state.described_nodes_cache(); - // - // // 1. get the rewarded set - // let rewarded_set = contract_cache - // .rewarded_set() - // .await - // .ok_or_else(AxumErrorResponse::internal)?; - // - // // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic - // let active_mixnodes = rewarded_set - // .active_mixnodes() - // .into_iter() - // .collect::>(); - // - // // 2. grab all annotations so that we could attach scores to the [nym] nodes - // let annotations = status_cache - // .node_annotations() - // .await - // .ok_or_else(AxumErrorResponse::internal)?; - // - // // 3. grab all legacy mixnodes - // // due to legacy endpoints we already have fully annotated data on them - // let annotated_legacy_mixnodes = status_cache - // .annotated_legacy_mixnodes() - // .await - // .ok_or_else(AxumErrorResponse::internal)?; - // - // // 4. grab all relevant described nym-nodes - // let describe_cache = describe_cache.get().await?; - // let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); - // - // // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it - // - // // 5. only return nodes that are present in the active set - // let mut active_skimmed_mixnodes = Vec::new(); - // - // for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { - // if !active_mixnodes.contains(mix_id) { - // continue; - // } - // - // if let Some(semver_compat) = semver_compatibility.as_ref() { - // let version = legacy.version(); - // if !version_checker::is_minor_version_compatible(version, semver_compat) { - // continue; - // } - // } - // - // // if we have self-described info, prefer it over contract data - // if let Some(described) = describe_cache.get_node(mix_id) { - // active_skimmed_mixnodes.push(described.to_skimmed_node( - // NodeRole::Mixnode { - // layer: legacy.mixnode_details.bond_information.layer.into(), - // }, - // legacy.node_performance.last_24h, - // )) - // } else { - // active_skimmed_mixnodes.push(legacy.into()); - // } - // } - // - // for nym_node in mixing_nym_nodes { - // // if this node is not an active mixnode, ignore it - // if !active_mixnodes.contains(&nym_node.node_id) { - // continue; - // } - // - // // if we have wrong version, ignore - // if let Some(semver_compat) = semver_compatibility.as_ref() { - // let version = nym_node.version(); - // if !version_checker::is_minor_version_compatible(version, semver_compat) { - // continue; - // } - // } - // - // // SAFETY: if we determined our node IS active, it MUST have a layer - // // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data - // #[allow(clippy::unwrap_used)] - // let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); - // - // // honestly, not sure under what exact circumstances this value could be missing, - // // but in that case just use 0 performance - // let annotation = annotations - // .get(&nym_node.node_id) - // .copied() - // .unwrap_or_default(); - // - // active_skimmed_mixnodes.push( - // nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), - // ); - // } - // - // // min of all caches - // let refreshed_at = [ - // rewarded_set.timestamp(), - // annotations.timestamp(), - // describe_cache.timestamp(), - // annotated_legacy_mixnodes.timestamp(), - // ] - // .into_iter() - // .min() - // .unwrap() - // .into(); - // - // Ok(Json(CachedNodesResponse { - // refreshed_at, - // nodes: active_skimmed_mixnodes, - // })) + // 3. return result + Ok(Json(CachedNodesResponse { + refreshed_at: active_mixnodes.refreshed_at, + // 2. remove pagination + nodes: active_mixnodes.0.nodes.data, + })) } /// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) @@ -292,24 +82,22 @@ pub(super) async fn deprecated_mixnodes_basic( )] pub(super) async fn nodes_basic( state: State, - query_params: Query, + Query(query_params): Query, ) -> PaginatedSkimmedNodes { - if let Some(role) = query_params.0.role { + if let Some(role) = query_params.role { return match role { NodeRoleQueryParam::ActiveMixnode => { - mixnodes_basic_all(state, Query(query_params.0.into())).await + mixnodes_basic_all(state, Query(query_params.into())).await } NodeRoleQueryParam::EntryGateway => { - entry_gateways_basic_all(state, Query(query_params.0.into())).await + entry_gateways_basic_all(state, Query(query_params.into())).await } NodeRoleQueryParam::ExitGateway => { - exit_gateways_basic_all(state, Query(query_params.0.into())).await + exit_gateways_basic_all(state, Query(query_params.into())).await } }; } - let query_params = query_params.0; - // TODO: implement pagination let semver = query_params.semver_compatibility; @@ -343,6 +131,9 @@ pub(super) async fn nodes_basic_active( state: State, query_params: Query, ) -> PaginatedSkimmedNodes { + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; todo!() } @@ -362,9 +153,123 @@ pub(super) async fn nodes_basic_standby( state: State, query_params: Query, ) -> PaginatedSkimmedNodes { + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; todo!() } +async fn mixnodes_basic( + state: State, + Query(query_params): Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; + let semver_compatibility = query_params.semver_compatibility; + + // 1. get the rewarded set + let rewarded_set = state.rewarded_set().await?; + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = state.node_annotations().await?; + + // 3. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + + // 4. start building the response + let mut nodes = Vec::new(); + + for nym_node in mixing_nym_nodes { + let node_id = nym_node.node_id; + + // if this node is not an active mixnode, ignore it + if active_only && !rewarded_set.is_active_mixnode(&node_id) { + continue; + } + + // if we have wrong version, ignore + if !semver(&semver_compatibility, nym_node.version()) { + continue; + } + + let role = match rewarded_set.try_get_mix_layer(&node_id) { + Some(layer) => NodeRole::Mixnode { layer }, + None => NodeRole::Inactive, + }; + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + + // 5. if we allow legacy mixnodes, repeat the procedure for mixnodes, otherwise return just nym-nodes + if query_params.no_legacy { + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + ]); + + return Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))); + } + + // 6. grab all legacy mixnodes + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_mixnodes = state.legacy_mixnode_annotations().await?; + + for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { + // if this node is not an active mixnode, ignore it + if active_only && !rewarded_set.is_active_mixnode(mix_id) { + continue; + } + + // if we have wrong version, ignore + if !semver(&semver_compatibility, legacy.version()) { + continue; + } + + let role = match rewarded_set.try_get_mix_layer(mix_id) { + Some(layer) => NodeRole::Mixnode { layer }, + None => NodeRole::Inactive, + }; + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(mix_id) { + nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) + } else { + match legacy.try_to_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity_key(); + trace!("node {id} is malformed: {err}") + } + } + } + } + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_mixnodes.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + /// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) /// that are currently bonded and support mixing role. #[utoipa::path( @@ -381,7 +286,7 @@ pub(super) async fn mixnodes_basic_all( state: State, query_params: Query, ) -> PaginatedSkimmedNodes { - todo!() + mixnodes_basic(state, query_params, false).await } /// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) @@ -400,7 +305,118 @@ pub(super) async fn mixnodes_basic_active( state: State, query_params: Query, ) -> PaginatedSkimmedNodes { - todo!() + mixnodes_basic(state, query_params, true).await +} + +async fn entry_gateways_basic( + state: State, + Query(query_params): Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; + let semver_compatibility = query_params.semver_compatibility; + + // 1. get the rewarded set + let rewarded_set = state.rewarded_set().await?; + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = state.node_annotations().await?; + + // 3. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let gateway_capable_nym_nodes = describe_cache.entry_capable_nym_nodes(); + + // 4. start building the response + let mut nodes = Vec::new(); + + for nym_node in gateway_capable_nym_nodes { + let node_id = nym_node.node_id; + + // if this node is not an active gateway, ignore it + if active_only && !rewarded_set.is_entry(&node_id) { + continue; + } + + // if we have wrong version, ignore + if !semver(&semver_compatibility, nym_node.version()) { + continue; + } + + let role = match rewarded_set.is_entry(&node_id) { + true => NodeRole::EntryGateway, + false => NodeRole::Inactive, + }; + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + + // 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes + if query_params.no_legacy { + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + ]); + + return Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))); + } + + // 6. grab all legacy gateways + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_gateways = state.legacy_gateways_annotations().await?; + + for (node_id, legacy) in annotated_legacy_gateways.iter() { + // if this node is not an active gateway, ignore it + if active_only && !rewarded_set.is_entry(&node_id) { + continue; + } + + // if we have wrong version, ignore + if !semver(&semver_compatibility, legacy.version()) { + continue; + } + + let role = match rewarded_set.is_entry(&node_id) { + true => NodeRole::EntryGateway, + false => NodeRole::Inactive, + }; + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) + } else { + match legacy.try_to_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.gateway_bond.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_gateways.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) } /// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) @@ -419,26 +435,7 @@ pub(super) async fn entry_gateways_basic_active( state: State, query_params: Query, ) -> PaginatedSkimmedNodes { - todo!() -} - -/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) -/// that are currently bonded and are in the active set with the exit role. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParamsWithRole), - path = "/exit-gateways/active", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponse) - ) -)] -pub(super) async fn exit_gateways_basic_active( - state: State, - query_params: Query, -) -> PaginatedSkimmedNodes { - todo!() + entry_gateways_basic(state, query_params, true).await } /// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) @@ -457,7 +454,137 @@ pub(super) async fn entry_gateways_basic_all( state: State, query_params: Query, ) -> PaginatedSkimmedNodes { - todo!() + entry_gateways_basic(state, query_params, false).await +} + +async fn exit_gateways_basic( + state: State, + Query(query_params): Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; + let semver_compatibility = query_params.semver_compatibility; + + // 1. get the rewarded set + let rewarded_set = state.rewarded_set().await?; + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = state.node_annotations().await?; + + // 3. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let gateway_capable_nym_nodes = describe_cache.exit_capable_nym_nodes(); + + // 4. start building the response + let mut nodes = Vec::new(); + + for nym_node in gateway_capable_nym_nodes { + let node_id = nym_node.node_id; + + // if this node is not an active gateway, ignore it + if active_only && !rewarded_set.is_exit(&node_id) { + continue; + } + + // if we have wrong version, ignore + if !semver(&semver_compatibility, nym_node.version()) { + continue; + } + + let role = match rewarded_set.is_exit(&node_id) { + true => NodeRole::ExitGateway, + false => NodeRole::Inactive, + }; + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + + // 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes + if query_params.no_legacy { + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + ]); + + return Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))); + } + + // 6. grab all legacy gateways + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_gateways = state.legacy_gateways_annotations().await?; + + for (node_id, legacy) in annotated_legacy_gateways.iter() { + // if this node is not an active gateway, ignore it + if active_only && !rewarded_set.is_exit(&node_id) { + continue; + } + + // if we have wrong version, ignore + if !semver(&semver_compatibility, legacy.version()) { + continue; + } + + let role = match rewarded_set.is_exit(&node_id) { + true => NodeRole::ExitGateway, + false => NodeRole::Inactive, + }; + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) + } else { + match legacy.try_to_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.gateway_bond.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_gateways.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with the exit role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/exit-gateways/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn exit_gateways_basic_active( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + exit_gateways_basic(state, query_params, true).await } /// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) @@ -476,5 +603,5 @@ pub(super) async fn exit_gateways_basic_all( state: State, query_params: Query, ) -> PaginatedSkimmedNodes { - todo!() + exit_gateways_basic(state, query_params, false).await } diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index a0a640333b..f0803d6677 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -5,11 +5,17 @@ use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::handlers::unstable; +use crate::node_status_api::models::AxumErrorResponse; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; +use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; use crate::support::caching::cache::SharedCache; +use crate::support::caching::Cache; use crate::support::storage; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_mixnet_contract_common::NodeId; use nym_task::TaskManager; +use std::collections::HashMap; +use tokio::sync::RwLockReadGuard; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -102,3 +108,50 @@ impl AppState { &self.node_info_cache } } + +// handler helpers to easily get data or return error response +impl AppState { + pub(crate) async fn describe_nodes_cache_data( + &self, + ) -> Result>, AxumErrorResponse> { + Ok(self.described_nodes_cache().get().await?) + } + + pub(crate) async fn rewarded_set( + &self, + ) -> Result>, AxumErrorResponse> { + self.nym_contract_cache() + .rewarded_set() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn node_annotations( + &self, + ) -> Result>>, AxumErrorResponse> { + self.node_status_cache() + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn legacy_mixnode_annotations( + &self, + ) -> Result>>, AxumErrorResponse> + { + self.node_status_cache() + .annotated_legacy_mixnodes() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn legacy_gateways_annotations( + &self, + ) -> Result>>, AxumErrorResponse> + { + self.node_status_cache() + .annotated_legacy_gateways() + .await + .ok_or_else(AxumErrorResponse::internal) + } +} From cd706aa67e1fe3e6386a3722a5b5b43e27cf7234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 24 Sep 2024 18:23:08 +0100 Subject: [PATCH 049/207] extracted common functionalities and implemented remaining skimmed routes --- nym-api/nym-api-requests/src/nym_nodes.rs | 21 + nym-api/src/node_describe_cache/mod.rs | 6 + .../src/node_describe_cache/query_helpers.rs | 2 +- .../nym_nodes/handlers/unstable/helpers.rs | 52 +- .../src/nym_nodes/handlers/unstable/mod.rs | 14 +- .../nym_nodes/handlers/unstable/skimmed.rs | 630 ++++++++---------- nym-api/src/support/http/state.rs | 6 +- 7 files changed, 367 insertions(+), 364 deletions(-) diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index bb7e9abe6c..27d3e6ba3d 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -6,6 +6,7 @@ use crate::pagination::{PaginatedResponse, Pagination}; use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; @@ -90,6 +91,26 @@ pub enum NodeRole { Inactive, } +impl NodeRole { + pub fn is_inactive(&self) -> bool { + matches!(self, NodeRole::Inactive) + } +} + +impl From> for NodeRole { + fn from(role: Option) -> Self { + match role { + Some(Role::EntryGateway) => NodeRole::EntryGateway, + Some(Role::Layer1) => NodeRole::Mixnode { layer: 1 }, + Some(Role::Layer2) => NodeRole::Mixnode { layer: 2 }, + Some(Role::Layer3) => NodeRole::Mixnode { layer: 3 }, + Some(Role::ExitGateway) => NodeRole::ExitGateway, + Some(Role::Standby) => NodeRole::Standby, + None => NodeRole::Inactive, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct BasicEntryInformation { pub hostname: Option, diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 28d231639a..e92a3ebe5b 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -162,6 +162,12 @@ impl DescribedNodes { self.nodes.values() } + pub fn all_nym_nodes(&self) -> impl Iterator { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + } + pub fn mixing_nym_nodes(&self) -> impl Iterator { self.nodes .values() diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index 3e6c5027b9..1752345f2c 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -12,7 +12,7 @@ use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_config::defaults::mainnet; use nym_mixnet_contract_common::NodeId; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; -use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; +use nym_node_requests::api::v1::node::models::AuxiliaryDetails; use nym_node_requests::api::Client; use pin_project::pin_project; use std::future::Future; diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs index 60309114d5..4f7a20155e 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -1,10 +1,60 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper; +use nym_api_requests::models::{ + GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, +}; +use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_bin_common::version_checker; +use nym_mixnet_contract_common::reward_params::Performance; use time::OffsetDateTime; +pub(crate) trait LegacyAnnotation { + fn version(&self) -> &str; + + fn performance(&self) -> Performance; + + fn identity(&self) -> &str; + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result; +} + +impl LegacyAnnotation for MixNodeBondAnnotated { + fn version(&self) -> &str { + self.version() + } + + fn performance(&self) -> Performance { + self.node_performance.last_24h + } + + fn identity(&self) -> &str { + self.identity_key() + } + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + self.try_to_skimmed_node(role) + } +} + +impl LegacyAnnotation for GatewayBondAnnotated { + fn version(&self) -> &str { + self.version() + } + + fn performance(&self) -> Performance { + self.node_performance.last_24h + } + + fn identity(&self) -> &str { + self.identity() + } + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + self.try_to_skimmed_node(role) + } +} + pub(crate) fn refreshed_at( iter: impl IntoIterator, ) -> OffsetDateTimeJsonSchemaWrapper { diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index a3f50283ac..add55ec427 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -23,10 +23,9 @@ use crate::nym_nodes::handlers::unstable::full_fat::nodes_detailed; use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded; use crate::nym_nodes::handlers::unstable::skimmed::{ - deprecated_gateways_basic, deprecated_mixnodes_basic, entry_gateways_basic_active, - entry_gateways_basic_all, exit_gateways_basic_active, exit_gateways_basic_all, - mixnodes_basic_active, mixnodes_basic_all, nodes_basic, nodes_basic_active, - nodes_basic_standby, + entry_gateways_basic_active, entry_gateways_basic_all, exit_gateways_basic_active, + exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, nodes_basic_active, + nodes_basic_all, }; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; @@ -46,9 +45,8 @@ pub(crate) fn nym_node_routes_unstable() -> Router { .nest( "/skimmed", Router::new() - .route("/", get(nodes_basic)) + .route("/", get(nodes_basic_all)) .route("/active", get(nodes_basic_active)) - .route("/standby", get(nodes_basic_standby)) .nest( "/mixnodes", Router::new() @@ -73,8 +71,8 @@ pub(crate) fn nym_node_routes_unstable() -> Router { Router::new().route("/", get(nodes_expanded)), ) .nest("/full-fat", Router::new().route("/", get(nodes_detailed))) - .route("/gateways/skimmed", get(deprecated_gateways_basic)) - .route("/mixnodes/skimmed", get(deprecated_mixnodes_basic)) + .route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic)) + .route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic)) } #[derive(Debug, Deserialize, utoipa::IntoParams)] diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index 6904d265cc..df263d2e06 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -1,19 +1,188 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver}; +use crate::nym_contract_cache::cache::CachedRewardedSet; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver, LegacyAnnotation}; use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; +use crate::support::caching::Cache; use crate::support::http::state::AppState; use axum::extract::{Query, State}; use axum::Json; +use nym_api_requests::models::{NodeAnnotation, NymNodeDescription}; use nym_api_requests::nym_nodes::{ CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode, }; +use nym_mixnet_contract_common::NodeId; +use std::collections::HashMap; +use std::future::Future; +use tokio::sync::RwLockReadGuard; use tracing::trace; pub type PaginatedSkimmedNodes = AxumResult>>; +/// Given all relevant caches, build part of response for JUST Nym Nodes +fn build_nym_nodes_response<'a, NI>( + rewarded_set: &CachedRewardedSet, + required_semver: &Option, + nym_nodes_subset: NI, + annotations: &HashMap, + active_only: bool, +) -> Vec +where + NI: Iterator + 'a, +{ + let mut nodes = Vec::new(); + for nym_node in nym_nodes_subset { + let node_id = nym_node.node_id; + + // if we have wrong version, ignore + if !semver(required_semver, nym_node.version()) { + continue; + } + + let role: NodeRole = rewarded_set.role(node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + nodes +} + +/// Given all relevant caches, add appropriate legacy nodes to the part of the response +fn add_legacy( + nodes: &mut Vec, + required_semver: &Option, + rewarded_set: &CachedRewardedSet, + describe_cache: &DescribedNodes, + annotated_legacy_nodes: &HashMap, + active_only: bool, +) where + LN: LegacyAnnotation, +{ + for (node_id, legacy) in annotated_legacy_nodes.iter() { + // if we have wrong version, ignore + if !semver(required_semver, legacy.version()) { + continue; + } + + let role: NodeRole = rewarded_set.role(*node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + nodes.push(described.to_skimmed_node(role, legacy.performance())) + } else { + match legacy.try_to_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } +} + +// hehe, what an abomination, but it's used in multiple different places and I hate copy-pasting code, +// especially if it has multiple loops, etc +async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>( + state: &'a AppState, + Query(query_params): Query, + nym_nodes_subset: NI, + annotated_legacy_nodes_getter: LG, + active_only: bool, +) -> PaginatedSkimmedNodes +where + // iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.) + NI: Iterator + 'a, + + // async function that returns cache of appropriate legacy nodes (mixnodes or gateways) + LG: Fn(&'a AppState) -> Fut, + Fut: + Future>>, AxumErrorResponse>>, + + // the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated) + LN: LegacyAnnotation + 'a, +{ + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; + let semver_compatibility = query_params.semver_compatibility; + + // 1. get the rewarded set + let rewarded_set = state.rewarded_set().await?; + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = state.node_annotations().await?; + + // 3. implicitly grab the relevant described nodes + // (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler) + let describe_cache = state.describe_nodes_cache_data().await?; + + // 4. start building the response + let mut nodes = build_nym_nodes_response( + &rewarded_set, + &semver_compatibility, + nym_nodes_subset, + &annotations, + active_only, + ); + + // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes + if query_params.no_legacy { + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + ]); + + return Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))); + } + + // 6. grab relevant legacy nodes + // (due to the existence of the legacy endpoints, we already have fully annotated data on them) + let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; + add_legacy( + &mut nodes, + &semver_compatibility, + &rewarded_set, + &describe_cache, + &annotated_legacy_nodes, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_nodes.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + /// Deprecated query that gets ALL gateways #[utoipa::path( tag = "Unstable Nym Nodes", @@ -68,6 +237,66 @@ pub(super) async fn deprecated_mixnodes_basic( })) } +async fn nodes_basic( + state: State, + Query(query_params): Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes + + // 1. grab all relevant described nym-nodes + let rewarded_set = state.rewarded_set().await?; + let semver_compatibility = &query_params.semver_compatibility; + + let describe_cache = state.describe_nodes_cache_data().await?; + let all_nym_nodes = describe_cache.all_nym_nodes(); + let annotations = state.node_annotations().await?; + let legacy_mixnodes = state.legacy_mixnode_annotations().await?; + let legacy_gateways = state.legacy_gateways_annotations().await?; + + let mut nodes = build_nym_nodes_response( + &rewarded_set, + semver_compatibility, + all_nym_nodes, + &annotations, + active_only, + ); + + // add legacy gateways to the response + add_legacy( + &mut nodes, + semver_compatibility, + &rewarded_set, + &describe_cache, + &legacy_gateways, + active_only, + ); + + // add legacy mixnodes to the response + add_legacy( + &mut nodes, + semver_compatibility, + &rewarded_set, + &describe_cache, + &legacy_mixnodes, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + legacy_mixnodes.timestamp(), + legacy_gateways.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + /// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) /// that are currently bonded. #[utoipa::path( @@ -80,7 +309,7 @@ pub(super) async fn deprecated_mixnodes_basic( (status = 200, body = PaginatedCachedNodesResponse) ) )] -pub(super) async fn nodes_basic( +pub(super) async fn nodes_basic_all( state: State, Query(query_params): Query, ) -> PaginatedSkimmedNodes { @@ -98,23 +327,9 @@ pub(super) async fn nodes_basic( }; } - // TODO: implement pagination - - let semver = query_params.semver_compatibility; - let no_legacy = query_params.no_legacy; - - // TODO: - /* - - `/v1/unstable/nym-nodes/skimmed` - now works with `exit` parameter - - `/v1/unstable/nym-nodes/skimmed` - introduced `no-legacy` flag to ignore legacy mixnodes/gateways (where applicable) - - `/v1/unstable/nym-nodes/skimmed` - will now return **ALL** nodes if no query parameter is provided - - */ - - Err(AxumErrorResponse::not_implemented()) + nodes_basic(state, Query(query_params.into()), false).await } -// - `/v1/unstable/nym-nodes/skimmed/active` - returns all Nym Nodes **AND** legacy mixnodes **AND** legacy gateways that are currently in the active set, unless `no-legacy` parameter is used /// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) /// that are currently bonded and are in the **active set** #[utoipa::path( @@ -129,145 +344,42 @@ pub(super) async fn nodes_basic( )] pub(super) async fn nodes_basic_active( state: State, - query_params: Query, + Query(query_params): Query, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - todo!() -} + if let Some(role) = query_params.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_active(state, Query(query_params.into())).await + } + }; + } -/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) -/// that are currently bonded and are in the **standby set** -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParamsWithRole), - path = "/skimmed/standby", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponse) - ) -)] -pub(super) async fn nodes_basic_standby( - state: State, - query_params: Query, -) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - todo!() + nodes_basic(state, Query(query_params.into()), true).await } async fn mixnodes_basic( state: State, - Query(query_params): Query, + query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; - - // 1. get the rewarded set - let rewarded_set = state.rewarded_set().await?; - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; - - // 3. grab all relevant described nym-nodes + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); - // 4. start building the response - let mut nodes = Vec::new(); - - for nym_node in mixing_nym_nodes { - let node_id = nym_node.node_id; - - // if this node is not an active mixnode, ignore it - if active_only && !rewarded_set.is_active_mixnode(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, nym_node.version()) { - continue; - } - - let role = match rewarded_set.try_get_mix_layer(&node_id) { - Some(layer) => NodeRole::Mixnode { layer }, - None => NodeRole::Inactive, - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations.get(&node_id).copied().unwrap_or_default(); - - nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // 5. if we allow legacy mixnodes, repeat the procedure for mixnodes, otherwise return just nym-nodes - if query_params.no_legacy { - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - ]); - - return Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))); - } - - // 6. grab all legacy mixnodes - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_mixnodes = state.legacy_mixnode_annotations().await?; - - for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { - // if this node is not an active mixnode, ignore it - if active_only && !rewarded_set.is_active_mixnode(mix_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, legacy.version()) { - continue; - } - - let role = match rewarded_set.try_get_mix_layer(mix_id) { - Some(layer) => NodeRole::Mixnode { layer }, - None => NodeRole::Inactive, - }; - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(mix_id) { - nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.identity_key(); - trace!("node {id} is malformed: {err}") - } - } - } - } - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_mixnodes.timestamp(), - ]); - - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_mixnode_annotations(), + active_only, + ) + .await } /// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) @@ -310,113 +422,21 @@ pub(super) async fn mixnodes_basic_active( async fn entry_gateways_basic( state: State, - Query(query_params): Query, + query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; - - // 1. get the rewarded set - let rewarded_set = state.rewarded_set().await?; - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; - - // 3. grab all relevant described nym-nodes + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; - let gateway_capable_nym_nodes = describe_cache.entry_capable_nym_nodes(); + let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes(); - // 4. start building the response - let mut nodes = Vec::new(); - - for nym_node in gateway_capable_nym_nodes { - let node_id = nym_node.node_id; - - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_entry(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, nym_node.version()) { - continue; - } - - let role = match rewarded_set.is_entry(&node_id) { - true => NodeRole::EntryGateway, - false => NodeRole::Inactive, - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations.get(&node_id).copied().unwrap_or_default(); - - nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes - if query_params.no_legacy { - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - ]); - - return Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))); - } - - // 6. grab all legacy gateways - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_gateways = state.legacy_gateways_annotations().await?; - - for (node_id, legacy) in annotated_legacy_gateways.iter() { - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_entry(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, legacy.version()) { - continue; - } - - let role = match rewarded_set.is_entry(&node_id) { - true => NodeRole::EntryGateway, - false => NodeRole::Inactive, - }; - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.gateway_bond.identity(); - trace!("node {id} is malformed: {err}") - } - } - } - } - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_gateways.timestamp(), - ]); - - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + ) + .await } /// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) @@ -459,113 +479,21 @@ pub(super) async fn entry_gateways_basic_all( async fn exit_gateways_basic( state: State, - Query(query_params): Query, + query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; - - // 1. get the rewarded set - let rewarded_set = state.rewarded_set().await?; - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; - - // 3. grab all relevant described nym-nodes + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; - let gateway_capable_nym_nodes = describe_cache.exit_capable_nym_nodes(); + let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes(); - // 4. start building the response - let mut nodes = Vec::new(); - - for nym_node in gateway_capable_nym_nodes { - let node_id = nym_node.node_id; - - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_exit(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, nym_node.version()) { - continue; - } - - let role = match rewarded_set.is_exit(&node_id) { - true => NodeRole::ExitGateway, - false => NodeRole::Inactive, - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations.get(&node_id).copied().unwrap_or_default(); - - nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes - if query_params.no_legacy { - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - ]); - - return Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))); - } - - // 6. grab all legacy gateways - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_gateways = state.legacy_gateways_annotations().await?; - - for (node_id, legacy) in annotated_legacy_gateways.iter() { - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_exit(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, legacy.version()) { - continue; - } - - let role = match rewarded_set.is_exit(&node_id) { - true => NodeRole::ExitGateway, - false => NodeRole::Inactive, - }; - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.gateway_bond.identity(); - trace!("node {id} is malformed: {err}") - } - } - } - } - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_gateways.timestamp(), - ]); - - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + ) + .await } /// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index f0803d6677..c20f18436b 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -111,9 +111,9 @@ impl AppState { // handler helpers to easily get data or return error response impl AppState { - pub(crate) async fn describe_nodes_cache_data( - &self, - ) -> Result>, AxumErrorResponse> { + pub(crate) async fn describe_nodes_cache_data<'a>( + &'a self, + ) -> Result>, AxumErrorResponse> { Ok(self.described_nodes_cache().get().await?) } From 2e365026b742e298718804bce75b97e154a29d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 24 Sep 2024 18:29:00 +0100 Subject: [PATCH 050/207] implemented the noise route --- nym-api/nym-api-requests/src/models.rs | 11 ++++++- nym-api/src/nym_contract_cache/cache/data.rs | 21 +------------- nym-api/src/nym_nodes/handlers/mod.rs | 30 +++++++++++++++++++- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index be88d48087..aaec49854e 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -9,7 +9,10 @@ use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; -use nym_crypto::asymmetric::x25519::{self, serde_helpers::bs58_x25519_pubkey}; +use nym_crypto::asymmetric::x25519::{ + self, + serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, +}; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; @@ -586,6 +589,11 @@ pub struct HostKeys { #[serde(with = "bs58_x25519_pubkey")] #[schemars(with = "String")] pub x25519: x25519::PublicKey, + + #[serde(default)] + #[serde(with = "option_bs58_x25519_pubkey")] + #[schemars(with = "Option")] + pub x25519_noise: Option, } impl From for HostKeys { @@ -593,6 +601,7 @@ impl From for HostKeys { HostKeys { ed25519: value.ed25519_identity, x25519: value.x25519_sphinx, + x25519_noise: value.x25519_noise, } } } diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index 622286be24..f32c19c9cd 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -69,18 +69,6 @@ impl CachedRewardedSet { } } - pub fn active_set_size(&self) -> usize { - self.entry_gateways.len() - + self.exit_gateways.len() - + self.layer1.len() - + self.layer2.len() - + self.layer3.len() - } - - pub fn rewarded_set_size(&self) -> usize { - self.active_set_size() + self.standby.len() - } - pub fn try_get_mix_layer(&self, node_id: &NodeId) -> Option { if self.layer1.contains(node_id) { Some(1) @@ -97,20 +85,13 @@ impl CachedRewardedSet { self.standby.contains(node_id) } - pub fn is_entry(&self, node_id: &NodeId) -> bool { - self.entry_gateways.contains(node_id) - } - - pub fn is_exit(&self, node_id: &NodeId) -> bool { - self.exit_gateways.contains(node_id) - } - pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { self.layer1.contains(node_id) || self.layer2.contains(node_id) || self.layer3.contains(node_id) } + #[allow(dead_code)] pub(crate) fn gateways(&self) -> HashSet { let mut gateways = HashSet::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 46e4b38d73..ad92a689c0 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -58,7 +58,35 @@ async fn nodes_noise( ) -> AxumResult>> { // TODO: implement it let _ = pagination; - todo!() + + let describe_cache = state.describe_nodes_cache_data().await?; + + let nodes = describe_cache + .all_nodes() + .filter_map(|n| { + n.description + .host_information + .keys + .x25519_noise + .map(|noise_key| (noise_key, n)) + }) + .map(|(noise_key, node)| NoiseDetails { + x25119_pubkey: noise_key, + mixnet_port: node.description.mix_port(), + ip_addresses: node.description.host_information.ip_address.clone(), + }) + .collect::>(); + + let total = nodes.len(); + + Ok(Json(PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: nodes, + })) } #[utoipa::path( From e79450a0824801fda954993a9312bf0c58a167ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 24 Sep 2024 18:37:35 +0100 Subject: [PATCH 051/207] cargo fmt --- explorer-api/src/ping/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index 952e681340..22486f6608 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::{NodeId, MixNode}; +use nym_mixnet_contract_common::{MixNode, NodeId}; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; From fb43e917cb2ea70bd9a025451ddd97415969b2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 25 Sep 2024 10:32:40 +0100 Subject: [PATCH 052/207] updated ts_rs and derived it for more types --- Cargo.lock | 16 ++---- Cargo.toml | 2 +- .../mixnet-contract/src/nym_node.rs | 5 ++ .../mixnet-contract/src/reward_params.rs | 7 +++ common/types/src/currency.rs | 38 +++---------- common/types/src/fees.rs | 38 +++---------- nym-api/nym-api-requests/src/models.rs | 54 +++++++++++++++++++ nym-api/nym-api-requests/src/pagination.rs | 10 ++++ nym-wallet/nym-wallet-types/Cargo.toml | 2 +- nym-wallet/src-tauri/Cargo.toml | 2 +- tools/ts-rs-cli/src/main.rs | 30 +++++++++-- 11 files changed, 124 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11ed1c4400..77221ef262 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "accessory" version = "1.3.1" @@ -9680,10 +9674,11 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" -version = "7.1.1" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6" +checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9" dependencies = [ + "lazy_static", "thiserror", "ts-rs-macros", ] @@ -9705,11 +9700,10 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "7.1.1" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f7f9b821696963053a89a7bd8b292dc34420aea8294d7b225274d488f3ec92" +checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ - "Inflector", "proc-macro2", "quote", "syn 2.0.66", diff --git a/Cargo.toml b/Cargo.toml index 81804b89da..04e5f5c8e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -327,7 +327,7 @@ tracing = "0.1.37" tracing-opentelemetry = "0.19.0" tracing-subscriber = "0.3.16" tracing-tree = "0.2.2" -ts-rs = "7.0.0" +ts-rs = "10.0.0" tungstenite = { version = "0.20.1", default-features = false } url = "2.5" utoipa = "4.2" diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs index fa5db3b39c..9453375dd3 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -12,6 +12,11 @@ use std::fmt::{Display, Formatter}; #[cw_serde] #[derive(PartialOrd, Copy, Hash, Eq)] #[repr(u8)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Role.ts") +)] pub enum Role { #[serde(rename = "eg", alias = "entry", alias = "entry_gateway")] EntryGateway = 0, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index da6e2d3dae..03cd89bd04 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -303,14 +303,21 @@ impl RewardedSetParams { } /// Parameters used for rewarding particular node. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NodeRewardingParameters.ts") +)] #[cw_serde] #[derive(Copy)] pub struct NodeRewardingParameters { /// Performance of the particular node in the current epoch. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub performance: Performance, /// Amount of work performed by this node in the current epoch /// also known as 'omega' in the paper + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub work_factor: WorkFactor, } diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index 26cda1a2db..5cbe901cc8 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -7,13 +7,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; - use std::fmt::{Display, Formatter}; use strum::{Display, EnumString, VariantNames}; -#[cfg(feature = "generate-ts")] -use ts_rs::{Dependency, TS}; - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -248,40 +244,20 @@ impl From for CoinMetadata { // tries to semi-replicate cosmos-sdk's DecCoin for being able to handle tokens with decimal amounts // https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/types/dec_coin.go #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DecCoin.ts") +)] pub struct DecCoin { + #[cfg_attr(feature = "generate-ts", ts(as = "CurrencyDenom"))] pub denom: Denom, // Decimal is already serialized to string and using string in its schema, so lets also go straight to string for ts_rs // todo: is `Decimal` the correct type to use? Do we want to depend on cosmwasm_std here? + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub amount: Decimal, } -// I had to implement it manually to correctly set dependencies -#[cfg(feature = "generate-ts")] -impl TS for DecCoin { - const EXPORT_TO: Option<&'static str> = Some("ts-packages/types/src/types/rust/DecCoin.ts"); - - fn decl() -> String { - format!("type {} = {};", Self::name(), Self::inline()) - } - - fn name() -> String { - "DecCoin".into() - } - - fn inline() -> String { - "{ denom: CurrencyDenom, amount: string }".into() - } - - fn dependencies() -> Vec { - vec![Dependency::from_ty::() - .expect("TS was incorrectly defined on `CurrencyDenom`")] - } - - fn transparent() -> bool { - false - } -} - impl DecCoin { pub fn new_base>(amount: impl Into, denom: S) -> Self { DecCoin { diff --git a/common/types/src/fees.rs b/common/types/src/fees.rs index cd98be9d40..c27b19fef5 100644 --- a/common/types/src/fees.rs +++ b/common/types/src/fees.rs @@ -2,13 +2,16 @@ use crate::currency::DecCoin; use nym_validator_client::nyxd::Fee; use serde::{Deserialize, Serialize}; -#[cfg(feature = "generate-ts")] -use ts_rs::{Dependency, TS}; - #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/FeeDetails.ts") +)] pub struct FeeDetails { // expected to be used by the wallet in order to display detailed fee information to the user pub amount: Option, + #[cfg_attr(feature = "generate-ts", ts(as = "ts_type_helpers::Fee"))] pub fee: Fee, } @@ -18,35 +21,6 @@ impl FeeDetails { } } -#[cfg(feature = "generate-ts")] -impl TS for FeeDetails { - const EXPORT_TO: Option<&'static str> = Some("ts-packages/types/src/types/rust/FeeDetails.ts"); - - fn decl() -> String { - format!("type {} = {};", Self::name(), Self::inline()) - } - - fn name() -> String { - "FeeDetails".into() - } - - fn inline() -> String { - "{ amount: DecCoin | null, fee: Fee }".into() - } - - fn dependencies() -> Vec { - vec![ - Dependency::from_ty::().expect("TS was incorrectly defined on `DecCoin`"), - Dependency::from_ty::() - .expect("TS was incorrectly defined on `ts_type_helpers::Fee`"), - ] - } - - fn transparent() -> bool { - false - } -} - // this should really be sealed and NEVER EVER used as "normal" types, // but due to our typescript requirements, we have to expose it to generate // the types... diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index aaec49854e..44279666e2 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -130,12 +130,23 @@ pub struct NodePerformance { // nym-api shouldn't be calculating apy or stake saturation for you. // it should just return its own metrics (performance) and then you can do with it as you wish #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts") +)] pub struct NodeAnnotation { + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub last_24h_performance: Performance, pub current_role: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts") +)] pub struct AnnotationResponse { #[schema(value_type = u32)] pub node_id: NodeId, @@ -143,6 +154,11 @@ pub struct AnnotationResponse { } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts") +)] pub struct NodePerformanceResponse { #[schema(value_type = u32)] pub node_id: NodeId, @@ -150,11 +166,17 @@ pub struct NodePerformanceResponse { } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts") +)] pub struct NodeDatePerformanceResponse { #[schema(value_type = u32)] pub node_id: NodeId, #[schema(value_type = String, example = "1970-01-01")] #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub date: Date, pub performance: Option, } @@ -490,6 +512,11 @@ pub struct GatewayStatusReportResponse { } #[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts") +)] pub struct PerformanceHistoryResponse { #[schema(value_type = u32)] pub node_id: NodeId, @@ -497,6 +524,11 @@ pub struct PerformanceHistoryResponse { } #[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts") +)] pub struct UptimeHistoryResponse { #[schema(value_type = u32)] pub node_id: NodeId, @@ -504,18 +536,30 @@ pub struct UptimeHistoryResponse { } #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts") +)] pub struct HistoricalUptimeResponse { #[schema(value_type = String, example = "1970-01-01")] #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub date: Date, pub uptime: Uptime, } #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts") +)] pub struct HistoricalPerformanceResponse { #[schema(value_type = String, example = "1970-01-01")] #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub date: Date, pub performance: f64, @@ -745,6 +789,11 @@ impl NymNodeDescription { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts") +)] pub enum DescribedNodeType { LegacyMixnode, LegacyGateway, @@ -752,6 +801,11 @@ pub enum DescribedNodeType { } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts") +)] pub struct DeclaredRoles { pub mixnode: bool, pub entry: bool, diff --git a/nym-api/nym-api-requests/src/pagination.rs b/nym-api/nym-api-requests/src/pagination.rs index 2d82c826ae..9a36caa988 100644 --- a/nym-api/nym-api-requests/src/pagination.rs +++ b/nym-api/nym-api-requests/src/pagination.rs @@ -6,6 +6,11 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Pagination.ts") +)] pub struct Pagination { pub total: usize, pub page: u32, @@ -13,6 +18,11 @@ pub struct Pagination { } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PaginatedResponse.ts") +)] pub struct PaginatedResponse { pub pagination: Pagination, pub data: Vec, diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index 9bfd3717b9..720027178b 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -10,7 +10,7 @@ hex-literal = "0.3.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } -ts-rs = "7.0.0" +ts-rs = "10.0.0" cosmwasm-std = "1.4.3" cosmrs = "=0.15.0" diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 61eee10d85..88032e5efb 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -67,7 +67,7 @@ nym-store-cipher = { path = "../../common/store-cipher", features = ["json"] } nym-crypto = { path = "../../common/crypto", features = ["rand"] } rand_chacha = "0.3" tempfile = "3.3.0" -ts-rs = "7.0.0" +ts-rs = "10.0.0" [features] default = ["custom-protocol"] diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index 8a7720664f..a876faf637 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -1,7 +1,14 @@ use nym_api_requests::models::{ - GatewayCoreStatusResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse, - MixnodeStatus, MixnodeStatusResponse, RewardEstimationResponse, SelectionChance, - StakeSaturationResponse, + AnnotationResponse, DeclaredRoles, DescribedNodeType, GatewayCoreStatusResponse, + HistoricalPerformanceResponse, HistoricalUptimeResponse, InclusionProbabilityResponse, + MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusResponse, NodeAnnotation, + NodeDatePerformanceResponse, NodePerformanceResponse, PerformanceHistoryResponse, + RewardEstimationResponse, SelectionChance, StakeSaturationResponse, UptimeHistoryResponse, +}; +use nym_api_requests::pagination::{PaginatedResponse, Pagination}; +use nym_mixnet_contract_common::nym_node::{NodeConfigUpdate, Role}; +use nym_mixnet_contract_common::reward_params::{ + ActiveSetUpdate, NodeRewardingParameters, RewardedSetParams, }; use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::{ @@ -70,11 +77,16 @@ fn main() { do_export!(MixNode); do_export!(MixNodeConfigUpdate); do_export!(RewardingParams); + do_export!(RewardedSetParams); + do_export!(NodeRewardingParameters); + do_export!(ActiveSetUpdate); do_export!(UnbondedMixnode); do_export!(RewardEstimate); do_export!(ContractInterval); do_export!(NymNode); do_export!(PendingNodeChanges); + do_export!(NodeConfigUpdate); + do_export!(Role); // common/types/src do_export!(Account); @@ -133,6 +145,18 @@ fn main() { do_export!(SelectionChance); do_export!(StakeSaturationResponse); do_export!(RewardEstimationResponse); + do_export!(NodeAnnotation); + do_export!(AnnotationResponse); + do_export!(NodePerformanceResponse); + do_export!(NodeDatePerformanceResponse); + do_export!(PerformanceHistoryResponse); + do_export!(UptimeHistoryResponse); + do_export!(HistoricalUptimeResponse); + do_export!(HistoricalPerformanceResponse); + do_export!(DescribedNodeType); + do_export!(DeclaredRoles); + do_export!(PaginatedResponse); + do_export!(Pagination); // nym-wallet do_export!(AppEnv); From c7f2ef70747ef2358c56110278d8e5bf75a64935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 25 Sep 2024 11:27:51 +0100 Subject: [PATCH 053/207] regenerated typescript types --- .../mixnet-contract/src/gateway.rs | 5 +- .../mixnet-contract/src/interval.rs | 2 +- .../mixnet-contract/src/mixnode.rs | 17 +++- .../mixnet-contract/src/nym_node.rs | 14 ++- .../mixnet-contract/src/reward_params.rs | 30 +++++-- .../mixnet-contract/src/rewarding/mod.rs | 5 +- .../vesting-contract/src/types.rs | 2 +- common/types/src/account.rs | 11 ++- common/types/src/currency.rs | 7 +- common/types/src/delegation.rs | 17 +++- common/types/src/deprecated.rs | 15 +++- common/types/src/fees.rs | 8 +- common/types/src/gas.rs | 4 +- common/types/src/gateway.rs | 4 +- common/types/src/mixnode.rs | 17 +++- common/types/src/nym_node.rs | 7 +- common/types/src/pending_events.rs | 20 ++++- common/types/src/transaction.rs | 17 +++- common/types/src/vesting.rs | 17 +++- nym-api/nym-api-requests/src/models.rs | 90 +++++++++++++++---- nym-api/nym-api-requests/src/pagination.rs | 7 +- nym-wallet/nym-wallet-types/src/admin.rs | 6 +- nym-wallet/nym-wallet-types/src/interval.rs | 2 +- nym-wallet/nym-wallet-types/src/network.rs | 2 +- .../nym-wallet-types/src/network_config.rs | 2 +- nym-wallet/src/requests/queries.ts | 58 ++++++------ nym-wallet/src/types/rust/AppEnv.ts | 2 +- nym-wallet/src/types/rust/AppVersion.ts | 2 +- nym-wallet/src/types/rust/Interval.ts | 9 +- nym-wallet/src/types/rust/Network.ts | 2 +- .../src/types/rust/OperatingCostRange.ts | 4 +- .../src/types/rust/ProfitMarginRange.ts | 2 +- nym-wallet/src/types/rust/StateParams.ts | 13 ++- nym-wallet/src/types/rust/ValidatorUrl.ts | 2 +- nym-wallet/src/types/rust/ValidatorUrls.ts | 6 +- tools/ts-rs-cli/Cargo.toml | 8 +- tools/ts-rs-cli/src/main.rs | 19 ++-- ts-packages/types/src/types/rust/Account.ts | 6 +- .../types/src/types/rust/AccountEntry.ts | 5 +- .../src/types/rust/AccountWithMnemonic.ts | 5 +- .../types/src/types/rust/ActiveSetUpdate.ts | 19 ++++ .../src/types/rust/AnnotationResponse.ts | 4 + ts-packages/types/src/types/rust/Balance.ts | 5 +- ts-packages/types/src/types/rust/Coin.ts | 5 +- ts-packages/types/src/types/rust/CosmosFee.ts | 7 +- .../types/src/types/rust/DeclaredRoles.ts | 3 + .../types/src/types/rust/Delegation.ts | 8 +- .../types/src/types/rust/DelegationEvent.ts | 4 +- .../types/src/types/rust/DelegationResult.ts | 6 +- .../types/rust/DelegationSummaryResponse.ts | 4 +- .../types/rust/DelegationWithEverything.ts | 10 +-- .../types/src/types/rust/DescribedNodeType.ts | 3 + ts-packages/types/src/types/rust/Gas.ts | 7 +- ts-packages/types/src/types/rust/GasInfo.ts | 10 ++- ts-packages/types/src/types/rust/Gateway.ts | 7 +- .../types/src/types/rust/GatewayBond.ts | 4 +- .../src/types/rust/GatewayConfigUpdate.ts | 4 +- .../types/rust/GatewayCoreStatusResponse.ts | 5 +- .../rust/HistoricalPerformanceResponse.ts | 3 + .../types/rust/HistoricalUptimeResponse.ts | 3 + .../rust/InclusionProbabilityResponse.ts | 5 +- ts-packages/types/src/types/rust/Interval.ts | 25 +++++- .../src/types/rust/IntervalRewardParams.ts | 44 ++++++++- .../rust/IntervalRewardingParamsUpdate.ts | 31 ++++++- .../types/src/types/rust/MixNodeBond.ts | 5 +- .../src/types/rust/MixNodeConfigUpdate.ts | 4 +- .../types/src/types/rust/MixNodeCostParams.ts | 5 +- .../types/src/types/rust/MixNodeDetails.ts | 7 +- ts-packages/types/src/types/rust/Mixnode.ts | 28 +++++- .../types/rust/MixnodeCoreStatusResponse.ts | 5 +- .../src/types/rust/MixnodeStatusResponse.ts | 4 +- .../types/src/types/rust/NodeAnnotation.ts | 4 + .../types/src/types/rust/NodeConfigUpdate.ts | 7 ++ .../types/rust/NodeDatePerformanceResponse.ts | 3 + .../src/types/rust/NodePerformanceResponse.ts | 3 + .../types/src/types/rust/NodeRewarding.ts | 12 ++- .../src/types/rust/NodeRewardingParameters.ts | 16 ++++ ts-packages/types/src/types/rust/NymNode.ts | 20 ++++- .../types/src/types/rust/NymNodeBond.ts | 40 ++++++++- .../types/src/types/rust/NymNodeDetails.ts | 24 ++++- .../src/types/rust/OriginalVestingResponse.ts | 6 +- .../types/src/types/rust/PaginatedResponse.ts | 4 + .../types/src/types/rust/Pagination.ts | 3 + .../types/src/types/rust/PendingEpochEvent.ts | 6 +- .../src/types/rust/PendingEpochEventData.ts | 1 + .../src/types/rust/PendingIntervalEvent.ts | 6 +- .../types/rust/PendingIntervalEventData.ts | 4 +- .../src/types/rust/PendingNodeChanges.ts | 2 +- .../types/rust/PerformanceHistoryResponse.ts | 5 ++ ts-packages/types/src/types/rust/Period.ts | 5 +- .../types/src/types/rust/PledgeData.ts | 5 +- .../types/src/types/rust/RewardEstimate.ts | 17 +++- .../types/rust/RewardEstimationResponse.ts | 4 +- .../types/src/types/rust/RewardedSetParams.ts | 20 +++++ .../types/src/types/rust/RewardingParams.ts | 14 ++- ts-packages/types/src/types/rust/Role.ts | 3 + .../src/types/rust/RpcTransactionResponse.ts | 4 +- .../types/src/types/rust/SendTxResult.ts | 4 +- .../src/types/rust/StakeSaturationResponse.ts | 6 +- .../src/types/rust/TransactionDetails.ts | 6 +- .../types/rust/TransactionExecuteResult.ts | 6 +- .../types/src/types/rust/UnbondedMixnode.ts | 20 ++++- .../src/types/rust/UptimeHistoryResponse.ts | 5 ++ .../src/types/rust/VestingAccountInfo.ts | 4 +- .../types/src/types/rust/VestingPeriod.ts | 5 +- .../src/types/rust/WrappedDelegationEvent.ts | 5 +- 106 files changed, 754 insertions(+), 300 deletions(-) create mode 100644 ts-packages/types/src/types/rust/ActiveSetUpdate.ts create mode 100644 ts-packages/types/src/types/rust/AnnotationResponse.ts create mode 100644 ts-packages/types/src/types/rust/DeclaredRoles.ts create mode 100644 ts-packages/types/src/types/rust/DescribedNodeType.ts create mode 100644 ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts create mode 100644 ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts create mode 100644 ts-packages/types/src/types/rust/NodeAnnotation.ts create mode 100644 ts-packages/types/src/types/rust/NodeConfigUpdate.ts create mode 100644 ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts create mode 100644 ts-packages/types/src/types/rust/NodePerformanceResponse.ts create mode 100644 ts-packages/types/src/types/rust/NodeRewardingParameters.ts create mode 100644 ts-packages/types/src/types/rust/PaginatedResponse.ts create mode 100644 ts-packages/types/src/types/rust/Pagination.ts create mode 100644 ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts create mode 100644 ts-packages/types/src/types/rust/RewardedSetParams.ts create mode 100644 ts-packages/types/src/types/rust/Role.ts create mode 100644 ts-packages/types/src/types/rust/UptimeHistoryResponse.ts diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index f819bedb50..ed0ee864e8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -135,7 +135,10 @@ impl Display for GatewayBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GatewayConfigUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/GatewayConfigUpdate.ts" + ) )] #[cw_serde] pub struct GatewayConfigUpdate { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 067a3aa50e..2a1c885f1c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -215,7 +215,7 @@ impl Display for EpochState { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Interval.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Interval.ts") )] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 779df2e125..43a33c5d3e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -541,7 +541,7 @@ impl MixNodeBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Mixnode.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Mixnode.ts") )] pub struct MixNode { /// Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com @@ -642,7 +642,10 @@ impl From for u8 { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts" + ) )] #[cw_serde] #[derive(Default, Copy)] @@ -665,7 +668,10 @@ impl PendingMixNodeChanges { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/UnbondedMixnode.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/UnbondedMixnode.ts" + ) )] #[cw_serde] pub struct UnbondedMixnode { @@ -689,7 +695,10 @@ pub struct UnbondedMixnode { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts" + ) )] #[cw_serde] pub struct MixNodeConfigUpdate { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs index 9453375dd3..6885b8998e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -15,7 +15,7 @@ use std::fmt::{Display, Formatter}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Role.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Role.ts") )] pub enum Role { #[serde(rename = "eg", alias = "entry", alias = "entry_gateway")] @@ -343,7 +343,7 @@ impl NymNodeBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NymNode.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/NymNode.ts") )] pub struct NymNode { /// Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com @@ -405,7 +405,10 @@ impl From for NymNode { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeConfigUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeConfigUpdate.ts" + ) )] #[cw_serde] #[derive(Default)] @@ -424,7 +427,10 @@ pub struct NodeConfigUpdate { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingNodeChanges.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingNodeChanges.ts" + ) )] pub struct PendingNodeChanges { pub pledge_change: Option, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 03cd89bd04..40e34b2888 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -14,7 +14,10 @@ pub type WorkFactor = Decimal; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardParams.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/IntervalRewardParams.ts" + ) )] #[cw_serde] #[derive(Copy)] @@ -80,7 +83,10 @@ impl IntervalRewardParams { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardingParams.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardingParams.ts" + ) )] #[cw_serde] #[derive(Copy)] @@ -241,7 +247,10 @@ impl RewardingParams { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardedSetParams.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardedSetParams.ts" + ) )] #[cw_serde] #[derive(Copy)] @@ -306,7 +315,10 @@ impl RewardedSetParams { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeRewardingParameters.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeRewardingParameters.ts" + ) )] #[cw_serde] #[derive(Copy)] @@ -338,7 +350,10 @@ impl NodeRewardingParameters { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts" + ) )] #[cw_serde] #[derive(Copy, Default)] @@ -392,7 +407,10 @@ impl IntervalRewardingParamsUpdate { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/ActiveSetUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/ActiveSetUpdate.ts" + ) )] #[cw_serde] #[derive(Copy, Default)] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index 18bdc3e562..e1c320f806 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -10,7 +10,10 @@ pub mod simulator; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardEstimate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardEstimate.ts" + ) )] #[cw_serde] #[derive(Copy, Default)] diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs index da3adbbada..0b15346804 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs @@ -10,7 +10,7 @@ use std::str::FromStr; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Period.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Period.ts") )] #[cw_serde] /// The vesting period. diff --git a/common/types/src/account.rs b/common/types/src/account.rs index 12bc12efd4..6315cccccd 100644 --- a/common/types/src/account.rs +++ b/common/types/src/account.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Account.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Account.ts") )] #[derive(Serialize, Deserialize, JsonSchema)] pub struct Account { @@ -31,7 +31,10 @@ impl Account { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/AccountWithMnemonic.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/AccountWithMnemonic.ts" + ) )] #[derive(Serialize, Deserialize)] pub struct AccountWithMnemonic { @@ -42,7 +45,7 @@ pub struct AccountWithMnemonic { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/AccountEntry.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/AccountEntry.ts") )] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AccountEntry { @@ -53,7 +56,7 @@ pub struct AccountEntry { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Balance.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Balance.ts") )] #[derive(Serialize, Deserialize)] pub struct Balance { diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index 5cbe901cc8..9d13bfbc56 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -13,7 +13,10 @@ use strum::{Display, EnumString, VariantNames}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/CurrencyDenom.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/CurrencyDenom.ts" + ) )] #[cfg_attr(feature = "generate-ts", ts(rename_all = "lowercase"))] #[derive( @@ -247,7 +250,7 @@ impl From for CoinMetadata { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DecCoin.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/DecCoin.ts") )] pub struct DecCoin { #[cfg_attr(feature = "generate-ts", ts(as = "CurrencyDenom"))] diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index 51ffa01e4e..f906c04cd1 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Delegation.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Delegation.ts") )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct Delegation { @@ -39,7 +39,10 @@ impl Delegation { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationWithEverything.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationWithEverything.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct DelegationWithEverything { @@ -70,7 +73,10 @@ pub struct DelegationWithEverything { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationResult.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationResult.ts" + ) )] #[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug)] pub struct DelegationResult { @@ -82,7 +88,10 @@ pub struct DelegationResult { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationSummaryResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationSummaryResponse.ts" + ) )] #[derive(Deserialize, Serialize)] pub struct DelegationsSummaryResponse { diff --git a/common/types/src/deprecated.rs b/common/types/src/deprecated.rs index 8d74026b3a..09b7b210d5 100644 --- a/common/types/src/deprecated.rs +++ b/common/types/src/deprecated.rs @@ -11,7 +11,10 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts" + ) )] #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub enum DelegationEventKind { @@ -22,7 +25,10 @@ pub enum DelegationEventKind { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts" + ) )] #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub struct DelegationEvent { @@ -36,7 +42,10 @@ pub struct DelegationEvent { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/WrappedDelegationEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/WrappedDelegationEvent.ts" + ) )] #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub struct WrappedDelegationEvent { diff --git a/common/types/src/fees.rs b/common/types/src/fees.rs index c27b19fef5..23c5e322f7 100644 --- a/common/types/src/fees.rs +++ b/common/types/src/fees.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/FeeDetails.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/FeeDetails.ts") )] pub struct FeeDetails { // expected to be used by the wallet in order to display detailed fee information to the user @@ -30,14 +30,14 @@ pub mod ts_type_helpers { use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)] - #[ts(export_to = "ts-packages/types/src/types/rust/Fee.ts")] + #[ts(export, export_to = "ts-packages/types/src/types/rust/Fee.ts")] pub enum Fee { Manual(CosmosFee), Auto(Option), } #[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)] - #[ts(export_to = "ts-packages/types/src/types/rust/CosmosFee.ts")] + #[ts(export, export_to = "ts-packages/types/src/types/rust/CosmosFee.ts")] // this should corresponds to cosmrs::tx::Fee // IMPORTANT NOTE: this should work as of cosmrs 0.7.1 due to their `FromStr` implementations // on the type. The below struct might have to get readjusted if we update cosmrs!! @@ -50,7 +50,7 @@ pub mod ts_type_helpers { // Note: I've got a feeling this one will bite us hard at some point... #[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)] - #[ts(export_to = "ts-packages/types/src/types/rust/Coin.ts")] + #[ts(export, export_to = "ts-packages/types/src/types/rust/Coin.ts")] // this should corresponds to cosmrs::Coin // IMPORTANT NOTE: this should work as of cosmrs 0.7.1 due to their `FromStr` implementations // on the type. The below struct might have to get readjusted if we update cosmrs!! diff --git a/common/types/src/gas.rs b/common/types/src/gas.rs index 9c389cc863..5e67d81fda 100644 --- a/common/types/src/gas.rs +++ b/common/types/src/gas.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Gas.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Gas.ts") )] #[derive(Deserialize, Serialize, Copy, Clone, Debug)] pub struct Gas { @@ -36,7 +36,7 @@ impl From for Gas { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GasInfo.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/GasInfo.ts") )] #[derive(Deserialize, Serialize, Copy, Clone, Debug)] pub struct GasInfo { diff --git a/common/types/src/gateway.rs b/common/types/src/gateway.rs index 6be4e6939b..93127460b0 100644 --- a/common/types/src/gateway.rs +++ b/common/types/src/gateway.rs @@ -10,7 +10,7 @@ use std::fmt; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Gateway.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Gateway.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] pub struct Gateway { @@ -51,7 +51,7 @@ impl From for Gateway { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GatewayBond.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/GatewayBond.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct GatewayBond { diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index c06d0f9ece..187cddf5cd 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -17,7 +17,10 @@ use std::net::IpAddr; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeDetails.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixNodeDetails.ts" + ) )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeDetails { @@ -46,7 +49,7 @@ impl MixNodeDetails { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeBond.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/MixNodeBond.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { @@ -80,7 +83,10 @@ impl MixNodeBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeRewarding.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeRewarding.ts" + ) )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct NodeRewarding { @@ -126,7 +132,10 @@ impl NodeRewarding { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts" + ) )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct NodeCostParams { diff --git a/common/types/src/nym_node.rs b/common/types/src/nym_node.rs index 4b4e46212a..a842bbb788 100644 --- a/common/types/src/nym_node.rs +++ b/common/types/src/nym_node.rs @@ -15,7 +15,10 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NymNodeDetails.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NymNodeDetails.ts" + ) )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct NymNodeDetails { @@ -51,7 +54,7 @@ impl NymNodeDetails { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NymNodeBond.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/NymNodeBond.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct NymNodeBond { diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index a78ba568da..15866f6cad 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -17,7 +17,10 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingEpochEvent.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct PendingEpochEvent { @@ -42,7 +45,10 @@ impl PendingEpochEvent { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEventData.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingEpochEventData.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub enum PendingEpochEventData { @@ -125,7 +131,10 @@ impl PendingEpochEventData { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingIntervalEvent.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct PendingIntervalEvent { @@ -153,7 +162,10 @@ impl PendingIntervalEvent { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEventData.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingIntervalEventData.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub enum PendingIntervalEventData { diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs index 9179a0170b..7599e4799d 100644 --- a/common/types/src/transaction.rs +++ b/common/types/src/transaction.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/SendTxResult.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/SendTxResult.ts") )] #[derive(Deserialize, Serialize, Debug)] pub struct SendTxResult { @@ -38,7 +38,10 @@ impl SendTxResult { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/TransactionDetails.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/TransactionDetails.ts" + ) )] #[derive(Deserialize, Serialize, Debug)] pub struct TransactionDetails { @@ -60,7 +63,10 @@ impl TransactionDetails { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/TransactionExecuteResult.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/TransactionExecuteResult.ts" + ) )] #[derive(Deserialize, Serialize, Debug)] pub struct TransactionExecuteResult { @@ -89,7 +95,10 @@ impl TransactionExecuteResult { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RpcTransactionResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RpcTransactionResponse.ts" + ) )] #[derive(Deserialize, Serialize)] pub struct RpcTransactionResponse { diff --git a/common/types/src/vesting.rs b/common/types/src/vesting.rs index 1071b21324..6e62081327 100644 --- a/common/types/src/vesting.rs +++ b/common/types/src/vesting.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PledgeData.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/PledgeData.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct PledgeData { @@ -32,7 +32,10 @@ impl PledgeData { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/OriginalVestingResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/OriginalVestingResponse.ts" + ) )] #[derive(Serialize, Deserialize, Debug)] pub struct OriginalVestingResponse { @@ -57,7 +60,10 @@ impl OriginalVestingResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/VestingAccountInfo.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/VestingAccountInfo.ts" + ) )] #[derive(Serialize, Deserialize, Debug)] pub struct VestingAccountInfo { @@ -86,7 +92,10 @@ impl VestingAccountInfo { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/VestingPeriod.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/VestingPeriod.ts" + ) )] #[derive(Serialize, Deserialize, Debug)] pub struct VestingPeriod { diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 44279666e2..ce84b7a53e 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -69,7 +69,10 @@ impl Display for RequestError { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts" + ) )] #[serde(rename_all = "snake_case")] pub enum MixnodeStatus { @@ -88,7 +91,10 @@ impl MixnodeStatus { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts" + ) )] pub struct MixnodeCoreStatusResponse { pub mix_id: NodeId, @@ -99,7 +105,10 @@ pub struct MixnodeCoreStatusResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts" + ) )] pub struct GatewayCoreStatusResponse { pub identity: String, @@ -110,7 +119,10 @@ pub struct GatewayCoreStatusResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts" + ) )] pub struct MixnodeStatusResponse { pub status: MixnodeStatus, @@ -133,7 +145,10 @@ pub struct NodePerformance { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts" + ) )] pub struct NodeAnnotation { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] @@ -145,7 +160,10 @@ pub struct NodeAnnotation { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts" + ) )] pub struct AnnotationResponse { #[schema(value_type = u32)] @@ -157,7 +175,10 @@ pub struct AnnotationResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts" + ) )] pub struct NodePerformanceResponse { #[schema(value_type = u32)] @@ -169,7 +190,10 @@ pub struct NodePerformanceResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts" + ) )] pub struct NodeDatePerformanceResponse { #[schema(value_type = u32)] @@ -359,7 +383,10 @@ pub struct ComputeRewardEstParam { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts" + ) )] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct RewardEstimationResponse { @@ -391,7 +418,10 @@ pub struct GatewayUptimeResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts" + ) )] pub struct StakeSaturationResponse { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] @@ -410,7 +440,10 @@ pub type StakeSaturation = Decimal; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/SelectionChance.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/SelectionChance.ts" + ) )] pub enum SelectionChance { High, @@ -452,7 +485,10 @@ impl fmt::Display for SelectionChance { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts" + ) )] pub struct InclusionProbabilityResponse { pub in_active: SelectionChance, @@ -515,7 +551,10 @@ pub struct GatewayStatusReportResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts" + ) )] pub struct PerformanceHistoryResponse { #[schema(value_type = u32)] @@ -527,7 +566,10 @@ pub struct PerformanceHistoryResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts" + ) )] pub struct UptimeHistoryResponse { #[schema(value_type = u32)] @@ -539,7 +581,10 @@ pub struct UptimeHistoryResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts" + ) )] pub struct HistoricalUptimeResponse { #[schema(value_type = String, example = "1970-01-01")] @@ -554,7 +599,10 @@ pub struct HistoricalUptimeResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts" + ) )] pub struct HistoricalPerformanceResponse { #[schema(value_type = String, example = "1970-01-01")] @@ -792,7 +840,10 @@ impl NymNodeDescription { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts" + ) )] pub enum DescribedNodeType { LegacyMixnode, @@ -804,7 +855,10 @@ pub enum DescribedNodeType { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts" + ) )] pub struct DeclaredRoles { pub mixnode: bool, diff --git a/nym-api/nym-api-requests/src/pagination.rs b/nym-api/nym-api-requests/src/pagination.rs index 9a36caa988..43a3458db7 100644 --- a/nym-api/nym-api-requests/src/pagination.rs +++ b/nym-api/nym-api-requests/src/pagination.rs @@ -9,7 +9,7 @@ use utoipa::ToSchema; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Pagination.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Pagination.ts") )] pub struct Pagination { pub total: usize, @@ -21,7 +21,10 @@ pub struct Pagination { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PaginatedResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PaginatedResponse.ts" + ) )] pub struct PaginatedResponse { pub pagination: Pagination, diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 0e76cba619..4808f20b40 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/StateParams.ts") + ts(export, export_to = "nym-wallet/src/types/rust/StateParams.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriContractStateParams { @@ -27,7 +27,7 @@ pub struct TauriContractStateParams { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/OperatingCostRange.ts") + ts(export, export_to = "nym-wallet/src/types/rust/OperatingCostRange.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriOperatingCostRange { @@ -38,7 +38,7 @@ pub struct TauriOperatingCostRange { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/ProfitMarginRange.ts") + ts(export, export_to = "nym-wallet/src/types/rust/ProfitMarginRange.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriProfitMarginRange { diff --git a/nym-wallet/nym-wallet-types/src/interval.rs b/nym-wallet/nym-wallet-types/src/interval.rs index 875f343043..3d17ae4813 100644 --- a/nym-wallet/nym-wallet-types/src/interval.rs +++ b/nym-wallet/nym-wallet-types/src/interval.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/Interval.ts") + ts(export, export_to = "nym-wallet/src/types/rust/Interval.ts") )] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] pub struct Interval { diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 08b3da69b1..97948a4248 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -14,7 +14,7 @@ mod sandbox; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/Network.ts") + ts(export, export_to = "nym-wallet/src/types/rust/Network.ts") )] #[derive(Copy, Clone, Debug, Deserialize, EnumIter, Eq, Hash, PartialEq, Serialize)] pub enum Network { diff --git a/nym-wallet/nym-wallet-types/src/network_config.rs b/nym-wallet/nym-wallet-types/src/network_config.rs index 8d867d1768..06e68d7f28 100644 --- a/nym-wallet/nym-wallet-types/src/network_config.rs +++ b/nym-wallet/nym-wallet-types/src/network_config.rs @@ -5,7 +5,7 @@ use std::fmt; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/ValidatorUrls.ts") + ts(export, export_to = "nym-wallet/src/types/rust/ValidatorUrls.ts") )] #[derive(Debug, Serialize, Deserialize)] pub struct ValidatorUrls { diff --git a/nym-wallet/src/requests/queries.ts b/nym-wallet/src/requests/queries.ts index dbb7bb6e18..ddd163ed48 100644 --- a/nym-wallet/src/requests/queries.ts +++ b/nym-wallet/src/requests/queries.ts @@ -1,20 +1,20 @@ import { - DecCoin, - GatewayBond, - InclusionProbabilityResponse, - MixNodeDetails, - MixnodeStatusResponse, - PendingIntervalEvent, - RewardEstimationResponse, - StakeSaturationResponse, - WrappedDelegationEvent, - NymNodeDetails, + DecCoin, + GatewayBond, + InclusionProbabilityResponse, + MixNodeDetails, + MixnodeStatusResponse, + PendingIntervalEvent, + RewardEstimationResponse, + StakeSaturationResponse, + WrappedDelegationEvent, + NymNodeDetails, } from '@nymproject/types'; -import {Interval, TGatewayReport, TNodeDescription} from 'src/types'; -import {invokeWrapper} from './wrapper'; +import { Interval, TGatewayReport, TNodeDescription } from 'src/types'; +import { invokeWrapper } from './wrapper'; export const getAllPendingDelegations = async () => - invokeWrapper('get_pending_delegation_events'); + invokeWrapper('get_pending_delegation_events'); export const getMixnodeBondDetails = async () => invokeWrapper('mixnode_bond_details'); export const getGatewayBondDetails = async () => invokeWrapper('gateway_bond_details'); @@ -22,44 +22,44 @@ export const getNymNodeBondDetails = async () => invokeWrapper invokeWrapper('get_mixnode_avg_uptime'); export const getPendingOperatorRewards = async (address: string) => - invokeWrapper('get_pending_operator_rewards', {address}); + invokeWrapper('get_pending_operator_rewards', { address }); export const getMixnodeStakeSaturation = async (mixId: number) => - invokeWrapper('mixnode_stake_saturation', {mixId}); + invokeWrapper('mixnode_stake_saturation', { mixId }); export const getMixnodeRewardEstimation = async (mixId: number) => - invokeWrapper('mixnode_reward_estimation', {mixId}); + invokeWrapper('mixnode_reward_estimation', { mixId }); export const getMixnodeStatus = async (mixId: number) => - invokeWrapper('mixnode_status', {mixId}); + invokeWrapper('mixnode_status', { mixId }); export const checkMixnodeOwnership = async () => invokeWrapper('owns_mixnode'); export const checkGatewayOwnership = async () => invokeWrapper('owns_gateway'); export const getInclusionProbability = async (mixId: number) => - invokeWrapper('mixnode_inclusion_probability', {mixId}); + invokeWrapper('mixnode_inclusion_probability', { mixId }); export const getCurrentInterval = async () => invokeWrapper('get_current_interval'); export const getNumberOfMixnodeDelegators = async (mixId: number) => - invokeWrapper('get_number_of_mixnode_delegators', {mixId}); + invokeWrapper('get_number_of_mixnode_delegators', { mixId }); export const getNodeDescription = async (host: string, port: number) => - invokeWrapper('get_mix_node_description', {host, port}); + invokeWrapper('get_mix_node_description', { host, port }); export const getPendingIntervalEvents = async () => - invokeWrapper('get_pending_interval_events'); + invokeWrapper('get_pending_interval_events'); export const getGatewayReport = async (identity: string) => - invokeWrapper('gateway_report', {identity}); + invokeWrapper('gateway_report', { identity }); export const computeMixnodeRewardEstimation = async (args: { - mixId: number; - performance: string; - pledgeAmount: number; - totalDelegation: number; - profitMarginPercent: string; - intervalOperatingCost: { denom: 'unym'; amount: string }; + mixId: number; + performance: string; + pledgeAmount: number; + totalDelegation: number; + profitMarginPercent: string; + intervalOperatingCost: { denom: 'unym'; amount: string }; }) => invokeWrapper('compute_mixnode_reward_estimation', args); -export const getMixnodeUptime = async (mixId: number) => invokeWrapper('get_mixnode_uptime', {mixId}); +export const getMixnodeUptime = async (mixId: number) => invokeWrapper('get_mixnode_uptime', { mixId }); diff --git a/nym-wallet/src/types/rust/AppEnv.ts b/nym-wallet/src/types/rust/AppEnv.ts index 49ebfc27d1..bd276ebace 100644 --- a/nym-wallet/src/types/rust/AppEnv.ts +++ b/nym-wallet/src/types/rust/AppEnv.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface AppEnv { ADMIN_ADDRESS: string | null, SHOW_TERMINAL: string | null, ENABLE_QA_MODE: string | null, } \ No newline at end of file +export type AppEnv = { ADMIN_ADDRESS: string | null; SHOW_TERMINAL: string | null; ENABLE_QA_MODE: string | null }; diff --git a/nym-wallet/src/types/rust/AppVersion.ts b/nym-wallet/src/types/rust/AppVersion.ts index b0624301c1..e7d71f0832 100644 --- a/nym-wallet/src/types/rust/AppVersion.ts +++ b/nym-wallet/src/types/rust/AppVersion.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface AppVersion { current_version: string, latest_version: string, is_update_available: boolean, } \ No newline at end of file +export type AppVersion = { current_version: string; latest_version: string; is_update_available: boolean }; diff --git a/nym-wallet/src/types/rust/Interval.ts b/nym-wallet/src/types/rust/Interval.ts index ab2c4c4bc9..f5e1a13161 100644 --- a/nym-wallet/src/types/rust/Interval.ts +++ b/nym-wallet/src/types/rust/Interval.ts @@ -1,3 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface Interval { id: number, epochs_in_interval: number, current_epoch_start_unix: bigint, current_epoch_id: number, epoch_length_seconds: bigint, total_elapsed_epochs: number, } \ No newline at end of file +export type Interval = { + id: number; + epochs_in_interval: number; + current_epoch_start_unix: bigint; + current_epoch_id: number; + epoch_length_seconds: bigint; + total_elapsed_epochs: number; +}; diff --git a/nym-wallet/src/types/rust/Network.ts b/nym-wallet/src/types/rust/Network.ts index 03c73643d6..d55f0bce67 100644 --- a/nym-wallet/src/types/rust/Network.ts +++ b/nym-wallet/src/types/rust/Network.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Network = "QA" | "SANDBOX" | "MAINNET"; \ No newline at end of file +export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; diff --git a/nym-wallet/src/types/rust/OperatingCostRange.ts b/nym-wallet/src/types/rust/OperatingCostRange.ts index 34ea2f65a4..7c87421672 100644 --- a/nym-wallet/src/types/rust/OperatingCostRange.ts +++ b/nym-wallet/src/types/rust/OperatingCostRange.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DecCoin } from "../../../../ts-packages/types/src/types/rust/DecCoin"; +import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; -export interface TauriOperatingCostRange { minimum: DecCoin, maximum: DecCoin, } \ No newline at end of file +export type TauriOperatingCostRange = { minimum: DecCoin; maximum: DecCoin }; diff --git a/nym-wallet/src/types/rust/ProfitMarginRange.ts b/nym-wallet/src/types/rust/ProfitMarginRange.ts index 631a4e1097..d177c98d42 100644 --- a/nym-wallet/src/types/rust/ProfitMarginRange.ts +++ b/nym-wallet/src/types/rust/ProfitMarginRange.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface TauriProfitMarginRange { minimum: string, maximum: string, } \ No newline at end of file +export type TauriProfitMarginRange = { minimum: string; maximum: string }; diff --git a/nym-wallet/src/types/rust/StateParams.ts b/nym-wallet/src/types/rust/StateParams.ts index aa6eb0366a..ebf1f33c56 100644 --- a/nym-wallet/src/types/rust/StateParams.ts +++ b/nym-wallet/src/types/rust/StateParams.ts @@ -1,6 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DecCoin } from "../../../../ts-packages/types/src/types/rust/DecCoin"; -import type { TauriOperatingCostRange } from "./OperatingCostRange"; -import type { TauriProfitMarginRange } from "./ProfitMarginRange"; +import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; +import type { TauriOperatingCostRange } from './OperatingCostRange'; +import type { TauriProfitMarginRange } from './ProfitMarginRange'; -export interface TauriContractStateParams { minimum_pledge: DecCoin, minimum_delegation: DecCoin | null, operating_cost: TauriOperatingCostRange, profit_margin: TauriProfitMarginRange, } \ No newline at end of file +export type TauriContractStateParams = { + minimum_pledge: DecCoin; + minimum_delegation: DecCoin | null; + operating_cost: TauriOperatingCostRange; + profit_margin: TauriProfitMarginRange; +}; diff --git a/nym-wallet/src/types/rust/ValidatorUrl.ts b/nym-wallet/src/types/rust/ValidatorUrl.ts index 7e94537f97..300941995e 100644 --- a/nym-wallet/src/types/rust/ValidatorUrl.ts +++ b/nym-wallet/src/types/rust/ValidatorUrl.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface ValidatorUrl { url: string, name: string | null, } \ No newline at end of file +export type ValidatorUrl = { url: string; name: string | null }; diff --git a/nym-wallet/src/types/rust/ValidatorUrls.ts b/nym-wallet/src/types/rust/ValidatorUrls.ts index 9a579b2f41..a0eeac8b94 100644 --- a/nym-wallet/src/types/rust/ValidatorUrls.ts +++ b/nym-wallet/src/types/rust/ValidatorUrls.ts @@ -1,4 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ValidatorUrl } from "./ValidatorUrl"; +import type { ValidatorUrl } from './ValidatorUrl'; -export interface ValidatorUrls { urls: Array, } \ No newline at end of file +export type Validator = { nyxd_url: string; nyxd_name: string | null; api_url: string | null }; + +export type ValidatorUrls = { urls: Array }; diff --git a/tools/ts-rs-cli/Cargo.toml b/tools/ts-rs-cli/Cargo.toml index 66ee5e5543..6c75876550 100644 --- a/tools/ts-rs-cli/Cargo.toml +++ b/tools/ts-rs-cli/Cargo.toml @@ -15,7 +15,7 @@ nym-validator-client = { path = "../../common/client-libs/validator-client", fea "generate-ts" ] } nym-api-requests = { path = "../../nym-api/nym-api-requests", features = ["generate-ts"] } -nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = [ "generate-ts" ]} -nym-vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", features = [ "generate-ts" ] } -nym-types = { path = "../../common/types", features = [ "generate-ts" ] } -nym-wallet-types = { path = "../../nym-wallet/nym-wallet-types", features = [ "generate-ts" ] } +nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["generate-ts"] } +nym-vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", features = ["generate-ts"] } +nym-types = { path = "../../common/types", features = ["generate-ts"] } +nym-wallet-types = { path = "../../nym-wallet/nym-wallet-types", features = ["generate-ts"] } diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index a876faf637..dfd7d0bdf9 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -60,7 +60,7 @@ macro_rules! do_export { }}; } -fn main() { +fn main() -> anyhow::Result<()> { println!("Starting export of types using ts-rs..."); println!(); @@ -175,7 +175,10 @@ fn main() { println!(); println!("Moving output files into place..."); - for file in WalkDir::new("./") + let source = Path::new("./bindings"); + + // move from ./bindings/foo/bar/X.ts into ../../foo/bar/X.ts + for file in WalkDir::new(source) .into_iter() .filter_map(|file| file.ok()) .filter(|f| { @@ -191,7 +194,9 @@ fn main() { { // construct the source and destination paths that can be used to replace the output file let src = file.path(); - let dst = dst_base.join(src); + let sourceless_src = src.strip_prefix(source)?; + + let dst = dst_base.join(sourceless_src); let dst_directory = dst.parent().expect("Could not get parent directory"); if !dst_directory.exists() { @@ -208,15 +213,19 @@ fn main() { println!("✅ {} => {}", file.path().display(), res.display()); } Err(e) => { - println!("❌ {}: {}", file.path().display(), e); + println!("❌ {}: {e}", file.path().display()); } }, Err(e) => { - println!("❌ {}: {}", file.path().display(), e); + println!("❌ {}: {e}", file.path().display()); } } } + // finally remove the ephemeral dir + std::fs::remove_dir_all(source)?; + println!(); println!("Done"); + Ok(()) } diff --git a/ts-packages/types/src/types/rust/Account.ts b/ts-packages/types/src/types/rust/Account.ts index da0afb666e..33471c6846 100644 --- a/ts-packages/types/src/types/rust/Account.ts +++ b/ts-packages/types/src/types/rust/Account.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CurrencyDenom } from './CurrencyDenom'; -export interface Account { - client_address: string; - base_mix_denom: string; - display_mix_denom: CurrencyDenom; -} +export type Account = { client_address: string; base_mix_denom: string; display_mix_denom: CurrencyDenom }; diff --git a/ts-packages/types/src/types/rust/AccountEntry.ts b/ts-packages/types/src/types/rust/AccountEntry.ts index 472a9befcd..f10dc906df 100644 --- a/ts-packages/types/src/types/rust/AccountEntry.ts +++ b/ts-packages/types/src/types/rust/AccountEntry.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface AccountEntry { - id: string; - address: string; -} +export type AccountEntry = { id: string; address: string }; diff --git a/ts-packages/types/src/types/rust/AccountWithMnemonic.ts b/ts-packages/types/src/types/rust/AccountWithMnemonic.ts index edbf9b9de6..df7450403a 100644 --- a/ts-packages/types/src/types/rust/AccountWithMnemonic.ts +++ b/ts-packages/types/src/types/rust/AccountWithMnemonic.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Account } from './Account'; -export interface AccountWithMnemonic { - account: Account; - mnemonic: string; -} +export type AccountWithMnemonic = { account: Account; mnemonic: string }; diff --git a/ts-packages/types/src/types/rust/ActiveSetUpdate.ts b/ts-packages/types/src/types/rust/ActiveSetUpdate.ts new file mode 100644 index 0000000000..8bf64f0368 --- /dev/null +++ b/ts-packages/types/src/types/rust/ActiveSetUpdate.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Specification on how the active set should be updated. + */ +export type ActiveSetUpdate = { + /** + * The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + */ + entry_gateways: number; + /** + * The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + */ + exit_gateways: number; + /** + * The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + */ + mixnodes: number; +}; diff --git a/ts-packages/types/src/types/rust/AnnotationResponse.ts b/ts-packages/types/src/types/rust/AnnotationResponse.ts new file mode 100644 index 0000000000..a344a73347 --- /dev/null +++ b/ts-packages/types/src/types/rust/AnnotationResponse.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NodeAnnotation } from './NodeAnnotation'; + +export type AnnotationResponse = { node_id: number; annotation: NodeAnnotation | null }; diff --git a/ts-packages/types/src/types/rust/Balance.ts b/ts-packages/types/src/types/rust/Balance.ts index fe11917327..6d3d51b5a4 100644 --- a/ts-packages/types/src/types/rust/Balance.ts +++ b/ts-packages/types/src/types/rust/Balance.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; -export interface Balance { - amount: DecCoin; - printable_balance: string; -} +export type Balance = { amount: DecCoin; printable_balance: string }; diff --git a/ts-packages/types/src/types/rust/Coin.ts b/ts-packages/types/src/types/rust/Coin.ts index 493c2e6b41..be2edc24fa 100644 --- a/ts-packages/types/src/types/rust/Coin.ts +++ b/ts-packages/types/src/types/rust/Coin.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface Coin { - denom: string; - amount: bigint; -} +export type Coin = { denom: string; amount: bigint }; diff --git a/ts-packages/types/src/types/rust/CosmosFee.ts b/ts-packages/types/src/types/rust/CosmosFee.ts index d77c4d3dd8..a741cbce59 100644 --- a/ts-packages/types/src/types/rust/CosmosFee.ts +++ b/ts-packages/types/src/types/rust/CosmosFee.ts @@ -1,9 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Coin } from './Coin'; -export interface CosmosFee { - amount: Array; - gas_limit: bigint; - payer: string | null; - granter: string | null; -} +export type CosmosFee = { amount: Array; gas_limit: bigint; payer: string | null; granter: string | null }; diff --git a/ts-packages/types/src/types/rust/DeclaredRoles.ts b/ts-packages/types/src/types/rust/DeclaredRoles.ts new file mode 100644 index 0000000000..6675391d48 --- /dev/null +++ b/ts-packages/types/src/types/rust/DeclaredRoles.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DeclaredRoles = { mixnode: boolean; entry: boolean; exit_nr: boolean; exit_ipr: boolean }; diff --git a/ts-packages/types/src/types/rust/Delegation.ts b/ts-packages/types/src/types/rust/Delegation.ts index 58de13e6cc..bef093e881 100644 --- a/ts-packages/types/src/types/rust/Delegation.ts +++ b/ts-packages/types/src/types/rust/Delegation.ts @@ -1,10 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; -export interface Delegation { - owner: string; - mix_id: number; - amount: DecCoin; - height: bigint; - proxy: string | null; -} +export type Delegation = { owner: string; mix_id: number; amount: DecCoin; height: bigint; proxy: string | null }; diff --git a/ts-packages/types/src/types/rust/DelegationEvent.ts b/ts-packages/types/src/types/rust/DelegationEvent.ts index 30ff21943f..4891306e80 100644 --- a/ts-packages/types/src/types/rust/DelegationEvent.ts +++ b/ts-packages/types/src/types/rust/DelegationEvent.ts @@ -2,10 +2,10 @@ import type { DecCoin } from './DecCoin'; import type { DelegationEventKind } from './DelegationEventKind'; -export interface DelegationEvent { +export type DelegationEvent = { kind: DelegationEventKind; mix_id: number; address: string; amount: DecCoin | null; proxy: string | null; -} +}; diff --git a/ts-packages/types/src/types/rust/DelegationResult.ts b/ts-packages/types/src/types/rust/DelegationResult.ts index d8f8a77ce3..765e11e98b 100644 --- a/ts-packages/types/src/types/rust/DelegationResult.ts +++ b/ts-packages/types/src/types/rust/DelegationResult.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; -export interface DelegationResult { - source_address: string; - target_address: string; - amount: DecCoin | null; -} +export type DelegationResult = { source_address: string; target_address: string; amount: DecCoin | null }; diff --git a/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts b/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts index 21e85d513b..bb86fd9ced 100644 --- a/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts +++ b/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts @@ -2,8 +2,8 @@ import type { DecCoin } from './DecCoin'; import type { DelegationWithEverything } from './DelegationWithEverything'; -export interface DelegationsSummaryResponse { +export type DelegationsSummaryResponse = { delegations: Array; total_delegations: DecCoin; total_rewards: DecCoin; -} +}; diff --git a/ts-packages/types/src/types/rust/DelegationWithEverything.ts b/ts-packages/types/src/types/rust/DelegationWithEverything.ts index 7071f291dd..a37755f1a7 100644 --- a/ts-packages/types/src/types/rust/DelegationWithEverything.ts +++ b/ts-packages/types/src/types/rust/DelegationWithEverything.ts @@ -1,9 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; import type { DelegationEvent } from './DelegationEvent'; -import type { MixNodeCostParams } from './MixNodeCostParams'; +import type { NodeCostParams } from './MixNodeCostParams'; -export interface DelegationWithEverything { +export type DelegationWithEverything = { owner: string; mix_id: number; node_identity: string; @@ -12,12 +12,12 @@ export interface DelegationWithEverything { accumulated_by_operator: DecCoin | null; block_height: bigint; delegated_on_iso_datetime: string | null; - cost_params: MixNodeCostParams | null; + cost_params: NodeCostParams | null; avg_uptime_percent: number | null; stake_saturation: string | null; uses_vesting_contract_tokens: boolean; unclaimed_rewards: DecCoin | null; + errors: string | null; pending_events: Array; mixnode_is_unbonding: boolean | null; - errors: string | null; -} +}; diff --git a/ts-packages/types/src/types/rust/DescribedNodeType.ts b/ts-packages/types/src/types/rust/DescribedNodeType.ts new file mode 100644 index 0000000000..3cfc95f140 --- /dev/null +++ b/ts-packages/types/src/types/rust/DescribedNodeType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DescribedNodeType = 'legacy_mixnode' | 'legacy_gateway' | 'nym_node'; diff --git a/ts-packages/types/src/types/rust/Gas.ts b/ts-packages/types/src/types/rust/Gas.ts index 2b93e47509..0329a5df89 100644 --- a/ts-packages/types/src/types/rust/Gas.ts +++ b/ts-packages/types/src/types/rust/Gas.ts @@ -1,5 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface Gas { +export type Gas = { + /** + * units of gas used + */ gas_units: bigint; -} +}; diff --git a/ts-packages/types/src/types/rust/GasInfo.ts b/ts-packages/types/src/types/rust/GasInfo.ts index 3e34323018..4ee0a2431e 100644 --- a/ts-packages/types/src/types/rust/GasInfo.ts +++ b/ts-packages/types/src/types/rust/GasInfo.ts @@ -1,7 +1,13 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Gas } from './Gas'; -export interface GasInfo { +export type GasInfo = { + /** + * GasWanted is the maximum units of work we allow this tx to perform. + */ gas_wanted: Gas; + /** + * GasUsed is the amount of gas actually consumed. + */ gas_used: Gas; -} +}; diff --git a/ts-packages/types/src/types/rust/Gateway.ts b/ts-packages/types/src/types/rust/Gateway.ts index 4c449b578d..3a9da4f7a7 100644 --- a/ts-packages/types/src/types/rust/Gateway.ts +++ b/ts-packages/types/src/types/rust/Gateway.ts @@ -1,11 +1,14 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface Gateway { +export type Gateway = { host: string; mix_port: number; clients_port: number; location: string; sphinx_key: string; + /** + * Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients + */ identity_key: string; version: string; -} +}; diff --git a/ts-packages/types/src/types/rust/GatewayBond.ts b/ts-packages/types/src/types/rust/GatewayBond.ts index 405c1cab92..a8f619b5b4 100644 --- a/ts-packages/types/src/types/rust/GatewayBond.ts +++ b/ts-packages/types/src/types/rust/GatewayBond.ts @@ -2,10 +2,10 @@ import type { DecCoin } from './DecCoin'; import type { Gateway } from './Gateway'; -export interface GatewayBond { +export type GatewayBond = { pledge_amount: DecCoin; owner: string; block_height: bigint; gateway: Gateway; proxy: string | null; -} +}; diff --git a/ts-packages/types/src/types/rust/GatewayConfigUpdate.ts b/ts-packages/types/src/types/rust/GatewayConfigUpdate.ts index acd40d4421..623a7fa638 100644 --- a/ts-packages/types/src/types/rust/GatewayConfigUpdate.ts +++ b/ts-packages/types/src/types/rust/GatewayConfigUpdate.ts @@ -1,9 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface GatewayConfigUpdate { +export type GatewayConfigUpdate = { host: string; mix_port: number; clients_port: number; location: string; version: string; -} +}; diff --git a/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts b/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts index 0316e2a889..57ff4032f5 100644 --- a/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts +++ b/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface GatewayCoreStatusResponse { - identity: string; - count: number; -} +export type GatewayCoreStatusResponse = { identity: string; count: number }; diff --git a/ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts b/ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts new file mode 100644 index 0000000000..9df1733a85 --- /dev/null +++ b/ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HistoricalPerformanceResponse = { date: string; performance: number }; diff --git a/ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts b/ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts new file mode 100644 index 0000000000..7bd83ffeda --- /dev/null +++ b/ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HistoricalUptimeResponse = { date: string; uptime: number }; diff --git a/ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts b/ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts index 3f80b794a0..09c97a0f6d 100644 --- a/ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts +++ b/ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { SelectionChance } from './SelectionChance'; -export interface InclusionProbabilityResponse { - in_active: SelectionChance; - in_reserve: SelectionChance; -} +export type InclusionProbabilityResponse = { in_active: SelectionChance; in_reserve: SelectionChance }; diff --git a/ts-packages/types/src/types/rust/Interval.ts b/ts-packages/types/src/types/rust/Interval.ts index a451f49de1..61863be4ca 100644 --- a/ts-packages/types/src/types/rust/Interval.ts +++ b/ts-packages/types/src/types/rust/Interval.ts @@ -1,10 +1,31 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface Interval { +/** + * Specification of a rewarding interval. + */ +export type Interval = { + /** + * Monotonously increasing id of this interval. + */ id: number; + /** + * Number of epochs in this interval. + */ epochs_in_interval: number; + /** + * The timestamp indicating the start of the current rewarding epoch. + */ current_epoch_start: string; + /** + * Monotonously increasing id of the current epoch in this interval. + */ current_epoch_id: number; + /** + * The duration of all epochs in this interval. + */ epoch_length: { secs: number; nanos: number }; + /** + * The total amount of elapsed epochs since the first epoch of the first interval. + */ total_elapsed_epochs: number; -} +}; diff --git a/ts-packages/types/src/types/rust/IntervalRewardParams.ts b/ts-packages/types/src/types/rust/IntervalRewardParams.ts index a32e967c6a..35984285e5 100644 --- a/ts-packages/types/src/types/rust/IntervalRewardParams.ts +++ b/ts-packages/types/src/types/rust/IntervalRewardParams.ts @@ -1,12 +1,52 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface IntervalRewardParams { +/** + * Parameters required by the mix-mining reward distribution that do not change during an interval. + */ +export type IntervalRewardParams = { + /** + * Current value of the rewarding pool. + * It is expected to be constant throughout the interval. + */ reward_pool: string; + /** + * Current value of the staking supply. + * It is expected to be constant throughout the interval. + */ staking_supply: string; + /** + * Defines the percentage of stake needed to reach saturation for all of the nodes in the rewarded set. + * Also known as `beta`. + */ staking_supply_scale_factor: string; + /** + * Current value of the computed reward budget per epoch, per node. + * It is expected to be constant throughout the interval. + */ epoch_reward_budget: string; + /** + * Current value of the stake saturation point. + * It is expected to be constant throughout the interval. + */ stake_saturation_point: string; + /** + * Current value of the sybil resistance percent (`alpha`). + * It is not really expected to be changing very often. + * As a matter of fact, unless there's a very specific reason, it should remain constant. + */ sybil_resistance: string; + /** + * Current active set work factor. + * It is not really expected to be changing very often. + * As a matter of fact, unless there's a very specific reason, it should remain constant. + */ active_set_work_factor: string; + /** + * Current maximum interval pool emission. + * Assuming all nodes in the rewarded set are fully saturated and have 100% performance, + * this % of the reward pool would get distributed in rewards to all operators and its delegators. + * It is not really expected to be changing very often. + * As a matter of fact, unless there's a very specific reason, it should remain constant. + */ interval_pool_emission: string; -} +}; diff --git a/ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts b/ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts index fffaa608d1..f3cc67df7c 100644 --- a/ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts +++ b/ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts @@ -1,11 +1,36 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RewardedSetParams } from './RewardedSetParams'; -export interface IntervalRewardingParamsUpdate { +/** + * Specification on how the rewarding params should be updated. + */ +export type IntervalRewardingParamsUpdate = { + /** + * Defines the new value of the reward pool. + */ reward_pool: string | null; + /** + * Defines the new value of the staking supply. + */ staking_supply: string | null; + /** + * Defines the new value of the staking supply scale factor. + */ staking_supply_scale_factor: string | null; + /** + * Defines the new value of the sybil resistance percent. + */ sybil_resistance_percent: string | null; + /** + * Defines the new value of the active set work factor. + */ active_set_work_factor: string | null; + /** + * Defines the new value of the interval pool emission rate. + */ interval_pool_emission: string | null; - rewarded_set_size: number | null; -} + /** + * Defines the parameters of the rewarded set. + */ + rewarded_set_params: RewardedSetParams | null; +}; diff --git a/ts-packages/types/src/types/rust/MixNodeBond.ts b/ts-packages/types/src/types/rust/MixNodeBond.ts index 173ce9599d..3034b30398 100644 --- a/ts-packages/types/src/types/rust/MixNodeBond.ts +++ b/ts-packages/types/src/types/rust/MixNodeBond.ts @@ -2,13 +2,12 @@ import type { DecCoin } from './DecCoin'; import type { MixNode } from './Mixnode'; -export interface MixNodeBond { +export type MixNodeBond = { mix_id: number; owner: string; original_pledge: DecCoin; - layer: string; mix_node: MixNode; proxy: string | null; bonding_height: bigint; is_unbonding: boolean; -} +}; diff --git a/ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts b/ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts index 7a88608543..60cef5816f 100644 --- a/ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts +++ b/ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts @@ -1,9 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface MixNodeConfigUpdate { +export type MixNodeConfigUpdate = { host: string; mix_port: number; verloc_port: number; http_api_port: number; version: string; -} +}; diff --git a/ts-packages/types/src/types/rust/MixNodeCostParams.ts b/ts-packages/types/src/types/rust/MixNodeCostParams.ts index fac1462984..4c65d08efc 100644 --- a/ts-packages/types/src/types/rust/MixNodeCostParams.ts +++ b/ts-packages/types/src/types/rust/MixNodeCostParams.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; -export interface MixNodeCostParams { - profit_margin_percent: string; - interval_operating_cost: DecCoin; -} +export type NodeCostParams = { profit_margin_percent: string; interval_operating_cost: DecCoin }; diff --git a/ts-packages/types/src/types/rust/MixNodeDetails.ts b/ts-packages/types/src/types/rust/MixNodeDetails.ts index 2e06ebf61f..81d0f1dbe9 100644 --- a/ts-packages/types/src/types/rust/MixNodeDetails.ts +++ b/ts-packages/types/src/types/rust/MixNodeDetails.ts @@ -1,8 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { MixNodeBond } from './MixNodeBond'; -import type { MixNodeRewarding } from './MixNodeRewarding'; +import type { NodeRewarding } from './NodeRewarding'; -export interface MixNodeDetails { - bond_information: MixNodeBond; - rewarding_details: MixNodeRewarding; -} +export type MixNodeDetails = { bond_information: MixNodeBond; rewarding_details: NodeRewarding }; diff --git a/ts-packages/types/src/types/rust/Mixnode.ts b/ts-packages/types/src/types/rust/Mixnode.ts index 8dddce4eb2..2456af17ab 100644 --- a/ts-packages/types/src/types/rust/Mixnode.ts +++ b/ts-packages/types/src/types/rust/Mixnode.ts @@ -1,11 +1,35 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface MixNode { +/** + * Information provided by the node operator during bonding that are used to allow other entities to use the services of this node. + */ +export type MixNode = { + /** + * Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com + */ host: string; + /** + * Port used by this mixnode for listening for mix packets. + */ mix_port: number; + /** + * Port used by this mixnode for listening for verloc requests. + */ verloc_port: number; + /** + * Port used by this mixnode for its http(s) API + */ http_api_port: number; + /** + * Base58-encoded x25519 public key used for sphinx key derivation. + */ sphinx_key: string; + /** + * Base58-encoded ed25519 EdDSA public key. + */ identity_key: string; + /** + * The self-reported semver version of this mixnode. + */ version: string; -} +}; diff --git a/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts b/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts index d5fb3fbcbb..c49b713e97 100644 --- a/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts +++ b/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface MixnodeCoreStatusResponse { - mix_id: number; - count: number; -} +export type MixnodeCoreStatusResponse = { mix_id: number; count: number }; diff --git a/ts-packages/types/src/types/rust/MixnodeStatusResponse.ts b/ts-packages/types/src/types/rust/MixnodeStatusResponse.ts index 2dd6bb6f54..7d680a2f32 100644 --- a/ts-packages/types/src/types/rust/MixnodeStatusResponse.ts +++ b/ts-packages/types/src/types/rust/MixnodeStatusResponse.ts @@ -1,6 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { MixnodeStatus } from './MixnodeStatus'; -export interface MixnodeStatusResponse { - status: MixnodeStatus; -} +export type MixnodeStatusResponse = { status: MixnodeStatus }; diff --git a/ts-packages/types/src/types/rust/NodeAnnotation.ts b/ts-packages/types/src/types/rust/NodeAnnotation.ts new file mode 100644 index 0000000000..28d5e5cfca --- /dev/null +++ b/ts-packages/types/src/types/rust/NodeAnnotation.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Role } from './Role'; + +export type NodeAnnotation = { last_24h_performance: string; current_role: Role | null }; diff --git a/ts-packages/types/src/types/rust/NodeConfigUpdate.ts b/ts-packages/types/src/types/rust/NodeConfigUpdate.ts new file mode 100644 index 0000000000..bb512a7fde --- /dev/null +++ b/ts-packages/types/src/types/rust/NodeConfigUpdate.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NodeConfigUpdate = { + host: string | null; + custom_http_port: number | null; + restore_default_http_port: boolean; +}; diff --git a/ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts b/ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts new file mode 100644 index 0000000000..12c85fe364 --- /dev/null +++ b/ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NodeDatePerformanceResponse = { node_id: number; date: string; performance: number | null }; diff --git a/ts-packages/types/src/types/rust/NodePerformanceResponse.ts b/ts-packages/types/src/types/rust/NodePerformanceResponse.ts new file mode 100644 index 0000000000..37ee3c165d --- /dev/null +++ b/ts-packages/types/src/types/rust/NodePerformanceResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NodePerformanceResponse = { node_id: number; performance: number | null }; diff --git a/ts-packages/types/src/types/rust/NodeRewarding.ts b/ts-packages/types/src/types/rust/NodeRewarding.ts index 3fa37c30bd..fd3273c8c2 100644 --- a/ts-packages/types/src/types/rust/NodeRewarding.ts +++ b/ts-packages/types/src/types/rust/NodeRewarding.ts @@ -1,4 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NodeCostParams } from "./MixNodeCostParams"; +import type { NodeCostParams } from './MixNodeCostParams'; -export interface NodeRewarding { cost_params: NodeCostParams, operator: string, delegates: string, total_unit_reward: string, unit_delegation: string, last_rewarded_epoch: number, unique_delegations: number, } \ No newline at end of file +export type NodeRewarding = { + cost_params: NodeCostParams; + operator: string; + delegates: string; + total_unit_reward: string; + unit_delegation: string; + last_rewarded_epoch: number; + unique_delegations: number; +}; diff --git a/ts-packages/types/src/types/rust/NodeRewardingParameters.ts b/ts-packages/types/src/types/rust/NodeRewardingParameters.ts new file mode 100644 index 0000000000..25dff5a2a6 --- /dev/null +++ b/ts-packages/types/src/types/rust/NodeRewardingParameters.ts @@ -0,0 +1,16 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters used for rewarding particular node. + */ +export type NodeRewardingParameters = { + /** + * Performance of the particular node in the current epoch. + */ + performance: string; + /** + * Amount of work performed by this node in the current epoch + * also known as 'omega' in the paper + */ + work_factor: string; +}; diff --git a/ts-packages/types/src/types/rust/NymNode.ts b/ts-packages/types/src/types/rust/NymNode.ts index 78bddc8007..4836322136 100644 --- a/ts-packages/types/src/types/rust/NymNode.ts +++ b/ts-packages/types/src/types/rust/NymNode.ts @@ -1,3 +1,21 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface NymNode { host: string, custom_http_port: number | null, identity_key: string, } \ No newline at end of file +/** + * Information provided by the node operator during bonding that are used to allow other entities to use the services of this node. + */ +export type NymNode = { + /** + * Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com + * that is used to discover other capabilities of this node. + */ + host: string; + /** + * Allow specifying custom port for accessing the http, and thus self-described, api + * of this node for the capabilities discovery. + */ + custom_http_port: number | null; + /** + * Base58-encoded ed25519 EdDSA public key. + */ + identity_key: string; +}; diff --git a/ts-packages/types/src/types/rust/NymNodeBond.ts b/ts-packages/types/src/types/rust/NymNodeBond.ts index b8f608f2c7..545c674c55 100644 --- a/ts-packages/types/src/types/rust/NymNodeBond.ts +++ b/ts-packages/types/src/types/rust/NymNodeBond.ts @@ -1,4 +1,40 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DecCoin } from "./DecCoin"; +import type { DecCoin } from './DecCoin'; -export interface NymNodeBond { node_id: number, owner: string, original_pledge: DecCoin, bonding_height: bigint, is_unbonding: boolean, host: string, custom_http_port: number | null, identity_key: string, } \ No newline at end of file +export type NymNodeBond = { + /** + * Unique id assigned to the bonded node. + */ + node_id: number; + /** + * Address of the owner of this nym-node. + */ + owner: string; + /** + * Original amount pledged by the operator of this node. + */ + original_pledge: DecCoin; + /** + * Block height at which this nym-node has been bonded. + */ + bonding_height: bigint; + /** + * Flag to indicate whether this node is in the process of unbonding, + * that will conclude upon the epoch finishing. + */ + is_unbonding: boolean; + /** + * Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com + * that is used to discover other capabilities of this node. + */ + host: string; + /** + * Allow specifying custom port for accessing the http, and thus self-described, api + * of this node for the capabilities discovery. + */ + custom_http_port: number | null; + /** + * Base58-encoded ed25519 EdDSA public key. + */ + identity_key: string; +}; diff --git a/ts-packages/types/src/types/rust/NymNodeDetails.ts b/ts-packages/types/src/types/rust/NymNodeDetails.ts index 7b6c0b5f2a..5bebd8492e 100644 --- a/ts-packages/types/src/types/rust/NymNodeDetails.ts +++ b/ts-packages/types/src/types/rust/NymNodeDetails.ts @@ -1,6 +1,22 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NodeRewarding } from "./NodeRewarding"; -import type { NymNodeBond } from "./NymNodeBond"; -import type { PendingNodeChanges } from "./PendingNodeChanges"; +import type { NodeRewarding } from './NodeRewarding'; +import type { NymNodeBond } from './NymNodeBond'; +import type { PendingNodeChanges } from './PendingNodeChanges'; -export interface NymNodeDetails { bond_information: NymNodeBond, rewarding_details: NodeRewarding, pending_changes: PendingNodeChanges, } \ No newline at end of file +/** + * Full details associated with given node. + */ +export type NymNodeDetails = { + /** + * Basic bond information of this node, such as owner address, original pledge, etc. + */ + bond_information: NymNodeBond; + /** + * Details used for computation of rewarding related data. + */ + rewarding_details: NodeRewarding; + /** + * Adjustments to the node that are scheduled to happen during future epoch/interval transitions. + */ + pending_changes: PendingNodeChanges; +}; diff --git a/ts-packages/types/src/types/rust/OriginalVestingResponse.ts b/ts-packages/types/src/types/rust/OriginalVestingResponse.ts index ea10cd24af..ffd03fe975 100644 --- a/ts-packages/types/src/types/rust/OriginalVestingResponse.ts +++ b/ts-packages/types/src/types/rust/OriginalVestingResponse.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; -export interface OriginalVestingResponse { - amount: DecCoin; - number_of_periods: number; - period_duration: bigint; -} +export type OriginalVestingResponse = { amount: DecCoin; number_of_periods: number; period_duration: bigint }; diff --git a/ts-packages/types/src/types/rust/PaginatedResponse.ts b/ts-packages/types/src/types/rust/PaginatedResponse.ts new file mode 100644 index 0000000000..7b64cdbb4d --- /dev/null +++ b/ts-packages/types/src/types/rust/PaginatedResponse.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Pagination } from './Pagination'; + +export type PaginatedResponse = { pagination: Pagination; data: Array }; diff --git a/ts-packages/types/src/types/rust/Pagination.ts b/ts-packages/types/src/types/rust/Pagination.ts new file mode 100644 index 0000000000..963ec90ae2 --- /dev/null +++ b/ts-packages/types/src/types/rust/Pagination.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Pagination = { total: number; page: number; size: number }; diff --git a/ts-packages/types/src/types/rust/PendingEpochEvent.ts b/ts-packages/types/src/types/rust/PendingEpochEvent.ts index 814934b75b..8b937f2164 100644 --- a/ts-packages/types/src/types/rust/PendingEpochEvent.ts +++ b/ts-packages/types/src/types/rust/PendingEpochEvent.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { PendingEpochEventData } from './PendingEpochEventData'; -export interface PendingEpochEvent { - id: number; - created_at: bigint; - event: PendingEpochEventData; -} +export type PendingEpochEvent = { id: number; created_at: bigint; event: PendingEpochEventData }; diff --git a/ts-packages/types/src/types/rust/PendingEpochEventData.ts b/ts-packages/types/src/types/rust/PendingEpochEventData.ts index 0b66138efc..9fc3c9da4c 100644 --- a/ts-packages/types/src/types/rust/PendingEpochEventData.ts +++ b/ts-packages/types/src/types/rust/PendingEpochEventData.ts @@ -5,5 +5,6 @@ export type PendingEpochEventData = | { Delegate: { owner: string; mix_id: number; amount: DecCoin; proxy: string | null } } | { Undelegate: { owner: string; mix_id: number; proxy: string | null } } | { PledgeMore: { mix_id: number; amount: DecCoin } } + | { DecreasePledge: { mix_id: number; decrease_by: DecCoin } } | { UnbondMixnode: { mix_id: number } } | { UpdateActiveSetSize: { new_size: number } }; diff --git a/ts-packages/types/src/types/rust/PendingIntervalEvent.ts b/ts-packages/types/src/types/rust/PendingIntervalEvent.ts index e5249be23d..db0abeae02 100644 --- a/ts-packages/types/src/types/rust/PendingIntervalEvent.ts +++ b/ts-packages/types/src/types/rust/PendingIntervalEvent.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { PendingIntervalEventData } from './PendingIntervalEventData'; -export interface PendingIntervalEvent { - id: number; - created_at: bigint; - event: PendingIntervalEventData; -} +export type PendingIntervalEvent = { id: number; created_at: bigint; event: PendingIntervalEventData }; diff --git a/ts-packages/types/src/types/rust/PendingIntervalEventData.ts b/ts-packages/types/src/types/rust/PendingIntervalEventData.ts index 152ae9fda6..ccde6b5c23 100644 --- a/ts-packages/types/src/types/rust/PendingIntervalEventData.ts +++ b/ts-packages/types/src/types/rust/PendingIntervalEventData.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { IntervalRewardingParamsUpdate } from './IntervalRewardingParamsUpdate'; -import type { MixNodeCostParams } from './MixNodeCostParams'; +import type { NodeCostParams } from './MixNodeCostParams'; export type PendingIntervalEventData = - | { ChangeMixCostParams: { mix_id: number; new_costs: MixNodeCostParams } } + | { ChangeMixCostParams: { mix_id: number; new_costs: NodeCostParams } } | { UpdateRewardingParams: { update: IntervalRewardingParamsUpdate } } | { UpdateIntervalConfig: { epochs_in_interval: number; epoch_duration_secs: bigint } }; diff --git a/ts-packages/types/src/types/rust/PendingNodeChanges.ts b/ts-packages/types/src/types/rust/PendingNodeChanges.ts index 3018f1e041..1dd646ccbd 100644 --- a/ts-packages/types/src/types/rust/PendingNodeChanges.ts +++ b/ts-packages/types/src/types/rust/PendingNodeChanges.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface PendingNodeChanges { pledge_change: number | null, cost_params_change: number | null, } \ No newline at end of file +export type PendingNodeChanges = { pledge_change: number | null; cost_params_change: number | null }; diff --git a/ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts b/ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts new file mode 100644 index 0000000000..63f5a3bbc1 --- /dev/null +++ b/ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HistoricalPerformanceResponse } from './HistoricalPerformanceResponse'; +import type { PaginatedResponse } from './PaginatedResponse'; + +export type PerformanceHistoryResponse = { node_id: number; history: PaginatedResponse }; diff --git a/ts-packages/types/src/types/rust/Period.ts b/ts-packages/types/src/types/rust/Period.ts index 2e0a752ee7..40ccf4f9cc 100644 --- a/ts-packages/types/src/types/rust/Period.ts +++ b/ts-packages/types/src/types/rust/Period.ts @@ -1,3 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Period = 'Before' | { in: number } | 'After'; +/** + * The vesting period. + */ +export type Period = 'Before' | { In: number } | 'After'; diff --git a/ts-packages/types/src/types/rust/PledgeData.ts b/ts-packages/types/src/types/rust/PledgeData.ts index 76adc23018..20fd4fd314 100644 --- a/ts-packages/types/src/types/rust/PledgeData.ts +++ b/ts-packages/types/src/types/rust/PledgeData.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; -export interface PledgeData { - amount: DecCoin; - block_time: bigint; -} +export type PledgeData = { amount: DecCoin; block_time: bigint }; diff --git a/ts-packages/types/src/types/rust/RewardEstimate.ts b/ts-packages/types/src/types/rust/RewardEstimate.ts index 9649b88981..31723c2e99 100644 --- a/ts-packages/types/src/types/rust/RewardEstimate.ts +++ b/ts-packages/types/src/types/rust/RewardEstimate.ts @@ -1,8 +1,21 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface RewardEstimate { +export type RewardEstimate = { + /** + * The amount of **decimal** coins that are going to get distributed to the node, + * i.e. the operator and all its delegators. + */ total_node_reward: string; + /** + * The share of the reward that is going to get distributed to the node operator. + */ operator: string; + /** + * The share of the reward that is going to get distributed among the node delegators. + */ delegates: string; + /** + * The operating cost of this node. Note: it's already included in the operator reward. + */ operating_cost: string; -} +}; diff --git a/ts-packages/types/src/types/rust/RewardEstimationResponse.ts b/ts-packages/types/src/types/rust/RewardEstimationResponse.ts index e92f0807ab..ec328194ec 100644 --- a/ts-packages/types/src/types/rust/RewardEstimationResponse.ts +++ b/ts-packages/types/src/types/rust/RewardEstimationResponse.ts @@ -3,9 +3,9 @@ import type { Interval } from './Interval'; import type { RewardEstimate } from './RewardEstimate'; import type { RewardingParams } from './RewardingParams'; -export interface RewardEstimationResponse { +export type RewardEstimationResponse = { estimation: RewardEstimate; reward_params: RewardingParams; epoch: Interval; as_at: number; -} +}; diff --git a/ts-packages/types/src/types/rust/RewardedSetParams.ts b/ts-packages/types/src/types/rust/RewardedSetParams.ts new file mode 100644 index 0000000000..61ba4ccfb6 --- /dev/null +++ b/ts-packages/types/src/types/rust/RewardedSetParams.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RewardedSetParams = { + /** + * The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + */ + entry_gateways: number; + /** + * The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + */ + exit_gateways: number; + /** + * The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + */ + mixnodes: number; + /** + * Number of nodes in the 'standby' set. (i.e. [`Role::Standby`]) + */ + standby: number; +}; diff --git a/ts-packages/types/src/types/rust/RewardingParams.ts b/ts-packages/types/src/types/rust/RewardingParams.ts index f00766eec4..25dd44778e 100644 --- a/ts-packages/types/src/types/rust/RewardingParams.ts +++ b/ts-packages/types/src/types/rust/RewardingParams.ts @@ -1,8 +1,14 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { IntervalRewardParams } from './IntervalRewardParams'; +import type { RewardedSetParams } from './RewardedSetParams'; -export interface RewardingParams { +/** + * Parameters used for reward calculation. + */ +export type RewardingParams = { + /** + * Parameters that should remain unchanged throughout an interval. + */ interval: IntervalRewardParams; - rewarded_set_size: number; - active_set_size: number; -} + rewarded_set: RewardedSetParams; +}; diff --git a/ts-packages/types/src/types/rust/Role.ts b/ts-packages/types/src/types/rust/Role.ts new file mode 100644 index 0000000000..9e96a76120 --- /dev/null +++ b/ts-packages/types/src/types/rust/Role.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Role = 'EntryGateway' | 'Layer1' | 'Layer2' | 'Layer3' | 'ExitGateway' | 'Standby'; diff --git a/ts-packages/types/src/types/rust/RpcTransactionResponse.ts b/ts-packages/types/src/types/rust/RpcTransactionResponse.ts index 2f9c3bdc8a..e9711bf47f 100644 --- a/ts-packages/types/src/types/rust/RpcTransactionResponse.ts +++ b/ts-packages/types/src/types/rust/RpcTransactionResponse.ts @@ -2,7 +2,7 @@ import type { DecCoin } from './DecCoin'; import type { Gas } from './Gas'; -export interface RpcTransactionResponse { +export type RpcTransactionResponse = { index: number; tx_result_json: string; block_height: bigint; @@ -10,4 +10,4 @@ export interface RpcTransactionResponse { gas_used: Gas; gas_wanted: Gas; fee: DecCoin | null; -} +}; diff --git a/ts-packages/types/src/types/rust/SendTxResult.ts b/ts-packages/types/src/types/rust/SendTxResult.ts index 02fb0b5b0c..15481d175a 100644 --- a/ts-packages/types/src/types/rust/SendTxResult.ts +++ b/ts-packages/types/src/types/rust/SendTxResult.ts @@ -3,7 +3,7 @@ import type { DecCoin } from './DecCoin'; import type { Gas } from './Gas'; import type { TransactionDetails } from './TransactionDetails'; -export interface SendTxResult { +export type SendTxResult = { block_height: bigint; code: number; details: TransactionDetails; @@ -11,4 +11,4 @@ export interface SendTxResult { gas_wanted: Gas; tx_hash: string; fee: DecCoin | null; -} +}; diff --git a/ts-packages/types/src/types/rust/StakeSaturationResponse.ts b/ts-packages/types/src/types/rust/StakeSaturationResponse.ts index 6be2ae0e0e..c5ef3d705e 100644 --- a/ts-packages/types/src/types/rust/StakeSaturationResponse.ts +++ b/ts-packages/types/src/types/rust/StakeSaturationResponse.ts @@ -1,7 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface StakeSaturationResponse { - saturation: string; - uncapped_saturation: string; - as_at: bigint; -} +export type StakeSaturationResponse = { saturation: string; uncapped_saturation: string; as_at: bigint }; diff --git a/ts-packages/types/src/types/rust/TransactionDetails.ts b/ts-packages/types/src/types/rust/TransactionDetails.ts index 043db71473..3ad842a513 100644 --- a/ts-packages/types/src/types/rust/TransactionDetails.ts +++ b/ts-packages/types/src/types/rust/TransactionDetails.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from './DecCoin'; -export interface TransactionDetails { - amount: DecCoin; - from_address: string; - to_address: string; -} +export type TransactionDetails = { amount: DecCoin; from_address: string; to_address: string }; diff --git a/ts-packages/types/src/types/rust/TransactionExecuteResult.ts b/ts-packages/types/src/types/rust/TransactionExecuteResult.ts index ce06484dd1..d2595e5848 100644 --- a/ts-packages/types/src/types/rust/TransactionExecuteResult.ts +++ b/ts-packages/types/src/types/rust/TransactionExecuteResult.ts @@ -2,10 +2,10 @@ import type { DecCoin } from './DecCoin'; import type { GasInfo } from './GasInfo'; -export interface TransactionExecuteResult { +export type TransactionExecuteResult = { logs_json: string; - data_json: string; + msg_responses_json: string; transaction_hash: string; gas_info: GasInfo; fee: DecCoin | null; -} +}; diff --git a/ts-packages/types/src/types/rust/UnbondedMixnode.ts b/ts-packages/types/src/types/rust/UnbondedMixnode.ts index 88fe0e3982..c213f6d461 100644 --- a/ts-packages/types/src/types/rust/UnbondedMixnode.ts +++ b/ts-packages/types/src/types/rust/UnbondedMixnode.ts @@ -1,8 +1,24 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface UnbondedMixnode { +/** + * Basic information of a node that used to be part of the mix network but has already unbonded. + */ +export type UnbondedMixnode = { + /** + * Base58-encoded ed25519 EdDSA public key. + */ identity_key: string; + /** + * Address of the owner of this mixnode. + */ owner: string; + /** + * Entity who bonded this mixnode on behalf of the owner. + * If exists, it's most likely the address of the vesting contract. + */ proxy: string | null; + /** + * Block height at which this mixnode has unbonded. + */ unbonding_height: number; -} +}; diff --git a/ts-packages/types/src/types/rust/UptimeHistoryResponse.ts b/ts-packages/types/src/types/rust/UptimeHistoryResponse.ts new file mode 100644 index 0000000000..cdcbc0e14c --- /dev/null +++ b/ts-packages/types/src/types/rust/UptimeHistoryResponse.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HistoricalUptimeResponse } from './HistoricalUptimeResponse'; +import type { PaginatedResponse } from './PaginatedResponse'; + +export type UptimeHistoryResponse = { node_id: number; history: PaginatedResponse }; diff --git a/ts-packages/types/src/types/rust/VestingAccountInfo.ts b/ts-packages/types/src/types/rust/VestingAccountInfo.ts index 538e941edb..6d09f24caf 100644 --- a/ts-packages/types/src/types/rust/VestingAccountInfo.ts +++ b/ts-packages/types/src/types/rust/VestingAccountInfo.ts @@ -2,10 +2,10 @@ import type { DecCoin } from './DecCoin'; import type { VestingPeriod } from './VestingPeriod'; -export interface VestingAccountInfo { +export type VestingAccountInfo = { owner_address: string; staking_address: string | null; start_time: bigint; periods: Array; amount: DecCoin; -} +}; diff --git a/ts-packages/types/src/types/rust/VestingPeriod.ts b/ts-packages/types/src/types/rust/VestingPeriod.ts index dae6901c10..5125eab75b 100644 --- a/ts-packages/types/src/types/rust/VestingPeriod.ts +++ b/ts-packages/types/src/types/rust/VestingPeriod.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface VestingPeriod { - start_time: bigint; - period_seconds: bigint; -} +export type VestingPeriod = { start_time: bigint; period_seconds: bigint }; diff --git a/ts-packages/types/src/types/rust/WrappedDelegationEvent.ts b/ts-packages/types/src/types/rust/WrappedDelegationEvent.ts index b0a5e80ab4..4e710530fb 100644 --- a/ts-packages/types/src/types/rust/WrappedDelegationEvent.ts +++ b/ts-packages/types/src/types/rust/WrappedDelegationEvent.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DelegationEvent } from './DelegationEvent'; -export interface WrappedDelegationEvent { - event: DelegationEvent; - node_identity: string; -} +export type WrappedDelegationEvent = { event: DelegationEvent; node_identity: string }; From 6879c211eedb2ff02dc4190690f56e3863bff457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 25 Sep 2024 16:05:41 +0100 Subject: [PATCH 054/207] cleaning up mixnet contract in preperation for merging --- .../mixnet-contract/src/error.rs | 6 - .../mixnet-contract/src/events.rs | 4 +- .../mixnet-contract/src/msg.rs | 12 - .../mixnet/schema/nym-mixnet-contract.json | 42 -- contracts/mixnet/schema/raw/execute.json | 42 -- contracts/mixnet/src/compat/helpers.rs | 1 - contracts/mixnet/src/compat/transactions.rs | 15 +- contracts/mixnet/src/contract.rs | 26 +- contracts/mixnet/src/delegations/helpers.rs | 9 +- contracts/mixnet/src/gateways/queries.rs | 3 +- .../mixnet/src/interval/pending_events.rs | 151 +++---- .../mixnet_contract_settings/transactions.rs | 1 - contracts/mixnet/src/mixnodes/transactions.rs | 14 +- contracts/mixnet/src/rewards/helpers.rs | 22 +- contracts/mixnet/src/rewards/queries.rs | 210 ++++------ contracts/mixnet/src/rewards/transactions.rs | 369 +++++++++--------- contracts/mixnet/src/support/helpers.rs | 6 +- contracts/mixnet/src/support/mod.rs | 3 +- contracts/mixnet/src/support/tests/mod.rs | 42 +- .../vesting/schema/nym-vesting-contract.json | 104 +---- contracts/vesting/schema/raw/execute.json | 104 +---- 21 files changed, 351 insertions(+), 835 deletions(-) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 9a67023224..aa61facb18 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -142,12 +142,6 @@ pub enum MixnetContractError { #[error("node {node_id} hasn't been assigned the role of {role} for this epoch")] IncorrectEpochRole { node_id: NodeId, role: Role }, - #[error("Mixnode {mix_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] - MixnodeNotInRewardedSet { - mix_id: NodeId, - absolute_epoch_id: u32, - }, - #[error("Mixnode {mix_id} is currently in the process of unbonding")] MixnodeIsUnbonding { mix_id: NodeId }, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index e0b7f5f506..4fccb50295 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -157,7 +157,7 @@ pub const PRIOR_UNIT_REWARD_KEY: &str = "prior_unit_reward"; pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason"; pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; -pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance"; +pub const ZERO_PERFORMANCE_OR_WORK_VALUE: &str = "zero_performance_or_work"; // rewarded set update pub const NUM_MIXNODES_KEY: &str = "num_mixnodes"; @@ -491,7 +491,7 @@ pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, node_id: interval.current_epoch_absolute_id().to_string(), ) .add_attribute(NODE_ID_KEY, node_id.to_string()) - .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_VALUE) + .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_OR_WORK_VALUE) } pub fn new_mix_rewarding_event( diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 5e7570714d..06caec5376 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -283,18 +283,8 @@ pub enum ExecuteMsg { TestingResolveAllPendingEvents { limit: Option, }, - - // TO BE REMOVED BEFORE MERGING - TestingUncheckedBondLegacyMixnode { - node: MixNode, - }, - TestingUncheckedBondLegacyGateway { - node: Gateway, - }, } -const bad_const: &str = "clippy will yell so that i'd remember to remove those legacy bonding ops"; - impl ExecuteMsg { pub fn default_memo(&self) -> String { match self { @@ -384,8 +374,6 @@ impl ExecuteMsg { ExecuteMsg::TestingResolveAllPendingEvents { .. } => { "resolving all pending events".into() } - ExecuteMsg::TestingUncheckedBondLegacyMixnode { .. } => "todo!".into(), - ExecuteMsg::TestingUncheckedBondLegacyGateway { .. } => "todo!".into(), } } } diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index 95f69ca095..bf25ebfd76 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -1158,48 +1158,6 @@ } }, "additionalProperties": false - }, - { - "type": "object", - "required": [ - "testing_unchecked_bond_legacy_mixnode" - ], - "properties": { - "testing_unchecked_bond_legacy_mixnode": { - "type": "object", - "required": [ - "node" - ], - "properties": { - "node": { - "$ref": "#/definitions/MixNode" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "testing_unchecked_bond_legacy_gateway" - ], - "properties": { - "testing_unchecked_bond_legacy_gateway": { - "type": "object", - "required": [ - "node" - ], - "properties": { - "node": { - "$ref": "#/definitions/Gateway" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false } ], "definitions": { diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index a71339ac52..2e220e58a3 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -955,48 +955,6 @@ } }, "additionalProperties": false - }, - { - "type": "object", - "required": [ - "testing_unchecked_bond_legacy_mixnode" - ], - "properties": { - "testing_unchecked_bond_legacy_mixnode": { - "type": "object", - "required": [ - "node" - ], - "properties": { - "node": { - "$ref": "#/definitions/MixNode" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "testing_unchecked_bond_legacy_gateway" - ], - "properties": { - "testing_unchecked_bond_legacy_gateway": { - "type": "object", - "required": [ - "node" - ], - "properties": { - "node": { - "$ref": "#/definitions/Gateway" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false } ], "definitions": { diff --git a/contracts/mixnet/src/compat/helpers.rs b/contracts/mixnet/src/compat/helpers.rs index 0664bed06b..46b845a05d 100644 --- a/contracts/mixnet/src/compat/helpers.rs +++ b/contracts/mixnet/src/compat/helpers.rs @@ -146,7 +146,6 @@ pub fn may_get_bond( #[cfg(test)] mod tests { use super::*; - use anyhow::bail; #[cfg(test)] mod reward_withdrawing_permission { diff --git a/contracts/mixnet/src/compat/transactions.rs b/contracts/mixnet/src/compat/transactions.rs index 77b20da946..05153d11ec 100644 --- a/contracts/mixnet/src/compat/transactions.rs +++ b/contracts/mixnet/src/compat/transactions.rs @@ -364,7 +364,7 @@ mod tests { MixnetContractError::ProfitMarginOutsideRange { .. } )); - // hundred + // a hundred test.add_dummy_nymnode("owner5", None); let sender = mock_info("owner5", &[]); let env = test.env(); @@ -561,7 +561,6 @@ mod tests { #[cfg(test)] mod withdrawing_operator_reward { use super::*; - use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::{ExtractBankMsg, TestSetup}; use cosmwasm_std::testing::mock_info; use cosmwasm_std::Addr; @@ -586,6 +585,7 @@ mod tests { #[test] fn for_legacy_mixnode() -> anyhow::Result<()> { let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); // no rewards test.add_legacy_mixnode("owner1", None); @@ -600,10 +600,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id, - test_helpers::performance(100.), - ); + test.reward_with_distribution(node_id, active_params); let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; let maybe_bank = res.unwrap_bank_msg(); @@ -634,6 +631,7 @@ mod tests { #[test] fn for_nymnode() -> anyhow::Result<()> { let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); // no rewards test.add_dummy_nymnode("owner1", None); @@ -648,10 +646,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id, - test_helpers::performance(100.), - ); + test.reward_with_distribution(node_id, active_params); let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; let maybe_bank = res.unwrap_bank_msg(); diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 36ffe22623..b3e3cc5df2 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -7,17 +7,15 @@ use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::nodes::storage as nymnodes_storage; use crate::queued_migrations::migrate_to_nym_nodes_usage; use crate::rewards::storage::RewardingStorage; -use crate::support::tests::legacy; use cosmwasm_std::{ - coin, entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, - Response, + entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, - NodeCostParams, OperatingCostRange, ProfitMarginRange, QueryMsg, + OperatingCostRange, ProfitMarginRange, QueryMsg, }; -use nym_contracts_common::{set_build_information, Percent}; +use nym_contracts_common::set_build_information; // version info for migration info const CONTRACT_NAME: &str = "crate:nym-mixnet-contract"; @@ -296,24 +294,6 @@ pub fn execute( ExecuteMsg::TestingResolveAllPendingEvents { limit } => { crate::testing::transactions::try_resolve_all_pending_events(deps, env, limit) } - ExecuteMsg::TestingUncheckedBondLegacyMixnode { node } => { - legacy::save_new_mixnode( - deps.storage, - env, - node, - NodeCostParams { - profit_margin_percent: Percent::from_percentage_value(20).unwrap(), - interval_operating_cost: coin(40_000_000, "unym"), - }, - info.sender, - info.funds[0].clone(), - )?; - Ok(Response::default()) - } - ExecuteMsg::TestingUncheckedBondLegacyGateway { node } => { - legacy::save_new_gateway(deps.storage, env, node, info.sender, info.funds[0].clone())?; - Ok(Response::default()) - } } } diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs index e82496f1a9..257015d5e2 100644 --- a/contracts/mixnet/src/delegations/helpers.rs +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -24,13 +24,14 @@ pub(crate) fn undelegate( #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers::{performance, TestSetup}; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::{Addr, Decimal, Uint128}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn undelegation_updates_mix_rewarding_storage_and_deletes_delegation() { let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); let mix_id = test.add_rewarded_set_nymnode_id("mix-owner", Some(Uint128::new(100_000_000_000))); @@ -40,11 +41,9 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let mix_rewarding = test.mix_rewarding(mix_id); let delegation = test.delegation(mix_id, delegator, &None); diff --git a/contracts/mixnet/src/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index 1cb19655e4..d6eae59487 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -83,11 +83,10 @@ pub(crate) fn query_preassigned_ids_paged( #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::contract::execute; use crate::support::tests; use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::{mock_env, mock_info}; + use cosmwasm_std::testing::mock_info; #[test] fn gateways_empty_on_init() { diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index 59e39e434e..ddacc44253 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -636,7 +636,6 @@ impl ContractExecutableEvent for PendingIntervalEventData { #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; use cosmwasm_std::Decimal; use mixnet_contract_common::Percent; @@ -835,33 +834,24 @@ mod tests { let delegation_og = 120_000_000u128; let delegation_new = 543_000_000u128; let delegation_coin_new = coin(delegation_new, TEST_COIN_DENOM); + let active_params = test.active_node_params(100.0); // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); + test.skip_to_next_epoch_end(); + test.reward_with_distribution_ignore_state(mix_id, active_params); let owner = "delegator"; test.add_immediate_delegation(owner, delegation_og, mix_id); test.skip_to_next_epoch_end(); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let storage_key = Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); @@ -918,19 +908,14 @@ mod tests { let delegation = 120_000_000u128; let delegation_coin = coin(120_000_000u128, TEST_COIN_DENOM); + let active_params = test.active_node_params(100.0); // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let storage_key = Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); @@ -1059,15 +1044,10 @@ mod tests { use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; use crate::interval::pending_events::unbond_mixnode; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::{ - try_decrease_mixnode_pledge, try_increase_mixnode_pledge, - }; use crate::rewards::storage as rewards_storage; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::{get_bank_send_msg, TestSetup}; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Uint128, WasmMsg}; + use cosmwasm_std::{Addr, Uint128}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -1153,18 +1133,13 @@ mod tests { let mix_details = mixnodes_storage::mixnode_bonds() .load(test.deps().storage, mix_id) .unwrap(); + let active_params = test.active_node_params(100.0); test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let expected_reward = dist1.operator + dist2.operator; let truncated_reward = truncate_reward_amount(expected_reward); @@ -1271,6 +1246,7 @@ mod tests { let pledge1 = Uint128::new(150_000_000); let pledge2 = Uint128::new(50_000_000); let pledge3 = Uint128::new(200_000_000); + let active_params = test.active_node_params(100.0); let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); @@ -1291,14 +1267,9 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1308,6 +1279,7 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); @@ -1319,10 +1291,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge]); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); let increase = test.coin(pledge2.u128()); increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); @@ -1363,14 +1332,10 @@ mod tests { // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1381,6 +1346,7 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); @@ -1398,10 +1364,8 @@ mod tests { // go few epochs of rewarding before adding more pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); cumulative_op_reward += dist.operator; cumulative_del_reward += dist.delegates; } @@ -1446,14 +1410,10 @@ mod tests { // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1565,6 +1525,7 @@ mod tests { let pledge1 = Uint128::new(200_000_000); let pledge_change = Uint128::new(50_000_000); let pledge3 = Uint128::new(150_000_000); + let active_params = test.active_node_params(100.0); let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); @@ -1585,14 +1546,9 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1602,6 +1558,7 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); @@ -1613,10 +1570,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge]); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); let decrease = test.coin(pledge_change.u128()); decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); @@ -1657,14 +1611,10 @@ mod tests { // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1675,6 +1625,7 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); @@ -1692,10 +1643,8 @@ mod tests { // go few epochs of rewarding before decreasing pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); cumulative_op_reward += dist.operator; cumulative_del_reward += dist.delegates; } @@ -1740,14 +1689,10 @@ mod tests { // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.legacy_reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 7f6ec4b6c7..47199ce605 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -75,7 +75,6 @@ pub(crate) fn try_update_contract_settings( #[cfg(test)] pub mod tests { use super::*; - use crate::constants::INITIAL_PLEDGE_AMOUNT; use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::support::tests::test_helpers; diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 52fe1a45ed..7541de10b4 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -529,8 +529,7 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - let node_id = test.add_legacy_mixnode(owner, None); - let details = test.mixnode_by_id(node_id).unwrap(); + test.add_legacy_mixnode(owner, None); let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -777,11 +776,8 @@ pub mod tests { #[cfg(test)] mod increasing_mixnode_pledge { - use crate::mixnodes::helpers::tests::{ - setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING, - }; - use super::*; + use crate::mixnodes::helpers::tests::setup_mix_combinations; #[test] fn cant_be_performed_if_epoch_transition_is_in_progress() { @@ -948,11 +944,8 @@ pub mod tests { #[cfg(test)] mod decreasing_mixnode_pledge { - use crate::mixnodes::helpers::tests::{ - setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING, - }; - use super::*; + use crate::mixnodes::helpers::tests::setup_mix_combinations; #[test] fn cant_be_performed_if_epoch_transition_is_in_progress() { @@ -1154,7 +1147,6 @@ pub mod tests { let events = test.pending_epoch_events(); assert!(events.is_empty()); - let sender = mock_info(owner, &[]); try_decrease_mixnode_pledge(test.deps_mut(), env, decrease.clone(), details).unwrap(); let events = test.pending_epoch_events(); diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index dfd07956ce..793f8b8f14 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -137,7 +137,7 @@ mod tests { use super::*; use crate::mixnodes::helpers::get_mixnode_details_by_id; use crate::rewards::models::RewardPoolChange; - use crate::support::tests::test_helpers::{assert_decimals, performance, TestSetup}; + use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; use cosmwasm_std::Uint128; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -259,6 +259,7 @@ mod tests { let pledge = Uint128::new(250_000_000); let pledge_dec = 250_000_000u32.into_base_decimal().unwrap(); let mix_id = test.add_legacy_mixnode("mix-owner", Some(pledge)); + let active_params = test.active_node_params(100.0); // no rewards let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -269,16 +270,13 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist3 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = test.reward_with_distribution_ignore_state(mix_id, active_params); let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -295,6 +293,7 @@ mod tests { #[test] fn withdrawing_delegator_reward() { let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); let delegation_amount = Uint128::new(2_500_000_000); let delegation_dec = 2_500_000_000_u32.into_base_decimal().unwrap(); @@ -311,16 +310,13 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist3 = - test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = test.reward_with_distribution_ignore_state(mix_id, active_params); let delegation_pre = test.delegation(mix_id, delegator, &None); let mix_rewarding = test.mix_rewarding(mix_id); diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 53b7ab9f99..dc296752f1 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -284,7 +284,7 @@ mod tests { assert!(res.amount_earned.is_none()); assert!(res.amount_earned_detailed.is_none()); assert!(res.amount_staked.is_none()); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -304,7 +304,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } #[test] @@ -313,15 +313,13 @@ mod tests { let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.operator; let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); @@ -333,15 +331,12 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.operator; let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); @@ -353,7 +348,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } } @@ -363,15 +358,13 @@ mod tests { let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.operator; let sender = mock_info(owner, &[]); @@ -386,7 +379,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -395,14 +388,12 @@ mod tests { let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); let env = test.env(); @@ -418,7 +409,7 @@ mod tests { assert!(res.amount_earned.is_none()); assert!(res.amount_earned_detailed.is_none()); assert!(res.amount_staked.is_none()); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } } @@ -442,7 +433,7 @@ mod tests { assert!(res.amount_earned.is_none()); assert!(res.amount_earned_detailed.is_none()); assert!(res.amount_staked.is_none()); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -463,13 +454,14 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } #[test] fn for_delegator_with_pending_reward() { let mut test = TestSetup::new(); let owner = "delegator"; + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(100_000_000); let mix_id = test @@ -480,10 +472,7 @@ mod tests { test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let res = @@ -494,15 +483,12 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let res = query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None) @@ -513,7 +499,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } } @@ -521,6 +507,7 @@ mod tests { fn for_node_that_is_unbonding() { let mut test = TestSetup::new(); let owner = "delegator"; + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(100_000_000); let mix_id = test @@ -531,10 +518,7 @@ mod tests { test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let sender = mock_info("mix-owner", &[]); @@ -548,13 +532,14 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] fn for_node_that_has_unbonded() { let mut test = TestSetup::new(); let owner = "delegator"; + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(100_000_000); let mix_id = test @@ -565,10 +550,7 @@ mod tests { test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let sender = mock_info("mix-owner", &[]); @@ -583,7 +565,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -595,6 +577,7 @@ mod tests { let del2 = "delegator2"; let del3 = "delegator3"; let del4 = "delegator4"; + let active_params = test.active_node_params(100.); let mix_id = test .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); @@ -605,58 +588,35 @@ mod tests { test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.add_immediate_delegation(del3, 500_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(85.0), - ); + let params = test.active_node_params(85.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(5.0), - ); + let params = test.active_node_params(5.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.add_immediate_delegation(del4, 5_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.add_immediate_delegation(del2, 250_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(98.0), - ); + let params = test.active_node_params(98.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.remove_immediate_delegation(del3, mix_id); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(98.0), - ); + let params = test.active_node_params(98.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let pending1 = query_pending_delegator_reward(test.deps(), del1.into(), mix_id, None).unwrap(); @@ -731,13 +691,11 @@ mod tests { let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); let env = test.env(); @@ -761,13 +719,11 @@ mod tests { let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); let env = test.env(); @@ -789,13 +745,11 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.force_change_rewarded_set(vec![]); let res = query_estimated_current_epoch_operator_reward( @@ -815,13 +769,11 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_operator_reward( test.deps(), @@ -840,13 +792,11 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let mix_rewarding = test.mix_rewarding(mix_id); let res = query_estimated_current_epoch_operator_reward( @@ -858,10 +808,8 @@ mod tests { .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + let params = test.active_node_params(95.); + let dist = test.reward_with_distribution_ignore_state(mix_id, params); let expected = EstimatedCurrentEpochRewardResponse { original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), @@ -910,13 +858,11 @@ mod tests { fn when_delegation_doesnt_exist() { let mut test = TestSetup::new(); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_delegator_reward( test.deps(), @@ -934,6 +880,7 @@ mod tests { fn when_node_is_unbonding() { let mut test = TestSetup::new(); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -941,10 +888,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info("mix-owner", &[]); let env = test.env(); @@ -967,6 +911,7 @@ mod tests { fn when_node_has_already_unbonded() { let mut test = TestSetup::new(); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -974,10 +919,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info("mix-owner", &[]); let env = test.env(); @@ -1001,6 +943,7 @@ mod tests { fn when_node_is_not_in_the_rewarded_set() { let mut test = TestSetup::new(); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -1008,10 +951,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.force_change_rewarded_set(vec![]); let res = query_estimated_current_epoch_delegator_reward( @@ -1032,16 +972,14 @@ mod tests { let mut test = TestSetup::new(); let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_delegator_reward( test.deps(), @@ -1079,10 +1017,8 @@ mod tests { .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + let params = test.active_node_params(95.); + let dist = test.reward_with_distribution_ignore_state(mix_id, params); let expected = EstimatedCurrentEpochRewardResponse { original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), @@ -1118,17 +1054,13 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + let params = test.active_node_params(95.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.add_immediate_delegation(del3, initial_stake3, mix_id); test.skip_to_next_epoch_end(); - test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(85.0), - ); + let params = test.active_node_params(85.0); + test.reward_with_distribution_ignore_state(mix_id, params); let mix_rewarding = test.mix_rewarding(mix_id); @@ -1165,10 +1097,8 @@ mod tests { let cur3 = initial_stake3_dec + est3; test.skip_to_next_epoch_end(); - let dist = test.legacy_reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + let params = test.active_node_params(95.0); + let dist = test.reward_with_distribution_ignore_state(mix_id, params); let share1 = cur1 / mix_rewarding.delegates * dist.delegates; let share2 = cur2 / mix_rewarding.delegates * dist.delegates; diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 21ed0e7b33..e3edbcc462 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -326,49 +326,29 @@ pub mod tests { use mixnet_contract_common::events::{ MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY, OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY, - ZERO_PERFORMANCE_VALUE, + ZERO_PERFORMANCE_OR_WORK_VALUE, }; use mixnet_contract_common::helpers::compare_decimals; use mixnet_contract_common::nym_node::Role; - use mixnet_contract_common::reward_params::Performance; + use mixnet_contract_common::reward_params::WorkFactor; use mixnet_contract_common::EpochStatus; // a simple wrapper to streamline checking for rewarding results trait TestRewarding { - #[deprecated] - fn legacy_rewarding( - &mut self, - node_id: NodeId, - performance: Performance, - ) -> Result; - fn execute_rewarding( &mut self, node_id: NodeId, rewarding_params: NodeRewardingParameters, ) -> Result; - fn assert_rewarding(&mut self, node_id: NodeId, performance: Performance) -> Response; + fn assert_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Response; } impl TestRewarding for TestSetup { - fn legacy_rewarding( - &mut self, - node_id: NodeId, - performance: Performance, - ) -> Result { - // this is rather temporary (I hope...) - let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); - - self.execute_rewarding( - node_id, - NodeRewardingParameters { - performance, - work_factor, - }, - ) - } - fn execute_rewarding( &mut self, node_id: NodeId, @@ -382,9 +362,13 @@ pub mod tests { } #[track_caller] - fn assert_rewarding(&mut self, node_id: NodeId, performance: Performance) -> Response { + fn assert_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Response { let caller = std::panic::Location::caller(); - self.legacy_rewarding(node_id, performance) + self.execute_rewarding(node_id, rewarding_params) .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) } } @@ -392,6 +376,7 @@ pub mod tests { #[cfg(test)] mod epoch_state_is_correctly_updated { use super::*; + use mixnet_contract_common::reward_params::WorkFactor; #[test] fn when_target_mixnode_unbonded() { @@ -407,6 +392,7 @@ pub mod tests { node_id_never_existed, ]); test.start_epoch_transition(); + let active_params = test.active_node_params(100.); let env = test.env(); @@ -437,9 +423,7 @@ pub mod tests { ) .unwrap(); - let performance = test_helpers::performance(100.0); - - test.assert_rewarding(node_id_unbonded, performance); + test.assert_rewarding(node_id_unbonded, active_params); assert_eq!( EpochState::Rewarding { last_rewarded: node_id_unbonded, @@ -450,7 +434,7 @@ pub mod tests { .state ); - test.assert_rewarding(node_id_unbonded_leftover, performance); + test.assert_rewarding(node_id_unbonded_leftover, active_params); assert_eq!( EpochState::Rewarding { last_rewarded: node_id_unbonded_leftover, @@ -461,7 +445,7 @@ pub mod tests { .state ); - test.assert_rewarding(node_id_never_existed, performance); + test.assert_rewarding(node_id_never_existed, active_params); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -479,7 +463,7 @@ pub mod tests { test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let zero_performance = test_helpers::performance(0.); + let zero_performance = test.active_node_params(0.); test.assert_rewarding(node_id, zero_performance); assert_eq!( EpochState::ReconcilingEvents, @@ -489,6 +473,69 @@ pub mod tests { ); } + #[test] + fn when_target_mixnode_has_zero_work_factor() { + let mut test = TestSetup::new(); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + + let params = NodeRewardingParameters::new( + test_helpers::performance(100.), + WorkFactor::zero(), + ); + test.assert_rewarding(node_id, params); + assert_eq!( + EpochState::ReconcilingEvents, + interval_storage::current_epoch_status(test.deps().storage) + .unwrap() + .state + ); + } + + #[test] + fn when_target_nymnode_has_zero_performance() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("node-owner", None); + + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + + let zero_performance = test.active_node_params(0.); + test.assert_rewarding(node_id, zero_performance); + assert_eq!( + EpochState::ReconcilingEvents, + interval_storage::current_epoch_status(test.deps().storage) + .unwrap() + .state + ); + } + + #[test] + fn when_target_node_has_zero_workfactor() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + + let params = NodeRewardingParameters::new( + test_helpers::performance(100.), + WorkFactor::zero(), + ); + test.assert_rewarding(node_id, params); + assert_eq!( + EpochState::ReconcilingEvents, + interval_storage::current_epoch_status(test.deps().storage) + .unwrap() + .state + ); + } + #[test] fn when_theres_only_one_node_to_reward() { let mut test = TestSetup::new(); @@ -497,9 +544,9 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); + let active_params = test.active_node_params(100.); - let performance = test_helpers::performance(100.0); - test.assert_rewarding(node_id, performance); + test.assert_rewarding(node_id, active_params); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -521,10 +568,10 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(ids.clone()); test.start_epoch_transition(); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); for node_id in ids { - test.assert_rewarding(node_id, performance); + test.assert_rewarding(node_id, active_params); let current_state = interval_storage::current_epoch_status(test.deps().storage) .unwrap() @@ -556,6 +603,7 @@ pub mod tests { for bad_state in bad_states { let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -565,7 +613,7 @@ pub mod tests { test.skip_to_current_epoch_end(); test.force_change_rewarded_set(vec![1, 2, 3]); - let res = test.legacy_rewarding(1, test_helpers::performance(100.0)); + let res = test.execute_rewarding(1, active_params); assert_eq!( res, @@ -638,14 +686,14 @@ pub mod tests { pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); for &node_id in &[ node_id_unbonded, node_id_unbonded_leftover, node_id_never_existed, ] { - let res = test.assert_rewarding(node_id, performance); + let res = test.assert_rewarding(node_id, active_params); let reason = find_attribute( Some(MixnetEventType::NodeRewarding.to_string()), @@ -666,8 +714,8 @@ pub mod tests { test.skip_to_next_epoch(); test.force_change_rewarded_set(vec![node_id]); - let performance = test_helpers::performance(100.); - let res = test.legacy_rewarding(node_id, performance); + let active_params = test.active_node_params(100.); + let res = test.execute_rewarding(node_id, active_params); assert!(matches!( res, Err(MixnetContractError::EpochInProgress { .. }) @@ -676,74 +724,10 @@ pub mod tests { // epoch is over (sanity check) test.skip_to_current_epoch_end(); test.start_epoch_transition(); - let res = test.legacy_rewarding(node_id, performance); + let res = test.execute_rewarding(node_id, active_params); assert!(res.is_ok()); } - #[test] - fn can_only_be_performed_for_nodes_in_rewarded_set() { - // let mut test = TestSetup::new(); - // - // let active_node_id = test.add_rewarded_legacy_mixnode("mix-owner-active", None); - // let standby_node_id = test.add_rewarded_legacy_mixnode("mix-owner-standby", None); - // let inactive_node_id = test.add_rewarded_legacy_mixnode("mix-owner-inactive", None); - // let sender = test.rewarding_validator(); - // - // test.skip_to_next_epoch_end(); - // - // // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive - // interval_storage::REWARDED_SET - // .save( - // test.deps_mut().storage, - // active_node_id, - // &RewardedSetNodeStatus::Active, - // ) - // .unwrap(); - // interval_storage::REWARDED_SET - // .save( - // test.deps_mut().storage, - // standby_node_id, - // &RewardedSetNodeStatus::Standby, - // ) - // .unwrap(); - // - // // actually add one more dummy node with high id so we wouldn't go into the next state - // interval_storage::REWARDED_SET - // .save( - // test.deps_mut().storage, - // 9001, - // &RewardedSetNodeStatus::Standby, - // ) - // .unwrap(); - // test.start_epoch_transition(); - // - // let performance = test_helpers::performance(100.); - // let env = test.env(); - // let res_active = try_reward_node( - // test.deps_mut(), - // env.clone(), - // sender.clone(), - // active_node_id, - // performance, - // ); - // let res_standby = try_reward_node( - // test.deps_mut(), - // env.clone(), - // sender.clone(), - // standby_node_id, - // performance, - // ); - // let res_inactive = - // try_reward_node(test.deps_mut(), env, sender, inactive_node_id, performance); - // - // assert!(res_active.is_ok()); - // assert!(res_standby.is_ok()); - // assert!(matches!( - // res_inactive, - // Err(MixnetContractError::MixnodeNotInRewardedSet { node_id, .. }) if node_id == inactive_node_id - // )); - } - #[test] fn can_only_be_performed_once_per_node_per_epoch() { let mut test = TestSetup::new(); @@ -752,13 +736,13 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.); + let active_params = test.active_node_params(100.); // first rewarding - test.assert_rewarding(node_id, performance); + test.assert_rewarding(node_id, active_params); // second rewarding - let res = test.legacy_rewarding(node_id, performance); + let res = test.execute_rewarding(node_id, active_params); assert!(matches!( res, Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id @@ -768,7 +752,7 @@ pub mod tests { test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let res = test.legacy_rewarding(node_id, performance); + let res = test.execute_rewarding(node_id, active_params); assert!(res.is_ok()); } @@ -780,17 +764,17 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); - let zero_performance = test_helpers::performance(0.); - let performance = test_helpers::performance(100.0); + let zero_perf_params = test.active_node_params(0.); + let active_params = test.active_node_params(100.); // first rewarding - let res = test.assert_rewarding(node_id, zero_performance); + let res = test.assert_rewarding(node_id, zero_perf_params); let reason = res.attribute(MixnetEventType::NodeRewarding, NO_REWARD_REASON_KEY); - assert_eq!(ZERO_PERFORMANCE_VALUE, reason); + assert_eq!(ZERO_PERFORMANCE_OR_WORK_VALUE, reason); // sanity check: it's still treated as rewarding, so we can't reward the node again // with different performance for the same epoch - let res = test.legacy_rewarding(node_id, zero_performance); + let res = test.execute_rewarding(node_id, zero_perf_params); assert!(matches!( res, Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id @@ -800,7 +784,47 @@ pub mod tests { test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let res = test.assert_rewarding(node_id, performance); + let res = test.assert_rewarding(node_id, active_params); + + // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) + let operator = res.attribute(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); + assert!(!operator.is_empty()); + assert_ne!("0", operator); + let delegates = res.attribute(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + assert_eq!("0", delegates); + } + + #[test] + fn requires_nonzero_work_factor() { + let mut test = TestSetup::new(); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id, 42]); + test.start_epoch_transition(); + + let zero_work_params = + NodeRewardingParameters::new(test_helpers::performance(100.), WorkFactor::zero()); + let active_params = test.active_node_params(100.); + + // first rewarding + let res = test.assert_rewarding(node_id, zero_work_params); + let reason = res.attribute(MixnetEventType::NodeRewarding, NO_REWARD_REASON_KEY); + assert_eq!(ZERO_PERFORMANCE_OR_WORK_VALUE, reason); + + // sanity check: it's still treated as rewarding, so we can't reward the node again + // with different performance for the same epoch + let res = test.execute_rewarding(node_id, zero_work_params); + assert!(matches!( + res, + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id + )); + + // but in the next epoch, as always, we're good again + test.skip_to_next_epoch_end(); + test.start_epoch_transition(); + + let res = test.assert_rewarding(node_id, active_params); // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) let operator = res.attribute(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); @@ -820,7 +844,7 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id1, node_id2, node_id3]); test.start_epoch_transition(); - let performance = test_helpers::performance(98.0); + let params = test.active_node_params(98.0); test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); @@ -842,7 +866,7 @@ pub mod tests { .load(test.deps().storage, node_id) .unwrap(); - let res = test.assert_rewarding(node_id, performance); + let res = test.assert_rewarding(node_id, params); let operator = res.decimal(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); let delegates = res.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); @@ -901,14 +925,11 @@ pub mod tests { test.start_epoch_transition(); for &node_id in &[node_id1, node_id2, node_id3] { let mut sim = test.instantiate_simulator(node_id); - let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id, - performance, - ); let node_params = NodeRewardingParameters::new( performance, global_rewarding_params.active_node_work(), ); + let dist = test.reward_with_distribution(node_id, node_params); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -934,14 +955,11 @@ pub mod tests { test.start_epoch_transition(); for &node_id in &[node_id1, node_id2, node_id3] { let mut sim = test.instantiate_simulator(node_id); - let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id, - performance, - ); let node_params = NodeRewardingParameters::new( performance, global_rewarding_params.active_node_work(), ); + let dist = test.reward_with_distribution(node_id, node_params); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -993,7 +1011,7 @@ pub mod tests { let actual_prior1 = test.mix_rewarding(node_id1); let actual_prior2 = test.mix_rewarding(node_id2); - let res1 = test.assert_rewarding(node_id1, performance); + let res1 = test.assert_rewarding(node_id1, node_params); let prior_delegates1 = res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); @@ -1034,7 +1052,7 @@ pub mod tests { None, ); - let res2 = test.assert_rewarding(node_id2, performance); + let res2 = test.assert_rewarding(node_id2, node_params); let prior_delegates2 = res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); @@ -1092,7 +1110,7 @@ pub mod tests { let actual_prior1 = test.mix_rewarding(node_id1); let actual_prior2 = test.mix_rewarding(node_id2); - let res1 = test.assert_rewarding(node_id1, performance); + let res1 = test.assert_rewarding(node_id1, node_params); let prior_delegates1 = res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); @@ -1141,7 +1159,7 @@ pub mod tests { None, ); - let res2 = test.assert_rewarding(node_id2, performance); + let res2 = test.assert_rewarding(node_id2, node_params); let prior_delegates2 = res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); @@ -1188,6 +1206,16 @@ pub mod tests { } } + #[cfg(test)] + mod gateway_role_rewarding { + use super::*; + + #[test] + fn unimplemented() { + todo!() + } + } + #[cfg(test)] mod withdrawing_delegator_reward { use crate::interval::pending_events; @@ -1205,6 +1233,7 @@ pub mod tests { .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); let node_id2 = test .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let active_params = test.active_node_params(100.); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -1222,14 +1251,8 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id1, node_id2]); test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id1, - test_helpers::performance(100.0), - ); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id2, - test_helpers::performance(100.0), - ); + test.reward_with_distribution(node_id1, active_params); + test.reward_with_distribution(node_id2, active_params); let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id1); assert_eq!( @@ -1260,6 +1283,7 @@ pub mod tests { .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); let node_id2 = test .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let active_params = test.active_node_params(100.); // very low stake so operating cost would be higher than total reward let low_stake_id = @@ -1276,14 +1300,8 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id1, low_stake_id]); test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id1, - test_helpers::performance(100.0), - ); - test.legacy_reward_with_distribution_and_legacy_work_factor( - low_stake_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution(node_id1, active_params); + test.reward_with_distribution(low_stake_id, active_params); let res1 = try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id1).unwrap(); @@ -1317,7 +1335,7 @@ pub mod tests { test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonding); test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonded_leftover); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); @@ -1325,14 +1343,8 @@ pub mod tests { for _ in 0..10 { test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id_unbonding, - performance, - ); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id_unbonded_leftover, - performance, - ); + test.reward_with_distribution(node_id_unbonding, active_params); + test.reward_with_distribution(node_id_unbonded_leftover, active_params); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1402,7 +1414,7 @@ pub mod tests { test.add_immediate_delegation(delegator3, amount_quad3, node_id_quad); test.add_immediate_delegation(delegator4, amount_quad4, node_id_quad); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id_single, node_id_quad]); @@ -1411,18 +1423,12 @@ pub mod tests { let mut accumulated_quad = Decimal::zero(); for _ in 0..10 { test.start_epoch_transition(); - let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id_single, - performance, - ); + let dist = test.reward_with_distribution(node_id_single, active_params); // sanity check to make sure test is actually doing what it's supposed to be doing assert!(!dist.delegates.is_zero()); accumulated_single += dist.delegates; - let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id_quad, - performance, - ); + let dist = test.reward_with_distribution(node_id_quad, active_params); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); @@ -1487,10 +1493,7 @@ pub mod tests { for _ in 0..10 { test.start_epoch_transition(); - let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id_quad, - performance, - ); + let dist = test.reward_with_distribution(node_id_quad, active_params); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1571,14 +1574,12 @@ pub mod tests { let node_id = test.add_rewarded_legacy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); let sender = mock_info("random-guy", &[]); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution(node_id, active_params); let res = try_withdraw_operator_reward(test.deps_mut(), sender.clone()); assert_eq!( @@ -1598,6 +1599,7 @@ pub mod tests { let node_id1 = test.add_rewarded_legacy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); test.add_rewarded_legacy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let active_params = test.active_node_params(100.); let sender1 = mock_info(owner1, &[]); let sender2 = mock_info(owner2, &[]); @@ -1606,10 +1608,7 @@ pub mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id1]); test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id1, - test_helpers::performance(100.0), - ); + test.reward_with_distribution(node_id1, active_params); let res1 = try_withdraw_operator_reward(test.deps_mut(), sender1).unwrap(); assert!(matches!( @@ -1638,21 +1637,15 @@ pub mod tests { // add some delegation to the second node so that it wouldn't be cleared upon unbonding test.add_immediate_delegation("delegator", 100_000_000u128, node_id_unbonded_leftover); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { test.start_epoch_transition(); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id_unbonding, - performance, - ); - test.legacy_reward_with_distribution_and_legacy_work_factor( - node_id_unbonded_leftover, - performance, - ); + test.reward_with_distribution(node_id_unbonding, active_params); + test.reward_with_distribution(node_id_unbonded_leftover, active_params); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 8a19de6fa2..586bd5186e 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -221,10 +221,8 @@ pub(crate) fn ensure_any_node_bonded( // current nym-node match nymnodes_storage::nym_nodes().may_load(storage, node_id)? { - None => return Err(MixnetContractError::NymNodeBondNotFound { node_id }), - Some(bond) if bond.is_unbonding => { - return Err(MixnetContractError::NodeIsUnbonding { node_id }) - } + None => Err(MixnetContractError::NymNodeBondNotFound { node_id }), + Some(bond) if bond.is_unbonding => Err(MixnetContractError::NodeIsUnbonding { node_id }), _ => Ok(()), } } diff --git a/contracts/mixnet/src/support/mod.rs b/contracts/mixnet/src/support/mod.rs index 41cb1691e3..ecef1156f3 100644 --- a/contracts/mixnet/src/support/mod.rs +++ b/contracts/mixnet/src/support/mod.rs @@ -4,6 +4,5 @@ pub(crate) mod helpers; pub(crate) mod legacy; -const foo: &str = "remove that hack"; -// #[cfg(test)] +#[cfg(test)] pub(crate) mod tests; diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 328c6ef56d..8df5592c17 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -73,11 +73,11 @@ pub mod test_helpers { use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ ContractStateParams, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, - Gateway, GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, - InstantiateMsg, Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, - NodeId, NymNode, NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, - Percent, ProfitMarginRange, RoleAssignment, SignableGatewayBondingMsg, - SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, + Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg, + Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, + NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, Percent, + ProfitMarginRange, RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, @@ -109,6 +109,7 @@ pub mod test_helpers { } } + #[allow(clippy::enum_variant_names)] pub enum NodeQueryType { ById(NodeId), ByIdentity(IdentityKey), @@ -997,6 +998,7 @@ pub mod test_helpers { self.rewarding_params().active_node_work() } + #[allow(dead_code)] pub fn standby_node_work(&self) -> WorkFactor { self.rewarding_params().standby_node_work() } @@ -1008,6 +1010,7 @@ pub mod test_helpers { } } + #[allow(dead_code)] pub fn standby_node_params(&self, performance: f32) -> NodeRewardingParameters { NodeRewardingParameters { performance: test_helpers::performance(performance), @@ -1015,6 +1018,7 @@ pub mod test_helpers { } } + #[track_caller] pub fn reward_with_distribution_ignore_state( &mut self, node_id: NodeId, @@ -1027,6 +1031,7 @@ pub mod test_helpers { ) } + #[track_caller] pub fn reward_with_distribution_with_state_bypass( &mut self, node_id: NodeId, @@ -1045,16 +1050,7 @@ pub mod test_helpers { res } - #[deprecated] - pub fn legacy_reward_with_distribution_with_state_bypass( - &mut self, - node_id: NodeId, - performance: Performance, - ) -> RewardDistribution { - let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); - self.reward_with_distribution_with_state_bypass(node_id, performance, work_factor) - } - + #[allow(dead_code)] #[track_caller] pub fn node_role(&self, node_id: NodeId) -> Role { if read_assigned_roles(&self.deps.storage, Role::EntryGateway) @@ -1117,21 +1113,7 @@ pub mod test_helpers { work_factor } - pub fn legacy_reward_with_distribution_and_legacy_work_factor( - &mut self, - node_id: NodeId, - performance: Performance, - ) -> RewardDistribution { - let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); - self.reward_with_distribution( - node_id, - NodeRewardingParameters { - performance, - work_factor, - }, - ) - } - + #[track_caller] pub fn reward_with_distribution( &mut self, node_id: NodeId, diff --git a/contracts/vesting/schema/nym-vesting-contract.json b/contracts/vesting/schema/nym-vesting-contract.json index 1a7b292784..26069a16c4 100644 --- a/contracts/vesting/schema/nym-vesting-contract.json +++ b/contracts/vesting/schema/nym-vesting-contract.json @@ -24,96 +24,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", "oneOf": [ - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -188,7 +98,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -418,7 +328,7 @@ "$ref": "#/definitions/Coin" }, "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -761,10 +671,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -937,7 +843,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -946,7 +852,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -954,7 +860,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" diff --git a/contracts/vesting/schema/raw/execute.json b/contracts/vesting/schema/raw/execute.json index 9723294d67..81565897d4 100644 --- a/contracts/vesting/schema/raw/execute.json +++ b/contracts/vesting/schema/raw/execute.json @@ -2,96 +2,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", "oneOf": [ - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -166,7 +76,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -396,7 +306,7 @@ "$ref": "#/definitions/Coin" }, "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -739,10 +649,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -915,7 +821,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -924,7 +830,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -932,7 +838,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" From 561566f3c418f2ff7df2097ea98ee9a5e29a31c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 16:35:07 +0100 Subject: [PATCH 055/207] fixed wasm sdk build --- common/wasm/client-core/src/helpers.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 8e001d94e3..3eafcbd623 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -67,10 +67,10 @@ pub async fn current_network_topology_async( }; let api_client = NymApiClient::new(url); - let mixnodes = api_client.get_basic_mixnodes().await?; - let gateways = api_client.get_basic_gateways().await?; + let mixnodes = api_client.get_basic_mixnodes(None).await?; + let gateways = api_client.get_basic_gateways(None).await?; - Ok(NymTopology::from_basic(&mixnodes, g & ateways).into()) + Ok(NymTopology::from_basic(&mixnodes, &gateways).into()) } #[wasm_bindgen(js_name = "currentNetworkTopology")] From 3d132742196e60c71dc6fb3c5c39bdcd3cb3d62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 16:36:02 +0100 Subject: [PATCH 056/207] changed CI to no longer build with 'axum' feature since it was removed --- .github/workflows/ci-build.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a68cc72562..089d7097fb 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - os: [arc-ubuntu-20.04, custom-runner-mac-m1] + os: [ arc-ubuntu-20.04, custom-runner-mac-m1 ] runs-on: ${{ matrix.os }} env: CARGO_TERM_COLOR: always @@ -62,14 +62,6 @@ jobs: with: command: build - # while disabled by default, this build ensures nothing is broken within - # `axum` feature - - name: Build with `axum` feature - uses: actions-rs/cargo@v1 - with: - command: build - args: --features axum - - name: Build all examples if: contains(matrix.os, 'ubuntu') uses: actions-rs/cargo@v1 From 59da117e8f7e5c17a18ea9f7a0fd51c9ac3ad5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 16:39:42 +0100 Subject: [PATCH 057/207] fixed wallet's lock file --- nym-wallet/Cargo.lock | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 895eb5fe65..80ef99ae94 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "addr2line" version = "0.20.0" @@ -5946,11 +5940,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", -<<<<<<< HEAD "toml_edit 0.22.22", -======= - "toml_edit 0.22.21", ->>>>>>> 2485d63d0 (fix legacy test) ] [[package]] @@ -5977,25 +5967,15 @@ dependencies = [ [[package]] name = "toml_edit" -<<<<<<< HEAD version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -======= -version = "0.22.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" ->>>>>>> 2485d63d0 (fix legacy test) dependencies = [ "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", -<<<<<<< HEAD "winnow 0.6.19", -======= - "winnow 0.6.18", ->>>>>>> 2485d63d0 (fix legacy test) ] [[package]] @@ -6106,21 +6086,21 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "ts-rs" -version = "7.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ff1f8c90369bc172200013ac17ae86e7b5def580687df4e6127883454ff2b0" +checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9" dependencies = [ + "lazy_static", "thiserror", "ts-rs-macros", ] [[package]] name = "ts-rs-macros" -version = "7.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2" +checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ - "Inflector", "proc-macro2", "quote", "syn 2.0.55", @@ -6871,15 +6851,9 @@ dependencies = [ [[package]] name = "winnow" -<<<<<<< HEAD version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" -======= -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" ->>>>>>> 2485d63d0 (fix legacy test) dependencies = [ "memchr", ] From 065b3891f226c32f91afeb6fbeb9bbd738509ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 16:40:13 +0100 Subject: [PATCH 058/207] removed unused error variant --- nym-wallet/src-tauri/src/error.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 573e931b6b..26f7bea95d 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -164,9 +164,9 @@ pub enum BackendError { #[error("there aren't any vesting delegations to migrate")] NoVestingDelegations, - - #[error("this operation is no longer allowed to be performed with vesting tokens. please move them to your liquid balance and try again")] - DisabledVestingOperation, + // + // #[error("this operation is no longer allowed to be performed with vesting tokens. please move them to your liquid balance and try again")] + // DisabledVestingOperation, } impl Serialize for BackendError { From 050a58affa0b60ec7cd8a91fcf42c7860cdbbc3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 16:45:52 +0100 Subject: [PATCH 059/207] fixed cli command for updating node cost params --- .../mixnode/settings/update_cost_params.rs | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs index c246bc8fba..bd0817ab45 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs @@ -2,13 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::SigningClient; -use anyhow::{anyhow, bail}; +use anyhow::anyhow; use clap::Parser; use cosmwasm_std::Uint128; use log::info; -use nym_mixnet_contract_common::{ - NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, -}; +use nym_mixnet_contract_common::{NodeCostParams, Percent}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::nyxd::CosmWasmCoin; @@ -30,37 +28,41 @@ pub struct Args { pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { let denom = client.current_chain_details().mix_denom.base.as_str(); - let default_profit_margin = - Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT).unwrap(); - - let mix_details = client + let current_parameters = if let Some(client_mixnode) = client .get_owned_mixnode(&client.address()) .await? .mixnode_details - .ok_or_else(|| anyhow!("the client does not own any mixnodes"))?; - let current_parameters = mix_details.rewarding_details.cost_params; - - let profit_margin_percent = current_parameters - .map(|rd| rd.cost_params.profit_margin_percent) - .unwrap_or(default_profit_margin); - - let profit_margin_value = args - .profit_margin_percent - .map(|pm| Percent::from_percentage_value(pm as u64)) - .unwrap_or(profit_margin_percent)?; - - let cost_params = NodeCostParams { - profit_margin_percent: profit_margin_value, - interval_operating_cost: CosmWasmCoin { - denom: denom.into(), - amount: Uint128::new( - args.interval_operating_cost - .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), - ), - }, + { + client_mixnode.rewarding_details.cost_params + } else { + client + .get_owned_nymnode(&client.address()) + .await? + .details + .ok_or_else(|| anyhow!("the client does not own any nodes"))? + .rewarding_details + .cost_params }; - info!("Starting mixnode params updating!"); + let profit_margin_percent = args + .profit_margin_percent + .map(|pm| Percent::from_percentage_value(pm as u64)) + .unwrap_or(Ok(current_parameters.profit_margin_percent))?; + + let interval_operating_cost = args + .interval_operating_cost + .map(|oc| CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(oc), + }) + .unwrap_or(current_parameters.interval_operating_cost); + + let cost_params = NodeCostParams { + profit_margin_percent, + interval_operating_cost, + }; + + info!("Starting cost params updating using {cost_params:?} !"); let res = client .update_cost_params(cost_params, None) .await From f76d677f644cab1b832aeee509b17dccb17b385c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 10 Oct 2024 13:24:11 +0100 Subject: [PATCH 060/207] additional rewarding-related unit tests --- .../contracts-common/src/helpers.rs | 27 ++ .../contracts-common/src/lib.rs | 2 + .../mixnet-contract/src/nym_node.rs | 8 +- .../mixnet-contract/src/types.rs | 4 + contracts/mixnet/src/compat/transactions.rs | 4 +- contracts/mixnet/src/delegations/helpers.rs | 2 +- contracts/mixnet/src/gateways/transactions.rs | 4 +- .../mixnet/src/interval/pending_events.rs | 31 +- contracts/mixnet/src/interval/transactions.rs | 2 +- contracts/mixnet/src/rewards/helpers.rs | 4 +- contracts/mixnet/src/rewards/queries.rs | 42 +- contracts/mixnet/src/rewards/transactions.rs | 433 +++++++++++++++--- contracts/mixnet/src/support/helpers.rs | 35 +- contracts/mixnet/src/support/tests/legacy.rs | 4 +- contracts/mixnet/src/support/tests/mod.rs | 170 ++++--- 15 files changed, 565 insertions(+), 207 deletions(-) create mode 100644 common/cosmwasm-smart-contracts/contracts-common/src/helpers.rs diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/helpers.rs b/common/cosmwasm-smart-contracts/contracts-common/src/helpers.rs new file mode 100644 index 0000000000..4bcc65b75b --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/helpers.rs @@ -0,0 +1,27 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{BankMsg, Coin, CosmosMsg, Response}; + +pub trait ResponseExt { + fn add_optional_message(self, msg: Option>>) -> Self; + + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self; +} + +impl ResponseExt for Response { + fn add_optional_message(self, msg: Option>>) -> Self { + if let Some(msg) = msg { + self.add_message(msg) + } else { + self + } + } + + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self { + self.add_message(BankMsg::Send { + to_address: to.as_ref().to_string(), + amount: vec![amount], + }) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index 301e109d80..a81fafdf41 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -10,4 +10,6 @@ pub mod events; pub mod signing; pub mod types; +pub mod helpers; + pub use types::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs index 6885b8998e..21286924c1 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -193,8 +193,6 @@ impl RewardedSetMetadata { } } - // important note: this currently does **NOT** include gateway role as they're not being rewarded - // and the metadata is primarily used for data lookup during epoch transition pub fn highest_rewarded_id(&self) -> NodeId { let mut highest = 0; if self.layer1_metadata.highest_id > highest { @@ -206,6 +204,12 @@ impl RewardedSetMetadata { if self.layer3_metadata.highest_id > highest { highest = self.layer3_metadata.highest_id; } + if self.entry_gateway_metadata.highest_id > highest { + highest = self.entry_gateway_metadata.highest_id; + } + if self.exit_gateway_metadata.highest_id > highest { + highest = self.exit_gateway_metadata.highest_id; + } if self.standby_metadata.highest_id > highest { highest = self.standby_metadata.highest_id; } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index ad71782c44..c2653053da 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -22,6 +22,10 @@ pub struct RoleAssignment { } impl RoleAssignment { + pub fn new(role: Role, nodes: Vec) -> RoleAssignment { + RoleAssignment { role, nodes } + } + pub fn is_final_assignment(&self) -> bool { self.role.is_standby() } diff --git a/contracts/mixnet/src/compat/transactions.rs b/contracts/mixnet/src/compat/transactions.rs index 05153d11ec..64630b564e 100644 --- a/contracts/mixnet/src/compat/transactions.rs +++ b/contracts/mixnet/src/compat/transactions.rs @@ -598,7 +598,7 @@ mod tests { let node_id = test.add_legacy_mixnode("owner2", None); let sender = mock_info("owner2", &[]); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); test.reward_with_distribution(node_id, active_params); @@ -644,7 +644,7 @@ mod tests { let node_id = test.add_dummy_nymnode("owner2", None); let sender = mock_info("owner2", &[]); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); test.reward_with_distribution(node_id, active_params); diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs index 257015d5e2..20ae4e8582 100644 --- a/contracts/mixnet/src/delegations/helpers.rs +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -40,7 +40,7 @@ mod tests { test.add_immediate_delegation(delegator, og_amount, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index da03caaad3..541e7e86c8 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -8,7 +8,6 @@ use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::nodes::helpers::save_new_nymnode_with_id; use crate::nodes::transactions::add_nym_node_inner; use crate::support::helpers::ensure_epoch_in_progress_state; -use crate::support::helpers::AttachSendTokens; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ @@ -16,6 +15,7 @@ use mixnet_contract_common::events::{ }; use mixnet_contract_common::gateway::GatewayConfigUpdate; use mixnet_contract_common::{Gateway, GatewayBondingPayload, NodeCostParams}; +use nym_contracts_common::helpers::ResponseExt; use nym_contracts_common::signing::MessageSignature; pub(crate) fn try_add_gateway( @@ -351,7 +351,7 @@ pub mod tests { assert_eq!(&Addr::unchecked("bob"), first_node.owner()); // add a node owned by fred - let fred_identity = test.add_legacy_gateway("fred", None); + let (fred_identity, _) = test.add_legacy_gateway("fred", None); // let's make sure we now have 2 nodes: let nodes = queries::query_gateways_paged(test.deps(), None, None) diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index ddacc44253..75d78c17b6 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -17,6 +17,7 @@ use mixnet_contract_common::pending_events::{ }; use mixnet_contract_common::reward_params::{ActiveSetUpdate, IntervalRewardingParamsUpdate}; use mixnet_contract_common::{BlockHeight, Delegation, NodeId}; +use nym_contracts_common::helpers::ResponseExt; use crate::delegations; use crate::delegations::storage as delegations_storage; @@ -28,7 +29,7 @@ use crate::nodes::helpers::{cleanup_post_unbond_nym_node_storage, get_node_detai use crate::nodes::storage as nymnodes_storage; use crate::rewards::storage as rewards_storage; use crate::rewards::storage::RewardingStorage; -use crate::support::helpers::{ensure_any_node_bonded, AttachSendTokens}; +use crate::support::helpers::ensure_any_node_bonded; pub(crate) trait ContractExecutableEvent { // note: the error only means a HARD error like we failed to read from storage. @@ -837,7 +838,7 @@ mod tests { let active_params = test.active_node_params(100.0); // perform some rewarding here to advance the unit delegation beyond the initial value - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); @@ -911,7 +912,7 @@ mod tests { let active_params = test.active_node_params(100.0); // perform some rewarding here to advance the unit delegation beyond the initial value - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); @@ -1001,7 +1002,7 @@ mod tests { let active_params = test.active_node_params(100.0); // perform some rewarding here to advance the unit delegation beyond the initial value - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); @@ -1135,7 +1136,7 @@ mod tests { .unwrap(); let active_params = test.active_node_params(100.0); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); @@ -1265,7 +1266,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); let dist1 = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); let dist2 = @@ -1289,7 +1290,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); let dist = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); @@ -1327,7 +1328,7 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few epochs of rewarding for _ in 0..500 { @@ -1356,7 +1357,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); let mut cumulative_op_reward = Decimal::zero(); let mut cumulative_del_reward = Decimal::zero(); @@ -1405,7 +1406,7 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few more epochs of rewarding for _ in 0..500 { @@ -1544,7 +1545,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); let dist1 = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); let dist2 = @@ -1568,7 +1569,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); let dist = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); @@ -1606,7 +1607,7 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few epochs of rewarding for _ in 0..500 { @@ -1635,7 +1636,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); let mut cumulative_op_reward = Decimal::zero(); let mut cumulative_del_reward = Decimal::zero(); @@ -1684,7 +1685,7 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few more epochs of rewarding for _ in 0..500 { diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index ca4c7c0563..c60e4440c0 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -924,7 +924,7 @@ mod tests { let mut test = TestSetup::new(); let rewarding_validator = test.rewarding_validator(); - test.force_change_rewarded_set(vec![1, 2, 3, 4, 5]); + test.force_change_mix_rewarded_set(vec![1, 2, 3, 4, 5]); test.skip_to_current_epoch_end(); let env = test.env(); diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 793f8b8f14..fba530e334 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -269,7 +269,7 @@ mod tests { assert_eq!(res.amount, Uint128::zero()); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); @@ -309,7 +309,7 @@ mod tests { assert_eq!(res.amount, Uint128::zero()); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index dc296752f1..64f55bcec9 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -316,7 +316,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); @@ -361,7 +361,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); @@ -391,7 +391,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); @@ -469,7 +469,7 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); @@ -515,7 +515,7 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); @@ -547,7 +547,7 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); @@ -585,7 +585,7 @@ mod tests { test.add_immediate_delegation(del2, 150_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); test.reward_with_distribution_ignore_state(mix_id, active_params); @@ -694,7 +694,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); @@ -722,7 +722,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); @@ -748,9 +748,9 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); - test.force_change_rewarded_set(vec![]); + test.force_change_mix_rewarded_set(vec![]); let res = query_estimated_current_epoch_operator_reward( test.deps(), @@ -772,7 +772,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_operator_reward( @@ -795,7 +795,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let mix_rewarding = test.mix_rewarding(mix_id); @@ -861,7 +861,7 @@ mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_delegator_reward( @@ -887,7 +887,7 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info("mix-owner", &[]); @@ -918,7 +918,7 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info("mix-owner", &[]); @@ -950,9 +950,9 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); - test.force_change_rewarded_set(vec![]); + test.force_change_mix_rewarded_set(vec![]); let res = query_estimated_current_epoch_delegator_reward( test.deps(), @@ -978,7 +978,7 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_delegator_reward( @@ -1004,7 +1004,7 @@ mod tests { test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mix_rewarding = test.mix_rewarding(mix_id); let res = query_estimated_current_epoch_delegator_reward( @@ -1053,7 +1053,7 @@ mod tests { test.add_immediate_delegation(del2, initial_stake2, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let params = test.active_node_params(95.0); test.reward_with_distribution_ignore_state(mix_id, params); diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index e3edbcc462..824b918521 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -13,7 +13,6 @@ use crate::rewards::helpers::update_and_save_last_rewarded; use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ ensure_any_node_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, - AttachSendTokens, }; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; @@ -29,6 +28,7 @@ use mixnet_contract_common::reward_params::{ ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters, }; use mixnet_contract_common::{Delegation, EpochState, MixNodeDetails, NodeId, NymNodeDetails}; +use nym_contracts_common::helpers::ResponseExt; pub(crate) fn try_reward_node( deps: DepsMut<'_>, @@ -315,10 +315,51 @@ pub mod tests { use crate::mixnodes::storage as mixnodes_storage; use crate::support::tests::fixtures::active_set_update_fixture; use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; + // a simple wrapper to streamline checking for rewarding results + trait TestRewarding { + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result; + + fn assert_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Response; + } + + impl TestRewarding for TestSetup { + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result { + let sender = self.rewarding_validator(); + self.execute_fn( + |deps, env, info| try_reward_node(deps, env, info, node_id, rewarding_params), + sender, + ) + } + + #[track_caller] + fn assert_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Response { + let caller = std::panic::Location::caller(); + self.execute_rewarding(node_id, rewarding_params) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + } + #[cfg(test)] - mod mixnode_rewarding { + mod legacy_mixnode_rewarding { use super::*; use crate::interval::pending_events; use crate::support::tests::test_helpers::{find_attribute, FindAttribute, TestSetup}; @@ -333,46 +374,6 @@ pub mod tests { use mixnet_contract_common::reward_params::WorkFactor; use mixnet_contract_common::EpochStatus; - // a simple wrapper to streamline checking for rewarding results - trait TestRewarding { - fn execute_rewarding( - &mut self, - node_id: NodeId, - rewarding_params: NodeRewardingParameters, - ) -> Result; - - fn assert_rewarding( - &mut self, - node_id: NodeId, - rewarding_params: NodeRewardingParameters, - ) -> Response; - } - - impl TestRewarding for TestSetup { - fn execute_rewarding( - &mut self, - node_id: NodeId, - rewarding_params: NodeRewardingParameters, - ) -> Result { - let sender = self.rewarding_validator(); - self.execute_fn( - |deps, env, info| try_reward_node(deps, env, info, node_id, rewarding_params), - sender, - ) - } - - #[track_caller] - fn assert_rewarding( - &mut self, - node_id: NodeId, - rewarding_params: NodeRewardingParameters, - ) -> Response { - let caller = std::panic::Location::caller(); - self.execute_rewarding(node_id, rewarding_params) - .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) - } - } - #[cfg(test)] mod epoch_state_is_correctly_updated { use super::*; @@ -386,7 +387,7 @@ pub mod tests { test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); let node_id_never_existed = 42; test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![ + test.force_change_mix_rewarded_set(vec![ node_id_unbonded, node_id_unbonded_leftover, node_id_never_existed, @@ -460,7 +461,7 @@ pub mod tests { let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); let zero_performance = test.active_node_params(0.); @@ -479,7 +480,7 @@ pub mod tests { let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); let params = NodeRewardingParameters::new( @@ -501,7 +502,7 @@ pub mod tests { let node_id = test.add_dummy_nymnode("node-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); let zero_performance = test.active_node_params(0.); @@ -520,7 +521,7 @@ pub mod tests { let node_id = test.add_dummy_nymnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); let params = NodeRewardingParameters::new( @@ -542,7 +543,7 @@ pub mod tests { let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); let active_params = test.active_node_params(100.); @@ -566,7 +567,7 @@ pub mod tests { } test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(ids.clone()); + test.force_change_mix_rewarded_set(ids.clone()); test.start_epoch_transition(); let active_params = test.active_node_params(100.); @@ -611,7 +612,7 @@ pub mod tests { .unwrap(); test.skip_to_current_epoch_end(); - test.force_change_rewarded_set(vec![1, 2, 3]); + test.force_change_mix_rewarded_set(vec![1, 2, 3]); let res = test.execute_rewarding(1, active_params); @@ -633,7 +634,7 @@ pub mod tests { // skip time to when the following epoch is over (since mixnodes are not eligible for rewarding // in the same epoch they're bonded and we need the rewarding epoch to be over) test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); let params = test.legacy_rewarding_params(node_id, 100.); @@ -656,7 +657,7 @@ pub mod tests { let node_id_unbonded_leftover = test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![ + test.force_change_mix_rewarded_set(vec![ node_id_unbonded, node_id_unbonded_leftover, node_id_never_existed, @@ -712,7 +713,7 @@ pub mod tests { // node is in the active set BUT the current epoch has just begun test.skip_to_next_epoch(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); let active_params = test.active_node_params(100.); let res = test.execute_rewarding(node_id, active_params); @@ -734,7 +735,7 @@ pub mod tests { let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id, 42]); + test.force_change_mix_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); let active_params = test.active_node_params(100.); @@ -762,7 +763,7 @@ pub mod tests { let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id, 42]); + test.force_change_mix_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); let zero_perf_params = test.active_node_params(0.); let active_params = test.active_node_params(100.); @@ -800,7 +801,7 @@ pub mod tests { let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id, 42]); + test.force_change_mix_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); let zero_work_params = @@ -842,7 +843,7 @@ pub mod tests { let node_id3 = test.add_rewarded_legacy_mixnode("mix-owner3", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id1, node_id2, node_id3]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2, node_id3]); test.start_epoch_transition(); let params = test.active_node_params(98.0); @@ -908,7 +909,7 @@ pub mod tests { test.skip_to_next_epoch_end(); test.start_epoch_transition(); - test.force_change_rewarded_set(vec![node_id1, node_id2, node_id3]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2, node_id3]); let performance = test_helpers::performance(98.0); test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); @@ -981,7 +982,7 @@ pub mod tests { let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", Some(operator2)); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id1, node_id2]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2]); let performance = test_helpers::performance(98.0); test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id1); @@ -1207,12 +1208,305 @@ pub mod tests { } #[cfg(test)] - mod gateway_role_rewarding { + mod legacy_gateway_rewarding { use super::*; + use crate::support::tests::test_helpers::FindAttribute; + use mixnet_contract_common::events::{BOND_NOT_FOUND_VALUE, NO_REWARD_REASON_KEY}; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::RoleAssignment; #[test] - fn unimplemented() { - todo!() + fn regardless_of_performance_or_work_they_get_nothing() { + let mut test = TestSetup::new(); + let (_, node_id) = test.add_legacy_gateway("owner", None); + + test.skip_to_next_epoch_end(); + test.force_assign_rewarded_set(vec![RoleAssignment::new( + Role::EntryGateway, + vec![node_id], + )]); + test.start_epoch_transition(); + + let rewarding_params = test.active_node_params(100.); + let res = test.assert_rewarding(node_id, rewarding_params); + + let reward_attr = res.any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, BOND_NOT_FOUND_VALUE); + + // make sure the epoch actually progressed (i.e. unrewarded gateway hasn't stalled it) + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + } + + // rewarding for entry gateway, exit gateway and standby nym-nodes + #[cfg(test)] + mod non_legacy_rewarding { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::FindAttribute; + use cosmwasm_std::{Decimal, Uint128}; + use mixnet_contract_common::events::{ + BOND_NOT_FOUND_VALUE, NO_REWARD_REASON_KEY, OPERATOR_REWARD_KEY, + ZERO_PERFORMANCE_OR_WORK_VALUE, + }; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::WorkFactor; + use mixnet_contract_common::RoleAssignment; + use std::collections::HashMap; + use std::ops::{Deref, DerefMut}; + + struct RewardingSetup { + standby_node: NodeId, + entry_node: NodeId, + exit_node: NodeId, + mixing_node: NodeId, + + inner: TestSetup, + } + + impl RewardingSetup { + pub fn new_rewarding_setup() -> Self { + let mut inner = TestSetup::new(); + let mixing_node = inner.add_dummy_nymnode("mixing-owner", None); + let entry_node = inner.add_dummy_nymnode("entry-owner", None); + let exit_node = inner.add_dummy_nymnode("exit-owner", None); + let standby_node = inner.add_dummy_nymnode("standby-owner", None); + + RewardingSetup { + standby_node, + entry_node, + exit_node, + mixing_node, + inner, + } + } + + pub fn nodes(&self) -> Vec { + vec![ + self.mixing_node, + self.entry_node, + self.exit_node, + self.standby_node, + ] + } + + pub fn reset_rewarded_set(&mut self) { + self.inner.force_assign_rewarded_set(vec![ + RoleAssignment { + role: Role::Layer1, + nodes: vec![self.mixing_node], + }, + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![self.entry_node], + }, + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![self.exit_node], + }, + RoleAssignment { + role: Role::Standby, + nodes: vec![self.standby_node], + }, + ]); + } + + pub fn local_node_role(&self, node_id: NodeId) -> Role { + match node_id { + n if n == self.mixing_node => Role::Layer1, + n if n == self.entry_node => Role::EntryGateway, + n if n == self.exit_node => Role::ExitGateway, + n if n == self.standby_node => Role::Standby, + _ => unreachable!(), + } + } + + pub fn add_to_rewarded_set(&mut self, node_id: NodeId) { + let role = self.local_node_role(node_id); + self.inner.force_assign_rewarded_set(vec![RoleAssignment { + role, + nodes: vec![node_id], + }]) + } + + pub fn reward_all( + &mut self, + performance: f32, + ) -> HashMap> { + let mut results = HashMap::new(); + + self.skip_to_next_epoch_end(); + self.reset_rewarded_set(); + self.start_epoch_transition(); + + let active_params = self.active_node_params(performance); + let standby_params = self.standby_node_params(performance); + + let mixing_node = self.mixing_node; + let entry_node = self.entry_node; + let exit_node = self.exit_node; + let standby_node = self.standby_node; + + results.insert( + mixing_node, + self.execute_rewarding(mixing_node, active_params), + ); + results.insert( + entry_node, + self.execute_rewarding(entry_node, active_params), + ); + results.insert(exit_node, self.execute_rewarding(exit_node, active_params)); + results.insert( + standby_node, + self.execute_rewarding(standby_node, standby_params), + ); + + results + } + } + + impl Deref for RewardingSetup { + type Target = TestSetup; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl DerefMut for RewardingSetup { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } + } + + #[test] + fn when_target_node_has_zero_performance() { + let mut test = RewardingSetup::new_rewarding_setup(); + let results = test.reward_all(0.); + for res in results.into_values() { + let reward_attr = res.unwrap().any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, ZERO_PERFORMANCE_OR_WORK_VALUE); + } + + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + + #[test] + fn when_target_node_has_zero_work_factor() { + let mut test = RewardingSetup::new_rewarding_setup(); + + test.skip_to_next_epoch_end(); + test.reset_rewarded_set(); + test.start_epoch_transition(); + + let params = + NodeRewardingParameters::new(test_helpers::performance(100.), WorkFactor::zero()); + + for node in test.nodes() { + let res = test.assert_rewarding(node, params); + let reward_attr = res.any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, ZERO_PERFORMANCE_OR_WORK_VALUE); + } + + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + + #[test] + fn when_theres_only_one_node_to_reward() { + let test_lookup = RewardingSetup::new_rewarding_setup(); + + for node in test_lookup.nodes() { + let mut actual_setup = RewardingSetup::new_rewarding_setup(); + actual_setup.add_to_rewarded_set(node); + let mut res = actual_setup.reward_all(100.); + + // get the response for this particular node + let res = res.remove(&node).unwrap().unwrap(); + let reward: Decimal = res.any_parsed_attribute(OPERATOR_REWARD_KEY); + assert!(!reward.is_zero()); + + let current = actual_setup.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + } + + #[test] + fn when_theres_multiple_nodes_to_reward() { + let mut test = RewardingSetup::new_rewarding_setup(); + let results = test.reward_all(100.); + for res in results.into_values() { + let reward: Decimal = res.unwrap().any_parsed_attribute(OPERATOR_REWARD_KEY); + assert!(!reward.is_zero()); + } + + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + + #[test] + fn cant_be_performed_for_unbonded_nodes() { + let test_lookup = RewardingSetup::new_rewarding_setup(); + + for node in test_lookup.nodes() { + let mut actual_setup = RewardingSetup::new_rewarding_setup(); + actual_setup.add_to_rewarded_set(node); + + let env = actual_setup.env(); + + // add some delegations to indicate the rewarding information shouldn't get removed + actual_setup.add_immediate_delegation("delegator", Uint128::new(12345678), node); + pending_events::unbond_nym_node(actual_setup.deps_mut(), &env, 123, node).unwrap(); + + let mut res = actual_setup.reward_all(100.); + + // get the response for this particular node + let res = res.remove(&node).unwrap().unwrap(); + let reward_attr = res.any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, BOND_NOT_FOUND_VALUE); + + let current = actual_setup.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + } + + #[test] + fn can_only_be_performed_once_per_node_per_epoch() { + let test_lookup = RewardingSetup::new_rewarding_setup(); + + let params = test_lookup.active_node_params(100.0); + for node in test_lookup.nodes() { + let mut actual_setup = RewardingSetup::new_rewarding_setup(); + + actual_setup.skip_to_next_epoch_end(); + + let extra = actual_setup.add_dummy_nymnode("foomp", None); + + // add extra node to the rewarded set so rewarding wouldn't immediately go into event reconciliation + let role = actual_setup.local_node_role(node); + actual_setup + .inner + .force_assign_rewarded_set(vec![RoleAssignment { + role, + nodes: vec![node, extra], + }]); + + actual_setup.start_epoch_transition(); + + // first rewarding + actual_setup.assert_rewarding(node, params); + + // second rewarding + let res = actual_setup.execute_rewarding(node, params).unwrap_err(); + assert_eq!( + res, + MixnetContractError::NodeAlreadyRewarded { + node_id: node, + absolute_epoch_id: 1, + } + ); + } } } @@ -1220,6 +1514,7 @@ pub mod tests { mod withdrawing_delegator_reward { use crate::interval::pending_events; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; + use cosmwasm_std::testing::mock_info; use cosmwasm_std::{BankMsg, CosmosMsg, Decimal, Uint128}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -1249,7 +1544,7 @@ pub mod tests { // perform some rewarding so that we'd have non-zero rewards test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id1, node_id2]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2]); test.start_epoch_transition(); test.reward_with_distribution(node_id1, active_params); test.reward_with_distribution(node_id2, active_params); @@ -1298,7 +1593,7 @@ pub mod tests { // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id1, low_stake_id]); + test.force_change_mix_rewarded_set(vec![node_id1, low_stake_id]); test.start_epoch_transition(); test.reward_with_distribution(node_id1, active_params); test.reward_with_distribution(low_stake_id, active_params); @@ -1337,7 +1632,7 @@ pub mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); + test.force_change_mix_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { @@ -1416,7 +1711,7 @@ pub mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id_single, node_id_quad]); + test.force_change_mix_rewarded_set(vec![node_id_single, node_id_quad]); // accumulate some rewards let mut accumulated_single = Decimal::zero(); @@ -1577,7 +1872,7 @@ pub mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); test.reward_with_distribution(node_id, active_params); @@ -1606,7 +1901,7 @@ pub mod tests { // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id1]); + test.force_change_mix_rewarded_set(vec![node_id1]); test.start_epoch_transition(); test.reward_with_distribution(node_id1, active_params); @@ -1639,7 +1934,7 @@ pub mod tests { let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); + test.force_change_mix_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 586bd5186e..8997882462 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -7,46 +7,13 @@ use crate::mixnodes::helpers::must_get_mixnode_bond_by_owner; use crate::mixnodes::storage as mixnodes_storage; use crate::nodes::helpers::must_get_node_bond_by_owner; use crate::nodes::storage as nymnodes_storage; -use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Response, Storage}; +use cosmwasm_std::{Addr, Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::PendingMixNodeChanges; use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond, NodeId}; use nym_contracts_common::IdentityKey; use nym_contracts_common::Percent; -// helper trait to attach `Msg` to a response if it's provided -#[allow(dead_code)] -pub(crate) trait AttachOptionalMessage { - fn add_optional_message(self, msg: Option>>) -> Self; -} - -impl AttachOptionalMessage for Response { - fn add_optional_message(self, msg: Option>>) -> Self { - if let Some(msg) = msg { - self.add_message(msg) - } else { - self - } - } -} - -pub(crate) trait AttachSendTokens { - fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self; -} - -impl AttachSendTokens for Response { - fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self { - self.add_message(BankMsg::Send { - to_address: to.as_ref().to_string(), - amount: vec![amount], - }) - } -} - -// pub fn debug_with_visibility>(api: &dyn Api, msg: S) { -// api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into())); -// } - pub(crate) fn validate_pledge( mut pledge: Vec, minimum_pledge: Coin, diff --git a/contracts/mixnet/src/support/tests/legacy.rs b/contracts/mixnet/src/support/tests/legacy.rs index d74808cdf1..2fc8cfe93a 100644 --- a/contracts/mixnet/src/support/tests/legacy.rs +++ b/contracts/mixnet/src/support/tests/legacy.rs @@ -52,7 +52,7 @@ pub(crate) fn save_new_gateway( gateway: Gateway, owner: Addr, pledge: Coin, -) -> Result { +) -> Result<(IdentityKey, NodeId), MixnetContractError> { let gateway_identity = gateway.identity_key.clone(); let bond = GatewayBond::new(pledge.clone(), owner.clone(), env.block.height, gateway); @@ -61,5 +61,5 @@ pub(crate) fn save_new_gateway( let id = next_nymnode_id_counter(storage)?; PREASSIGNED_LEGACY_IDS.save(storage, gateway_identity.clone(), &id)?; - Ok(gateway_identity) + Ok((gateway_identity, id)) } diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 8df5592c17..ba0eecaabb 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -159,6 +159,7 @@ pub mod test_helpers { pub owner: MessageInfo, } + #[allow(unused)] impl TestSetup { pub fn new() -> Self { let deps = init_contract(); @@ -337,6 +338,12 @@ pub mod test_helpers { interval_storage::current_interval(self.deps().storage).unwrap() } + pub fn current_epoch_state(&self) -> EpochState { + interval_storage::current_epoch_status(self.deps().storage) + .unwrap() + .state + } + pub fn active_roles_bucket(&self) -> RoleStorageBucket { ACTIVE_ROLES_BUCKET.load(self.deps().storage).unwrap() } @@ -395,52 +402,54 @@ pub mod test_helpers { .unwrap(); } - pub fn immediately_assign_lowest_mix_layer(&mut self, node_id: NodeId) -> Role { - let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); - - let mut layer1 = read_assigned_roles(&self.deps.storage, Role::Layer1).unwrap(); - let mut layer2 = read_assigned_roles(&self.deps.storage, Role::Layer2).unwrap(); - let mut layer3 = read_assigned_roles(&self.deps.storage, Role::Layer3).unwrap(); + pub fn lowest_mix_layer(&mut self) -> Role { + let layer1 = read_assigned_roles(&self.deps.storage, Role::Layer1).unwrap(); + let layer2 = read_assigned_roles(&self.deps.storage, Role::Layer2).unwrap(); + let layer3 = read_assigned_roles(&self.deps.storage, Role::Layer3).unwrap(); let l1 = layer1.len(); let l2 = layer2.len(); let l3 = layer3.len(); if l1 <= l2 && l1 <= l3 { - layer1.push(node_id); - ROLES - .save( - &mut self.deps.storage, - (active_bucket as u8, Role::Layer1), - &layer1, - ) - .unwrap(); - Role::Layer1 } else if l2 <= l3 && l2 <= l1 { - layer2.push(node_id); - ROLES - .save( - &mut self.deps.storage, - (active_bucket as u8, Role::Layer2), - &layer2, - ) - .unwrap(); - Role::Layer2 } else { - layer3.push(node_id); - ROLES - .save( - &mut self.deps.storage, - (active_bucket as u8, Role::Layer3), - &layer3, - ) - .unwrap(); - Role::Layer3 } } + pub fn immediately_assign_lowest_mix_layer(&mut self, node_id: NodeId) -> Role { + let layer = self.lowest_mix_layer(); + self.immediately_add_to_role(node_id, layer); + layer + } + + pub fn immediately_add_to_role(&mut self, node_id: NodeId, role: Role) { + let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); + let mut current = read_assigned_roles(self.deps().storage, role).unwrap(); + current.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, role), + ¤t, + ) + .unwrap(); + } + + pub fn immediately_assign_standby_role(&mut self, node_id: NodeId) { + self.immediately_add_to_role(node_id, Role::Standby) + } + + pub fn immediately_assign_exit_gateway_role(&mut self, node_id: NodeId) { + self.immediately_add_to_role(node_id, Role::ExitGateway) + } + + pub fn immediately_assign_entry_gateway_role(&mut self, node_id: NodeId) { + self.immediately_add_to_role(node_id, Role::EntryGateway) + } + pub fn add_rewarded_set_nymnode( &mut self, owner: &str, @@ -513,7 +522,7 @@ pub mod test_helpers { ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap(); signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone()) .unwrap(); - let node_id = legacy::save_new_mixnode( + legacy::save_new_mixnode( &mut self.deps.storage, env, mixnode, @@ -521,8 +530,38 @@ pub mod test_helpers { info.sender, info.funds[0].clone(), ) - .unwrap(); + .unwrap() + } + pub fn add_rewarded_mixing_node(&mut self, owner: &str, stake: Option) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_lowest_mix_layer(node_id); + node_id + } + + pub fn add_rewarded_entry_gateway_node( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_entry_gateway_role(node_id); + node_id + } + + pub fn add_rewarded_exit_gateway_node( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_exit_gateway_role(node_id); + node_id + } + + pub fn add_standby_node(&mut self, owner: &str, stake: Option) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_standby_role(node_id); node_id } @@ -537,7 +576,11 @@ pub mod test_helpers { node_id } - pub fn add_legacy_gateway(&mut self, sender: &str, stake: Option) -> IdentityKey { + pub fn add_legacy_gateway( + &mut self, + sender: &str, + stake: Option, + ) -> (IdentityKey, NodeId) { let stake = self.make_gateway_pledge(stake); let (gateway, _) = self.gateway_with_signature(sender, Some(stake.clone())); @@ -929,7 +972,7 @@ pub mod test_helpers { pub fn reset_role_assignment(&mut self) { let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); - for role in vec![ + for role in [ Role::EntryGateway, Role::ExitGateway, Role::Layer1, @@ -943,32 +986,35 @@ pub mod test_helpers { } } - // note: this does NOT assign gateway role - pub fn force_change_rewarded_set(&mut self, nodes: Vec) { - // reset current assignment + pub fn force_assign_rewarded_set(&mut self, assignment: Vec) { self.reset_role_assignment(); - let mut roles = HashMap::new(); - for node in nodes { - let role = self.immediately_assign_lowest_mix_layer(node); - let assigned = roles.entry(role).or_insert(Vec::new()); - assigned.push(node) - } // we cheat a bit to write to the 'active' bucket instead swap_active_role_bucket(self.deps_mut().storage).unwrap(); + for role_assignment in assignment { + let mut sorted_assignment = role_assignment.clone(); + sorted_assignment.nodes.sort(); - for (role, assignment) in roles { - save_assignment( - self.deps_mut().storage, - RoleAssignment { - role, - nodes: assignment, - }, - ) - .unwrap(); + save_assignment(self.deps_mut().storage, sorted_assignment).unwrap(); + } + swap_active_role_bucket(self.deps_mut().storage).unwrap(); + } + + // note: this does NOT assign gateway role + pub fn force_change_mix_rewarded_set(&mut self, nodes: Vec) { + let mut roles = HashMap::new(); + for node in nodes { + let layer = self.lowest_mix_layer(); + let assigned = roles.entry(layer).or_insert(Vec::new()); + assigned.push(node) } - swap_active_role_bucket(self.deps_mut().storage).unwrap(); + let roles = roles + .into_iter() + .map(|(role, nodes)| RoleAssignment { role, nodes }) + .collect(); + + self.force_assign_rewarded_set(roles) } pub fn instantiate_simulator(&self, node: NodeId) -> Simulator { @@ -1266,6 +1312,18 @@ pub mod test_helpers { E: Into>, S: Into; + fn any_attribute(&self, attribute: &str) -> String { + self.attribute::<_, String>(None, attribute) + } + + fn any_parsed_attribute(&self, attribute: &str) -> T + where + T: FromStr, + ::Err: Debug, + { + self.parsed_attribute::<_, String, T>(None, attribute) + } + fn parsed_attribute(&self, event_type: E, attribute: &str) -> T where E: Into>, From 45f4eeeff22f01e6da03c990cab8ab807494fe39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 10 Oct 2024 15:38:00 +0100 Subject: [PATCH 061/207] fixed linter issues and outdated tests --- Cargo.lock | 88 ++++++++ .../contract_traits/mixnet_query_client.rs | 1 - .../contract_traits/mixnet_signing_client.rs | 7 - .../mixnode/settings/update_cost_params.rs | 60 +---- .../nymnode/settings/update_cost_params.rs | 66 +++--- .../contracts-common/src/types.rs | 1 - .../mixnet-contract/src/interval.rs | 2 +- .../mixnet-contract/src/nym_node.rs | 1 - .../mixnet-contract/src/rewarding/helpers.rs | 8 - common/nymsphinx/src/receiver.rs | 5 +- common/types/src/pending_events.rs | 55 ++++- .../src/support/fixtures.rs | 9 +- .../src/support/setup.rs | 206 +++++++++++++++--- nym-api/Cargo.toml | 1 + nym-api/nym-api-requests/src/ecash/models.rs | 2 +- nym-api/src/ecash/tests/issued_credentials.rs | 97 ++++----- nym-api/src/ecash/tests/mod.rs | 124 ++++++----- nym-api/src/epoch_operations/mod.rs | 5 +- .../src/network_monitor/monitor/preparer.rs | 4 +- nym-api/src/node_status_api/models.rs | 8 - nym-api/src/nym_contract_cache/cache/mod.rs | 6 +- nym-api/src/support/http/state.rs | 6 +- nym-api/src/support/storage/manager.rs | 144 ++++++------ nym-network-monitor/src/accounting.rs | 20 +- nym-wallet/Cargo.lock | 18 +- .../examples/manually_overwrite_topology.rs | 3 - .../src/manager/local_client.rs | 2 +- .../src/manager/local_nodes.rs | 54 +---- 28 files changed, 586 insertions(+), 417 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77221ef262..2e74311fa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,16 @@ dependencies = [ "nom", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -396,6 +406,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + [[package]] name = "autocfg" version = "1.3.0" @@ -525,6 +541,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-test" +version = "16.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3254184de359bbae2a8ca10b050870a7a4f1c8332a2c27d53f360b9835bb3911" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum 0.7.7", + "bytes", + "cookie", + "http 1.1.0", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.1", + "url", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -2016,6 +2061,12 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.8.1" @@ -4256,6 +4307,7 @@ dependencies = [ "async-trait", "axum 0.7.7", "axum-extra", + "axum-test", "bincode", "bip39", "bloomfilter", @@ -7066,6 +7118,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -7572,6 +7634,16 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "reserve-port" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283" +dependencies = [ + "lazy_static", + "thiserror", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -7782,6 +7854,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 0.2.12", + "mime", + "mime_guess", + "rand", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index ba90467f32..21529ad086 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -732,7 +732,6 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; - use nym_mixnet_contract_common::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index 5a7b8fae26..b253c3dc94 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -845,13 +845,6 @@ mod tests { client.update_nymnode_config(update, None).ignore() } - ExecuteMsg::TestingUncheckedBondLegacyMixnode { .. } => { - todo!("purposely not implemented") - } - ExecuteMsg::TestingUncheckedBondLegacyGateway { .. } => { - todo!("purposely not implemented") - } - #[cfg(feature = "contract-testing")] MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => { client.testing_resolve_all_pending_events(None).ignore() diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs index bd0817ab45..cd145737ca 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs @@ -2,13 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::SigningClient; -use anyhow::anyhow; +use crate::validator::mixnet::operators::nymnode; use clap::Parser; -use cosmwasm_std::Uint128; -use log::info; -use nym_mixnet_contract_common::{NodeCostParams, Percent}; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; -use nym_validator_client::nyxd::CosmWasmCoin; #[derive(Debug, Parser)] pub struct Args { @@ -26,48 +21,13 @@ pub struct Args { } pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { - let denom = client.current_chain_details().mix_denom.base.as_str(); - - let current_parameters = if let Some(client_mixnode) = client - .get_owned_mixnode(&client.address()) - .await? - .mixnode_details - { - client_mixnode.rewarding_details.cost_params - } else { - client - .get_owned_nymnode(&client.address()) - .await? - .details - .ok_or_else(|| anyhow!("the client does not own any nodes"))? - .rewarding_details - .cost_params - }; - - let profit_margin_percent = args - .profit_margin_percent - .map(|pm| Percent::from_percentage_value(pm as u64)) - .unwrap_or(Ok(current_parameters.profit_margin_percent))?; - - let interval_operating_cost = args - .interval_operating_cost - .map(|oc| CosmWasmCoin { - denom: denom.into(), - amount: Uint128::new(oc), - }) - .unwrap_or(current_parameters.interval_operating_cost); - - let cost_params = NodeCostParams { - profit_margin_percent, - interval_operating_cost, - }; - - info!("Starting cost params updating using {cost_params:?} !"); - let res = client - .update_cost_params(cost_params, None) - .await - .expect("failed to update cost params"); - - info!("Cost params result: {:?}", res); - Ok(()) + // the below can handle both, nymnode and legacy mixnode + nymnode::settings::update_cost_params::update_cost_params( + nymnode::settings::update_cost_params::Args { + profit_margin_percent: args.profit_margin_percent, + interval_operating_cost: args.interval_operating_cost, + }, + client, + ) + .await } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs index b7168f243a..5668c92e0d 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs @@ -6,9 +6,7 @@ use anyhow::anyhow; use clap::Parser; use cosmwasm_std::Uint128; use log::info; -use nym_mixnet_contract_common::{ - NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, -}; +use nym_mixnet_contract_common::{NodeCostParams, Percent}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::nyxd::CosmWasmCoin; @@ -18,7 +16,7 @@ pub struct Args { long, help = "input your profit margin as follows; (so it would be 20, rather than 0.2)" )] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -30,37 +28,41 @@ pub struct Args { pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { let denom = client.current_chain_details().mix_denom.base.as_str(); - let default_profit_margin = - Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT).unwrap(); - - let node_details = client - .get_owned_nymnode(&client.address()) + let current_parameters = if let Some(client_mixnode) = client + .get_owned_mixnode(&client.address()) .await? - .details - .ok_or_else(|| anyhow!("the client does not own any nodes"))?; - let current_parameters = node_details.rewarding_details.cost_params; - - let profit_margin_percent = current_parameters - .map(|rd| rd.cost_params.profit_margin_percent) - .unwrap_or(default_profit_margin); - - let profit_margin_value = args - .profit_margin_percent - .map(|pm| Percent::from_percentage_value(pm as u64)) - .unwrap_or(profit_margin_percent)?; - - let cost_params = NodeCostParams { - profit_margin_percent: profit_margin_value, - interval_operating_cost: CosmWasmCoin { - denom: denom.into(), - amount: Uint128::new( - args.interval_operating_cost - .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), - ), - }, + .mixnode_details + { + client_mixnode.rewarding_details.cost_params + } else { + client + .get_owned_nymnode(&client.address()) + .await? + .details + .ok_or_else(|| anyhow!("the client does not own any nodes"))? + .rewarding_details + .cost_params }; - info!("Starting nym node params updating!"); + let profit_margin_percent = args + .profit_margin_percent + .map(|pm| Percent::from_percentage_value(pm as u64)) + .unwrap_or(Ok(current_parameters.profit_margin_percent))?; + + let interval_operating_cost = args + .interval_operating_cost + .map(|oc| CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(oc), + }) + .unwrap_or(current_parameters.interval_operating_cost); + + let cost_params = NodeCostParams { + profit_margin_percent, + interval_operating_cost, + }; + + info!("Starting cost params updating using {cost_params:?} !"); let res = client .update_cost_params(cost_params, None) .await diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 30a01c17a9..96d329e753 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -272,7 +272,6 @@ impl ContractBuildInformation { #[cfg(test)] mod tests { use super::*; - use cosmwasm_std::Fraction; #[test] fn percent_serde() { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 2a1c885f1c..e36935d66c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -369,7 +369,7 @@ impl Interval { } pub fn ensure_current_epoch_is_over(&self, env: &Env) -> Result<(), MixnetContractError> { - if !self.is_current_epoch_over(&env) { + if !self.is_current_epoch_over(env) { return Err(MixnetContractError::EpochInProgress { current_block_time: env.block.time.seconds(), epoch_start: self.current_epoch_start_unix_timestamp(), diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs index 21286924c1..375fd34374 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -287,7 +287,6 @@ impl NymNodeDetails { } } -/// #[cw_serde] pub struct NymNodeBond { /// Unique id assigned to the bonded node. diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs index d9c0bb5ea9..4d9ca75459 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -18,11 +18,3 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { truncate_decimal(reward) } - -pub fn legacy_standby_work_factor() -> Decimal { - todo!() -} - -pub fn legacy_active_work_factor() -> Decimal { - todo!() -} diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index 9799d774fd..8def09db67 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -235,7 +235,6 @@ mod message_receiver { 1, vec![mix::LegacyNode { mix_id: 123, - owner: None, host: "10.20.30.40".parse().unwrap(), mix_host: "10.20.30.40:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -255,7 +254,6 @@ mod message_receiver { 2, vec![mix::LegacyNode { mix_id: 234, - owner: None, host: "11.21.31.41".parse().unwrap(), mix_host: "11.21.31.41:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -275,7 +273,6 @@ mod message_receiver { 3, vec![mix::LegacyNode { mix_id: 456, - owner: None, host: "12.22.32.42".parse().unwrap(), mix_host: "12.22.32.42:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -295,7 +292,7 @@ mod message_receiver { // currently coco_nodes don't really exist so this is still to be determined mixes, vec![gateway::LegacyNode { - owner: None, + node_id: 789, host: "1.2.3.4".parse().unwrap(), mix_host: "1.2.3.4:1789".parse().unwrap(), clients_ws_port: 9000, diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index 15866f6cad..9f98af61a3 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -4,12 +4,13 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use crate::mixnode::NodeCostParams; +use nym_mixnet_contract_common::reward_params::ActiveSetUpdate; use nym_mixnet_contract_common::{ BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, NodeId, PendingEpochEvent as MixnetContractPendingEpochEvent, PendingEpochEventKind as MixnetContractPendingEpochEventKind, PendingIntervalEvent as MixnetContractPendingIntervalEvent, - PendingIntervalEventKind as MixnetContractPendingIntervalEventKind, + PendingIntervalEventKind as MixnetContractPendingIntervalEventKind, PendingIntervalEventKind, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -22,7 +23,7 @@ use serde::{Deserialize, Serialize}; export_to = "ts-packages/types/src/types/rust/PendingEpochEvent.ts" ) )] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct PendingEpochEvent { pub id: EpochEventId, pub created_at: BlockHeight, @@ -50,7 +51,7 @@ impl PendingEpochEvent { export_to = "ts-packages/types/src/types/rust/PendingEpochEventData.ts" ) )] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub enum PendingEpochEventData { Delegate { owner: String, @@ -77,6 +78,20 @@ pub enum PendingEpochEventData { UpdateActiveSetSize { new_size: u32, }, + NymNodePledgeMore { + node_id: NodeId, + amount: DecCoin, + }, + NymNodeDecreasePledge { + node_id: NodeId, + decrease_by: DecCoin, + }, + UnbondNymNode { + node_id: NodeId, + }, + UpdateActiveSet { + update: ActiveSetUpdate, + }, } impl PendingEpochEventData { @@ -123,7 +138,25 @@ impl PendingEpochEventData { MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { Ok(PendingEpochEventData::UnbondMixnode { mix_id }) } - _ => todo!(), + MixnetContractPendingEpochEventKind::NymNodePledgeMore { node_id, amount } => { + Ok(PendingEpochEventData::NymNodePledgeMore { + node_id, + amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, + }) + } + MixnetContractPendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + } => Ok(PendingEpochEventData::NymNodeDecreasePledge { + node_id, + decrease_by: reg.attempt_convert_to_display_dec_coin(decrease_by.into())?, + }), + MixnetContractPendingEpochEventKind::UnbondNymNode { node_id } => { + Ok(PendingEpochEventData::UnbondNymNode { node_id }) + } + MixnetContractPendingEpochEventKind::UpdateActiveSet { update } => { + Ok(PendingEpochEventData::UpdateActiveSet { update }) + } } } } @@ -173,7 +206,10 @@ pub enum PendingIntervalEventData { mix_id: NodeId, new_costs: NodeCostParams, }, - + ChangeNymNodeCostParams { + node_id: NodeId, + new_costs: NodeCostParams, + }, UpdateRewardingParams { update: IntervalRewardingParamsUpdate, }, @@ -207,7 +243,14 @@ impl PendingIntervalEventData { epochs_in_interval, epoch_duration_secs, }), - _ => todo!(), + PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs } => { + Ok(PendingIntervalEventData::ChangeNymNodeCostParams { + node_id, + new_costs: NodeCostParams::from_mixnet_contract_mixnode_cost_params( + new_costs, reg, + )?, + }) + } } } } diff --git a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs index 9910720bb7..ac03d341ea 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs @@ -4,6 +4,7 @@ use crate::support::setup::{MIX_DENOM, REWARDING_VALIDATOR}; use cosmwasm_std::Decimal; use nym_contracts_common::Percent; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; use nym_mixnet_contract_common::InitialRewardingParams; use std::time::Duration; @@ -21,8 +22,12 @@ pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg { sybil_resistance: Percent::from_percentage_value(30).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_size: 240, - active_set_size: 100, + rewarded_set_params: RewardedSetParams { + entry_gateways: 70, + exit_gateways: 50, + mixnodes: 120, + standby: 0, + }, }, profit_margin: Default::default(), interval_operating_cost: Default::default(), diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index e2fc5deb38..e360777a94 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -5,14 +5,15 @@ use crate::support::fixtures; use crate::support::helpers::{ mixnet_contract_wrapper, rewarding_validator, test_rng, vesting_contract_wrapper, }; -use cosmwasm_std::{coins, Addr, Coin, Timestamp}; +use cosmwasm_std::{coins, Addr, Coin, Decimal, Timestamp}; use cw_multi_test::{App, AppBuilder, Executor}; use nym_contracts_common::signing::{ContractMessageContent, MessageSignature, Nonce}; use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::nym_node::{EpochAssignmentResponse, Role, RolesMetadataResponse}; +use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance}; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, LayerAssignment, MixnodeBondingPayload, NodeCostParams, - PagedRewardedSetResponse, RewardingParams, SignableMixNodeBondingMsg, + CurrentIntervalResponse, MixnodeBondingPayload, NodeCostParams, RewardedSet, RewardingParams, + RoleAssignment, SignableMixNodeBondingMsg, }; use nym_mixnet_contract_common::{ ExecuteMsg as MixnetExecuteMsg, MixNode, QueryMsg as MixnetQueryMsg, @@ -118,18 +119,95 @@ impl TestSetup { }) } - pub fn full_mixnet_epoch_operations(&mut self) { - let current_rewarded_set: PagedRewardedSetResponse = self + fn get_rewarded_set(&self) -> RewardedSet { + let metadata: RolesMetadataResponse = self .app .wrap() .query_wasm_smart( self.mixnet_contract(), - &MixnetQueryMsg::GetRewardedSet { - limit: Some(9999), - start_after: None, + &MixnetQueryMsg::GetRewardedSetMetadata {}, + ) + .unwrap(); + + let entry: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { + role: Role::EntryGateway, }, ) .unwrap(); + assert_eq!(entry.epoch_id, metadata.metadata.epoch_id); + + let exit: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { + role: Role::ExitGateway, + }, + ) + .unwrap(); + assert_eq!(exit.epoch_id, metadata.metadata.epoch_id); + + let layer1: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { role: Role::Layer1 }, + ) + .unwrap(); + assert_eq!(layer1.epoch_id, metadata.metadata.epoch_id); + + let layer2: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { role: Role::Layer2 }, + ) + .unwrap(); + assert_eq!(layer2.epoch_id, metadata.metadata.epoch_id); + + let layer3: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { role: Role::Layer3 }, + ) + .unwrap(); + assert_eq!(layer3.epoch_id, metadata.metadata.epoch_id); + + let standby: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { + role: Role::Standby, + }, + ) + .unwrap(); + assert_eq!(standby.epoch_id, metadata.metadata.epoch_id); + + RewardedSet { + entry_gateways: entry.nodes, + exit_gateways: exit.nodes, + layer1: layer1.nodes, + layer2: layer2.nodes, + layer3: layer3.nodes, + standby: standby.nodes, + } + } + + pub fn full_mixnet_epoch_operations(&mut self) { + let rewarded_set = self.get_rewarded_set(); + let current_params: RewardingParams = self .app .wrap() @@ -150,16 +228,30 @@ impl TestSetup { ) .unwrap(); + let work = + Decimal::one() / Decimal::from_ratio(rewarded_set.rewarded_set_size() as u64, 1u64); + let params = NodeRewardingParameters::new(Performance::hundred(), work); + + let mut nodes = rewarded_set + .layer1 + .iter() + .chain(rewarded_set.layer2.iter()) + .chain(rewarded_set.layer3.iter()) + .chain(rewarded_set.entry_gateways.iter()) + .chain(rewarded_set.exit_gateways.iter()) + .chain(rewarded_set.standby.iter()) + .copied() + .collect::>(); + + nodes.sort(); + // reward - for (mix_id, _status) in ¤t_rewarded_set.nodes { + for (node_id) in nodes { self.app .execute_contract( rewarding_validator(), self.mixnet_contract(), - &MixnetExecuteMsg::RewardNode { - node_id: *mix_id, - performance: Performance::hundred(), - }, + &MixnetExecuteMsg::RewardNode { node_id, params }, &[], ) .unwrap(); @@ -176,22 +268,86 @@ impl TestSetup { .unwrap(); // don't bother changing the active set, use the same node for update and advance - let new_rewarded_set = current_rewarded_set - .nodes - .into_iter() - .enumerate() - .map(|(i, (node, _))| { - LayerAssignment::new(node, ((i as u8 % 3) + 1).try_into().unwrap()) - }) - .collect(); self.app .execute_contract( rewarding_validator(), self.mixnet_contract(), - &MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size: current_params.active_set_size, + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::EntryGateway, + nodes: rewarded_set.entry_gateways, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::ExitGateway, + nodes: rewarded_set.exit_gateways, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer1, + nodes: rewarded_set.layer1, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer2, + nodes: rewarded_set.layer2, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer3, + nodes: rewarded_set.layer3, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Standby, + nodes: rewarded_set.standby, + }, }, &[], ) diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 48521ecf46..19207fafe5 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -134,6 +134,7 @@ sqlx = { workspace = true, features = [ ] } [dev-dependencies] +axum-test = "16.2.0" tempfile = { workspace = true } cw3 = { workspace = true } cw-utils = { workspace = true } diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs index 64c1a2dccb..35d6df7a07 100644 --- a/nym-api/nym-api-requests/src/ecash/models.rs +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -383,7 +383,7 @@ pub struct EpochCredentialsResponse { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)] #[serde(rename_all = "camelCase")] pub struct IssuedCredentialsResponse { - // note: BTreeMap returns ordered results so it's fine to use it with pagination + // note: BTreeMap returns ordered results, so it's fine to use it with pagination pub credentials: BTreeMap, } diff --git a/nym-api/src/ecash/tests/issued_credentials.rs b/nym-api/src/ecash/tests/issued_credentials.rs index 5a86d70923..877e974d05 100644 --- a/nym-api/src/ecash/tests/issued_credentials.rs +++ b/nym-api/src/ecash/tests/issued_credentials.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::tests::{voucher_fixture, TestFixture}; +use axum::http::StatusCode; use nym_api_requests::ecash::models::{ EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, Pagination, }; use nym_api_requests::ecash::CredentialsRequestBody; use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_ROUTES}; -use rocket::http::Status; use std::collections::BTreeMap; #[tokio::test] @@ -19,11 +19,10 @@ async fn epoch_credentials() { let test_fixture = TestFixture::new().await; // initially we expect 0 issued - let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; + let response = test_fixture.axum.get(&route_epoch1).await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); assert_eq!(parsed_response.epoch_id, 1); assert_eq!(parsed_response.total_issued, 0); @@ -33,10 +32,9 @@ async fn epoch_credentials() { test_fixture.issue_dummy_credential().await; // now there should be one - let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch1).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); assert_eq!(parsed_response.epoch_id, 1); assert_eq!(parsed_response.total_issued, 1); @@ -45,11 +43,10 @@ async fn epoch_credentials() { // and another test_fixture.issue_dummy_credential().await; - let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; + let response = test_fixture.axum.get(&route_epoch1).await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); // note that first epoch credential didn't change assert_eq!(parsed_response.epoch_id, 1); @@ -58,10 +55,9 @@ async fn epoch_credentials() { test_fixture.set_epoch(2); - let response = test_fixture.rocket.get(&route_epoch2).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch2).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); // note the epoch change assert_eq!(parsed_response.epoch_id, 2); @@ -70,10 +66,9 @@ async fn epoch_credentials() { test_fixture.issue_dummy_credential().await; - let response = test_fixture.rocket.get(&route_epoch2).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch2).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); // note the epoch change assert_eq!(parsed_response.epoch_id, 2); @@ -81,10 +76,9 @@ async fn epoch_credentials() { assert_eq!(parsed_response.first_epoch_credential_id, Some(3)); // random epoch in the future - let response = test_fixture.rocket.get(&route_epoch42).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch42).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); assert_eq!(parsed_response.epoch_id, 42); assert_eq!(parsed_response.total_issued, 0); assert_eq!(parsed_response.first_epoch_credential_id, None); @@ -114,10 +108,9 @@ async fn issued_credential() { test_fixture.add_deposit(&voucher2); // random credential that was never issued - let response = test_fixture.rocket.get(route(42)).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route(42)).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialResponse = response.json(); assert!(parsed_response.credential.is_none()); let cred1 = test_fixture.issue_credential(request1.clone()).await; @@ -125,16 +118,14 @@ async fn issued_credential() { test_fixture.set_epoch(3); let cred2 = test_fixture.issue_credential(request2.clone()).await; - let response = test_fixture.rocket.get(route(1)).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route(1)).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialResponse = response.json(); let issued1 = parsed_response.credential.unwrap(); - let response = test_fixture.rocket.get(route(2)).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route(2)).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialResponse = response.json(); let issued2 = parsed_response.credential.unwrap(); // TODO: currently we have no signature checks @@ -192,37 +183,33 @@ async fn issued_credentials() { let issued13 = test_fixture.issued_unchecked(13).await; let response = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![5], pagination: None, }) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response.json(); assert_eq!(parsed_response.credentials[&5], issued5); assert!(!parsed_response.credentials.contains_key(&13)); let response = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![5, 13], pagination: None, }) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response.json(); assert_eq!(parsed_response.credentials[&5], issued5); assert_eq!(parsed_response.credentials[&13], issued13); let response_paginated = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![], @@ -231,11 +218,9 @@ async fn issued_credentials() { limit: Some(2), }), }) - .dispatch() .await; - assert_eq!(response_paginated.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap(); + assert_eq!(response_paginated.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response_paginated.json(); let mut expected = BTreeMap::new(); expected.insert(1, issued1); @@ -243,7 +228,7 @@ async fn issued_credentials() { assert_eq!(expected, parsed_response.credentials); let response_paginated = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![], @@ -252,11 +237,9 @@ async fn issued_credentials() { limit: Some(3), }), }) - .dispatch() .await; - assert_eq!(response_paginated.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap(); + assert_eq!(response_paginated.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response_paginated.json(); let mut expected = BTreeMap::new(); expected.insert(3, issued3); diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index fb037a85f0..aba83c5ec4 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,13 +1,24 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::ecash; +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::error::{EcashError, Result}; use crate::ecash::keys::KeyPairWithEpoch; use crate::ecash::state::EcashState; use crate::ecash::storage::EcashStorageExt; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; +use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; use async_trait::async_trait; +use axum::Router; +use axum_test::http::StatusCode; +use axum_test::TestServer; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{ from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, @@ -31,6 +42,7 @@ use nym_coconut_dkg_common::types::{ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_compact_ecash::BlindedSignature; use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth}; +use nym_config::defaults::NymNetworkDetails; use nym_contracts_common::IdentityKey; use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::TicketType; @@ -45,8 +57,6 @@ use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse} use nym_validator_client::{EcashApiClient, NymApiClient}; use rand::rngs::OsRng; use rand::RngCore; -use rocket::http::Status; -use rocket::local::asynchronous::Client; use std::collections::{BTreeMap, HashMap}; use std::ops::Deref; use std::str::FromStr; @@ -863,7 +873,7 @@ impl super::client::Client for DummyClient { .votes .contains_key(&(voter.clone(), proposal_id)) { - todo!("already voted"); + panic!("unhandled case: already voted"); } chain.multisig_contract.votes.insert( (voter.clone(), proposal_id), @@ -1234,7 +1244,7 @@ fn dummy_signature() -> identity::Signature { } struct TestFixture { - rocket: Client, + axum: TestServer, storage: NymApiStorage, chain_state: SharedFakeChain, epoch: Arc, @@ -1243,6 +1253,21 @@ struct TestFixture { } impl TestFixture { + fn build_app_state(storage: NymApiStorage) -> AppState { + AppState { + nym_contract_cache: NymContractCache::new(), + node_status_cache: NodeStatusCache::new(), + circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), + storage, + described_nodes_cache: SharedCache::::new(), + network_details: NetworkDetails::new( + "localhost".to_string(), + NymNetworkDetails::new_empty(), + ), + node_info_cache: unstable::NodeInfoCache::default(), + } + } + async fn new() -> Self { let mut rng = crate::ecash::tests::fixtures::test_rng([69u8; 32]); let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0); @@ -1283,28 +1308,24 @@ impl TestFixture { .parse() .unwrap(); - let rocket = rocket::build() - .manage( - EcashState::new( - ecash_contract, - nyxd_client, - identity, - staged_key_pair, - comm_channel, - storage.clone(), - ) - .await - .unwrap(), - ) - .mount( - "/v1/ecash", - ecash::routes_open_api(&Default::default(), true).0, - ); + let ecash_state = EcashState::new( + ecash_contract, + nyxd_client, + identity, + staged_key_pair, + comm_channel, + storage.clone(), + ) + .await + .unwrap(); TestFixture { - rocket: Client::tracked(rocket) - .await - .expect("valid rocket instance"), + axum: TestServer::new( + Router::new() + .nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) + .with_state(Self::build_app_state(storage.clone())), + ) + .unwrap(), storage, chain_state, epoch, @@ -1351,26 +1372,24 @@ impl TestFixture { async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse { let response = self - .rocket - .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) + .axum + .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&req) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - serde_json::from_str(&response.into_string().await.unwrap()).unwrap() + assert_eq!(response.status_code(), StatusCode::OK); + response.json() } async fn issued_credential(&self, id: i64) -> Option { let response = self - .rocket - .get(format!( + .axum + .get(&format!( "/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}" )) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - serde_json::from_str(&response.into_string().await.unwrap()).unwrap() + assert_eq!(response.status_code(), StatusCode::OK); + response.json() } async fn issued_unchecked(&self, id: i64) -> IssuedTicketbookBody { @@ -1385,6 +1404,7 @@ impl TestFixture { #[cfg(test)] mod credential_tests { use super::*; + use axum::http::StatusCode; use nym_compact_ecash::ttp_keygen; #[tokio::test] @@ -1416,23 +1436,15 @@ mod credential_tests { .unwrap(); let response = test_fixture - .rocket - .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) + .axum + .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&request_body) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - let expected_response = BlindedSignatureResponse::new(sig); - // This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893 - // let blinded_signature_response = response - // .into_json::() - // .await - // .unwrap(); - let blinded_signature_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let expected_response = BlindedSignatureResponse::new(sig); + let blinded_signature_response = response.json::(); + assert_eq!( blinded_signature_response.to_bytes(), expected_response.to_bytes() @@ -1596,19 +1608,13 @@ mod credential_tests { let request_body = voucher.create_blind_sign_request_body(&signing_data); let response = test - .rocket - .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) + .axum + .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) .json(&request_body) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - // This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893 - // assert!(response.into_json::().is_some()); - let blinded_signature_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ); - assert!(blinded_signature_response.is_ok()); + assert_eq!(response.status_code(), StatusCode::OK); + let _ = response.json::(); } #[test] diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index e7ccdf7240..aeae8977fe 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -119,7 +119,10 @@ impl EpochAdvancer { let legacy_mixnodes = self.nym_contract_cache.legacy_mixnodes_filtered().await; let legacy_gateways = self.nym_contract_cache.legacy_gateways_filtered().await; - let nym_nodes = self.nym_contract_cache.nym_nodes_filtered().await; + + // TODO: for the purposes of rewarding, this might have to grab some pre-filtered nodes instead, + // such as ones that use up to date version or have correct 'peanut' score + let nym_nodes = self.nym_contract_cache.nym_nodes().await; if legacy_mixnodes.is_empty() && legacy_gateways.is_empty() && nym_nodes.is_empty() { // that's a bit weird, but ok diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 94c8d116f5..69a0ae3e4d 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -539,14 +539,14 @@ impl PacketPreparer { if let Some(rewarded_set) = rewarded_set { let mut rng = thread_rng(); for mix in mixing_nym_nodes { - if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, &mix) { + if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, mix) { tested_mixnodes.push(TestableNode::from(&parsed)); } } } for gateway in gateway_capable_nym_nodes { - if let Some(parsed) = self.nym_node_to_legacy_gateway(&gateway) { + if let Some(parsed) = self.nym_node_to_legacy_gateway(gateway) { tested_gateways.push((&parsed, gateway.node_id).into()) } } diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index ce4c0eb1b1..92e788047e 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -456,11 +456,3 @@ impl NymApiStorageError { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn uptime_response_conversion() {} -} diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index 1f40768a9e..ac7f89b0c5 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -69,6 +69,7 @@ impl NymContractCache { } } + #[allow(clippy::too_many_arguments)] pub(crate) async fn update( &self, mixnodes: Vec, @@ -182,6 +183,7 @@ impl NymContractCache { .map(|g| g.owner.to_string()) } + #[allow(dead_code)] pub async fn legacy_mixnode_owner(&self, node_id: NodeId) -> Option { self.get(|c| &c.legacy_mixnodes) .await? @@ -257,10 +259,6 @@ impl NymContractCache { .into_inner() } - pub async fn nym_nodes_filtered(&self) -> Vec { - todo!() - } - pub async fn rewarded_set(&self) -> Option>> { self.get(|cache| &cache.rewarded_set).await } diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index c20f18436b..f0803d6677 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -111,9 +111,9 @@ impl AppState { // handler helpers to easily get data or return error response impl AppState { - pub(crate) async fn describe_nodes_cache_data<'a>( - &'a self, - ) -> Result>, AxumErrorResponse> { + pub(crate) async fn describe_nodes_cache_data( + &self, + ) -> Result>, AxumErrorResponse> { Ok(self.described_nodes_cache().get().await?) } diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index 392efa6af4..f3f79ce128 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -10,7 +10,7 @@ use crate::support::storage::models::{ use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; use nym_types::monitoring::NodeResult; use sqlx::FromRow; -use time::Date; +use time::{Date, OffsetDateTime}; use tracing::info; #[derive(Clone)] @@ -580,41 +580,39 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { info!("Inserting {} mixnode statuses", mixnode_results.len()); - todo!("look at what broke during rebasing"); - // - // let timestamp = OffsetDateTime::now_utc().unix_timestamp(); - // // insert it all in a transaction to make sure all nodes are updated at the same time - // // (plus it's a nice guard against new nodes) - // let mut tx = self.connection_pool.begin().await?; - // for mixnode_result in mixnode_results { - // let mixnode_id = sqlx::query!( - // r#" - // INSERT OR IGNORE INTO mixnode_details_v2(node_id, identity_key) VALUES (?, ?); - // SELECT id FROM mixnode_details_v2 WHERE node_id = ?; - // "#, - // mixnode_result.node_id, - // mixnode_result.identity, - // mixnode_result.node_id, - // ) - // .fetch_one(&mut tx) - // .await? - // .id; - // - // // insert the actual status - // sqlx::query!( - // r#" - // INSERT INTO mixnode_status_v2 (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); - // "#, - // mixnode_id, - // mixnode_result.reliability, - // timestamp - // ) - // .execute(&mut tx) - // .await?; - // } - // - // // finally commit the transaction - // tx.commit().await + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + // insert it all in a transaction to make sure all nodes are updated at the same time + // (plus it's a nice guard against new nodes) + let mut tx = self.connection_pool.begin().await?; + for mixnode_result in mixnode_results { + let mixnode_id = sqlx::query!( + r#" + INSERT OR IGNORE INTO mixnode_details_v2(node_id, identity_key) VALUES (?, ?); + SELECT id FROM mixnode_details_v2 WHERE node_id = ?; + "#, + mixnode_result.node_id, + mixnode_result.identity, + mixnode_result.node_id, + ) + .fetch_one(&mut tx) + .await? + .id; + + // insert the actual status + sqlx::query!( + r#" + INSERT INTO mixnode_status_v2 (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); + "#, + mixnode_id, + mixnode_result.reliability, + timestamp + ) + .execute(&mut tx) + .await?; + } + + // finally commit the transaction + tx.commit().await } /// Tries to submit gateway [`NodeResult`] from the network monitor to the database. @@ -672,45 +670,43 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { info!("Inserting {} gateway statuses", gateway_results.len()); - todo!("look at what broke during rebasing"); + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + // insert it all in a transaction to make sure all nodes are updated at the same time + // (plus it's a nice guard against new nodes) + let mut tx = self.connection_pool.begin().await?; - // let timestamp = OffsetDateTime::now_utc().unix_timestamp(); - // // insert it all in a transaction to make sure all nodes are updated at the same time - // // (plus it's a nice guard against new nodes) - // let mut tx = self.connection_pool.begin().await?; - // - // for gateway_result in gateway_results { - // // if gateway info doesn't exist, insert it and get its id - // - // // same ID "problem" as described for mixnode insertion - // let gateway_id = sqlx::query!( - // r#" - // INSERT OR IGNORE INTO gateway_details_v2(identity, node_id) VALUES (?, ?); - // SELECT id FROM gateway_details_v2 WHERE identity = ?; - // "#, - // gateway_result.identity, - // gateway_result.node_id, - // gateway_result.identity, - // ) - // .fetch_one(&mut tx) - // .await? - // .id; - // - // // insert the actual status - // sqlx::query!( - // r#" - // INSERT INTO gateway_status_v2 (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); - // "#, - // gateway_id, - // gateway_result.reliability, - // timestamp - // ) - // .execute(&mut tx) - // .await?; - // } - // - // // finally commit the transaction - // tx.commit().await + for gateway_result in gateway_results { + // if gateway info doesn't exist, insert it and get its id + + // same ID "problem" as described for mixnode insertion + let gateway_id = sqlx::query!( + r#" + INSERT OR IGNORE INTO gateway_details_v2(identity, node_id) VALUES (?, ?); + SELECT id FROM gateway_details_v2 WHERE identity = ?; + "#, + gateway_result.identity, + gateway_result.node_id, + gateway_result.identity, + ) + .fetch_one(&mut tx) + .await? + .id; + + // insert the actual status + sqlx::query!( + r#" + INSERT INTO gateway_status_v2 (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); + "#, + gateway_id, + gateway_result.reliability, + timestamp + ) + .execute(&mut tx) + .await?; + } + + // finally commit the transaction + tx.commit().await } /// Saves the information about which nodes were used as core nodes during this particular diff --git a/nym-network-monitor/src/accounting.rs b/nym-network-monitor/src/accounting.rs index d658e9dc63..4dea0bcc74 100644 --- a/nym-network-monitor/src/accounting.rs +++ b/nym-network-monitor/src/accounting.rs @@ -15,16 +15,16 @@ use utoipa::ToSchema; use crate::{NYM_API_URL, PRIVATE_KEY, TOPOLOGY}; struct HydratedRoute { - mix_nodes: Vec, - gateway_node: gateway::Node, + mix_nodes: Vec, + gateway_node: gateway::LegacyNode, } #[derive(Serialize, Deserialize, Debug, Default, ToSchema)] -struct GatewayStats(u32, u32, Option); +struct GatewayStats(u32, u32); impl GatewayStats { - fn new(sent: u32, recv: u32, owner: Option) -> Self { - GatewayStats(sent, recv, owner) + fn new(sent: u32, recv: u32) -> Self { + GatewayStats(sent, recv) } fn success(&self) -> u32 { @@ -60,9 +60,9 @@ pub struct NetworkAccount { topology: NymTopology, tested_nodes: HashSet, #[serde(skip)] - mix_details: HashMap, + mix_details: HashMap, #[serde(skip)] - gateway_details: HashMap, + gateway_details: HashMap, } impl NetworkAccount { @@ -82,7 +82,6 @@ impl NetworkAccount { complete_routes, incomplete_routes, node.identity_key.to_base58_string(), - node.owner.clone(), ) } @@ -186,7 +185,7 @@ impl NetworkAccount { let gateway_stats_entry = self .gateway_stats .entry(route.gateway_node.identity_key.to_base58_string()) - .or_insert(GatewayStats::new(0, 0, route.gateway_node.owner.clone())); + .or_insert(GatewayStats::new(0, 0)); self.gateway_details.insert( route.gateway_node.identity_key.to_base58_string(), route.gateway_node, @@ -265,7 +264,6 @@ pub struct NodeStats { incomplete_routes: usize, reliability: f64, identity: String, - owner: Option, } impl NodeStats { @@ -274,7 +272,6 @@ impl NodeStats { complete_routes: usize, incomplete_routes: usize, identity: String, - owner: Option, ) -> Self { NodeStats { mix_id, @@ -282,7 +279,6 @@ impl NodeStats { incomplete_routes, reliability: complete_routes as f64 / (complete_routes + incomplete_routes) as f64, identity, - owner, } } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 80ef99ae94..445c5f297a 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -167,9 +167,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -1631,9 +1631,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -3360,6 +3360,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -3448,6 +3449,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.4", @@ -5760,18 +5762,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", diff --git a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs index bfd62d1a8f..67f4a773a7 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -21,7 +21,6 @@ async fn main() { 1, vec![mix::LegacyNode { mix_id: 63, - owner: None, host: "172.105.92.48".parse().unwrap(), mix_host: "172.105.92.48:1789".parse().unwrap(), identity_key: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK" @@ -38,7 +37,6 @@ async fn main() { 2, vec![mix::LegacyNode { mix_id: 23, - owner: None, host: "178.79.143.65".parse().unwrap(), mix_host: "178.79.143.65:1789".parse().unwrap(), identity_key: "4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz" @@ -55,7 +53,6 @@ async fn main() { 3, vec![mix::LegacyNode { mix_id: 66, - owner: None, host: "139.162.247.97".parse().unwrap(), mix_host: "139.162.247.97:1789".parse().unwrap(), identity_key: "66UngapebhJRni3Nj52EW1qcNsWYiuonjkWJzHFsmyYY" diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index 1a199f410f..f4afb4961e 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -113,7 +113,7 @@ impl NetworkManager { if let Some(node) = gateways .nodes .iter() - .find(|gw| &gw.ed25519_identity_pubkey == identity) + .find(|gw| &gw.ed25519_identity_pubkey.to_base58_string() == identity) { return SocketAddr::new( node.ip_addresses[0], diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index d5ea4be530..9492de53c8 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -9,7 +9,7 @@ use crate::manager::NetworkManager; use console::style; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::RoleAssignment; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; use nym_validator_client::DirectSigningHttpRpcNyxdClient; use serde::{Deserialize, Serialize}; use std::fs; @@ -372,7 +372,7 @@ impl NetworkManager { ctx: &LocalNodesCtx<'a>, ) -> Result<(), NetworkManagerError> { ctx.println(format!( - "🔌 {}Assigning mixnodes to the active set...", + "🔌 {}Assigning nodes to the active set...", style("[4/5]").bold().dim() )); @@ -386,58 +386,17 @@ impl NetworkManager { let fut = rewarder.reconcile_epoch_events(None, None); ctx.async_with_progress(fut).await?; - ctx.set_pb_message("[BROKEN] finally assigning the active set..."); - // let fut = rewarder.get_rewarding_parameters(); - // let rewarding_params = ctx.async_with_progress(fut).await?; - // let active_set_size = rewarding_params.active_set_size; - - let unused_variable = "this has to be fixed up and refactored...."; - /* - fn generate_role_assignment_messages( - &self, - rewarded_set: RewardedSet, - ) -> Vec<(ExecuteMsg, Vec)> { - // currently we just assign all of them together, - // but the contract is ready to handle them separately should we need it - // if the tx is too big - let mut msgs = Vec::new(); - for (role, nodes) in [ - (Role::ExitGateway, rewarded_set.exit_gateways), - (Role::EntryGateway, rewarded_set.entry_gateways), - (Role::Layer1, rewarded_set.layer1), - (Role::Layer2, rewarded_set.layer2), - (Role::Layer3, rewarded_set.layer3), - (Role::Standby, rewarded_set.standby), - ] { - msgs.push(( - ExecuteMsg::AssignRoles { - assignment: RoleAssignment { role, nodes }, - }, - Vec::new(), - )); - } - msgs - } - */ - + ctx.set_pb_message("finally assigning the active set... entry..."); let fut = rewarder.assign_roles( RoleAssignment { - role: Role::ExitGateway, + role: Role::EntryGateway, nodes: vec![4], }, None, ); ctx.async_with_progress(fut).await?; - let fut = rewarder.assign_roles( - RoleAssignment { - role: Role::EntryGateway, - nodes: vec![], - }, - None, - ); - ctx.async_with_progress(fut).await?; - + ctx.set_pb_message("finally assigning the active set... layer1..."); let fut = rewarder.assign_roles( RoleAssignment { role: Role::Layer1, @@ -447,6 +406,7 @@ impl NetworkManager { ); ctx.async_with_progress(fut).await?; + ctx.set_pb_message("finally assigning the active set... layer2..."); let fut = rewarder.assign_roles( RoleAssignment { role: Role::Layer2, @@ -456,6 +416,7 @@ impl NetworkManager { ); ctx.async_with_progress(fut).await?; + ctx.set_pb_message("finally assigning the active set... layer3..."); let fut = rewarder.assign_roles( RoleAssignment { role: Role::Layer3, @@ -465,6 +426,7 @@ impl NetworkManager { ); ctx.async_with_progress(fut).await?; + ctx.set_pb_message("finally assigning the active set... [empty] standby..."); let fut = rewarder.assign_roles( RoleAssignment { role: Role::Standby, From c3e3ef1737cb2a4b174b1a3c4a344bab4a47348d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 10 Oct 2024 15:42:01 +0100 Subject: [PATCH 062/207] removed axum feature from ci-build.yml for clippy --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 089d7097fb..77d220e633 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -87,4 +87,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --workspace --all-targets --features axum -- -D warnings + args: --workspace --all-targets -- -D warnings From 493390d92bbdcf67828b54b467143f0a7a97d669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 11 Oct 2024 16:05:53 +0100 Subject: [PATCH 063/207] bugfix: fix expected return type on /v1/gateways endpoint --- .../validator-client/src/client.rs | 9 ++------ .../validator-client/src/nym_api/mod.rs | 5 ++--- explorer-api/src/gateways/models.rs | 21 +++++++++---------- explorer-api/src/tasks.rs | 6 ++---- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index eee67ba509..fc68ae748c 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -17,7 +17,6 @@ use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; -use nym_api_requests::legacy::LegacyGatewayBondWithId; use nym_api_requests::models::{ GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, @@ -240,9 +239,7 @@ impl Client { Ok(self.nym_api.get_active_mixnodes_detailed().await?) } - pub async fn get_cached_gateways( - &self, - ) -> Result, ValidatorClientError> { + pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } @@ -324,9 +321,7 @@ impl NymApiClient { Ok(self.nym_api.get_mixnodes().await?) } - pub async fn get_cached_gateways( - &self, - ) -> Result, ValidatorClientError> { + pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 2ecadd7641..a664a503ab 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -10,7 +10,6 @@ use nym_api_requests::ecash::models::{ VerifyEcashTicketBody, }; use nym_api_requests::ecash::VerificationKeyResponse; -use nym_api_requests::legacy::LegacyGatewayBondWithId; use nym_api_requests::models::{ AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, }; @@ -38,7 +37,7 @@ use nym_contracts_common::IdentityKey; pub use nym_http_api_client::Client; use nym_http_api_client::{ApiClient, NO_PARAMS}; use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{IdentityKeyRef, NodeId}; +use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; use time::format_description::BorrowedFormatItem; use time::Date; @@ -98,7 +97,7 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_gateways(&self) -> Result, NymAPIError> { + async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } diff --git a/explorer-api/src/gateways/models.rs b/explorer-api/src/gateways/models.rs index c3a2cd9062..a3ed406067 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -3,8 +3,7 @@ use crate::{cache::Cache, location::LocationCacheItem}; use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; -use nym_mixnet_contract_common::IdentityKey; -use nym_validator_client::legacy::LegacyGatewayBondWithId; +use nym_mixnet_contract_common::{GatewayBond, IdentityKey}; use serde::Serialize; use std::{sync::Arc, time::SystemTime}; use tokio::sync::RwLock; @@ -12,7 +11,7 @@ use tokio::sync::RwLock; use super::location::GatewayLocationCache; pub(crate) struct GatewayCache { - pub(crate) gateways: Cache, + pub(crate) gateways: Cache, } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -38,20 +37,20 @@ impl ThreadsafeGatewayCache { fn create_detailed_gateway( &self, - bond: LegacyGatewayBondWithId, + bond: GatewayBond, location: Option<&LocationCacheItem>, ) -> PrettyDetailedGatewayBond { PrettyDetailedGatewayBond { - pledge_amount: bond.bond.pledge_amount, - owner: bond.bond.owner, - block_height: bond.bond.block_height, - gateway: bond.bond.gateway, - proxy: bond.bond.proxy, + pledge_amount: bond.pledge_amount, + owner: bond.owner, + block_height: bond.block_height, + gateway: bond.gateway, + proxy: bond.proxy, location: location.and_then(|l| l.location.clone()), } } - pub(crate) async fn get_gateways(&self) -> Vec { + pub(crate) async fn get_gateways(&self) -> Vec { self.gateways.read().await.gateways.get_all() } @@ -107,7 +106,7 @@ impl ThreadsafeGatewayCache { .insert(identy_key, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn update_cache(&self, gateways: Vec) { + pub(crate) async fn update_cache(&self, gateways: Vec) { let mut guard = self.gateways.write().await; for gateway in gateways { diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index 52c0192538..f14408f1c8 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -1,8 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_mixnet_contract_common::GatewayBond; use nym_task::TaskClient; -use nym_validator_client::legacy::LegacyGatewayBondWithId; use nym_validator_client::models::MixNodeBondAnnotated; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse}; @@ -47,9 +47,7 @@ impl ExplorerApiTasks { .await } - async fn retrieve_all_gateways( - &self, - ) -> Result, ValidatorClientError> { + async fn retrieve_all_gateways(&self) -> Result, ValidatorClientError> { info!("About to retrieve all gateways..."); self.state .inner From 67462a9f47fcd011025faa2edd4f025fff8c0c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 11 Oct 2024 16:27:49 +0100 Subject: [PATCH 064/207] split the existing tools into separate modules --- .../initialise_mixnet_vesting_with_states.rs | 4 + .../src/commands/initialise_with_state.rs | 56 +++ .../importer-cli/src/commands/mod.rs | 78 +++ .../importer-cli/src/commands/prepare.rs | 82 +++ .../importer-cli/src/commands/set_state.rs | 126 +++++ .../src/commands/swap_contract.rs | 104 ++++ .../importer-cli/src/helpers.rs | 51 ++ .../importer-cli/src/main.rs | 469 +----------------- .../importer-cli/src/state.rs | 40 ++ 9 files changed, 547 insertions(+), 463 deletions(-) create mode 100644 tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs create mode 100644 tools/internal/contract-state-importer/importer-cli/src/commands/initialise_with_state.rs create mode 100644 tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs create mode 100644 tools/internal/contract-state-importer/importer-cli/src/commands/prepare.rs create mode 100644 tools/internal/contract-state-importer/importer-cli/src/commands/set_state.rs create mode 100644 tools/internal/contract-state-importer/importer-cli/src/commands/swap_contract.rs create mode 100644 tools/internal/contract-state-importer/importer-cli/src/helpers.rs create mode 100644 tools/internal/contract-state-importer/importer-cli/src/state.rs diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs new file mode 100644 index 0000000000..6fd33e527d --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use clap::ArgGroup; diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_with_state.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_with_state.rs new file mode 100644 index 0000000000..4e11ccc579 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_with_state.rs @@ -0,0 +1,56 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::commands::prepare::create_importer_contract; +use crate::commands::set_state::set_importer_state; +use crate::commands::swap_contract::swap_contract; +use clap::ArgGroup; +use clap::Args; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +#[clap(group(ArgGroup::new("contract").required(true)))] +pub struct InitialiseWithStateArgs { + /// Path to the .wasm file with the importer contract + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" + #[clap(long)] + pub importer_contract_path: Option, + + /// Path to the file containing state dump of a cosmwasm contract + #[clap(long)] + pub raw_state: PathBuf, + + /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_code_id: Option, + + /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_path: Option, + + #[clap(long)] + pub migrate_msg: Option, +} + +pub async fn initialise_with_state( + args: InitialiseWithStateArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + let importer_address = create_importer_contract(args.importer_contract_path, &client).await?; + set_importer_state(args.raw_state, Some(importer_address.clone()), &client).await?; + swap_contract( + args.target_contract_code_id, + args.target_contract_path, + Some(importer_address.clone()), + args.migrate_msg, + &client, + ) + .await?; + + info!("the contract is ready at {importer_address}"); + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs new file mode 100644 index 0000000000..cd81aa3a14 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs @@ -0,0 +1,78 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::commands::initialise_with_state::{initialise_with_state, InitialiseWithStateArgs}; +use crate::commands::prepare::{execute_prepare_contract, PrepareArgs}; +use crate::commands::set_state::{execute_set_state, SetStateArgs}; +use crate::commands::swap_contract::{execute_swap_contract, SwapContractArgs}; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; +use std::path::PathBuf; +use std::sync::OnceLock; + +pub mod initialise_mixnet_vesting_with_states; +pub mod initialise_with_state; +pub mod prepare; +pub mod set_state; +pub mod swap_contract; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub struct Cli { + /// Path pointing to an env file that configures the CLI. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + #[clap(long)] + pub(crate) mnemonic: bip39::Mnemonic, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +pub(crate) enum Commands { + /// Upload and instantiates the importer contract + PrepareContract(PrepareArgs), + + /// Set the state of the previously instantiated importer contract with the provided state dump + SetState(SetStateArgs), + + /// Swap the importer contract code with the one corresponding to the previously uploaded state dump + SwapContract(SwapContractArgs), + + /// Combines the functionalities of `prepare-contract`, `set-state` and `swap-contract` + InitialiseWithState(InitialiseWithStateArgs), +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + let network_details = NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + let nyxd_url = network_details + .endpoints + .first() + .expect("network details are not defined") + .nyxd_url + .as_str(); + + let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + client_config, + nyxd_url, + self.mnemonic, + )?; + match self.command { + Commands::PrepareContract(args) => execute_prepare_contract(args, client).await, + Commands::SetState(args) => execute_set_state(args, client).await, + Commands::SwapContract(args) => execute_swap_contract(args, client).await, + Commands::InitialiseWithState(args) => initialise_with_state(args, client).await, + } + } +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/prepare.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/prepare.rs new file mode 100644 index 0000000000..40856ff3f4 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/prepare.rs @@ -0,0 +1,82 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::importer_contract_path; +use crate::state::CachedState; +use clap::Args; +use importer_contract::contract::EmptyMessage; +use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::fs; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +pub struct PrepareArgs { + /// Path to the .wasm file with the importer contract + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" + #[clap(long)] + pub importer_contract_path: Option, +} + +pub async fn create_importer_contract( + explicit_contract_path: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result { + info!("attempting to create the importer contract"); + + let importer_path = importer_contract_path(explicit_contract_path)?; + info!( + "going to use the following importer contract: '{}'", + fs::canonicalize(&importer_path)?.display() + ); + + let mut data = Vec::new(); + File::open(importer_path)?.read_to_end(&mut data)?; + + let res = client.upload(data, "", None).await?; + let importer_code_id = res.code_id; + info!( + " ✅ uploaded the importer contract in {}", + res.transaction_hash + ); + + let res = client + .instantiate( + importer_code_id, + &EmptyMessage {}, + "importer-contract".into(), + "", + Some(InstantiateOptions::default().with_admin(client.address())), + None, + ) + .await?; + let importer_address = res.contract_address; + info!( + " ✅ instantiated the importer contract in {}", + res.transaction_hash + ); + + info!("IMPORTER CONTRACT ADDRESS: {importer_address}"); + + CachedState { + importer_address: importer_address.clone(), + state_imported: false, + } + .save()?; + + Ok(importer_address) +} + +pub async fn execute_prepare_contract( + args: PrepareArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + create_importer_contract(args.importer_contract_path, &client).await?; + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/set_state.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/set_state.rs new file mode 100644 index 0000000000..0989243202 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/set_state.rs @@ -0,0 +1,126 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::importer_contract_address; +use crate::state::CachedState; +use anyhow::bail; +use clap::Args; +use importer_contract::{base85rs, ExecuteMsg}; +use nym_validator_client::nyxd::cosmwasm_client::types::Model; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::fs::File; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +pub struct SetStateArgs { + /// Explicit address of the initialised importer contract. + /// If not set, the value from the cached state will be attempted to be used + #[clap(long)] + pub importer_contract_address: Option, + + /// Path to the file containing state dump of a cosmwasm contract + #[clap(long)] + pub raw_state: PathBuf, +} + +fn approximate_size(pair: &Model) -> usize { + base85rs::encode(&pair.key).len() + base85rs::encode(&pair.value).len() +} + +fn models_to_exec(data: Vec) -> ExecuteMsg { + let pairs = data + .into_iter() + .map(|kv| (kv.key, kv.value)) + .collect::>(); + + pairs.into() +} + +fn split_into_importable_execute_msgs( + kv_pairs: Vec, + approximate_max_chunk: usize, +) -> Vec { + let mut chunks: Vec = Vec::new(); + + let mut current_wip_chunk = Vec::new(); + let mut current_chunk_size = 0; + for kv in kv_pairs { + if current_chunk_size + approximate_size(&kv) > approximate_max_chunk { + let taken = std::mem::take(&mut current_wip_chunk); + chunks.push(models_to_exec(taken)); + current_chunk_size = 0; + } + current_chunk_size += approximate_size(&kv); + current_wip_chunk.push(kv); + } + + if !current_wip_chunk.is_empty() { + chunks.push(models_to_exec(current_wip_chunk)) + } + chunks +} + +pub async fn set_importer_state( + state_dump_path: PathBuf, + explicit_importer_address: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + info!("attempting to set the importer contract state"); + + // this is the value that we found to be optimal during v1->v2 mixnet migration + const MAX_CHUNK_SIZE: usize = 350 * 1000; + + let importer_address = importer_contract_address(explicit_importer_address)?; + + if let Ok(state) = CachedState::load() { + if state.state_imported && state.importer_address == importer_address { + bail!("the state has already been imported for {importer_address}") + } + } + + let dump_file = File::open(state_dump_path)?; + info!("attempting to decode the state dump. for bigger contracts this might take a while..."); + let kv_pairs: Vec = serde_json::from_reader(&dump_file)?; + + info!("there are {} key-value pairs to import", kv_pairs.len()); + info!("attempting to split them into {MAX_CHUNK_SIZE}B chunks ExecuteMsgs..."); + + let chunks = split_into_importable_execute_msgs(kv_pairs, MAX_CHUNK_SIZE); + info!("obtained {} execute msgs", chunks.len()); + + let total = chunks.len(); + for (i, msg) in chunks.into_iter().enumerate() { + info!("executing message {}/{total}...", i + 1); + let res = client + .execute( + &importer_address, + &msg, + None, + "importing contract state", + Vec::new(), + ) + .await?; + info!(" ✅ OK: {}", res.transaction_hash); + } + + info!("Finished migrating storage to {importer_address}!"); + + CachedState { + importer_address, + state_imported: true, + } + .save()?; + + Ok(()) +} + +pub async fn execute_set_state( + args: SetStateArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + set_importer_state(args.raw_state, args.importer_contract_address, &client).await?; + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/swap_contract.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/swap_contract.rs new file mode 100644 index 0000000000..da969cab40 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/swap_contract.rs @@ -0,0 +1,104 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::importer_contract_address; +use crate::state::CachedState; +use anyhow::bail; +use clap::ArgGroup; +use clap::Args; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +#[clap(group(ArgGroup::new("contract").required(true)))] +pub struct SwapContractArgs { + /// Explicit address of the initialised importer contract. + /// If not set, the value from the cached state will be attempted to be used + #[clap(long)] + pub importer_contract_address: Option, + + /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_code_id: Option, + + /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state + #[clap(long, group = "contract")] + pub target_contract_path: Option, + + /// The custom migrate message used for migrating into target contract. + /// If none is provided an empty object will be used instead, i.e. '{}' + #[clap(long)] + pub migrate_msg: Option, +} + +pub async fn swap_contract( + target_code_id: Option, + target_contract_path: Option, + explicit_importer_address: Option, + migrate_msg: Option, + client: &DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + info!("attempting to swap the contract code"); + + let importer_address = importer_contract_address(explicit_importer_address)?; + + if let Ok(state) = CachedState::load() { + if !state.state_imported && state.importer_address == importer_address { + bail!("the state hasn't been imported for {importer_address}") + } + } + + // one of those must have been set via clap + let code_id = match target_code_id { + Some(explicit) => explicit, + None => { + // upload the contract + let mut data = Vec::new(); + File::open(target_contract_path.unwrap())?.read_to_end(&mut data)?; + + let res = client.upload(data, "", None).await?; + info!( + " ✅ uploaded the target contract in {}", + res.transaction_hash + ); + res.code_id + } + }; + + let migrate_msg = migrate_msg.unwrap_or(serde_json::Value::Object(Default::default())); + let res = client + .migrate( + &importer_address, + code_id, + &migrate_msg, + "migrating into target contract", + None, + ) + .await?; + info!( + " ✅ migrated into the target contract: {}", + res.transaction_hash + ); + + Ok(()) +} + +pub async fn execute_swap_contract( + args: SwapContractArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + swap_contract( + args.target_contract_code_id, + args.target_contract_path, + args.importer_contract_address, + args.migrate_msg, + &client, + ) + .await?; + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/helpers.rs b/tools/internal/contract-state-importer/importer-cli/src/helpers.rs new file mode 100644 index 0000000000..7f876c181c --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/helpers.rs @@ -0,0 +1,51 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::CachedState; +use anyhow::bail; +use nym_validator_client::nyxd::AccountId; +use std::env::current_dir; +use std::fs; +use std::path::PathBuf; +use tracing::debug; + +// this only works if the cli is called from somewhere within the nym directory +// (which realistically is going to be the case most of the time) +pub fn importer_contract_path(explicit: Option) -> anyhow::Result { + if let Some(explicit) = explicit { + return Ok(explicit); + } + + for ancestor in current_dir()?.ancestors() { + debug!("checking {:?}", fs::canonicalize(ancestor)); + for content in ancestor.read_dir()? { + let dir_entry = content?; + let Ok(name) = dir_entry.file_name().into_string() else { + continue; + }; + + if name == "target" { + let maybe_contract_path = dir_entry + .path() + .join("wasm32-unknown-unknown") + .join("release") + .join("importer_contract.wasm"); + + if maybe_contract_path.exists() { + return Ok(maybe_contract_path); + } + } + } + } + + bail!("could not find importer_contract.wasm") +} + +pub fn importer_contract_address(explicit: Option) -> anyhow::Result { + if let Some(explicit) = explicit { + return Ok(explicit); + } + + let state = CachedState::load()?; + Ok(state.importer_address) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/main.rs b/tools/internal/contract-state-importer/importer-cli/src/main.rs index 842217363c..6962f3c092 100644 --- a/tools/internal/contract-state-importer/importer-cli/src/main.rs +++ b/tools/internal/contract-state-importer/importer-cli/src/main.rs @@ -1,471 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use anyhow::bail; -use clap::ArgGroup; -use clap::{Args, Parser, Subcommand}; -use importer_contract::contract::EmptyMessage; -use importer_contract::{base85rs, ExecuteMsg}; -use nym_bin_common::bin_info; +use crate::commands::Cli; +use clap::Parser; use nym_bin_common::logging::setup_tracing_logger; -use nym_network_defaults::{setup_env, NymNetworkDetails}; -use nym_validator_client::nyxd::cosmwasm_client::types::{InstantiateOptions, Model}; -use nym_validator_client::nyxd::AccountId; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; -use serde::{Deserialize, Serialize}; -use std::env::current_dir; -use std::fs; -use std::fs::{create_dir_all, File}; -use std::io::Read; -use std::path::PathBuf; -use std::sync::OnceLock; -use tracing::{debug, info}; +use nym_network_defaults::setup_env; -fn pretty_build_info_static() -> &'static str { - static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); - PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) -} - -#[derive(Parser)] -#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] -pub struct Cli { - /// Path pointing to an env file that configures the CLI. - #[clap(short, long)] - pub(crate) config_env_file: Option, - - #[clap(long)] - pub(crate) mnemonic: bip39::Mnemonic, - - #[clap(subcommand)] - command: Commands, -} - -#[derive(Serialize, Deserialize)] -pub struct CachedState { - pub importer_address: AccountId, - pub state_imported: bool, -} - -impl CachedState { - pub fn save(&self) -> anyhow::Result<()> { - let path = cached_state_file(); - if let Some(parent) = path.parent() { - create_dir_all(parent)?; - } - let file = File::create(&path)?; - serde_json::to_writer_pretty(file, self)?; - - info!("saved cached details to {}", path.display()); - Ok(()) - } - - pub fn load() -> anyhow::Result { - let file = File::open(cached_state_file())?; - Ok(serde_json::from_reader(&file)?) - } -} - -fn cached_state_file() -> PathBuf { - dirs::cache_dir() - .unwrap() - .join("contract-state-importer") - .join(".state.json") -} - -// this only works if the cli is called from somewhere within the nym directory -// (which realistically is going to be the case most of the time) -fn importer_contract_path(explicit: Option) -> anyhow::Result { - if let Some(explicit) = explicit { - return Ok(explicit); - } - - for ancestor in current_dir()?.ancestors() { - debug!("checking {:?}", fs::canonicalize(ancestor)); - for content in ancestor.read_dir()? { - let dir_entry = content?; - let Ok(name) = dir_entry.file_name().into_string() else { - continue; - }; - - if name == "target" { - let maybe_contract_path = dir_entry - .path() - .join("wasm32-unknown-unknown") - .join("release") - .join("importer_contract.wasm"); - - if maybe_contract_path.exists() { - return Ok(maybe_contract_path); - } - } - } - } - - bail!("could not find importer_contract.wasm") -} - -fn importer_contract_address(explicit: Option) -> anyhow::Result { - if let Some(explicit) = explicit { - return Ok(explicit); - } - - let state = CachedState::load()?; - Ok(state.importer_address) -} - -#[derive(Args, Clone)] -pub struct PrepareArgs { - /// Path to the .wasm file with the importer contract - /// If not provided, the CLI will attempt to traverse the parent directories until it finds - /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" - #[clap(long)] - pub importer_contract_path: Option, -} - -#[derive(Args, Clone)] -pub struct SetStateArgs { - /// Explicit address of the initialised importer contract. - /// If not set, the value from the cached state will be attempted to be used - #[clap(long)] - pub importer_contract_address: Option, - - /// Path to the file containing state dump of a cosmwasm contract - #[clap(long)] - pub raw_state: PathBuf, -} - -#[derive(Args, Clone)] -#[clap(group(ArgGroup::new("contract").required(true)))] -pub struct SwapContractArgs { - /// Explicit address of the initialised importer contract. - /// If not set, the value from the cached state will be attempted to be used - #[clap(long)] - pub importer_contract_address: Option, - - /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_code_id: Option, - - /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_path: Option, - - /// The custom migrate message used for migrating into target contract. - /// If none is provided an empty object will be used instead, i.e. '{}' - #[clap(long)] - pub migrate_msg: Option, -} - -#[derive(Args, Clone)] -#[clap(group(ArgGroup::new("contract").required(true)))] -pub struct InitialiseWithStateArgs { - /// Path to the .wasm file with the importer contract - /// If not provided, the CLI will attempt to traverse the parent directories until it finds - /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" - #[clap(long)] - pub importer_contract_path: Option, - - /// Path to the file containing state dump of a cosmwasm contract - #[clap(long)] - pub raw_state: PathBuf, - - /// Code id of the previously uploaded cosmwasm smart contract that will be applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_code_id: Option, - - /// Path to a cosmwasm smart contract that will be uploaded and applied to the imported state - #[clap(long, group = "contract")] - pub target_contract_path: Option, - - #[clap(long)] - pub migrate_msg: Option, -} - -#[derive(Subcommand)] -pub(crate) enum Commands { - /// Upload and instantiates the importer contract - PrepareContract(PrepareArgs), - - /// Set the state of the previously instantiated importer contract with the provided state dump - SetState(SetStateArgs), - - /// Swap the importer contract code with the one corresponding to the previously uploaded state dump - SwapContract(SwapContractArgs), - - /// Combines the functionalities of `prepare-contract`, `set-state` and `swap-contract` - InitialiseWithState(InitialiseWithStateArgs), -} - -async fn create_importer_contract( - explicit_contract_path: Option, - client: &DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result { - info!("attempting to create the importer contract"); - - let importer_path = importer_contract_path(explicit_contract_path)?; - info!( - "going to use the following importer contract: '{}'", - fs::canonicalize(&importer_path)?.display() - ); - - let mut data = Vec::new(); - File::open(importer_path)?.read_to_end(&mut data)?; - - let res = client.upload(data, "", None).await?; - let importer_code_id = res.code_id; - info!( - " ✅ uploaded the importer contract in {}", - res.transaction_hash - ); - - let res = client - .instantiate( - importer_code_id, - &EmptyMessage {}, - "importer-contract".into(), - "", - Some(InstantiateOptions::default().with_admin(client.address())), - None, - ) - .await?; - let importer_address = res.contract_address; - info!( - " ✅ instantiated the importer contract in {}", - res.transaction_hash - ); - - info!("IMPORTER CONTRACT ADDRESS: {importer_address}"); - - CachedState { - importer_address: importer_address.clone(), - state_imported: false, - } - .save()?; - - Ok(importer_address) -} - -async fn execute_prepare_contract( - args: PrepareArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - create_importer_contract(args.importer_contract_path, &client).await?; - - Ok(()) -} - -fn approximate_size(pair: &Model) -> usize { - base85rs::encode(&pair.key).len() + base85rs::encode(&pair.value).len() -} - -fn models_to_exec(data: Vec) -> ExecuteMsg { - let pairs = data - .into_iter() - .map(|kv| (kv.key, kv.value)) - .collect::>(); - - pairs.into() -} - -fn split_into_importable_execute_msgs( - kv_pairs: Vec, - approximate_max_chunk: usize, -) -> Vec { - let mut chunks: Vec = Vec::new(); - - let mut current_wip_chunk = Vec::new(); - let mut current_chunk_size = 0; - for kv in kv_pairs { - if current_chunk_size + approximate_size(&kv) > approximate_max_chunk { - let taken = std::mem::take(&mut current_wip_chunk); - chunks.push(models_to_exec(taken)); - current_chunk_size = 0; - } - current_chunk_size += approximate_size(&kv); - current_wip_chunk.push(kv); - } - - if !current_wip_chunk.is_empty() { - chunks.push(models_to_exec(current_wip_chunk)) - } - chunks -} - -async fn set_importer_state( - state_dump_path: PathBuf, - explicit_importer_address: Option, - client: &DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - info!("attempting to set the importer contract state"); - - // this is the value that we found to be optimal during v1->v2 mixnet migration - const MAX_CHUNK_SIZE: usize = 350 * 1000; - - let importer_address = importer_contract_address(explicit_importer_address)?; - - if let Ok(state) = CachedState::load() { - if state.state_imported && state.importer_address == importer_address { - bail!("the state has already been imported for {importer_address}") - } - } - - let dump_file = File::open(state_dump_path)?; - info!("attempting to decode the state dump. for bigger contracts this might take a while..."); - let kv_pairs: Vec = serde_json::from_reader(&dump_file)?; - - info!("there are {} key-value pairs to import", kv_pairs.len()); - info!("attempting to split them into {MAX_CHUNK_SIZE}B chunks ExecuteMsgs..."); - - let chunks = split_into_importable_execute_msgs(kv_pairs, MAX_CHUNK_SIZE); - info!("obtained {} execute msgs", chunks.len()); - - let total = chunks.len(); - for (i, msg) in chunks.into_iter().enumerate() { - info!("executing message {}/{total}...", i + 1); - let res = client - .execute( - &importer_address, - &msg, - None, - "importing contract state", - Vec::new(), - ) - .await?; - info!(" ✅ OK: {}", res.transaction_hash); - } - - info!("Finished migrating storage to {importer_address}!"); - - CachedState { - importer_address, - state_imported: true, - } - .save()?; - - Ok(()) -} - -async fn execute_set_state( - args: SetStateArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - set_importer_state(args.raw_state, args.importer_contract_address, &client).await?; - - Ok(()) -} - -async fn swap_contract( - target_code_id: Option, - target_contract_path: Option, - explicit_importer_address: Option, - migrate_msg: Option, - client: &DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - info!("attempting to swap the contract code"); - - let importer_address = importer_contract_address(explicit_importer_address)?; - - if let Ok(state) = CachedState::load() { - if !state.state_imported && state.importer_address == importer_address { - bail!("the state hasn't been imported for {importer_address}") - } - } - - // one of those must have been set via clap - let code_id = match target_code_id { - Some(explicit) => explicit, - None => { - // upload the contract - let mut data = Vec::new(); - File::open(target_contract_path.unwrap())?.read_to_end(&mut data)?; - - let res = client.upload(data, "", None).await?; - info!( - " ✅ uploaded the target contract in {}", - res.transaction_hash - ); - res.code_id - } - }; - - let migrate_msg = migrate_msg.unwrap_or(serde_json::Value::Object(Default::default())); - let res = client - .migrate( - &importer_address, - code_id, - &migrate_msg, - "migrating into target contract", - None, - ) - .await?; - info!( - " ✅ migrated into the target contract: {}", - res.transaction_hash - ); - - Ok(()) -} - -async fn execute_swap_contract( - args: SwapContractArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - swap_contract( - args.target_contract_code_id, - args.target_contract_path, - args.importer_contract_address, - args.migrate_msg, - &client, - ) - .await?; - - Ok(()) -} - -async fn initialise_with_state( - args: InitialiseWithStateArgs, - client: DirectSigningHttpRpcNyxdClient, -) -> anyhow::Result<()> { - let importer_address = create_importer_contract(args.importer_contract_path, &client).await?; - set_importer_state(args.raw_state, Some(importer_address.clone()), &client).await?; - swap_contract( - args.target_contract_code_id, - args.target_contract_path, - Some(importer_address.clone()), - args.migrate_msg, - &client, - ) - .await?; - - info!("the contract is ready at {importer_address}"); - - Ok(()) -} - -impl Cli { - pub async fn execute(self) -> anyhow::Result<()> { - let network_details = NymNetworkDetails::new_from_env(); - let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; - let nyxd_url = network_details - .endpoints - .first() - .expect("network details are not defined") - .nyxd_url - .as_str(); - - let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - client_config, - nyxd_url, - self.mnemonic, - )?; - match self.command { - Commands::PrepareContract(args) => execute_prepare_contract(args, client).await, - Commands::SetState(args) => execute_set_state(args, client).await, - Commands::SwapContract(args) => execute_swap_contract(args, client).await, - Commands::InitialiseWithState(args) => initialise_with_state(args, client).await, - } - } -} +pub mod commands; +pub mod helpers; +pub mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/tools/internal/contract-state-importer/importer-cli/src/state.rs b/tools/internal/contract-state-importer/importer-cli/src/state.rs new file mode 100644 index 0000000000..499632d543 --- /dev/null +++ b/tools/internal/contract-state-importer/importer-cli/src/state.rs @@ -0,0 +1,40 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_validator_client::nyxd::AccountId; +use serde::{Deserialize, Serialize}; +use std::fs::{create_dir_all, File}; +use std::path::PathBuf; +use tracing::info; + +#[derive(Serialize, Deserialize)] +pub struct CachedState { + pub importer_address: AccountId, + pub state_imported: bool, +} + +impl CachedState { + pub fn save(&self) -> anyhow::Result<()> { + let path = cached_state_file(); + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + let file = File::create(&path)?; + serde_json::to_writer_pretty(file, self)?; + + info!("saved cached details to {}", path.display()); + Ok(()) + } + + pub fn load() -> anyhow::Result { + let file = File::open(cached_state_file())?; + Ok(serde_json::from_reader(&file)?) + } +} + +pub fn cached_state_file() -> PathBuf { + dirs::cache_dir() + .unwrap() + .join("contract-state-importer") + .join(".state.json") +} From e333aca8a1be7d7f54a2566b08d92791c608333f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 11 Oct 2024 17:34:37 +0100 Subject: [PATCH 065/207] introduced specialised subcommand to importer-cli to import mixnet/vesting contracts --- Cargo.lock | 2 + .../importer-cli/Cargo.toml | 3 + .../initialise_mixnet_vesting_with_states.rs | 161 +++++++++++++++++- .../importer-cli/src/commands/mod.rs | 10 ++ .../importer-cli/src/helpers.rs | 85 ++++++++- 5 files changed, 257 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00d23fa76e..09d749a7b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3315,8 +3315,10 @@ dependencies = [ "dirs 5.0.1", "importer-contract", "nym-bin-common", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-validator-client", + "nym-vesting-contract-common", "serde", "serde_json", "tokio", diff --git a/tools/internal/contract-state-importer/importer-cli/Cargo.toml b/tools/internal/contract-state-importer/importer-cli/Cargo.toml index 45a2302209..db708db2e8 100644 --- a/tools/internal/contract-state-importer/importer-cli/Cargo.toml +++ b/tools/internal/contract-state-importer/importer-cli/Cargo.toml @@ -24,3 +24,6 @@ importer-contract = { path = "../importer-contract" } nym-validator-client = { path = "../../../../common/client-libs/validator-client" } nym-bin-common = { path = "../../../../common/bin-common", features = ["basic_tracing"] } nym-network-defaults = { path = "../../../../common/network-defaults" } + +nym-mixnet-contract-common = { path = "../../../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-vesting-contract-common = { path = "../../../../common/cosmwasm-smart-contracts/vesting-contract" } \ No newline at end of file diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs index 6fd33e527d..9f4d8ee349 100644 --- a/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/initialise_mixnet_vesting_with_states.rs @@ -1,4 +1,163 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use clap::ArgGroup; +use crate::commands::prepare::create_importer_contract; +use crate::commands::set_state::set_importer_state; +use crate::commands::swap_contract::swap_contract; +use crate::helpers::{mixnet_contract_path, vesting_contract_path}; +use clap::Args; +use importer_contract::ExecuteMsg; +use nym_mixnet_contract_common::{Addr, ContractState}; +use nym_validator_client::nyxd::{AccountId, CosmWasmClient}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use serde::Serialize; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone)] +pub struct InitialiseMixnetVestingWithStatesArgs { + /// Path to the .wasm file with the importer contract + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/importer_contract.wasm" + #[clap(long)] + pub importer_contract_path: Option, + + /// Path to the file containing state dump of the mixnet contract + #[clap(long)] + pub mixnet_state: PathBuf, + + /// Path to the file containing state dump of the vesting contract + #[clap(long)] + pub vesting_state: PathBuf, + + /// Path to mixnet contract that will be uploaded and applied to the imported state + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/mixnet_contract.wasm" + #[clap(long)] + pub mixnet_contract_path: Option, + + /// Path to the vesting contract that will be uploaded and applied to the imported state + /// If not provided, the CLI will attempt to traverse the parent directories until it finds + /// "target/wasm32-unknown-unknown/release/vesting_contract.wasm" + #[clap(long)] + pub vesting_contract_path: Option, +} + +pub async fn update_kv( + client: &DirectSigningHttpRpcNyxdClient, + contract: &AccountId, + key: &[u8], + value: &T, +) -> anyhow::Result<()> +where + T: Serialize, +{ + let msg = ExecuteMsg::from(vec![(key.to_vec(), serde_json::to_vec(value)?)]); + client + .execute(contract, &msg, None, "updating kv...", Vec::new()) + .await?; + Ok(()) +} + +#[allow(deprecated)] +pub async fn execute_initialise_mixnet_vesting_with_states( + args: InitialiseMixnetVestingWithStatesArgs, + client: DirectSigningHttpRpcNyxdClient, +) -> anyhow::Result<()> { + let mixnet_contract = mixnet_contract_path(args.mixnet_contract_path)?; + let vesting_contract = vesting_contract_path(args.vesting_contract_path)?; + + info!( + "using the following mixnet contract: {}", + mixnet_contract.display() + ); + info!( + "using the following vesting contract: {}", + vesting_contract.display() + ); + + let admin = client.address(); + + // 1. import mixnet state + info!("importing the mixnet contract state..."); + let mixnet_importer_address = + create_importer_contract(args.importer_contract_path.clone(), &client).await?; + set_importer_state( + args.mixnet_state, + Some(mixnet_importer_address.clone()), + &client, + ) + .await?; + + // 2. import vesting state + info!("importing the vesting contract state..."); + let vesting_importer_address = + create_importer_contract(args.importer_contract_path, &client).await?; + set_importer_state( + args.vesting_state, + Some(vesting_importer_address.clone()), + &client, + ) + .await?; + + // 3. adjust few state entries + // update vesting admin: + info!("adjusting internal contract states..."); + let new_vesting_admin = Addr::unchecked(admin.as_ref()); + update_kv( + &client, + &vesting_importer_address, + b"adm", + &new_vesting_admin, + ) + .await?; + + // update mixnet contract address in the vesting contract + let new_mixnet_contract_address = Addr::unchecked(mixnet_importer_address.as_ref()); + update_kv( + &client, + &vesting_importer_address, + b"mix", + &new_mixnet_contract_address, + ) + .await?; + + // update admins and vesting contract addresses in the mixnet contract + let raw_contract_state = client + .query_contract_raw(&mixnet_importer_address, b"state".to_vec()) + .await?; + let mut contract_state: ContractState = serde_json::from_slice(&raw_contract_state)?; + contract_state.vesting_contract_address = Addr::unchecked(vesting_importer_address.as_ref()); + contract_state.owner = Some(Addr::unchecked(admin.as_ref())); + contract_state.rewarding_validator_address = Addr::unchecked(admin.as_ref()); + update_kv(&client, &mixnet_importer_address, b"state", &contract_state).await?; + + let contract_admin = Some(Addr::unchecked(admin.as_ref())); + update_kv(&client, &mixnet_importer_address, b"admin", &contract_admin).await?; + + // 4. apply the correct contract codes + info!("swapping the mixnet contract to the correct .wasm..."); + swap_contract( + None, + Some(mixnet_contract), + Some(mixnet_importer_address.clone()), + None, + &client, + ) + .await?; + + info!("swapping the vesting contract to the correct .wasm..."); + swap_contract( + None, + Some(vesting_contract), + Some(vesting_importer_address.clone()), + None, + &client, + ) + .await?; + + info!("the contracts are ready"); + info!("MIXNET: {mixnet_importer_address} VESTING: {vesting_importer_address}"); + + Ok(()) +} diff --git a/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs b/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs index cd81aa3a14..e7dacbbea1 100644 --- a/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs +++ b/tools/internal/contract-state-importer/importer-cli/src/commands/mod.rs @@ -1,6 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::commands::initialise_mixnet_vesting_with_states::{ + execute_initialise_mixnet_vesting_with_states, InitialiseMixnetVestingWithStatesArgs, +}; use crate::commands::initialise_with_state::{initialise_with_state, InitialiseWithStateArgs}; use crate::commands::prepare::{execute_prepare_contract, PrepareArgs}; use crate::commands::set_state::{execute_set_state, SetStateArgs}; @@ -50,6 +53,10 @@ pub(crate) enum Commands { /// Combines the functionalities of `prepare-contract`, `set-state` and `swap-contract` InitialiseWithState(InitialiseWithStateArgs), + + /// Specialised variant of `initialise-with-state` for the mixnet and vesting contracts that sets + /// internal state keys + InitialiseMixnetVestingWithStates(InitialiseMixnetVestingWithStatesArgs), } impl Cli { @@ -73,6 +80,9 @@ impl Cli { Commands::SetState(args) => execute_set_state(args, client).await, Commands::SwapContract(args) => execute_swap_contract(args, client).await, Commands::InitialiseWithState(args) => initialise_with_state(args, client).await, + Commands::InitialiseMixnetVestingWithStates(args) => { + execute_initialise_mixnet_vesting_with_states(args, client).await + } } } } diff --git a/tools/internal/contract-state-importer/importer-cli/src/helpers.rs b/tools/internal/contract-state-importer/importer-cli/src/helpers.rs index 7f876c181c..ad25324f4b 100644 --- a/tools/internal/contract-state-importer/importer-cli/src/helpers.rs +++ b/tools/internal/contract-state-importer/importer-cli/src/helpers.rs @@ -3,15 +3,63 @@ use crate::state::CachedState; use anyhow::bail; +use nym_validator_client::nyxd::cosmwasm_client::types::Model; use nym_validator_client::nyxd::AccountId; +use serde::de::DeserializeOwned; +use serde::Serialize; use std::env::current_dir; use std::fs; use std::path::PathBuf; use tracing::debug; +pub fn importer_contract_path(explicit: Option) -> anyhow::Result { + contract_path("importer_contract.wasm", explicit) +} + +pub fn mixnet_contract_path(explicit: Option) -> anyhow::Result { + contract_path("mixnet_contract.wasm", explicit) +} + +pub fn vesting_contract_path(explicit: Option) -> anyhow::Result { + contract_path("vesting_contract.wasm", explicit) +} + +fn find_contract_in(root: PathBuf, target_name: &str) -> anyhow::Result> { + for content in root.read_dir()? { + let dir_entry = content?; + + let path = dir_entry.path(); + debug!("checking {:?}", fs::canonicalize(path.clone())); + + let Ok(name) = dir_entry.file_name().into_string() else { + continue; + }; + + if name == "target" { + let maybe_contract_path = dir_entry + .path() + .join("wasm32-unknown-unknown") + .join("release") + .join(target_name); + + if maybe_contract_path.exists() { + return Ok(Some(maybe_contract_path)); + } + } + + if path.is_dir() { + if let Some(found) = find_contract_in(path, target_name)? { + return Ok(Some(found)); + } + } + } + + Ok(None) +} + // this only works if the cli is called from somewhere within the nym directory // (which realistically is going to be the case most of the time) -pub fn importer_contract_path(explicit: Option) -> anyhow::Result { +pub fn contract_path(target_name: &str, explicit: Option) -> anyhow::Result { if let Some(explicit) = explicit { return Ok(explicit); } @@ -29,16 +77,24 @@ pub fn importer_contract_path(explicit: Option) -> anyhow::Result) -> anyhow::Result { @@ -49,3 +105,26 @@ pub fn importer_contract_address(explicit: Option) -> anyhow::Result< let state = CachedState::load()?; Ok(state.importer_address) } + +pub fn find_value(raw_state: &[Model], key: &[u8]) -> anyhow::Result> +where + T: DeserializeOwned, +{ + let Some(entry) = raw_state.iter().find(|kv| kv.key == key) else { + return Ok(None); + }; + + Ok(Some(serde_json::from_slice(&entry.value)?)) +} + +pub fn update_value(raw_state: &mut [Model], key: &[u8], value: &T) -> anyhow::Result<()> +where + T: Serialize, +{ + let Some(entry) = raw_state.iter_mut().find(|kv| kv.key == key) else { + bail!("couldn't find corresponding state entry") + }; + + entry.value = serde_json::to_vec(value)?; + Ok(()) +} From a103acaf70c7473c8217bb52e66ad299269dca66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 10 Oct 2024 13:45:37 +0200 Subject: [PATCH 066/207] Import vpn-api from a414f4a2ee0 --- nym-vpn-api/vpn-api-lib-wasm/Cargo.toml | 38 + nym-vpn-api/vpn-api-lib-wasm/Makefile | 4 + .../internal-dev/bootstrap.js | 2 + .../vpn-api-lib-wasm/internal-dev/index.html | 17 + .../vpn-api-lib-wasm/internal-dev/index.js | 49 + .../internal-dev/package.json | 40 + .../internal-dev/webpack.config.js | 33 + .../vpn-api-lib-wasm/internal-dev/yarn.lock | 2253 +++++++++++++++++ nym-vpn-api/vpn-api-lib-wasm/src/error.rs | 35 + nym-vpn-api/vpn-api-lib-wasm/src/lib.rs | 274 ++ nym-vpn-api/vpn-api-requests/Cargo.toml | 42 + nym-vpn-api/vpn-api-requests/src/api/mod.rs | 4 + .../vpn-api-requests/src/api/v1/mod.rs | 18 + .../src/api/v1/ticketbook/mod.rs | 4 + .../src/api/v1/ticketbook/models.rs | 282 +++ nym-vpn-api/vpn-api-requests/src/client.rs | 172 ++ nym-vpn-api/vpn-api-requests/src/helpers.rs | 40 + nym-vpn-api/vpn-api-requests/src/lib.rs | 84 + nym-vpn-api/vpn-api-server/.gitignore | 1 + nym-vpn-api/vpn-api-server/Cargo.toml | 66 + nym-vpn-api/vpn-api-server/build.rs | 22 + .../vpn-api-server/migrations/01_initial.sql | 19 + .../migrations/02_cherry_picking_chaos.sql | 78 + nym-vpn-api/vpn-api-server/src/cli.rs | 88 + nym-vpn-api/vpn-api-server/src/config/mod.rs | 20 + .../vpn-api-server/src/credentials/mod.rs | 4 + .../src/credentials/ticketbook/mod.rs | 302 +++ nym-vpn-api/vpn-api-server/src/error.rs | 126 + nym-vpn-api/vpn-api-server/src/helpers.rs | 42 + .../vpn-api-server/src/http/helpers.rs | 13 + .../src/http/middleware/auth.rs | 115 + .../src/http/middleware/logging.rs | 64 + .../vpn-api-server/src/http/middleware/mod.rs | 5 + nym-vpn-api/vpn-api-server/src/http/mod.rs | 52 + .../vpn-api-server/src/http/router/api/mod.rs | 15 + .../src/http/router/api/v1/mod.rs | 25 + .../src/http/router/api/v1/openapi.rs | 121 + .../src/http/router/api/v1/ticketbook/mod.rs | 400 +++ .../http/router/api/v1/ticketbook/shares.rs | 206 ++ .../vpn-api-server/src/http/router/mod.rs | 44 + .../vpn-api-server/src/http/state/mod.rs | 683 +++++ nym-vpn-api/vpn-api-server/src/http/types.rs | 64 + nym-vpn-api/vpn-api-server/src/main.rs | 95 + .../vpn-api-server/src/nym_api_helpers.rs | 181 ++ .../vpn-api-server/src/storage/manager.rs | 413 +++ nym-vpn-api/vpn-api-server/src/storage/mod.rs | 460 ++++ .../vpn-api-server/src/storage/models.rs | 86 + nym-vpn-api/vpn-api-server/src/tasks.rs | 36 + nym-vpn-api/vpn-api-server/src/webhook.rs | 75 + 49 files changed, 7312 insertions(+) create mode 100644 nym-vpn-api/vpn-api-lib-wasm/Cargo.toml create mode 100644 nym-vpn-api/vpn-api-lib-wasm/Makefile create mode 100644 nym-vpn-api/vpn-api-lib-wasm/internal-dev/bootstrap.js create mode 100644 nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.html create mode 100644 nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.js create mode 100644 nym-vpn-api/vpn-api-lib-wasm/internal-dev/package.json create mode 100644 nym-vpn-api/vpn-api-lib-wasm/internal-dev/webpack.config.js create mode 100644 nym-vpn-api/vpn-api-lib-wasm/internal-dev/yarn.lock create mode 100644 nym-vpn-api/vpn-api-lib-wasm/src/error.rs create mode 100644 nym-vpn-api/vpn-api-lib-wasm/src/lib.rs create mode 100644 nym-vpn-api/vpn-api-requests/Cargo.toml create mode 100644 nym-vpn-api/vpn-api-requests/src/api/mod.rs create mode 100644 nym-vpn-api/vpn-api-requests/src/api/v1/mod.rs create mode 100644 nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/mod.rs create mode 100644 nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/models.rs create mode 100644 nym-vpn-api/vpn-api-requests/src/client.rs create mode 100644 nym-vpn-api/vpn-api-requests/src/helpers.rs create mode 100644 nym-vpn-api/vpn-api-requests/src/lib.rs create mode 100644 nym-vpn-api/vpn-api-server/.gitignore create mode 100644 nym-vpn-api/vpn-api-server/Cargo.toml create mode 100644 nym-vpn-api/vpn-api-server/build.rs create mode 100644 nym-vpn-api/vpn-api-server/migrations/01_initial.sql create mode 100644 nym-vpn-api/vpn-api-server/migrations/02_cherry_picking_chaos.sql create mode 100644 nym-vpn-api/vpn-api-server/src/cli.rs create mode 100644 nym-vpn-api/vpn-api-server/src/config/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/credentials/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/error.rs create mode 100644 nym-vpn-api/vpn-api-server/src/helpers.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/helpers.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/middleware/auth.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/middleware/logging.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/middleware/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/router/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/state/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/http/types.rs create mode 100644 nym-vpn-api/vpn-api-server/src/main.rs create mode 100644 nym-vpn-api/vpn-api-server/src/nym_api_helpers.rs create mode 100644 nym-vpn-api/vpn-api-server/src/storage/manager.rs create mode 100644 nym-vpn-api/vpn-api-server/src/storage/mod.rs create mode 100644 nym-vpn-api/vpn-api-server/src/storage/models.rs create mode 100644 nym-vpn-api/vpn-api-server/src/tasks.rs create mode 100644 nym-vpn-api/vpn-api-server/src/webhook.rs diff --git a/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml b/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml new file mode 100644 index 0000000000..bf6770b05a --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "nym-vpn-api-lib-wasm" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +bs58 = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +time = { workspace = true, features = ["wasm-bindgen"] } +thiserror.workspace = true +zeroize = { workspace = true } + +# wasm-specific deps +getrandom = { version = "0.2", features = ["js"] } +wasm-bindgen = { workspace = true } +js-sys = "0.3.70" +tsify = { workspace = true, features = ["js"] } +serde-wasm-bindgen = "0.6.5" + +# NYM: +nym-bin-common = { workspace = true } +nym-crypto = { workspace = true, features = ["asymmetric"] } +nym-compact-ecash = { workspace = true } +nym-credentials = { workspace = true } +nym-credentials-interface = { workspace = true } +nym-ecash-time = { workspace = true, features = ["expiration"] } +nym-vpn-api-requests = { path = "../vpn-api-requests", default-features = false, features = ["tsify"] } +wasm-utils = { workspace = true } + diff --git a/nym-vpn-api/vpn-api-lib-wasm/Makefile b/nym-vpn-api/vpn-api-lib-wasm/Makefile new file mode 100644 index 0000000000..2edce6e5fc --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/Makefile @@ -0,0 +1,4 @@ +build: + wasm-pack build --scope nymproject --target web --out-dir ../../dist/wasm/nym-vpn-api-lib-wasm + wasm-opt -Oz -o ../../dist/wasm/nym-vpn-api-lib-wasm/nym_vpn_api_lib_wasm_bg.wasm ../../dist/wasm/nym-vpn-api-lib-wasm/nym_vpn_api_lib_wasm_bg.wasm + diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/bootstrap.js b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/bootstrap.js new file mode 100644 index 0000000000..362cdb2431 --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/bootstrap.js @@ -0,0 +1,2 @@ +import("./index.js") + .catch(e => console.error("Error importing `index.js`:", e)); diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.html b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.html new file mode 100644 index 0000000000..5cd272596a --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.html @@ -0,0 +1,17 @@ + + + + + + + Nym WebAssembly Demo + + + + + +

everything happens in the console

+ + + + \ No newline at end of file diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.js b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.js new file mode 100644 index 0000000000..3921411115 --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.js @@ -0,0 +1,49 @@ +import init, { NymIssuanceTicketbook } from "@nymproject/nym-vpn-api-lib-wasm"; + +async function main() { + await init(); + + let cryptoData = new NymIssuanceTicketbook({}); + + console.log("getting partial vks"); + const partialVksRes = await fetch("http://localhost:8080/api/v1/ticketbook/partial-verification-keys", { + headers: new Headers({ "Authorization": "Bearer foomp" }) + }); + const partialVks = await partialVksRes.json(); + console.debug(partialVks); + + console.log("getting master vk"); + const masterVkRes = await fetch("http://localhost:8080/api/v1/ticketbook/master-verification-key", { + headers: new Headers({ "Authorization": "Bearer foomp" }) + }); + const masterVk = await masterVkRes.json(); + console.debug(masterVk); + + let request = cryptoData.buildRequestPayload(false); + console.log(request); + + + console.log("getting blinded wallet shares"); + const sharesRes = await fetch("http://localhost:8080/api/v1/ticketbook/obtain?include-coin-index-signatures=true&include-expiration-date-signatures=true", { + method: "POST", + headers: new Headers( + { + "Authorization": "Bearer foomp", + "Content-Type": "application/json" + } + ), + body: request + }); + + const credentialShares = await sharesRes.json(); + console.log(credentialShares); + + console.log("unblinding shares"); + const unblinded = cryptoData.unblindWalletShares(credentialShares, partialVks, masterVk); + + const serialised = unblinded.serialise(); + console.log("serialised:\n", serialised); +} + + +main(); diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/package.json b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/package.json new file mode 100644 index 0000000000..17032501e8 --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/package.json @@ -0,0 +1,40 @@ +{ + "name": "create-wasm-app", + "version": "0.1.0", + "description": "create an app to consume rust-generated wasm packages", + "main": "index.js", + "bin": { + "create-wasm-app": ".bin/create-wasm-app.js" + }, + "scripts": { + "build": "webpack --config webpack.config.js", + "build:wasm": "cd ../ && make wasm-build", + "start": "webpack-dev-server --port 8001" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rustwasm/create-wasm-app.git" + }, + "keywords": [ + "webassembly", + "wasm", + "rust", + "webpack" + ], + "author": "Dave Hrycyszyn ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/nymtech/nym/issues" + }, + "homepage": "https://nymtech.net/docs", + "devDependencies": { + "copy-webpack-plugin": "^11.0.0", + "hello-wasm-pack": "^0.1.0", + "webpack": "^5.70.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4" + }, + "dependencies": { + "@nymproject/nym-vpn-api-lib-wasm": "file:../../../dist/wasm/nym-vpn-api-lib-wasm" + } +} diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/webpack.config.js b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/webpack.config.js new file mode 100644 index 0000000000..d5f839485f --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/webpack.config.js @@ -0,0 +1,33 @@ +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const path = require("path"); + +module.exports = { + performance: { + hints: false, + maxEntrypointSize: 512000, + maxAssetSize: 512000 + }, + entry: { + bootstrap: "./bootstrap.js" + // worker: "./worker.js" + }, + output: { + path: path.resolve(__dirname, "dist"), + filename: "[name].js" + }, + mode: "development", + // mode: 'production', + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + "index.html", + { + from: "../../../dist/wasm/nym-vpn-api-lib-wasm/*.(js|wasm)", + to: "[name][ext]" + } + ] + }) + + ], + experiments: { syncWebAssembly: true } +}; diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/yarn.lock b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/yarn.lock new file mode 100644 index 0000000000..43e804cb6e --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/internal-dev/yarn.lock @@ -0,0 +1,2253 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nymproject/nym-vpn-api-lib-wasm@file:../../../dist/wasm/nym-vpn-api-lib-wasm": + version "0.1.0" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.15" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== + dependencies: + "@types/node" "*" + +"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "22.5.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958" + integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg== + dependencies: + undici-types "~6.19.2" + +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.33": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.5": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.21.10: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +caniuse-lite@^1.0.30001646: + version "1.0.30001653" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz#b8af452f8f33b1c77f122780a4aecebea0caca56" + integrity sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw== + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +copy-webpack-plugin@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" + integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== + dependencies: + fast-glob "^3.2.11" + glob-parent "^6.0.1" + globby "^13.1.1" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.13.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" + integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.11, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.0.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^13.1.1: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.3.0" + ignore "^5.2.4" + merge2 "^1.4.1" + slash "^4.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hello-wasm-pack@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz#482a2e3371828056ac35f5b5fec76c0b99dcd530" + integrity sha512-3hx0GDkDLf/a9ThCMV2qG4mwza8N/MCtm8aeFFc/cdBCL2zMJ1kW1wjNl7xPqD1lz8Yl5+uhnc/cpui4dLwz/w== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +launch-editor@^2.6.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.1.tgz#3bda72af213ec9b46b170e39661916ec66c2f463" + integrity sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@^4.9.2: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.7.4: + version "4.15.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.4" + ws "^8.13.0" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.70.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== diff --git a/nym-vpn-api/vpn-api-lib-wasm/src/error.rs b/nym-vpn-api/vpn-api-lib-wasm/src/error.rs new file mode 100644 index 0000000000..7322254064 --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/src/error.rs @@ -0,0 +1,35 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde_wasm_bindgen::Error; +use thiserror::Error; +use wasm_utils::wasm_error; + +#[derive(Debug, Error)] +pub enum VpnApiLibError { + #[error("{0}")] + Json(String), + + #[error("[ecash] cryptographic failure: {source}")] + EcashFailure { + #[from] + source: nym_compact_ecash::CompactEcashError, + }, + + #[error("provided invalid ticket type")] + MalformedTicketType, + + #[error("the provided shares and issuers are not from the same epoch! {shares} and {issuers}")] + InconsistentEpochId { shares: u64, issuers: u64 }, + + #[error("failed to recover ed25519 private key from its base58 representation")] + MalformedEd25519Key, +} + +wasm_error!(VpnApiLibError); + +impl From for VpnApiLibError { + fn from(value: Error) -> Self { + VpnApiLibError::Json(value.to_string()) + } +} diff --git a/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs b/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs new file mode 100644 index 0000000000..44603fdbc2 --- /dev/null +++ b/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs @@ -0,0 +1,274 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiLibError; +use nym_compact_ecash::scheme::keygen::KeyPairUser; +use nym_compact_ecash::scheme::withdrawal::RequestInfo; +use nym_compact_ecash::{ + aggregate_wallets, issue_verify, withdrawal_request, Base58, BlindedSignature, + VerificationKeyAuth, WithdrawalRequest, +}; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, + IssuedTicketBook, +}; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_time::{ecash_default_expiration_date, EcashTime}; +use nym_vpn_api_requests::api::v1::ticketbook::models::{ + MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use time::Date; +use tsify::Tsify; +use wasm_bindgen::prelude::*; +use wasm_utils::console_error; +use zeroize::Zeroizing; + +pub mod error; + +#[derive(Tsify, Debug, Default, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct WalletShares(Vec); + +pub type WalletIssuers = PartialVerificationKeysResponse; + +impl From> for WalletShares { + fn from(shares: Vec) -> Self { + WalletShares(shares) + } +} + +#[derive(Tsify, Debug, Default, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct NymIssuanceTicketbookOpts { + #[tsify(optional)] + pub ticketbook_type: Option, + + // bs58-encoded user secret key used for seeding the ecash crypto keypair generation + // I reiterate, this is a **SECRET** key, not a public key. + #[tsify(optional)] + pub user_secret_key: Option, +} + +#[wasm_bindgen] +#[derive(Debug)] +#[allow(dead_code)] +pub struct NymIssuanceTicketbook { + /// ecash keypair related to the credential + ecash_keypair: KeyPairUser, + + withdrawal_request: WithdrawalRequest, + + ticketbook_type: TicketType, + + expiration_date: Date, + + request_info: Zeroizing, +} + +#[wasm_bindgen] +impl NymIssuanceTicketbook { + #[wasm_bindgen(constructor)] + pub fn new(opts: NymIssuanceTicketbookOpts) -> Result { + let ecash_keypair = match opts.user_secret_key { + None => KeyPairUser::new(), + Some(maybe_sk) => { + let pk = ed25519::PrivateKey::from_base58_string(maybe_sk) + .map(Zeroizing::new) + .map_err(|_| VpnApiLibError::MalformedEd25519Key)?; + let bytes = Zeroizing::new(pk.to_bytes()); + KeyPairUser::new_seeded(&bytes) + } + }; + + let ticketbook_type = match opts.ticketbook_type { + None => TicketType::default(), + Some(typ) => typ + .parse() + .map_err(|_| VpnApiLibError::MalformedTicketType)?, + }; + + let expiration_date = ecash_default_expiration_date(); + + let (withdrawal_request, request_info) = withdrawal_request( + ecash_keypair.secret_key(), + expiration_date.ecash_unix_timestamp(), + ticketbook_type.encode(), + )?; + + Ok(NymIssuanceTicketbook { + ecash_keypair, + withdrawal_request, + ticketbook_type: Default::default(), + expiration_date, + request_info: Zeroizing::new(request_info), + }) + } + + #[wasm_bindgen(js_name = "buildRequestPayload")] + pub fn build_request_payload(&self, is_freepass_request: bool) -> String { + serde_json::to_string(&TicketbookRequest { + withdrawal_request: self.withdrawal_request.clone().into(), + ecash_pubkey: self.ecash_keypair.public_key(), + expiration_date: self.expiration_date, + ticketbook_type: self.ticketbook_type, + is_freepass_request, + }) + .unwrap() + } + + #[wasm_bindgen(js_name = "getWithdrawalRequest")] + pub fn get_encoded_withdrawal_request(&self) -> String { + self.withdrawal_request.to_bs58() + } + + #[wasm_bindgen(js_name = "getEncodedPublicKey")] + pub fn get_encoded_public_key(&self) -> String { + self.ecash_keypair.public_key().to_bs58() + } + + // + // #[wasm_bindgen(js_name = "unblindShare")] + // pub fn unblind_share(&self, share: UnblindableShare) -> Result { + // let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; + // let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; + // + // Ok(blinded_sig + // .unblind(&vk, &self.pedersen_commitments_openings) + // .into()) + // } + // + #[wasm_bindgen(js_name = "unblindWalletShares")] + pub fn unblind_wallet_shares( + self, + shares: JsValue, + issuers: WalletIssuers, + master_key: MasterVerificationKeyResponse, + ) -> Result { + // we couldn't derive all the required abi traits due to crypto types deep in the stack + let shares: TicketbookWalletSharesResponse = serde_wasm_bindgen::from_value(shares)?; + + if shares.epoch_id != issuers.epoch_id { + console_error!( + "the provided shares and issuers are not from the same epoch! {} and {}", + shares.epoch_id, + issuers.epoch_id + ); + return Err(VpnApiLibError::InconsistentEpochId { + shares: shares.epoch_id, + issuers: issuers.epoch_id, + }); + } + + let master_vk = VerificationKeyAuth::try_from_bs58(master_key.bs58_encoded_key)?; + + let mut decoded_keys = HashMap::new(); + for key in issuers.keys { + let vk = VerificationKeyAuth::try_from_bs58(key.bs58_encoded_key)?; + decoded_keys.insert(key.node_index, vk); + } + + let mut partial_wallets = Vec::new(); + for share in shares.shares { + let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; + let Some(vk) = decoded_keys.get(&share.node_index) else { + console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); + continue; + }; + + match issue_verify( + vk, + self.ecash_keypair.secret_key(), + &blinded_sig, + &self.request_info, + share.node_index, + ) { + Ok(partial_wallet) => partial_wallets.push(partial_wallet), + Err(err) => { + console_error!( + "failed to unblind partial wallet corresponding to index {}: {err}", + share.node_index + ) + } + } + } + + let aggregated_wallet = aggregate_wallets( + &master_vk, + self.ecash_keypair.secret_key(), + &partial_wallets, + &self.request_info, + )?; + + Ok(NymIssuedTicketbook { + inner_ticketbook: IssuedTicketBook::new( + aggregated_wallet.into_wallet_signatures(), + shares.epoch_id, + self.ecash_keypair.into(), + self.ticketbook_type, + self.expiration_date, + ), + master_vk: EpochVerificationKey { + epoch_id: shares.epoch_id, + key: master_vk, + }, + expiration_date_signatures: shares + .aggregated_expiration_date_signatures + .map(|s| s.signatures), + coin_index_signatures: shares + .aggregated_coin_index_signatures + .map(|s| s.signatures), + }) + } +} + +#[wasm_bindgen] +pub struct NymIssuedTicketbook { + inner_ticketbook: IssuedTicketBook, + + master_vk: EpochVerificationKey, + expiration_date_signatures: Option, + coin_index_signatures: Option, +} + +#[wasm_bindgen] +impl NymIssuedTicketbook { + pub fn serialise(self) -> FullSerialisedNymIssuedTicketbook { + let serialised = self + .inner_ticketbook + .begin_export() + .with_master_verification_key(&self.master_vk) + .with_maybe_expiration_date_signatures(&self.expiration_date_signatures) + .with_maybe_coin_index_signatures(&self.coin_index_signatures) + .finalize_export(); + + FullSerialisedNymIssuedTicketbook { + serialisation_revision: serialised.revision, + bs58_encoded_data: bs58::encode(serialised.data).into_string(), + } + } +} + +#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct FullSerialisedNymIssuedTicketbook { + pub serialisation_revision: u8, + pub bs58_encoded_data: String, +} + +#[wasm_bindgen(start)] +pub fn main() { + wasm_utils::console_log!("[rust main]: rust module loaded"); + wasm_utils::console_log!( + "vpn-api-lib version used:\n{}", + nym_bin_common::bin_info!().pretty_print() + ); + wasm_utils::console_log!("[rust main]: setting panic hook"); + wasm_utils::set_panic_hook(); +} diff --git a/nym-vpn-api/vpn-api-requests/Cargo.toml b/nym-vpn-api/vpn-api-requests/Cargo.toml new file mode 100644 index 0000000000..bf37ca2d3d --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "nym-vpn-api-requests" +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] +async-trait = { workspace = true } +schemars = { workspace = true, features = ["preserve_order", "uuid1"] } +uuid = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +time = { workspace = true, features = ["serde", "formatting", "parsing"] } +tsify = { workspace = true, optional = true } +reqwest = { workspace = true, features = ["json"] } +wasm-bindgen = { workspace = true, optional = true } + +## openapi: +utoipa = { workspace = true, optional = true } + +nym-credentials.workspace = true +nym-credentials-interface.workspace = true +nym-http-api-common = { workspace = true, optional = true } +nym-http-api-client = { workspace = true } +nym-serde-helpers = { workspace = true, features = ["bs58"] } + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer] +workspace = true +features = ["tokio"] + + +[features] +default = ["query-types"] +query-types = ["nym-http-api-common"] +openapi = ["utoipa"] +tsify = ["dep:tsify", "wasm-bindgen"] diff --git a/nym-vpn-api/vpn-api-requests/src/api/mod.rs b/nym-vpn-api/vpn-api-requests/src/api/mod.rs new file mode 100644 index 0000000000..60df60aa1f --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/src/api/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod v1; diff --git a/nym-vpn-api/vpn-api-requests/src/api/v1/mod.rs b/nym-vpn-api/vpn-api-requests/src/api/v1/mod.rs new file mode 100644 index 0000000000..2e131231cd --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/src/api/v1/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// pub mod bandwidth_voucher; +// pub mod freepass; +pub mod ticketbook; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ErrorResponse { + #[cfg_attr(feature = "openapi",schema(value_type = Option, example = "c48f9ce3-a1e9-4886-8000-13f290f34501"))] + pub uuid: Option, + pub message: String, +} diff --git a/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/mod.rs b/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/mod.rs new file mode 100644 index 0000000000..363be75767 --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod models; diff --git a/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/models.rs b/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/models.rs new file mode 100644 index 0000000000..6ce58e81af --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/models.rs @@ -0,0 +1,282 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_credentials::ecash::bandwidth::serialiser::signatures::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, +}; +use nym_credentials_interface::{PublicKeyUser, TicketType, WithdrawalRequest}; +use schemars::gen::SchemaGenerator; +use schemars::schema::Schema; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; +use time::Date; + +#[cfg(feature = "query-types")] +use nym_http_api_common::Output; + +#[cfg(feature = "tsify")] +use tsify::Tsify; +use uuid::Uuid; + +#[cfg(feature = "tsify")] +use wasm_bindgen::prelude::wasm_bindgen; + +#[derive(JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct PlaceholderJsonSchemaImpl {} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookRequest { + /// base58 encoded withdrawal request + pub withdrawal_request: WithdrawalRequestBs58Wrapper, + + /// bs58-encoded **ECASH** public key. + /// this is **NOT** a device key or anything like that. + /// it is derived from user's **SECRET** key! + /// + /// you **MUST** provide a valid value otherwise blacklisting won't work + #[schemars(with = "String")] + #[serde(with = "bs58_ecash")] + pub ecash_pubkey: PublicKeyUser, + + // needs to be explicit in case user creates request at 23:59:59.999, but it reaches vpn-api at 00:00:00.001 + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + + #[schemars(with = "String")] + pub ticketbook_type: TicketType, + + pub is_freepass_request: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookAsyncRequest { + #[serde(flatten)] + pub inner: TicketbookRequest, + + /// unique id of the device + pub device_id: String, + /// unique id of the credential + pub credential_id: String, + /// secret used for webhook responses + pub secret: String, +} + +mod bs58_ecash { + use nym_credentials_interface::Base58; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(req: &T, serializer: S) -> Result + where + T: Base58, + { + serializer.serialize_str(&req.to_bs58()) + } + + pub fn deserialize<'de, D: Deserializer<'de>, T>(deserializer: D) -> Result + where + T: Base58, + { + let s = ::deserialize(deserializer)?; + T::try_from_bs58(&s).map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "openapi", schema(value_type = String))] +pub struct WithdrawalRequestBs58Wrapper(#[serde(with = "bs58_ecash")] pub WithdrawalRequest); + +impl From for WithdrawalRequest { + fn from(value: WithdrawalRequestBs58Wrapper) -> Self { + value.0 + } +} + +impl From for WithdrawalRequestBs58Wrapper { + fn from(value: WithdrawalRequest) -> Self { + WithdrawalRequestBs58Wrapper(value) + } +} + +impl Deref for WithdrawalRequestBs58Wrapper { + type Target = WithdrawalRequest; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WithdrawalRequestBs58Wrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// implementation taken from: https://github.com/GREsau/schemars/pull/207 +impl JsonSchema for WithdrawalRequestBs58Wrapper { + fn is_referenceable() -> bool { + true + } + + fn schema_name() -> String { + "WithdrawalRequestBs58Wrapper".into() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + // during serialisation we just use bs58 representation + String::json_schema(gen) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct CurrentEpochResponse { + pub epoch_id: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct PartialVerificationKeysResponse { + pub epoch_id: u64, + pub keys: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct PartialVerificationKey { + pub node_index: u64, + pub bs58_encoded_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct MasterVerificationKeyResponse { + pub epoch_id: u64, + pub bs58_encoded_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct DepositResponse { + pub current_deposit_amount: u128, + pub current_deposit_denom: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AggregatedExpirationDateSignaturesResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + #[cfg_attr(feature = "openapi", schema(value_type = PlaceholderJsonSchemaImpl))] + pub signatures: AggregatedExpirationDateSignatures, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AggregatedCoinIndicesSignaturesResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + #[cfg_attr(feature = "openapi", schema(value_type = PlaceholderJsonSchemaImpl))] + pub signatures: AggregatedCoinIndicesSignatures, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct WalletShare { + pub node_index: u64, + pub bs58_encoded_share: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookWalletSharesResponse { + pub epoch_id: u64, + pub shares: Vec, + pub master_verification_key: Option, + pub aggregated_coin_index_signatures: Option, + pub aggregated_expiration_date_signatures: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookWalletSharesAsyncResponse { + pub id: i64, + + // maybe redundant, but could be useful for debugging + pub uuid: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct BlindedWalletSharesResponse { + pub id: i64, + pub status: String, + pub device_id: String, + pub credential_id: String, + pub data: Option, + pub error_message: Option, + pub created: String, + pub updated: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct WebhookBlindedSharesResponse { + pub blinded_shares: BlindedWalletSharesResponse, + pub secret: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct TicketbookObtainQueryParams { + pub output: Option, + + pub include_master_verification_key: bool, + + pub include_coin_index_signatures: bool, + + pub include_expiration_date_signatures: bool, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct SharesQueryParams { + pub output: Option, + + pub include_master_verification_key: bool, + + pub include_coin_index_signatures: bool, + + pub include_expiration_date_signatures: bool, +} diff --git a/nym-vpn-api/vpn-api-requests/src/client.rs b/nym-vpn-api/vpn-api-requests/src/client.rs new file mode 100644 index 0000000000..301656db79 --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/src/client.rs @@ -0,0 +1,172 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::api::v1::ticketbook::models::{ + MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookRequest, + TicketbookWalletSharesResponse, +}; +use async_trait::async_trait; +use nym_http_api_client::{parse_response, HttpClientError, Params, PathSegments, NO_PARAMS}; +use reqwest::IntoUrl; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +pub use nym_http_api_client::Client; +pub type VpnApiClientError = HttpClientError; + +#[allow(dead_code)] +pub struct VpnApiClient { + inner: Client, + bearer_token: String, +} + +pub fn new_client( + base_url: impl IntoUrl, + bearer_token: impl Into, +) -> Result { + Ok(VpnApiClient { + inner: Client::builder(base_url)? + .with_user_agent(format!( + "nym-vpn-api-requests/{}", + env!("CARGO_PKG_VERSION") + )) + .build()?, + bearer_token: bearer_token.into(), + }) +} + +// TODO: do it properly by implementing auth headers on `ApiClient` trait +#[allow(dead_code)] +#[async_trait(?Send)] +pub trait NymVpnApiClient { + async fn simple_get(&self, path: PathSegments<'_>) -> Result + where + T: DeserializeOwned; + + async fn simple_post( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result + where + B: Serialize + ?Sized, + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef; + + async fn get_partial_verification_keys( + &self, + ) -> Result { + self.simple_get(&["/api", "/v1", "/ticketbook", "/partial-verification-keys"]) + .await + } + + async fn get_master_verification_key( + &self, + ) -> Result { + self.simple_get(&["/api", "/v1", "/ticketbook", "/master-verification-key"]) + .await + } + + async fn get_ticketbook_wallet_shares( + &self, + request: &TicketbookRequest, + full_response: bool, + ) -> Result { + let params = vec![("full-response", full_response.to_string())]; + + self.simple_post(&["/api", "/v1", "/ticketbook", "/obtain"], ¶ms, request) + .await + } + // + // async fn get_bandwidth_voucher_blinded_shares( + // &self, + // blind_sign_request: BlindSignRequest, + // ) -> Result; +} + +#[async_trait(?Send)] +impl NymVpnApiClient for VpnApiClient { + async fn simple_get(&self, path: PathSegments<'_>) -> Result + where + T: DeserializeOwned, + { + let req = self + .inner + .create_get_request(path, NO_PARAMS) + .bearer_auth(&self.bearer_token) + .send(); + + // the only reason for that target lock is so that I could call this method from an ephemeral test + // running in non-wasm mode (since I wanted to use tokio) + + #[cfg(target_arch = "wasm32")] + let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), req) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)??; + + #[cfg(not(target_arch = "wasm32"))] + let res = req.await?; + + parse_response(res, false).await + } + + async fn simple_post( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result + where + B: Serialize + ?Sized, + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + { + let req = self + .inner + .create_post_request(path, params, json_body) + .bearer_auth(&self.bearer_token) + .send(); + + // the only reason for that target lock is so that I could call this method from an ephemeral test + // running in non-wasm mode (since I wanted to use tokio) + + #[cfg(target_arch = "wasm32")] + let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), req) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)??; + + #[cfg(not(target_arch = "wasm32"))] + let res = req.await?; + + parse_response(res, false).await + } + + // async fn get_bandwidth_voucher_blinded_shares( + // &self, + // blind_sign_request: BlindSignRequest, + // ) -> Result { + // let req = self.inner.create_post_request( + // &["/api", "/v1", "/bandwidth-voucher", "/obtain"], + // NO_PARAMS, + // &BandwidthVoucherRequest { blind_sign_request }, + // ); + // + // let fut = req.bearer_auth(&self.bearer_token).send(); + // + // // the only reason for that target lock is so that I could call this method from an ephemeral test + // // running in non-wasm mode (since I wanted to use tokio) + // + // #[cfg(target_arch = "wasm32")] + // let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), fut) + // .await + // .map_err(|_timeout| HttpClientError::RequestTimeout)??; + // + // #[cfg(not(target_arch = "wasm32"))] + // let res = fut.await?; + // + // parse_response(res, false).await + // } +} diff --git a/nym-vpn-api/vpn-api-requests/src/helpers.rs b/nym-vpn-api/vpn-api-requests/src/helpers.rs new file mode 100644 index 0000000000..5420be21c5 --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/src/helpers.rs @@ -0,0 +1,40 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use time::format_description::{modifier, BorrowedFormatItem, Component}; + +const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), +]; + +pub(crate) mod date_serde { + use crate::helpers::DATE_FORMAT; + use serde::ser::Error; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + use time::Date; + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + Date::parse(&s, DATE_FORMAT).map_err(de::Error::custom) + } + + pub(crate) fn serialize(datetime: &Date, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DATE_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} diff --git a/nym-vpn-api/vpn-api-requests/src/lib.rs b/nym-vpn-api/vpn-api-requests/src/lib.rs new file mode 100644 index 0000000000..fbbd38ee02 --- /dev/null +++ b/nym-vpn-api/vpn-api-requests/src/lib.rs @@ -0,0 +1,84 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +pub mod api; +pub mod client; +mod helpers; + +macro_rules! absolute_route { + ( $name:ident, $parent:expr, $suffix:expr ) => { + pub fn $name() -> String { + format!("{}{}", $parent, $suffix) + } + }; +} + +pub mod routes { + pub const ROOT: &str = "/"; + pub const API: &str = "/api"; + + pub mod api { + pub const V1: &str = "/v1"; + + absolute_route!(v1_absolute, super::API, V1); + + pub mod v1 { + use super::*; + + pub const SWAGGER: &str = "/swagger"; + pub const TICKETBOOK: &str = "/ticketbook"; + + // define helper functions to get absolute routes + absolute_route!(swagger_absolute, v1_absolute(), SWAGGER); + absolute_route!(ticketbook_absolute, v1_absolute(), TICKETBOOK); + + pub mod ticketbook { + use super::*; + + pub const OBTAIN: &str = "/obtain"; + pub const OBTAIN_ASYNC: &str = "/obtain-async"; + pub const DEPOSIT_AMOUNT: &str = "/deposit-amount"; + pub const MASTER_KEY: &str = "/master-verification-key"; + pub const PARTIAL_KEYS: &str = "/partial-verification-keys"; + pub const CURRENT_EPOCH: &str = "/current-epoch"; + pub const SHARES: &str = "/shares"; + + absolute_route!( + obtain_wallet_shares_absolute, + ticketbook_absolute(), + OBTAIN + ); + absolute_route!( + obtain_async_wallet_shares_absolute, + ticketbook_absolute(), + OBTAIN_ASYNC + ); + absolute_route!( + current_deposit_amount_absolute, + ticketbook_absolute(), + DEPOSIT_AMOUNT + ); + absolute_route!(master_key_absolute, ticketbook_absolute(), MASTER_KEY); + absolute_route!(partial_keys_absolute, ticketbook_absolute(), PARTIAL_KEYS); + absolute_route!(current_epoch_absolute, ticketbook_absolute(), CURRENT_EPOCH); + absolute_route!(shares_absolute, ticketbook_absolute(), SHARES); + + pub mod shares { + use super::*; + + pub const SHARE_BY_ID: &str = "/:share_id"; + pub const SHARE_BY_DEVICE_AND_CREDENTIAL_ID: &str = + "/device/:device_id/credential/:credential_id"; + + absolute_route!(share_by_id_absolute, shares_absolute(), SHARE_BY_ID); + absolute_route!(share_by_device_and_credential_id_absolute, shares_absolute(), SHARE_BY_DEVICE_AND_CREDENTIAL_ID); + } + } + } + } +} diff --git a/nym-vpn-api/vpn-api-server/.gitignore b/nym-vpn-api/vpn-api-server/.gitignore new file mode 100644 index 0000000000..1dc2d107db --- /dev/null +++ b/nym-vpn-api/vpn-api-server/.gitignore @@ -0,0 +1 @@ +.db \ No newline at end of file diff --git a/nym-vpn-api/vpn-api-server/Cargo.toml b/nym-vpn-api/vpn-api-server/Cargo.toml new file mode 100644 index 0000000000..bca4bad573 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "nym-vpn-api" +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] +async-trait.workspace = true +axum.workspace = true +anyhow.workspace = true +bip39 = { workspace = true, features = ["zeroize"] } +bs58.workspace = true +cfg-if = { workspace = true } +colored.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +dotenv.workspace = true +futures.workspace = true +humantime.workspace = true +rand.workspace = true +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +strum = { workspace = true, features = ["derive"] } +strum_macros.workspace = true +time.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } +tokio-util = { workspace = true, features = ["rt"] } +tower.workspace = true +tower-http = { workspace = true, features = ["cors"], optional = true } +tracing.workspace = true +url.workspace = true +uuid = { workspace = true, features = ["serde"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +zeroize.workspace = true + +nym-bin-common = { workspace = true, features = ["basic_tracing"] } +nym-compact-ecash = { workspace = true } +nym-config = { workspace = true } +nym-crypto = { workspace = true, features = ["asymmetric", "rand", "serde"] } +nym-credentials = { workspace = true } +nym-credentials-interface = { workspace = true } +nym-http-api-common = { workspace = true, features = ["utoipa"] } +nym-validator-client = { workspace = true } +nym-network-defaults = { workspace = true } + +nym-vpn-api-requests = { path = "../vpn-api-requests", features = ["openapi"] } + +[dev-dependencies] +tempfile = { workspace = true } + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + +[features] +default = ["cors"] +cors = ["tower-http"] \ No newline at end of file diff --git a/nym-vpn-api/vpn-api-server/build.rs b/nym-vpn-api/vpn-api-server/build.rs new file mode 100644 index 0000000000..9e5c18ec6c --- /dev/null +++ b/nym-vpn-api/vpn-api-server/build.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#[tokio::main] +async fn main() { + use sqlx::{Connection, SqliteConnection}; + use std::env; + + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{out_dir}/nym-vpn-api-example.sqlite"); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); +} diff --git a/nym-vpn-api/vpn-api-server/migrations/01_initial.sql b/nym-vpn-api/vpn-api-server/migrations/01_initial.sql new file mode 100644 index 0000000000..c3900d9b36 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/migrations/01_initial.sql @@ -0,0 +1,19 @@ +/* + * Copyright 2023 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE blinded_shares +( + id INTEGER NOT NULL PRIMARY KEY, + status TEXT NOT NULL, + device_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + data TEXT DEFAULT NULL, + error_message TEXT DEFAULT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE UNIQUE INDEX blinded_shares_index ON blinded_shares (credential_id, device_id); + diff --git a/nym-vpn-api/vpn-api-server/migrations/02_cherry_picking_chaos.sql b/nym-vpn-api/vpn-api-server/migrations/02_cherry_picking_chaos.sql new file mode 100644 index 0000000000..674b6d328d --- /dev/null +++ b/nym-vpn-api/vpn-api-server/migrations/02_cherry_picking_chaos.sql @@ -0,0 +1,78 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +DROP TABLE blinded_shares; +CREATE TABLE blinded_shares +( + id INTEGER NOT NULL PRIMARY KEY, +-- added request_uuid to tie it to deposit and actual share data + request_uuid TEXT NOT NULL REFERENCES ticketbook_deposit(request_uuid), + status TEXT NOT NULL, + device_id TEXT NOT NULL, + credential_id TEXT NOT NULL, +-- replaced the explicit data field in favour of separate table alongside +-- the information on the number of shares available (need min. threshold) + available_shares INTEGER NOT NULL DEFAULT 0, + error_message TEXT DEFAULT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE UNIQUE INDEX blinded_shares_index ON blinded_shares (credential_id, device_id); + + +CREATE TABLE ticketbook_deposit ( + deposit_id INTEGER PRIMARY KEY NOT NULL, + deposit_tx_hash TEXT NOT NULL, + requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + request_uuid TEXT UNIQUE NOT NULL, + deposit_amount TEXT NOT NULL, + client_pubkey BLOB NOT NULL, + ed25519_deposit_private_key BLOB NOT NULL +); + +CREATE TABLE partial_blinded_wallet ( + corresponding_deposit INTEGER NOT NULL REFERENCES ticketbook_deposit(deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + blinded_signature BLOB NOT NULL +); + +CREATE TABLE partial_blinded_wallet_failure ( + corresponding_deposit INTEGER NOT NULL REFERENCES ticketbook_deposit(deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + failure_message TEXT NOT NULL +); + +-- copied (+rev) from nym-api +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + serialization_revision INTEGER NOT NULL, + serialised_key BLOB NOT NULL +); + +CREATE TABLE global_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all indices + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE global_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + epoch_id INTEGER NOT NULL, + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); diff --git a/nym-vpn-api/vpn-api-server/src/cli.rs b/nym-vpn-api/vpn-api-server/src/cli.rs new file mode 100644 index 0000000000..480f84ec9c --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/cli.rs @@ -0,0 +1,88 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::default_database_filepath; +use crate::webhook::ZkNymWebHookConfig; +use clap::builder::ArgPredicate; +use clap::Parser; +use nym_bin_common::bin_info; +use std::fs::create_dir_all; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; +use std::sync::OnceLock; +use tracing::info; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +// if needed this could be split into subcommands +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, about, long_version = pretty_build_info_static())] +pub struct Cli { + #[clap(flatten)] + pub(crate) webhook: ZkNymWebHookConfig, + + /// Path pointing to an env file that configures the binary. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + /// Specifies the custom port value used for the api server. + /// default: `8080` + #[clap( + long, + env = "VPN_API_PORT", + default_value = "8080", + default_value_if("bind_address", ArgPredicate::IsPresent, None) + )] + pub port: Option, + + /// Specifies the custom bind address value used for the api server. + /// default: `0.0.0.0:8080` + #[clap(long, env = "VPN_API_BIND_ADDRESS")] + pub bind_address: Option, + + /// Specifies the mnemonic authorised for making deposits for "free pass" ticketbooks + #[clap(long, env = "VPN_API_MNEMONIC")] + pub mnemonic: bip39::Mnemonic, + + /// Bearer token for accessing the http endpoints. + #[clap(long, env = "VPN_API_AUTH_TOKEN", alias = "http-bearer-token")] + pub(crate) http_auth_token: String, + + #[clap(long, env = "VPN_API_PERSISTENT_STORAGE_STORAGE")] + pub(crate) persistent_storage_path: Option, +} + +impl Cli { + pub fn bind_address(&self) -> SocketAddr { + // SAFETY: + // if `bind_address` hasn't been specified, `port` will default to "8080", + // so some value will always be available to use + #[allow(clippy::unwrap_used)] + self.bind_address.unwrap_or_else(|| { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.port.unwrap()) + }) + } + + pub fn persistent_storage_path(&self) -> PathBuf { + self.persistent_storage_path.clone().unwrap_or_else(|| { + // if this blows up, then we shouldn't continue + #[allow(clippy::expect_used)] + let default_path = default_database_filepath(); + if let Some(parent) = default_path.parent() { + // make sure it exists + #[allow(clippy::unwrap_used)] + create_dir_all(parent).unwrap(); + } + + info!( + "setting the storage path path to {}", + default_path.display() + ); + + default_path + }) + } +} diff --git a/nym-vpn-api/vpn-api-server/src/config/mod.rs b/nym-vpn-api/vpn-api-server/src/config/mod.rs new file mode 100644 index 0000000000..128d6f53dc --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/config/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_config::{must_get_home, DEFAULT_DATA_DIR, NYM_DIR}; +use std::path::PathBuf; + +pub const DEFAULT_NYM_VPN_API_DIR: &str = "nym-vpn-api"; + +pub const DEFAULT_DB_FILENAME: &str = "nym-vpn-api.sqlite"; + +pub fn default_data_directory() -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_NYM_VPN_API_DIR) + .join(DEFAULT_DATA_DIR) +} + +pub fn default_database_filepath() -> PathBuf { + default_data_directory().join(DEFAULT_DB_FILENAME) +} diff --git a/nym-vpn-api/vpn-api-server/src/credentials/mod.rs b/nym-vpn-api/vpn-api-server/src/credentials/mod.rs new file mode 100644 index 0000000000..a6ee988de5 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/credentials/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod ticketbook; diff --git a/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs b/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs new file mode 100644 index 0000000000..14624dc7c1 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs @@ -0,0 +1,302 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::state::ApiState; +use futures::{stream, StreamExt}; +use nym_credentials::IssuanceTicketBook; +use nym_credentials_interface::Base58; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::ecash::BlindSignRequestBody; +use nym_validator_client::nyxd::contract_traits::EcashSigningClient; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_vpn_api_requests::api::v1::ticketbook::models::{ + TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, +}; +use rand::rngs::OsRng; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex; +use tokio::time::timeout; +use tracing::{debug, error, info, instrument}; +use uuid::Uuid; + +// use the same type alias as our contract without importing the whole thing just for this single line +pub type NodeId = u64; + +#[instrument( + skip(state, request_data, request, requested_on), + fields( + expiration_date = %request_data.expiration_date, + ticketbook_type = %request_data.ticketbook_type + ) +)] +pub(crate) async fn try_obtain_wallet_shares( + state: &ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, +) -> Result, VpnApiError> { + let mut rng = OsRng; + + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + + let epoch = state.current_epoch_id().await?; + let deposit_amount = state.deposit_amount().await?; + let threshold = state.ecash_threshold(epoch).await?; + let expiration_date = request_data.expiration_date; + + // before we commit to making the deposit, ensure we have required signatures cached and stored + let _ = state.master_verification_key(Some(epoch)).await?; + let _ = state.master_coin_index_signatures(Some(epoch)).await?; + let _ = state + .master_expiration_date_signatures(expiration_date) + .await?; + let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); + + let chain_write_permit = state.start_chain_tx().await; + + info!("starting the deposit!"); + // TODO: batch those up + // TODO: batch those up + let deposit_res = chain_write_permit + .make_ticketbook_deposit( + ed25519_keypair.public_key().to_base58_string(), + deposit_amount.clone(), + None, + ) + .await?; + + // explicitly drop it here so other tasks could start using it + drop(chain_write_permit); + + let deposit_id = deposit_res.parse_singleton_u32_contract_data()?; + let tx_hash = deposit_res.transaction_hash; + info!(deposit_id = %deposit_id, tx_hash = %tx_hash, "deposit finished"); + + // store the deposit information so if we fail, we could perhaps still reuse it for another issuance + state + .storage() + .insert_deposit_data( + deposit_id, + tx_hash, + requested_on, + request, + deposit_amount, + &request_data.ecash_pubkey, + &ed25519_keypair, + ) + .await?; + + let plaintext = + IssuanceTicketBook::request_plaintext(&request_data.withdrawal_request, deposit_id); + let signature = ed25519_keypair.private_key().sign(plaintext); + + let credential_request = BlindSignRequestBody::new( + request_data.withdrawal_request.into(), + deposit_id, + signature, + request_data.ecash_pubkey, + request_data.expiration_date, + request_data.ticketbook_type, + ); + + let wallet_shares = Arc::new(Mutex::new(HashMap::new())); + + info!("attempting to contract all nym-apis for the partial wallets..."); + stream::iter(ecash_api_clients) + .for_each_concurrent(None, |client| async { + // move the client into the block + let client = client; + + debug!("contacting {client} for blinded partial wallet"); + let res = timeout( + Duration::from_secs(5), + client.api_client.blind_sign(&credential_request), + ) + .await + .map_err(|_| VpnApiError::EcashApiRequestTimeout { + client_repr: client.to_string(), + }) + .and_then(|res| res.map_err(Into::into)); + + // 1. try to store it + if let Err(err) = state + .storage() + .insert_partial_wallet_share( + deposit_id, + epoch, + expiration_date, + client.node_id, + &res, + ) + .await + { + error!("failed to persist issued partial share: {err}") + } + + // 2. add it to the map + match res { + Ok(share) => { + wallet_shares + .lock() + .await + .insert(client.node_id, share.blinded_signature); + } + Err(err) => { + error!("failed to obtain partial blinded wallet share from {client}: {err}") + } + } + }) + .await; + + // SAFETY: the futures have completed, so we MUST have the only arc reference + #[allow(clippy::unwrap_used)] + let wallet_shares = Arc::into_inner(wallet_shares).unwrap().into_inner(); + let shares = wallet_shares.len(); + + if shares < threshold as usize { + return Err(VpnApiError::InsufficientNumberOfCredentials { + available: shares, + threshold, + }); + } + + Ok(wallet_shares + .into_iter() + .map(|(node_index, share)| WalletShare { + node_index, + bs58_encoded_share: share.to_bs58(), + }) + .collect()) +} + +// same as try_obtain_wallet_shares, but writes failures into the db +async fn try_obtain_wallet_shares_async( + state: &ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, + device_id: &str, + credential_id: &str, +) -> Result, VpnApiError> { + let shares = match try_obtain_wallet_shares(state, request, requested_on, request_data).await { + Ok(shares) => shares, + Err(err) => { + let obtained = match err { + VpnApiError::InsufficientNumberOfCredentials { available, .. } => available, + _ => 0, + }; + + // currently there's no retry mechanisms, but, who knows, that might change + if let Err(err) = state + .storage() + .update_pending_async_blinded_shares_error( + obtained, + device_id, + credential_id, + &err.to_string(), + ) + .await + { + error!("failed to update database with the error information: {err}") + } + return Err(err); + } + }; + + Ok(shares) +} + +async fn try_obtain_blinded_ticketbook_async_inner( + state: &ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainQueryParams, +) -> Result<(), VpnApiError> { + let epoch_id = state.current_epoch_id().await?; + + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + + // 1. try to obtain global data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .global_data( + params.include_master_verification_key, + params.include_coin_index_signatures, + params.include_expiration_date_signatures, + epoch_id, + request_data.inner.expiration_date, + ) + .await?; + + // 2. try to obtain shares (failures are written to the DB) + let shares = try_obtain_wallet_shares_async( + state, + request, + requested_on, + request_data.inner, + device_id, + credential_id, + ) + .await?; + + // 3. update the storage, if possible + // (as long as we can trigger webhook, we should still be good) + if let Err(err) = state + .storage() + .update_pending_async_blinded_shares_issued(shares.len(), device_id, credential_id) + .await + { + error!(uuid = %request, "failed to update db with issued information: {err}") + } + + // 4. build the response + let response = TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }; + + // 5. call the webhook + state + .zk_nym_web_hook() + .try_trigger(request, &response) + .await; + + Ok(()) +} + +#[instrument(skip_all, fields(credential_id = %request_data.credential_id, device_id = %request_data.device_id))] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn try_obtain_blinded_ticketbook_async( + state: ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainQueryParams, +) { + if let Err(err) = try_obtain_blinded_ticketbook_async_inner( + &state, + request, + requested_on, + request_data, + params, + ) + .await + { + error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}") + } else { + info!(uuid = %request, "managed to resolve the blinded ticketbook issuance") + } +} diff --git a/nym-vpn-api/vpn-api-server/src/error.rs b/nym-vpn-api/vpn-api-server/src/error.rs new file mode 100644 index 0000000000..4ffba86be7 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/error.rs @@ -0,0 +1,126 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::error::NyxdError; +use std::io; +use std::net::SocketAddr; +use thiserror::Error; +use time::OffsetDateTime; + +#[derive(Debug, Error)] +pub enum VpnApiError { + #[error("encountered an internal io error: {source}")] + IoError { + #[from] + source: io::Error, + }, + + #[error("could not derive valid client url with the provided webhook parameters")] + InvalidWebhookUrl, + + #[error("failed to serialise recovery data: {source}")] + SerdeJsonFailure { + #[from] + source: serde_json::Error, + }, + + #[error("the provided expiration date is too late")] + ExpirationDateTooLate, + + #[error("the provided expiration date is too early")] + ExpirationDateTooEarly, + + #[error("failed to bind to {address}: {source}. Are you sure nothing else is running on the specified port and your user has sufficient permission to bind to the requested address?")] + SocketBindFailure { + address: SocketAddr, + source: io::Error, + }, + + #[error("the api server failed with the following message: {source}")] + HttpServerFailure { source: io::Error }, + + #[error("the ecash contract address is not set")] + UnavailableEcashContract, + + #[error("the DKG contract address is not set")] + UnavailableDKGContract, + + #[error("the bandwidth contract doesn't have any admin set")] + MissingBandwidthContractAdmin, + + #[error( + "the provided mnemonic does not correspond to the current admin of the bandwidth contract" + )] + MismatchedMnemonic, + + #[error("failed to interact with the nyx chain: {source}")] + NyxdFailure { + #[from] + source: NyxdError, + }, + + #[error("validator client error: {0}")] + ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), + + #[error("failed to perform ecash operation: {source}")] + EcashApiFailure { + #[from] + source: EcashApiError, + }, + + #[error("Compact ecash internal error: {0}")] + CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError), + + #[error("there are no rpc endpoints provided in the environment")] + NoNyxEndpointsAvailable, + + #[error("the threshold value for epoch {epoch_id} is not available")] + UnavailableThreshold { epoch_id: EpochId }, + + #[error( + "we have only {available} api clients available while the minimum threshold is {threshold}" + )] + InsufficientNumberOfSigners { available: usize, threshold: u64 }, + + #[error( + "we have only managed to obtain {available} partial credentials while the minimum threshold is {threshold}" + )] + InsufficientNumberOfCredentials { available: usize, threshold: u64 }, + + #[error("failed to interact with the credentials: {source}")] + CredentialsFailure { + #[from] + source: nym_credentials::Error, + }, + + #[error("the DKG has not yet been initialised in the system")] + UninitialisedDkg, + + #[error("credentials can't yet be issued in the system. approximate expected availability: {availability}")] + CredentialsNotYetIssuable { availability: OffsetDateTime }, + + #[error("reached seemingly impossible ecash failure")] + UnknownEcashFailure, + + #[error("experienced internal database error: {0}")] + InternalDatabaseError(#[from] sqlx::Error), + + #[error("experienced internal storage error: {reason}")] + DatabaseInconsistency { reason: String }, + + #[error("failed to perform startup SQL migration: {0}")] + StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), + + #[error("timed out while attempting to obtain partial wallet from {client_repr}")] + EcashApiRequestTimeout { client_repr: String }, +} + +impl VpnApiError { + pub fn database_inconsistency>(reason: S) -> VpnApiError { + VpnApiError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/nym-vpn-api/vpn-api-server/src/helpers.rs b/nym-vpn-api/vpn-api-server/src/helpers.rs new file mode 100644 index 0000000000..12ba427a70 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/helpers.rs @@ -0,0 +1,42 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use time::OffsetDateTime; +use tracing::{debug, info, warn}; + +pub struct LockTimer { + created: OffsetDateTime, + message: String, +} + +impl LockTimer { + pub fn new>(message: S) -> Self { + LockTimer { + message: message.into(), + ..Default::default() + } + } +} + +impl Drop for LockTimer { + fn drop(&mut self) { + let time_taken = OffsetDateTime::now_utc() - self.created; + let time_taken_formatted = humantime::format_duration(time_taken.unsigned_abs()); + if time_taken > time::Duration::SECOND * 10 { + warn!(time_taken = %time_taken_formatted, "{}", self.message) + } else if time_taken > time::Duration::SECOND * 5 { + info!(time_taken = %time_taken_formatted, "{}", self.message) + } else { + debug!(time_taken = %time_taken_formatted, "{}", self.message) + }; + } +} + +impl Default for LockTimer { + fn default() -> Self { + LockTimer { + created: OffsetDateTime::now_utc(), + message: "released the lock".to_string(), + } + } +} diff --git a/nym-vpn-api/vpn-api-server/src/http/helpers.rs b/nym-vpn-api/vpn-api-server/src/http/helpers.rs new file mode 100644 index 0000000000..df35c59221 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/helpers.rs @@ -0,0 +1,13 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use rand::rngs::OsRng; +use rand::RngCore; +use uuid::Uuid; + +pub fn random_uuid() -> Uuid { + let mut bytes = [0u8; 16]; + let mut rng = OsRng; + rng.fill_bytes(&mut bytes); + Uuid::from_bytes(bytes) +} diff --git a/nym-vpn-api/vpn-api-server/src/http/middleware/auth.rs b/nym-vpn-api/vpn-api-server/src/http/middleware/auth.rs new file mode 100644 index 0000000000..ce9c4e0912 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/middleware/auth.rs @@ -0,0 +1,115 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::IntoResponse; +use axum::{extract::Request, response::Response}; +use futures::future::BoxFuture; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tower::{Layer, Service}; +use tracing::{debug, instrument, trace}; +use zeroize::Zeroizing; + +#[derive(Debug, Clone)] +pub struct AuthLayer { + bearer_token: Arc>, +} + +impl AuthLayer { + pub fn new(bearer_token: Arc>) -> Self { + AuthLayer { bearer_token } + } +} + +impl Layer for AuthLayer { + type Service = RequireAuth; + + fn layer(&self, inner: S) -> Self::Service { + RequireAuth::new(inner, self.bearer_token.clone()) + } +} + +#[derive(Debug, Clone)] +pub struct RequireAuth { + inner: S, + bearer_token: Arc>, +} + +impl RequireAuth { + pub fn new(inner: S, bearer_token: Arc>) -> Self { + RequireAuth { + inner, + bearer_token, + } + } + + fn check_auth_header(&self, header: Option<&HeaderValue>) -> Result<(), &'static str> { + let Some(token) = header else { + trace!("missing header"); + return Err("`Authorization` header is missing"); + }; + + let Ok(authorization) = token.to_str() else { + trace!("invalid header"); + return Err("`Authorization` header contains invalid characters"); + }; + + debug!("header value: '{authorization}'"); + + let split = authorization.split_once(' '); + let bearer_token = match split { + // Found proper bearer + Some(("Bearer", contents)) => contents, + // Found empty bearer; + _ if authorization == "Bearer" => "", + // Found nothing + _ => return Err("`Authorization` header must be a bearer token"), + }; + + debug!("parsed token: '{bearer_token}'"); + + if self.bearer_token.is_empty() && bearer_token.is_empty() { + return Ok(()); + } + if bearer_token.is_empty() { + return Err("`Authorization` header must contain non-empty `Bearer` token"); + } + + if self.bearer_token.as_str() != bearer_token { + return Err("`Authorization` header does not contain the correct `Bearer` token"); + } + + Ok(()) + } +} + +impl Service for RequireAuth +where + S: Service + Send + 'static, + S: Send + Sync + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + #[instrument(skip_all, fields(uri = %req.uri()))] + fn call(&mut self, req: Request) -> Self::Future { + debug!("checking the auth"); + + let auth_header = req.headers().get(header::AUTHORIZATION); + + match self.check_auth_header(auth_header) { + Ok(_authorised) => Box::pin(self.inner.call(req)), + Err(err) => { + Box::pin(async move { Ok((StatusCode::UNAUTHORIZED, err).into_response()) }) + } + } + } +} diff --git a/nym-vpn-api/vpn-api-server/src/http/middleware/logging.rs b/nym-vpn-api/vpn-api-server/src/http/middleware/logging.rs new file mode 100644 index 0000000000..825104ec5b --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/middleware/logging.rs @@ -0,0 +1,64 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use axum::{ + extract::{ConnectInfo, Request}, + http::{ + header::{HOST, USER_AGENT}, + HeaderValue, + }, + middleware::Next, + response::IntoResponse, +}; +use colored::*; +use std::net::SocketAddr; +use tokio::time::Instant; +use tracing::info; + +/// Simple logger for requests +pub async fn logger( + ConnectInfo(addr): ConnectInfo, + req: Request, + next: Next, +) -> impl IntoResponse { + let method = req.method().to_string().green(); + let uri = req.uri().to_string().blue(); + let agent = header_map( + req.headers().get(USER_AGENT), + "Unknown User Agent".to_string(), + ); + + let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); + + let start = Instant::now(); + let res = next.run(req).await; + let time_taken = start.elapsed(); + let status = res.status(); + let print_status = if status.is_client_error() || status.is_server_error() { + status.to_string().red() + } else if status.is_success() { + status.to_string().green() + } else { + status.to_string().yellow() + }; + + let taken = "time taken".bold(); + + let time_taken = match time_taken.as_millis() { + ms if ms > 500 => format!("{taken}: {}", format!("{ms}ms").red()), + ms if ms > 200 => format!("{taken}: {}", format!("{ms}ms").yellow()), + ms if ms > 50 => format!("{taken}: {}", format!("{ms}ms").bright_yellow()), + ms => format!("{taken}: {ms}ms"), + }; + + let agent_str = "agent".bold(); + info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"); + + res +} + +fn header_map(header: Option<&HeaderValue>, msg: String) -> String { + header + .map(|x| x.to_str().unwrap_or(&msg).to_string()) + .unwrap_or(msg) +} diff --git a/nym-vpn-api/vpn-api-server/src/http/middleware/mod.rs b/nym-vpn-api/vpn-api-server/src/http/middleware/mod.rs new file mode 100644 index 0000000000..b51d9a59b5 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/middleware/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod auth; +pub mod logging; diff --git a/nym-vpn-api/vpn-api-server/src/http/mod.rs b/nym-vpn-api/vpn-api-server/src/http/mod.rs new file mode 100644 index 0000000000..1267b3abfb --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/mod.rs @@ -0,0 +1,52 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::router::build_router; +use crate::http::state::ApiState; +use axum::Router; +use std::net::SocketAddr; +use tokio_util::sync::CancellationToken; +use tracing::info; + +pub mod helpers; +pub mod middleware; +pub mod router; +pub mod state; +pub mod types; + +pub struct HttpServer { + bind_address: SocketAddr, + cancellation: CancellationToken, + router: Router, +} + +impl HttpServer { + pub fn new(bind_address: SocketAddr, state: ApiState, auth_token: String) -> Self { + HttpServer { + bind_address, + cancellation: state.cancellation_token(), + router: build_router(state, auth_token), + } + } + + pub async fn run_forever(self) -> Result<(), VpnApiError> { + let address = self.bind_address; + info!("starting the http server on http://{address}"); + + let listener = tokio::net::TcpListener::bind(address) + .await + .map_err(|source| VpnApiError::SocketBindFailure { address, source })?; + + let cancellation = self.cancellation; + + axum::serve( + listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(async move { cancellation.cancelled().await }) + .await + .map_err(|source| VpnApiError::HttpServerFailure { source }) + } +} diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs new file mode 100644 index 0000000000..59b9f9bcea --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::state::ApiState; +use axum::Router; +use nym_vpn_api_requests::routes; + +use crate::http::middleware::auth::AuthLayer; +pub(crate) use nym_http_api_common::{Output, OutputParams}; + +pub mod v1; + +pub(super) fn routes(auth_layer: AuthLayer) -> Router { + Router::new().nest(routes::api::V1, v1::routes(auth_layer)) +} diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs new file mode 100644 index 0000000000..a6929d3591 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::middleware::auth::AuthLayer; +use crate::http::state::ApiState; +use axum::Router; +use nym_vpn_api_requests::routes::api::v1; + +// pub mod bandwidth_voucher; +// pub mod freepass; +pub mod openapi; +pub mod ticketbook; + +pub(super) fn routes(auth_layer: AuthLayer) -> Router { + // from docs: + // ``` + // Note that the middleware is only applied to existing routes. + // So you have to first add your routes (and / or fallback) and then call layer afterwards. + // Additional routes added after layer is called will not have the middleware added. + // ``` + // thus we first add relevant API routes, then the auth layer and finally the swagger routes + Router::new() + .nest(v1::TICKETBOOK, ticketbook::routes().route_layer(auth_layer)) + .merge(openapi::route()) +} diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs new file mode 100644 index 0000000000..ed215b6c3b --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs @@ -0,0 +1,121 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::router::api; +use crate::http::types::RequestError; +use axum::Router; +use nym_vpn_api_requests::api as api_requests; +use nym_vpn_api_requests::routes::api::{v1, v1_absolute}; +use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme}; +use utoipa::{Modify, OpenApi}; +use utoipa_swagger_ui::SwaggerUi; + +/* +#[derive(OpenApi)] +#[openapi( + info(title = "Nym VPN Api"), + paths( + api::v1::freepass::generate_freepass, + api::v1::bandwidth_voucher::obtain_bandwidth_voucher_shares, + api::v1::bandwidth_voucher::obtain_async_bandwidth_voucher_shares, + api::v1::bandwidth_voucher::current_deposit, + api::v1::bandwidth_voucher::prehashed_public_attributes, + api::v1::bandwidth_voucher::partial_verification_keys, + api::v1::bandwidth_voucher::master_verification_key, + api::v1::bandwidth_voucher::current_epoch, + api::v1::bandwidth_voucher::shares::query_for_shares_by_id, + ), + components( + schemas( + api::Output, + api::OutputParams, + api_requests::v1::ErrorResponse, + api_requests::v1::freepass::models::FreepassCredentialResponse, + api_requests::v1::freepass::models::FreepassQueryParams, + api_requests::v1::bandwidth_voucher::models::DepositResponse, + api_requests::v1::bandwidth_voucher::models::AttributesResponse, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherResponse, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherAsyncResponse, + api_requests::v1::bandwidth_voucher::models::PartialVerificationKeysResponse, + api_requests::v1::bandwidth_voucher::models::CurrentEpochResponse, + api_requests::v1::bandwidth_voucher::models::CredentialShare, + api_requests::v1::bandwidth_voucher::models::PartialVerificationKey, + api_requests::v1::bandwidth_voucher::models::MasterVerificationKeyResponse, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherAsyncRequest, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherRequest, + api_requests::v1::bandwidth_voucher::models::BlindSignRequestJsonSchemaWrapper + ), + responses(RequestError), + ), + modifiers(&SecurityAddon), +)] +pub(crate) struct ApiDoc; + */ + +#[derive(OpenApi)] +#[openapi( + info(title = "Nym VPN Api"), + paths( + api::v1::ticketbook::obtain_ticketbook_shares, + api::v1::ticketbook::obtain_ticketbook_shares_async, + api::v1::ticketbook::current_deposit, + api::v1::ticketbook::partial_verification_keys, + api::v1::ticketbook::master_verification_key, + api::v1::ticketbook::current_epoch, + api::v1::ticketbook::shares::query_for_shares_by_id, + api::v1::ticketbook::shares::query_for_shares_by_device_id_and_credential_id, + ), + components( + schemas( + api::Output, + api::OutputParams, + api_requests::v1::ErrorResponse, + api_requests::v1::ticketbook::models::DepositResponse, + api_requests::v1::ticketbook::models::PartialVerificationKeysResponse, + api_requests::v1::ticketbook::models::CurrentEpochResponse, + api_requests::v1::ticketbook::models::PartialVerificationKey, + api_requests::v1::ticketbook::models::MasterVerificationKeyResponse, + api_requests::v1::ticketbook::models::TicketbookRequest, + api_requests::v1::ticketbook::models::TicketbookAsyncRequest, + api_requests::v1::ticketbook::models::WithdrawalRequestBs58Wrapper, + api_requests::v1::ticketbook::models::PartialVerificationKey, + api_requests::v1::ticketbook::models::AggregatedExpirationDateSignaturesResponse, + api_requests::v1::ticketbook::models::AggregatedCoinIndicesSignaturesResponse, + api_requests::v1::ticketbook::models::WalletShare, + api_requests::v1::ticketbook::models::TicketbookWalletSharesResponse, + api_requests::v1::ticketbook::models::TicketbookWalletSharesAsyncResponse, + api_requests::v1::ticketbook::models::BlindedWalletSharesResponse, + api_requests::v1::ticketbook::models::WebhookBlindedSharesResponse, + api_requests::v1::ticketbook::models::TicketbookObtainQueryParams, + api_requests::v1::ticketbook::models::SharesQueryParams, + api_requests::v1::ticketbook::models::PlaceholderJsonSchemaImpl, + ), + responses(RequestError), + ), + modifiers(&SecurityAddon), +)] +pub(crate) struct ApiDoc; + +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_scheme( + "auth_token", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ) + } + } +} + +// if reverse proxy doesn't work, we might have to look into: https://github.com/juhaku/utoipa/issues/842 +pub(crate) fn route() -> Router { + // provide absolute path to the openapi.json + let config = + utoipa_swagger_ui::Config::from(format!("{}/api-docs/openapi.json", v1_absolute())); + SwaggerUi::new(v1::SWAGGER) + .url("/api-docs/openapi.json", ApiDoc::openapi()) + .config(config) + .into() +} diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs new file mode 100644 index 0000000000..9c6a1932cf --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs @@ -0,0 +1,400 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::credentials::ticketbook::{ + try_obtain_blinded_ticketbook_async, try_obtain_wallet_shares, +}; +use crate::http::helpers::random_uuid; +use crate::http::state::ApiState; +use crate::http::types::RequestError; +use crate::nym_api_helpers::ensure_sane_expiration_date; +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use nym_compact_ecash::Base58; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_vpn_api_requests::api::v1::ticketbook::models::{ + CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse, PartialVerificationKey, + PartialVerificationKeysResponse, TicketbookAsyncRequest, TicketbookObtainQueryParams, + TicketbookRequest, TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse, +}; +use nym_vpn_api_requests::routes::api::v1::ticketbook; +use time::OffsetDateTime; +use tracing::{error, info, span, warn, Level}; + +pub(crate) mod shares; + +pub type FormattedDepositResponse = FormattedResponse; +pub type FormattedCurrentEpochResponse = FormattedResponse; +pub type FormattedMasterVerificationKeyResponse = FormattedResponse; +pub type FormattedPartialVerificationKeysResponse = + FormattedResponse; +pub type FormattedTicketbookWalletSharesResponse = + FormattedResponse; +pub type FormattedTicketbookWalletSharesAsyncResponse = + FormattedResponse; + +/// Attempt to obtain blinded shares of an ecash ticketbook wallet +#[utoipa::path( + post, + path = "/obtain", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + request_body( + content = TicketbookRequest, + description = "cryptographic material required for obtaining ticketbook wallet shares", + content_type = "application/json" + ), + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesResponse), + ("application/yaml" = TicketbookWalletSharesResponse), + )), + (status = 400, description = "the provided request hasn't been created against correct attributes"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 422, description = "provided request was malformed"), + (status = 500, body = ErrorResponse, description = "failed to obtain a ticketbook"), + (status = 503, body = ErrorResponse, description = "ticketbooks can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(TicketbookObtainQueryParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn obtain_ticketbook_shares( + State(state): State, + Query(params): Query, + Json(payload): Json, +) -> Result { + let uuid = random_uuid(); + let requested_on = OffsetDateTime::now_utc(); + + let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); + let _entered = span.enter(); + info!(""); + + let output = params.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(Some(uuid)).await?; + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; + + if let Err(err) = ensure_sane_expiration_date(payload.expiration_date) { + warn!("failure due to invalid expiration date"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::BAD_REQUEST, + )); + } + + // if additional data was requested, grab them first in case there are any cache/network issues + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .response_global_data( + params.include_master_verification_key, + params.include_expiration_date_signatures, + params.include_coin_index_signatures, + epoch_id, + payload.expiration_date, + uuid, + ) + .await?; + + let shares = try_obtain_wallet_shares(&state, uuid, requested_on, payload) + .await + .inspect_err(|err| warn!("request failure: {err}")) + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + info!("request was successful!"); + Ok(output.to_response(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + })) +} + +/// Attempt to obtain blinded shares of an ecash ticketbook wallet asynchronously +#[utoipa::path( + post, + path = "/obtain-async", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + request_body( + content = TicketbookAsyncRequest, + description = "cryptographic material required for obtaining ticketbook wallet shares", + content_type = "application/json" + ), + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesAsyncResponse), + ("application/yaml" = TicketbookWalletSharesAsyncResponse), + )), + (status = 400, description = "the provided request hasn't been created against correct attributes"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 409, description = "shares were already requested"), + (status = 422, description = "provided request was malformed"), + (status = 500, body = ErrorResponse, description = "failed to obtain a ticketbook"), + (status = 503, body = ErrorResponse, description = "ticketbooks can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(TicketbookObtainQueryParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn obtain_ticketbook_shares_async( + State(state): State, + Query(params): Query, + Json(payload): Json, +) -> Result { + let uuid = random_uuid(); + let requested_on = OffsetDateTime::now_utc(); + + let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); + let _entered = span.enter(); + info!(""); + + let output = params.output.unwrap_or_default(); + + // 1. perform basic validation + state.ensure_not_in_epoch_transition(Some(uuid)).await?; + + if let Err(err) = ensure_sane_expiration_date(payload.inner.expiration_date) { + warn!("failure due to invalid expiration date"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::BAD_REQUEST, + )); + } + + // 2. store the request to retrieve the id + let pending = match state + .storage() + .insert_new_pending_async_shares_request(uuid, &payload.device_id, &payload.credential_id) + .await + { + Err(err) => { + error!("failed to insert new pending async shares: {err}"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::CONFLICT, + )); + } + Ok(pending) => pending, + }; + + // 3. try to spawn a new task attempting to resolve the request + if state + .try_spawn(try_obtain_blinded_ticketbook_async( + state.clone(), + uuid, + requested_on, + payload, + params, + )) + .is_none() + { + // we're going through the shutdown + return Err(RequestError::new_with_uuid( + "server shutdown in progress", + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + + // 4. in the meantime, return the id to the user + Ok(output.to_response(TicketbookWalletSharesAsyncResponse { + id: pending.id, + uuid, + })) +} + +/// Obtain the current value of the bandwidth voucher deposit +#[utoipa::path( + get, + path = "/deposit-amount", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = DepositResponse), + ("application/yaml" = DepositResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current deposit information"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn current_deposit( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + let current_deposit = state + .deposit_amount() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(DepositResponse { + current_deposit_amount: current_deposit.amount, + current_deposit_denom: current_deposit.denom, + })) +} + +/// Obtain partial verification keys of all signers for the current epoch. +#[utoipa::path( + get, + path = "/partial-verification-keys", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = PartialVerificationKeysResponse), + ("application/yaml" = PartialVerificationKeysResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current epoch information"), + (status = 503, body = ErrorResponse, description = "credentials can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn partial_verification_keys( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(None).await?; + + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + let signers = state + .ecash_clients(epoch_id) + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(PartialVerificationKeysResponse { + epoch_id, + keys: signers + .iter() + .map(|signer| PartialVerificationKey { + node_index: signer.node_id, + bs58_encoded_key: signer.verification_key.to_bs58(), + }) + .collect(), + })) +} + +/// Obtain the master verification key for the current epoch. +#[utoipa::path( + get, + path = "/master-verification-key", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = MasterVerificationKeyResponse), + ("application/yaml" = MasterVerificationKeyResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current epoch information"), + (status = 503, body = ErrorResponse, description = "credentials can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn master_verification_key( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(None).await?; + + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + let key = state + .master_verification_key(Some(epoch_id)) + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + })) +} + +/// Obtain the id of the current epoch. +/// This is exposed to allow clients to cache verification keys. +#[utoipa::path( + get, + path = "/current-epoch", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = CurrentEpochResponse), + ("application/yaml" = CurrentEpochResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current epoch information"), + (status = 503, body = ErrorResponse, description = "credentials can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn current_epoch( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(None).await?; + + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(CurrentEpochResponse { epoch_id })) +} + +pub(crate) fn routes() -> Router { + Router::new() + .route(ticketbook::DEPOSIT_AMOUNT, get(current_deposit)) + .route(ticketbook::MASTER_KEY, get(master_verification_key)) + .route(ticketbook::PARTIAL_KEYS, get(partial_verification_keys)) + .route(ticketbook::CURRENT_EPOCH, get(current_epoch)) + .route(ticketbook::OBTAIN, post(obtain_ticketbook_shares)) + .route( + ticketbook::OBTAIN_ASYNC, + post(obtain_ticketbook_shares_async), + ) + .nest(ticketbook::SHARES, shares::routes()) +} diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs new file mode 100644 index 0000000000..2f23dbc3aa --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs @@ -0,0 +1,206 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::helpers::random_uuid; +use crate::http::router::api::v1::ticketbook::FormattedTicketbookWalletSharesResponse; +use crate::http::state::ApiState; +use crate::http::types::RequestError; +use crate::storage::models::MinimalWalletShare; +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::routing::get; +use axum::Router; +use nym_http_api_common::OutputParams; +use nym_validator_client::nym_api::EpochId; +use nym_vpn_api_requests::api::v1::ticketbook::models::{ + SharesQueryParams, TicketbookWalletSharesResponse, +}; +use nym_vpn_api_requests::routes::api::v1::ticketbook::shares; +use tracing::{debug, span, warn, Level}; +use uuid::Uuid; + +async fn shares_to_response( + state: ApiState, + uuid: Uuid, + shares: Vec, + params: SharesQueryParams, +) -> Result { + // in all calls we ensured the shares are non-empty + #[allow(clippy::unwrap_used)] + let first = shares.first().unwrap(); + let expiration_date = first.expiration_date; + let epoch_id = first.epoch_id as EpochId; + + let threshold = state.response_ecash_threshold(uuid, epoch_id).await?; + if shares.len() < threshold as usize { + return Err(RequestError::new_server_error( + VpnApiError::InsufficientNumberOfCredentials { + available: shares.len(), + threshold, + }, + uuid, + )); + } + + // grab any requested additional data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .response_global_data( + params.include_master_verification_key, + params.include_expiration_date_signatures, + params.include_coin_index_signatures, + epoch_id, + expiration_date, + uuid, + ) + .await?; + + // finally produce a response + Ok(params + .output + .unwrap_or_default() + .to_response(TicketbookWalletSharesResponse { + epoch_id, + shares: shares.into_iter().map(Into::into).collect(), + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + })) +} + +/// Query by id for blinded shares of a bandwidth voucher +#[utoipa::path( + get, + path = "/{share_id}", + context_path = "/api/v1/ticketbook/shares", + tag = "Ticketbook Wallet Shares", + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesResponse), + ("application/yaml" = TicketbookWalletSharesResponse), + )), + (status = 404, description = "share_id not found"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to query for bandwidth blinded shares"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn query_for_shares_by_id( + State(state): State, + Query(params): Query, + Path(share_id): Path, +) -> Result { + let uuid = random_uuid(); + + let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); + let _entered = span.enter(); + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_shares_id(share_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("not found"); + return Err(RequestError::new_with_uuid( + format!("not found - share_id = {share_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares + } + Err(err) => { + warn!("db failure: {err}"); + return Err(RequestError::new_with_uuid( + format!("oh no, something went wrong {err}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + }; + + shares_to_response(state, uuid, shares, params).await +} + +/// Query by id for blinded wallet shares of a ticketbook +#[utoipa::path( + get, + path = "/device/{device_id}/credential/{credential_id}", + context_path = "/api/v1/ticketbook/shares", + tag = "Ticketbook Wallet Shares", + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesResponse), + ("application/yaml" = TicketbookWalletSharesResponse), + )), + (status = 404, description = "share_id not found"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to query for bandwidth blinded shares"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn query_for_shares_by_device_id_and_credential_id( + State(state): State, + Query(params): Query, + Path((device_id, credential_id)): Path<(String, String)>, +) -> Result { + let uuid = random_uuid(); + + let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); + let _entered = span.enter(); + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("not found"); + return Err(RequestError::new_with_uuid( + format!("not found - device_id = {device_id}, credential_id = {credential_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares + } + Err(err) => { + warn!("db failure: {err}"); + return Err(RequestError::new_with_uuid( + format!("oh no, something went wrong {err}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + }; + + shares_to_response(state, uuid, shares, params).await +} + +pub(crate) fn routes() -> Router { + Router::new() + .route(shares::SHARE_BY_ID, get(query_for_shares_by_id)) + .route( + shares::SHARE_BY_DEVICE_AND_CREDENTIAL_ID, + get(query_for_shares_by_device_id_and_credential_id), + ) +} diff --git a/nym-vpn-api/vpn-api-server/src/http/router/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/mod.rs new file mode 100644 index 0000000000..6e2ac305c6 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/router/mod.rs @@ -0,0 +1,44 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::middleware::auth::AuthLayer; +use crate::http::middleware::logging; +use crate::http::state::ApiState; +use axum::response::Redirect; +use axum::routing::{get, MethodRouter}; +use axum::Router; +use nym_vpn_api_requests::routes; +use std::sync::Arc; +use zeroize::Zeroizing; + +pub mod api; + +fn swagger_redirect() -> MethodRouter { + // redirects with 303 status code + get(|| async { Redirect::to("/api/v1/swagger/") }) +} + +pub fn build_router(state: ApiState, auth_token: String) -> Router { + // let auth_layer = from_extractor::(); + let auth_middleware = AuthLayer::new(Arc::new(Zeroizing::new(auth_token))); + + let router = Router::new() + // just redirect root and common typos for swagger for the current api version page (v1) + .route("/", swagger_redirect()) + .route("/swagger", swagger_redirect()) + .route("/swagger/", swagger_redirect()) + .route("/swagger/index.html", swagger_redirect()) + .nest(routes::API, api::routes(auth_middleware)) + // we don't have to be using middleware, but we already had that code + // we might want something like: https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs#L44 instead + .layer(axum::middleware::from_fn(logging::logger)) + .with_state(state); + + cfg_if::cfg_if! { + if #[cfg(feature = "cors")] { + router.layer(tower_http::cors::CorsLayer::very_permissive()) + } else { + router + } + } +} diff --git a/nym-vpn-api/vpn-api-server/src/http/state/mod.rs b/nym-vpn-api/vpn-api-server/src/http/state/mod.rs new file mode 100644 index 0000000000..8ffd1bcc94 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/state/mod.rs @@ -0,0 +1,683 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::helpers::LockTimer; +use crate::http::types::RequestError; +use crate::nym_api_helpers::{ + ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, + CachedImmutableItems, +}; +use crate::storage::VpnApiStorage; +use crate::webhook::ZkNymWebHookConfig; +use axum::http::StatusCode; +use bip39::Mnemonic; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_annotated_indices_signatures, CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, +}; +use nym_compact_ecash::Base58; +use nym_credentials::ecash::utils::{ecash_today, EcashTime}; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, +}; +use nym_credentials_interface::VerificationKeyAuth; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, EcashQueryClient, NymContractsProvider, PagedDkgQueryClient, +}; +use nym_validator_client::nyxd::{Coin, NyxdClient}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient}; +use nym_vpn_api_requests::api::v1::ticketbook::models::{ + AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, + MasterVerificationKeyResponse, +}; +use std::future::Future; +use std::ops::Deref; +use std::sync::Arc; +use time::{Date, OffsetDateTime}; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tokio_util::task::TaskTracker; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +// currently we need to hold our keypair so that we could request a freepass credential +#[derive(Clone)] +pub struct ApiState { + inner: Arc, +} + +// a lot of functionalities, mostly to do with caching and storage is just copy-pasted from nym-api, +// since we have to do more or less the same work +impl ApiState { + pub async fn new( + storage: VpnApiStorage, + zk_nym_web_hook_config: ZkNymWebHookConfig, + mnemonic: Mnemonic, + ) -> Result { + let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + + let nyxd_url = network_details + .endpoints + .first() + .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? + .nyxd_url + .as_str(); + + let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; + + if client.ecash_contract_address().is_none() { + return Err(VpnApiError::UnavailableEcashContract); + } + + if client.dkg_contract_address().is_none() { + return Err(VpnApiError::UnavailableDKGContract); + } + + let state = ApiState { + inner: Arc::new(ApiStateInner { + storage, + client: RwLock::new(client), + ecash_state: EcashState::default(), + zk_nym_web_hook_config, + task_tracker: TaskTracker::new(), + cancellation_token: CancellationToken::new(), + }), + }; + + // since this is startup, + // might as well do all the needed network queries to establish needed global signatures + // if we don't already have them + state.build_initial_cache().await?; + + Ok(state) + } + + async fn build_initial_cache(&self) -> Result<(), VpnApiError> { + let today = ecash_today().date(); + + let epoch_id = self.current_epoch_id().await?; + let _ = self.deposit_amount().await?; + let _ = self.master_verification_key(Some(epoch_id)).await?; + let _ = self.ecash_threshold(epoch_id).await?; + let _ = self.ecash_clients(epoch_id).await?; + let _ = self.master_coin_index_signatures(Some(epoch_id)).await?; + let _ = self.master_expiration_date_signatures(today).await?; + + Ok(()) + } + + pub(crate) fn try_spawn(&self, task: F) -> Option> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + // don't spawn new task if we've received cancellation token + if self.inner.cancellation_token.is_cancelled() { + None + } else { + self.inner.task_tracker.reopen(); + // TODO: later use a task queue since most requests will be blocked waiting on chain permit anyway + let join_handle = self.inner.task_tracker.spawn(task); + self.inner.task_tracker.close(); + Some(join_handle) + } + } + + pub(crate) async fn cancel_and_wait(&self) { + self.inner.cancellation_token.cancel(); + self.inner.task_tracker.wait().await + } + + pub(crate) fn cancellation_token(&self) -> CancellationToken { + self.inner.cancellation_token.clone() + } + + pub(crate) fn zk_nym_web_hook(&self) -> &ZkNymWebHookConfig { + &self.inner.zk_nym_web_hook_config + } + + async fn ensure_credentials_issuable(&self) -> Result<(), VpnApiError> { + let epoch = self.current_epoch().await?; + + if epoch.state.is_final() { + Ok(()) + } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { + // SAFETY: the timestamp values in our DKG contract should be valid timestamps, + // otherwise it means the chain is seriously misbehaving + #[allow(clippy::unwrap_used)] + let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap(); + + Err(VpnApiError::CredentialsNotYetIssuable { + availability: finish_dt, + }) + } else if epoch.state.is_waiting_initialisation() { + return Err(VpnApiError::UninitialisedDkg); + } else { + Err(VpnApiError::UnknownEcashFailure) + } + } + + pub(crate) fn storage(&self) -> &VpnApiStorage { + &self.inner.storage + } + + pub async fn deposit_amount(&self) -> Result { + let read_guard = self.inner.ecash_state.required_deposit_cache.read().await; + if read_guard.is_valid() { + return Ok(read_guard.required_amount.clone()); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.required_deposit_cache.write().await; + let deposit_amount = self + .query_chain() + .await + .get_required_deposit_amount() + .await?; + + write_guard.update(deposit_amount.clone().into()); + + Ok(deposit_amount.into()) + } + + async fn current_epoch(&self) -> Result { + let read_guard = self.inner.ecash_state.cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch) + } + + pub async fn current_epoch_id(&self) -> Result { + let read_guard = self.inner.ecash_state.cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch.epoch_id); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch.epoch_id) + } + + pub(crate) async fn query_chain(&self) -> RwLockReadGuard { + let _acquire_timer = LockTimer::new("acquire chain query permit"); + self.inner.client.read().await + } + + pub(crate) async fn start_chain_tx(&self) -> ChainWritePermit { + let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); + + ChainWritePermit { + lock_timer: LockTimer::new("exclusive chain access permit"), + inner: self.inner.client.write().await, + } + } + + pub(crate) async fn global_data( + &self, + include_master_verification_key: bool, + include_expiration_date_signatures: bool, + include_coin_index_signatures: bool, + epoch_id: EpochId, + expiration_date: Date, + ) -> Result< + ( + Option, + Option, + Option, + ), + VpnApiError, + > { + let master_verification_key = if include_master_verification_key { + debug!("including master verification key in the response"); + Some( + self.master_verification_key(Some(epoch_id)) + .await + .map(|key| MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_expiration_date_signatures = if include_expiration_date_signatures { + debug!("including expiration date signatures in the response"); + Some( + self.master_expiration_date_signatures(expiration_date) + .await + .map(|signatures| AggregatedExpirationDateSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_coin_index_signatures = if include_coin_index_signatures { + debug!("including coin index signatures in the response"); + Some( + self.master_coin_index_signatures(Some(epoch_id)) + .await + .map(|signatures| AggregatedCoinIndicesSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + Ok(( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + )) + } + + pub(crate) async fn response_global_data( + &self, + include_master_verification_key: bool, + include_expiration_date_signatures: bool, + include_coin_index_signatures: bool, + epoch_id: EpochId, + expiration_date: Date, + uuid: Uuid, + ) -> Result< + ( + Option, + Option, + Option, + ), + RequestError, + > { + self.global_data( + include_master_verification_key, + include_expiration_date_signatures, + include_coin_index_signatures, + epoch_id, + expiration_date, + ) + .await + .map_err(|err| RequestError::new_server_error(err, uuid)) + } + + pub async fn ensure_not_in_epoch_transition( + &self, + uuid: Option, + ) -> Result<(), RequestError> { + if let Err(err) = self.ensure_credentials_issuable().await { + return if let Some(uuid) = uuid { + Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::SERVICE_UNAVAILABLE, + )) + } else { + Err(RequestError::new( + err.to_string(), + StatusCode::SERVICE_UNAVAILABLE, + )) + }; + } + Ok(()) + } + + pub(crate) async fn ecash_clients( + &self, + epoch_id: EpochId, + ) -> Result>, VpnApiError> { + self.inner + .ecash_state + .epoch_clients + .get_or_init(epoch_id, || async { + Ok(self + .query_chain() + .await + .get_all_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::, EcashApiError>>()?) + }) + .await + } + + pub(crate) async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + self.inner + .ecash_state + .threshold_values + .get_or_init(epoch_id, || async { + if let Some(threshold) = self + .query_chain() + .await + .get_epoch_threshold(epoch_id) + .await? + { + Ok(threshold) + } else { + Err(VpnApiError::UnavailableThreshold { epoch_id }) + } + }) + .await + .map(|t| *t) + } + + pub(crate) async fn response_ecash_threshold( + &self, + uuid: Uuid, + epoch_id: EpochId, + ) -> Result { + self.ecash_threshold(epoch_id) + .await + .map_err(|err| RequestError::new_server_error(err, uuid)) + } + + pub(crate) async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result, VpnApiError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .master_verification_key + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(stored) = self + .inner + .storage + .get_master_verification_key(epoch_id) + .await? + { + return Ok(stored.key); + } + + info!("attempting to establish master verification key for epoch {epoch_id}..."); + + // 2. perform actual aggregation + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + if all_apis.len() < threshold as usize { + return Err(VpnApiError::InsufficientNumberOfSigners { + threshold, + available: all_apis.len(), + }); + } + + let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?; + + let epoch = EpochVerificationKey { + epoch_id, + key: master_key, + }; + + // 3. save the key in the storage for when we reboot + self.inner + .storage + .insert_master_verification_key(&epoch) + .await?; + + Ok(epoch.key) + }) + .await + } + + pub(crate) async fn master_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result, VpnApiError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(master_sigs) = self + .inner + .storage + .get_master_coin_index_signatures(epoch_id) + .await? + { + return Ok(master_sigs); + } + + info!( + "attempting to establish master coin index signatures for epoch {epoch_id}..." + ); + + // 2. go around APIs and attempt to aggregate the data + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_coin_indices_signatures(Some(epoch_id)) + .await? + .signatures; + Ok(CoinIndexSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &master_vk, + &shares, + )?; + + let sigs = AggregatedCoinIndicesSignatures { + epoch_id, + signatures: aggregated, + }; + + // 3. save the signatures in the storage for when we reboot + self.inner + .storage + .insert_master_coin_index_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } + + pub(crate) async fn master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, VpnApiError> { + self.inner + .ecash_state + .expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(master_sigs) = self + .inner + .storage + .get_master_expiration_date_signatures(expiration_date) + .await? + { + return Ok(master_sigs); + } + + + info!( + "attempting to establish master expiration date signatures for {expiration_date}..." + ); + + // 3. go around APIs and attempt to aggregate the data + let epoch_id = self.current_epoch_id().await?; + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_expiration_date_signatures(Some(expiration_date)) + .await? + .signatures; + Ok(ExpirationDateSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_expiration_signatures( + &master_vk, + expiration_date.ecash_unix_timestamp(), + &shares, + )?; + + let sigs = AggregatedExpirationDateSignatures { + epoch_id, + expiration_date, + signatures: aggregated, + }; + + // 4. save the signatures in the storage for when we reboot + self.inner + .storage + .insert_master_expiration_date_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } +} + +struct ApiStateInner { + storage: VpnApiStorage, + + client: RwLock, + + zk_nym_web_hook_config: ZkNymWebHookConfig, + + ecash_state: EcashState, + + task_tracker: TaskTracker, + + cancellation_token: CancellationToken, +} + +pub(crate) struct CachedDeposit { + valid_until: OffsetDateTime, + required_amount: Coin, +} + +impl CachedDeposit { + const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; + + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + fn update(&mut self, required_amount: Coin) { + self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; + self.required_amount = required_amount; + } +} + +impl Default for CachedDeposit { + fn default() -> Self { + CachedDeposit { + valid_until: OffsetDateTime::UNIX_EPOCH, + required_amount: Coin { + amount: u128::MAX, + denom: "unym".to_string(), + }, + } + } +} + +#[derive(Default)] +pub(crate) struct EcashState { + pub(crate) required_deposit_cache: RwLock, + + pub(crate) cached_epoch: RwLock, + + pub(crate) master_verification_key: CachedImmutableEpochItem, + + pub(crate) threshold_values: CachedImmutableEpochItem, + + pub(crate) epoch_clients: CachedImmutableEpochItem>, + + pub(crate) coin_index_signatures: CachedImmutableEpochItem, + + pub(crate) expiration_date_signatures: + CachedImmutableItems, +} + +// explicitly wrap the WriteGuard for extra information regarding time taken +pub(crate) struct ChainWritePermit<'a> { + // it's not really dead, we only care about it being dropped + #[allow(dead_code)] + lock_timer: LockTimer, + inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, +} + +impl<'a> Deref for ChainWritePermit<'a> { + type Target = DirectSigningHttpRpcNyxdClient; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} diff --git a/nym-vpn-api/vpn-api-server/src/http/types.rs b/nym-vpn-api/vpn-api-server/src/http/types.rs new file mode 100644 index 0000000000..34406480da --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/http/types.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use nym_vpn_api_requests::api::v1::ErrorResponse; +use utoipa::ToResponse; +use uuid::Uuid; + +#[derive(Debug, Clone, ToResponse)] +#[response(description = "Error response with additional message")] +pub struct RequestError { + pub inner: ErrorResponse, + + pub status: StatusCode, +} + +impl RequestError { + pub fn new>(message: S, status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + uuid: None, + message: message.into(), + }, + status, + } + } + + pub fn new_status(status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + uuid: None, + message: String::new(), + }, + status, + } + } + + pub fn new_server_error(err: VpnApiError, uuid: Uuid) -> Self { + RequestError::new_with_uuid(err.to_string(), uuid, StatusCode::INTERNAL_SERVER_ERROR) + } + + pub fn new_with_uuid>(message: S, uuid: Uuid, status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + uuid: Some(uuid), + message: message.into(), + }, + status, + } + } + + pub fn from_err(err: E, status: StatusCode) -> Self { + Self::new(err.to_string(), status) + } +} + +impl IntoResponse for RequestError { + fn into_response(self) -> Response { + (self.status, Json(self.inner)).into_response() + } +} diff --git a/nym-vpn-api/vpn-api-server/src/main.rs b/nym-vpn-api/vpn-api-server/src/main.rs new file mode 100644 index 0000000000..b534f442c8 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/main.rs @@ -0,0 +1,95 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +use crate::cli::Cli; +use crate::error::VpnApiError; +use crate::http::state::ApiState; +use crate::http::HttpServer; +use crate::storage::VpnApiStorage; +use crate::tasks::StoragePruner; +use clap::Parser; +use nym_bin_common::logging::setup_tracing_logger; +use nym_network_defaults::setup_env; +use tracing::{info, trace}; + +pub mod cli; +pub mod config; +pub mod credentials; +pub mod error; +pub mod helpers; +pub mod http; +pub mod nym_api_helpers; +pub mod storage; +pub mod tasks; +mod webhook; + +pub async fn wait_for_signal() { + use tokio::signal::unix::{signal, SignalKind}; + + // if we fail to setup the signals, we should just blow up + #[allow(clippy::expect_used)] + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); + #[allow(clippy::expect_used)] + let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel"); + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + info!("Received SIGINT"); + }, + _ = sigterm.recv() => { + info!("Received SIGTERM"); + } + _ = sigquit.recv() => { + info!("Received SIGQUIT"); + } + } +} + +async fn run_api(cli: Cli) -> Result<(), VpnApiError> { + // create the tasks + let bind_address = cli.bind_address(); + + let storage = VpnApiStorage::init(cli.persistent_storage_path()).await?; + let mnemonic = cli.mnemonic; + let auth_token = cli.http_auth_token; + let webhook_cfg = cli.webhook; + let api_state = ApiState::new(storage.clone(), webhook_cfg, mnemonic).await?; + let http_server = HttpServer::new(bind_address, api_state.clone(), auth_token); + + let storage_pruner = StoragePruner::new(api_state.cancellation_token(), storage); + + // spawn all the tasks + api_state.try_spawn(http_server.run_forever()); + api_state.try_spawn(storage_pruner.run_forever()); + + // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) + wait_for_signal().await; + + // cancel all the tasks and wait for all task to terminate + api_state.cancel_and_wait().await; + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + std::env::set_var( + "RUST_LOG", + "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn,nym_validator_client=info", + ); + + let cli = Cli::parse(); + cli.webhook.ensure_valid_client_url()?; + trace!("args: {cli:#?}"); + + setup_env(cli.config_env_file.as_ref()); + setup_tracing_logger(); + + run_api(cli).await?; + Ok(()) +} diff --git a/nym-vpn-api/vpn-api-server/src/nym_api_helpers.rs b/nym-vpn-api/vpn-api-server/src/nym_api_helpers.rs new file mode 100644 index 0000000000..c862cf8047 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/nym_api_helpers.rs @@ -0,0 +1,181 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// TODO: this was just copied from nym-api; +// it should have been therefore extracted to a common crate instead and imported as dependency + +use crate::error::VpnApiError; +use futures::{stream, StreamExt}; +use nym_credentials::ecash::utils::{cred_exp_date, ecash_today}; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; +use nym_validator_client::EcashApiClient; +use std::cmp::min; +use std::collections::HashMap; +use std::future::Future; +use std::hash::Hash; +use std::ops::Deref; +use time::{Date, OffsetDateTime}; +use tokio::sync::{Mutex, RwLock, RwLockReadGuard}; +use tracing::warn; + +pub(crate) struct CachedEpoch { + valid_until: OffsetDateTime, + pub(crate) current_epoch: Epoch, +} + +impl Default for CachedEpoch { + fn default() -> Self { + CachedEpoch { + valid_until: OffsetDateTime::UNIX_EPOCH, + current_epoch: Epoch::default(), + } + } +} + +impl CachedEpoch { + pub(crate) fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + pub(crate) fn update(&mut self, epoch: Epoch) { + let now = OffsetDateTime::now_utc(); + + let validity_duration = if let Some(epoch_finish) = epoch.deadline { + #[allow(clippy::unwrap_used)] + let state_end = + OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); + let until_epoch_state_end = state_end - now; + // make it valid until the next epoch transition or next 5min, whichever is smaller + min(until_epoch_state_end, 5 * time::Duration::MINUTE) + } else { + 5 * time::Duration::MINUTE + }; + + self.valid_until = now + validity_duration; + self.current_epoch = epoch; + } +} + +// a map of items that never change for given key +pub(crate) struct CachedImmutableItems { + // I wonder if there's a more efficient structure with OnceLock or OnceCell or something + inner: RwLock>, +} + +// an item that stays constant throughout given epoch +pub(crate) type CachedImmutableEpochItem = CachedImmutableItems; + +impl Default for CachedImmutableItems { + fn default() -> Self { + CachedImmutableItems { + inner: RwLock::new(HashMap::new()), + } + } +} + +impl Deref for CachedImmutableItems { + type Target = RwLock>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl CachedImmutableItems +where + K: Eq + Hash, +{ + pub(crate) async fn get_or_init(&self, key: K, f: F) -> Result, E> + where + F: FnOnce() -> U, + U: Future>, + K: Clone, + { + // 1. see if we already have the item cached + let guard = self.inner.read().await; + if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) { + return Ok(item); + } + + // 2. attempt to retrieve (and cache) it + let mut write_guard = self.inner.write().await; + + // see if another task has already set the item whilst we were waiting for the lock + if write_guard.get(&key).is_some() { + let read_guard = write_guard.downgrade(); + + // SAFETY: we just checked the entry exists and we never dropped the guard + #[allow(clippy::unwrap_used)] + return Ok(RwLockReadGuard::map(read_guard, |map| { + map.get(&key).unwrap() + })); + } + + let init = f().await?; + write_guard.insert(key.clone(), init); + + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap())) + } +} + +pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), VpnApiError> { + let today = ecash_today(); + + if expiration_date < today.date() { + // what's the point of signatures with expiration in the past? + return Err(VpnApiError::ExpirationDateTooEarly); + } + + // SAFETY: we're nowhere near MAX date + #[allow(clippy::unwrap_used)] + if expiration_date > cred_exp_date().date().next_day().unwrap() { + // don't allow issuing signatures too far in advance (1 day beyond current value is fine) + return Err(VpnApiError::ExpirationDateTooLate); + } + + Ok(()) +} + +pub(crate) async fn query_all_threshold_apis( + all_apis: Vec, + threshold: u64, + f: F, +) -> Result, VpnApiError> +where + F: Fn(EcashApiClient) -> U, + U: Future>, +{ + let shares = Mutex::new(Vec::with_capacity(all_apis.len())); + + stream::iter(all_apis) + .for_each_concurrent(8, |api| async { + // can't be bothered to restructure the code to appease the borrow checker properly, + // so just assign this to a variable + let disp = api.to_string(); + match f(api).await { + Ok(partial_share) => shares.lock().await.push(partial_share), + Err(err) => { + warn!("failed to obtain partial threshold data from API: {disp}: {err}") + } + } + }) + .await; + + let shares = shares.into_inner(); + + if shares.len() < threshold as usize { + return Err(VpnApiError::InsufficientNumberOfSigners { + threshold, + available: shares.len(), + }); + } + + Ok(shares) +} diff --git a/nym-vpn-api/vpn-api-server/src/storage/manager.rs b/nym-vpn-api/vpn-api-server/src/storage/manager.rs new file mode 100644 index 0000000000..c1dd782e00 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/storage/manager.rs @@ -0,0 +1,413 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::storage::models::{ + BlindedShares, BlindedSharesStatus, MinimalWalletShare, RawCoinIndexSignatures, + RawExpirationDateSignatures, RawVerificationKey, +}; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; +use time::{Date, OffsetDateTime}; + +#[derive(Clone)] +pub(crate) struct SqliteStorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +impl SqliteStorageManager { + pub(crate) async fn load_blinded_shares_status_by_shares_id( + &self, + id: i64, + ) -> Result, sqlx::Error> { + let res = sqlx::query_as( + r#" + SELECT * + FROM blinded_shares + WHERE id = ?; + "#, + ) + .bind(id) + .fetch_optional(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn load_wallet_shares_by_shares_id( + &self, + id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + MinimalWalletShare, + r#" + SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date + FROM partial_blinded_wallet as t1 + JOIN ticketbook_deposit as t2 + on t1.corresponding_deposit = t2.deposit_id + JOIN blinded_shares as t3 + ON t2.request_uuid = t3.request_uuid + WHERE t3.id = ?; + "#, + id + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn load_blinded_shares_status_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, sqlx::Error> { + let res = sqlx::query_as( + r#" + SELECT * + FROM blinded_shares + WHERE device_id = ? AND credential_id = ?; + "#, + ) + .bind(device_id) + .bind(credential_id) + .fetch_optional(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn load_wallet_shares_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + MinimalWalletShare, + r#" + SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date + FROM partial_blinded_wallet as t1 + JOIN ticketbook_deposit as t2 + on t1.corresponding_deposit = t2.deposit_id + JOIN blinded_shares as t3 + ON t2.request_uuid = t3.request_uuid + WHERE t3.device_id = ? AND t3.credential_id = ?; + "#, + device_id, + credential_id + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_new_pending_async_shares_request( + &self, + request: String, + device_id: &str, + credential_id: &str, + ) -> Result { + let now = OffsetDateTime::now_utc(); + let res = sqlx::query_as( + r#" + INSERT INTO blinded_shares (status, request_uuid, device_id, credential_id, created, updated) + VALUES (?, ?, ?, ?, ?, ?) + RETURNING * + "#, + ) + .bind(BlindedSharesStatus::Pending) + .bind(request) + .bind(device_id) + .bind(credential_id) + .bind(now) + .bind(now) + .fetch_one(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn update_pending_async_blinded_shares_issued( + &self, + available_shares: i64, + device_id: &str, + credential_id: &str, + ) -> Result { + let now = OffsetDateTime::now_utc(); + let res = sqlx::query_as( + r#" + UPDATE blinded_shares + SET status = ?, updated = ?, error_message = NULL, available_shares = ? + WHERE device_id = ? AND credential_id = ? + RETURNING *; + "#, + ) + .bind(BlindedSharesStatus::Issued) + .bind(now) + .bind(available_shares) + .bind(device_id) + .bind(credential_id) + .fetch_one(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn update_pending_async_blinded_shares_error( + &self, + available_shares: i64, + device_id: &str, + credential_id: &str, + error: &str, + ) -> Result { + let now = time::OffsetDateTime::now_utc(); + let res = sqlx::query_as( + r#" + UPDATE blinded_shares + SET status = ?, error_message = ?, updated = ?, available_shares = ? + WHERE device_id = ? AND credential_id = ? + RETURNING *; + "#, + ) + .bind(BlindedSharesStatus::Error) + .bind(error) + .bind(now) + .bind(available_shares) + .bind(device_id) + .bind(credential_id) + .fetch_one(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn prune_old_blinded_shares( + &self, + delete_after: OffsetDateTime, + ) -> Result<(), VpnApiError> { + sqlx::query!( + r#" + DELETE FROM blinded_shares WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn prune_old_partial_blinded_wallets( + &self, + delete_after: OffsetDateTime, + ) -> Result<(), VpnApiError> { + sqlx::query!( + r#" + DELETE FROM partial_blinded_wallet WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn prune_old_partial_blinded_wallet_failures( + &self, + delete_after: OffsetDateTime, + ) -> Result<(), VpnApiError> { + sqlx::query!( + r#" + DELETE FROM partial_blinded_wallet_failure WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawVerificationKey, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_key, serialization_revision as "serialization_revision: u8" + FROM master_verification_key WHERE epoch_id = ? + "#, + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_master_verification_key( + &self, + serialisation_revision: u8, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?)", + epoch_id, + data, + serialisation_revision + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawCoinIndexSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + FROM global_coin_index_signatures WHERE epoch_id = ? + "#, + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_master_coin_index_signatures( + &self, + serialisation_revision: u8, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?)", + epoch_id, + data, + serialisation_revision + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + FROM global_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_master_expiration_date_signatures( + &self, + serialisation_revision: u8, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision) + VALUES (?, ?, ?, ?) + "#, + expiration_date, + epoch_id, + data, + serialisation_revision + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn insert_deposit_data( + &self, + deposit_id: DepositId, + deposit_tx_hash: String, + requested_on: OffsetDateTime, + request_uuid: String, + deposit_amount: String, + client_pubkey: &[u8], + deposit_ed25519_private_key: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO ticketbook_deposit(deposit_id, deposit_tx_hash, requested_on, request_uuid, deposit_amount, client_pubkey, ed25519_deposit_private_key) + VALUES (?, ?, ?, ?, ?, ?, ?) + "#, + deposit_id, + deposit_tx_hash, + requested_on, + request_uuid, + deposit_amount, + client_pubkey, + deposit_ed25519_private_key, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_partial_wallet_share( + &self, + deposit_id: DepositId, + epoch_id: i64, + expiration_date: Date, + node_id: i64, + created: OffsetDateTime, + share: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature) + VALUES (?, ?, ?, ?, ?, ?) + "#, + deposit_id, + epoch_id, + expiration_date, + node_id, + created, + share + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_partial_wallet_issuance_failure( + &self, + deposit_id: DepositId, + epoch_id: i64, + expiration_date: Date, + node_id: i64, + created: OffsetDateTime, + failure_message: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message) + VALUES (?, ?, ?, ?, ?, ?) + "#, + deposit_id, + epoch_id, + expiration_date, + node_id, + created, + failure_message + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } +} diff --git a/nym-vpn-api/vpn-api-server/src/storage/mod.rs b/nym-vpn-api/vpn-api-server/src/storage/mod.rs new file mode 100644 index 0000000000..e165907871 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/storage/mod.rs @@ -0,0 +1,460 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::credentials::ticketbook::NodeId; +use crate::error::VpnApiError; +use crate::storage::manager::SqliteStorageManager; +use crate::storage::models::{BlindedShares, MinimalWalletShare}; +use nym_compact_ecash::PublicKeyUser; +use nym_credentials::ecash::bandwidth::issuance::Hash; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, +}; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::ecash::BlindedSignatureResponse; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; +use nym_validator_client::nyxd::Coin; +use sqlx::ConnectOptions; +use std::fmt::Debug; +use std::path::Path; +use time::{Date, OffsetDateTime}; +use tracing::{debug, error, info, instrument}; +use uuid::Uuid; +use zeroize::Zeroizing; + +mod manager; +pub mod models; + +#[derive(Clone)] +pub struct VpnApiStorage { + pub(crate) storage_manager: SqliteStorageManager, +} + +impl VpnApiStorage { + #[instrument] + pub async fn init + Debug>(database_path: P) -> Result { + debug!("Attempting to connect to database"); + + let opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true) + .disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + + Ok(VpnApiStorage { + storage_manager: SqliteStorageManager { connection_pool }, + }) + } + + #[allow(dead_code)] + pub(crate) async fn load_blinded_shares_status_by_shares_id( + &self, + id: i64, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_blinded_shares_status_by_shares_id(id) + .await?) + } + + pub(crate) async fn load_wallet_shares_by_shares_id( + &self, + id: i64, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_wallet_shares_by_shares_id(id) + .await?) + } + + #[allow(dead_code)] + pub(crate) async fn load_blinded_shares_status_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_blinded_shares_status_by_device_and_credential_id(device_id, credential_id) + .await?) + } + + pub(crate) async fn load_wallet_shares_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_wallet_shares_by_device_and_credential_id(device_id, credential_id) + .await?) + } + + pub(crate) async fn insert_new_pending_async_shares_request( + &self, + request: Uuid, + device_id: &str, + credential_id: &str, + ) -> Result { + Ok(self + .storage_manager + .insert_new_pending_async_shares_request(request.to_string(), device_id, credential_id) + .await?) + } + + pub(crate) async fn update_pending_async_blinded_shares_issued( + &self, + available_shares: usize, + device_id: &str, + credential_id: &str, + ) -> Result { + self.storage_manager + .update_pending_async_blinded_shares_issued( + available_shares as i64, + device_id, + credential_id, + ) + .await + } + + pub(crate) async fn update_pending_async_blinded_shares_error( + &self, + available_shares: usize, + device_id: &str, + credential_id: &str, + error: &str, + ) -> Result { + self.storage_manager + .update_pending_async_blinded_shares_error( + available_shares as i64, + device_id, + credential_id, + error, + ) + .await + } + + pub(crate) async fn prune_old_blinded_shares(&self) -> Result<(), VpnApiError> { + let max_age = OffsetDateTime::now_utc() - time::Duration::days(31); + + self.storage_manager + .prune_old_partial_blinded_wallets(max_age) + .await?; + self.storage_manager + .prune_old_partial_blinded_wallet_failures(max_age) + .await?; + self.storage_manager.prune_old_blinded_shares(max_age).await + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn insert_deposit_data( + &self, + deposit_id: DepositId, + deposit_tx_hash: Hash, + requested_on: OffsetDateTime, + request: Uuid, + deposit_amount: Coin, + client_ecash_pubkey: &PublicKeyUser, + ed22519_keypair: &ed25519::KeyPair, + ) -> Result<(), VpnApiError> { + debug!("inserting deposit data"); + + let private_key_bytes = Zeroizing::new(ed22519_keypair.private_key().to_bytes()); + + self.storage_manager + .insert_deposit_data( + deposit_id, + deposit_tx_hash.to_string(), + requested_on, + request.to_string(), + deposit_amount.to_string(), + &client_ecash_pubkey.to_bytes(), + private_key_bytes.as_ref(), + ) + .await?; + Ok(()) + } + + pub(crate) async fn insert_partial_wallet_share( + &self, + deposit_id: DepositId, + epoch_id: EpochId, + expiration_date: Date, + node_id: NodeId, + res: &Result, + ) -> Result<(), VpnApiError> { + debug!("inserting partial wallet share"); + let now = OffsetDateTime::now_utc(); + + match res { + Ok(share) => { + self.storage_manager + .insert_partial_wallet_share( + deposit_id, + epoch_id as i64, + expiration_date, + node_id as i64, + now, + &share.blinded_signature.to_bytes(), + ) + .await?; + } + Err(err) => { + self.storage_manager + .insert_partial_wallet_issuance_failure( + deposit_id, + epoch_id as i64, + expiration_date, + node_id as i64, + now, + err.to_string(), + ) + .await? + } + } + Ok(()) + } + + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, VpnApiError> { + let Some(raw) = self + .storage_manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let deserialised = + EpochVerificationKey::try_unpack(&raw.serialised_key, raw.serialization_revision) + .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + Ok(Some(deserialised)) + } + + pub(crate) async fn insert_master_verification_key( + &self, + key: &EpochVerificationKey, + ) -> Result<(), VpnApiError> { + let packed = key.pack(); + Ok(self + .storage_manager + .insert_master_verification_key(packed.revision, key.epoch_id as i64, &packed.data) + .await?) + } + + pub(crate) async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result, VpnApiError> { + let Some(raw) = self + .storage_manager + .get_master_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let deserialised = AggregatedCoinIndicesSignatures::try_unpack( + &raw.serialised_signatures, + raw.serialization_revision, + ) + .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + Ok(Some(deserialised)) + } + + pub(crate) async fn insert_master_coin_index_signatures( + &self, + signatures: &AggregatedCoinIndicesSignatures, + ) -> Result<(), VpnApiError> { + let packed = signatures.pack(); + self.storage_manager + .insert_master_coin_index_signatures( + packed.revision, + signatures.epoch_id as i64, + &packed.data, + ) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, VpnApiError> { + let Some(raw) = self + .storage_manager + .get_master_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let deserialised = AggregatedExpirationDateSignatures::try_unpack( + &raw.serialised_signatures, + raw.serialization_revision, + ) + .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + Ok(Some(deserialised)) + } + + pub(crate) async fn insert_master_expiration_date_signatures( + &self, + signatures: &AggregatedExpirationDateSignatures, + ) -> Result<(), VpnApiError> { + let packed = signatures.pack(); + self.storage_manager + .insert_master_expiration_date_signatures( + packed.revision, + signatures.epoch_id as i64, + signatures.expiration_date, + &packed.data, + ) + .await?; + Ok(()) + } +} + +#[allow(clippy::expect_used)] +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests { + use super::*; + use crate::http::helpers; + use crate::storage::models::BlindedSharesStatus; + use nym_compact_ecash::scheme::keygen::KeyPairUser; + use rand::rngs::OsRng; + use rand::RngCore; + use std::ops::Deref; + use tempfile::{NamedTempFile, TempPath}; + + // create the wrapper so the underlying file gets deleted when it's no longer needed + struct StorageTestWrapper { + inner: VpnApiStorage, + _path: TempPath, + } + + impl StorageTestWrapper { + async fn new() -> anyhow::Result { + let file = NamedTempFile::new()?; + let path = file.into_temp_path(); + + println!("Creating database at {:?}...", path); + + Ok(StorageTestWrapper { + inner: VpnApiStorage::init(&path).await?, + _path: path, + }) + } + + async fn insert_dummy_deposit(&self, uuid: Uuid) -> anyhow::Result { + let mut rng = OsRng; + let deposit_id = rng.next_u32(); + let tx_hash = Hash::Sha256(Default::default()); + let requested_on = OffsetDateTime::now_utc(); + let deposit_amount = Coin::new(1, "ufoomp"); + let client_keypair = KeyPairUser::new(); + let client_ecash_pubkey = &client_keypair.public_key(); + + let deposit_keypair = ed25519::KeyPair::new(&mut rng); + + self.inner + .insert_deposit_data( + deposit_id, + tx_hash, + requested_on, + uuid, + deposit_amount, + client_ecash_pubkey, + &deposit_keypair, + ) + .await?; + + Ok(deposit_id) + } + } + + impl Deref for StorageTestWrapper { + type Target = VpnApiStorage; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + async fn get_storage() -> anyhow::Result { + StorageTestWrapper::new().await + } + + #[tokio::test] + async fn test_creation() -> anyhow::Result<()> { + let storage = get_storage().await; + assert!(storage.is_ok()); + + Ok(()) + } + + #[tokio::test] + async fn test_add() -> anyhow::Result<()> { + let storage = get_storage().await?; + + let dummy_uuid = helpers::random_uuid(); + println!("🚀 insert_pending_blinded_share..."); + + storage.insert_dummy_deposit(dummy_uuid).await?; + let res = storage + .insert_new_pending_async_shares_request(dummy_uuid, "1234", "1234") + .await; + if let Err(e) = &res { + println!("❌ {}", e); + } + assert!(res.is_ok()); + let res = res.unwrap(); + println!("res = {:?}", res); + assert_eq!(res.status, BlindedSharesStatus::Pending); + + println!("🚀 update_pending_blinded_share_error..."); + let res = storage + .update_pending_async_blinded_shares_error(0, "1234", "1234", "this is an error") + .await; + if let Err(e) = &res { + println!("❌ {}", e); + } + assert!(res.is_ok()); + let res = res.unwrap(); + println!("res = {:?}", res); + assert!(res.error_message.is_some()); + assert_eq!(res.status, BlindedSharesStatus::Error); + + println!("🚀 update_pending_blinded_share_data..."); + let res = storage + .update_pending_async_blinded_shares_issued(42, "1234", "1234") + .await; + if let Err(e) = &res { + println!("❌ {}", e); + } + assert!(res.is_ok()); + let res = res.unwrap(); + println!("res = {:?}", res); + assert_eq!(res.status, BlindedSharesStatus::Issued); + assert!(res.error_message.is_none()); + + Ok(()) + } +} diff --git a/nym-vpn-api/vpn-api-server/src/storage/models.rs b/nym-vpn-api/vpn-api-server/src/storage/models.rs new file mode 100644 index 0000000000..191db6d250 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/storage/models.rs @@ -0,0 +1,86 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, Type}; +use std::convert::Into; +use strum_macros::{Display, EnumString}; +use time::{Date, OffsetDateTime}; + +#[derive(Serialize, Deserialize, Debug, Clone, EnumString, Type, PartialEq, Display)] +#[sqlx(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BlindedSharesStatus { + Pending, + Issued, + Error, +} + +#[derive(Serialize, Deserialize, Debug, Clone, FromRow)] +pub struct BlindedShares { + pub id: i64, + pub request_uuid: String, + pub status: BlindedSharesStatus, + pub device_id: String, + pub credential_id: String, + pub available_shares: i64, + pub error_message: Option, + pub created: OffsetDateTime, + pub updated: OffsetDateTime, +} + +pub struct FullBlindedShares { + pub status: BlindedShares, + pub shares: (), +} + +#[derive(FromRow)] +pub struct RawExpirationDateSignatures { + #[allow(dead_code)] + pub epoch_id: u32, + pub serialised_signatures: Vec, + pub serialization_revision: u8, +} + +#[derive(FromRow)] +pub struct RawCoinIndexSignatures { + #[allow(dead_code)] + pub epoch_id: u32, + pub serialised_signatures: Vec, + pub serialization_revision: u8, +} + +#[derive(FromRow)] +pub struct RawVerificationKey { + #[allow(dead_code)] + pub epoch_id: u32, + pub serialised_key: Vec, + pub serialization_revision: u8, +} + +#[derive(FromRow)] +pub struct WalletShare { + #[allow(dead_code)] + pub corresponding_deposit: i64, + pub node_id: i64, + #[allow(dead_code)] + pub created: OffsetDateTime, + pub blinded_signature: Vec, +} + +#[derive(FromRow)] +pub struct MinimalWalletShare { + pub epoch_id: i64, + pub expiration_date: Date, + pub node_id: i64, + pub blinded_signature: Vec, +} + +impl From for nym_vpn_api_requests::api::v1::ticketbook::models::WalletShare { + fn from(value: MinimalWalletShare) -> Self { + nym_vpn_api_requests::api::v1::ticketbook::models::WalletShare { + node_index: value.node_id as u64, + bs58_encoded_share: bs58::encode(&value.blinded_signature).into_string(), + } + } +} diff --git a/nym-vpn-api/vpn-api-server/src/tasks.rs b/nym-vpn-api/vpn-api-server/src/tasks.rs new file mode 100644 index 0000000000..6389f58452 --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/tasks.rs @@ -0,0 +1,36 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::storage::VpnApiStorage; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; + +pub struct StoragePruner { + cancellation_token: CancellationToken, + storage: VpnApiStorage, +} + +impl StoragePruner { + pub fn new(cancellation_token: CancellationToken, storage: VpnApiStorage) -> Self { + Self { + cancellation_token, + storage, + } + } + + pub async fn run_forever(self) { + while !self.cancellation_token.is_cancelled() { + tokio::select! { + _ = self.cancellation_token.cancelled() => { + // The token was cancelled, task can shut down + } + _ = tokio::time::sleep(std::time::Duration::from_secs(60 * 60)) => { + match self.storage.prune_old_blinded_shares().await { + Ok(_res) => info!("🧹 Pruning old blinded shares complete"), + Err(err) => error!("Failed to prune old blinded shares: {err}"), + } + } + } + } + } +} diff --git a/nym-vpn-api/vpn-api-server/src/webhook.rs b/nym-vpn-api/vpn-api-server/src/webhook.rs new file mode 100644 index 0000000000..9ebdca250f --- /dev/null +++ b/nym-vpn-api/vpn-api-server/src/webhook.rs @@ -0,0 +1,75 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use clap::Args; +use reqwest::header::AUTHORIZATION; +use serde::Serialize; +use tracing::{debug, error, instrument, span, Level}; +use url::Url; +use uuid::Uuid; + +#[derive(Args, Debug, Clone)] +pub struct ZkNymWebHookConfig { + #[clap(long, env = "WEBHOOK_ZK_NYMS_URL")] + pub webhook_url: Url, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_ID")] + pub webhook_client_id: String, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_SECRET")] + pub webhook_client_secret: String, +} + +impl ZkNymWebHookConfig { + pub fn ensure_valid_client_url(&self) -> Result<(), VpnApiError> { + self.client_url() + .map_err(|_| VpnApiError::InvalidWebhookUrl) + .map(|_| ()) + } + + fn client_url(&self) -> Result { + self.webhook_url.join(&self.webhook_client_id) + } + + fn unchecked_client_url(&self) -> Url { + // we ensured we have valid url on startup + #[allow(clippy::unwrap_used)] + self.client_url().unwrap() + } + + fn bearer_token(&self) -> String { + format!("Bearer {}", self.webhook_client_secret) + } + + #[instrument(skip_all)] + pub async fn try_trigger(&self, original_uuid: Uuid, payload: &T) { + let url = self.unchecked_client_url(); + let span = span!(Level::DEBUG, "webhook", uuid = %original_uuid, url = %url); + let _entered = span.enter(); + + debug!("🕸️ about to trigger the webhook"); + + match reqwest::Client::new() + .post(url.clone()) + .header(AUTHORIZATION, self.bearer_token()) + .json(payload) + .send() + .await + { + Ok(res) => { + if !res.status().is_success() { + error!("❌🕸️ failed to call webhook: {res:?}"); + } else { + debug!("✅🕸️ webhook triggered successfully: {res:?}"); + if let Ok(body) = res.text().await { + debug!("body = {body}"); + } + } + } + Err(err) => { + error!("failed to call webhook: {err}") + } + } + } +} From 0ddaf3b50be3a98d4904ee583e24c92c1e4c8070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 10 Oct 2024 14:02:44 +0200 Subject: [PATCH 067/207] Import Cargo workspace files --- nym-vpn-api/Cargo.lock | 5485 ++++++++++++++++++++++++++++++++++++++++ nym-vpn-api/Cargo.toml | 97 + 2 files changed, 5582 insertions(+) create mode 100644 nym-vpn-api/Cargo.lock create mode 100644 nym-vpn-api/Cargo.toml diff --git a/nym-vpn-api/Cargo.lock b/nym-vpn-api/Cargo.lock new file mode 100644 index 0000000000..207a02b456 --- /dev/null +++ b/nym-vpn-api/Cargo.lock @@ -0,0 +1,5485 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bip32" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa13fae8b6255872fd86f7faf4b41168661d7d78609f7bfe6771b85c6739a15b" +dependencies = [ + "bs58", + "hmac", + "k256", + "rand_core 0.6.4", + "ripemd", + "sha2 0.10.8", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "bip39" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" +dependencies = [ + "bitcoin_hashes", + "rand", + "rand_core 0.6.4", + "serde", + "unicode-normalization", + "zeroize", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" +dependencies = [ + "digest 0.9.0", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "serdect 0.3.0-rc.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "bnum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.8", + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +dependencies = [ + "serde", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.23", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +dependencies = [ + "shlex", +] + +[[package]] +name = "celes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b9a21273925d7cc9e8a9a5f068122341336813c607014f5ef64f82b6acba58" +dependencies = [ + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cosmos-sdk-proto" +version = "0.22.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" +dependencies = [ + "prost", + "prost-types", + "tendermint-proto", +] + +[[package]] +name = "cosmrs" +version = "0.17.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" +dependencies = [ + "bip32", + "cosmos-sdk-proto", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint", + "tendermint-rpc", + "thiserror", +] + +[[package]] +name = "cosmwasm-crypto" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6aa9f904de106fa16443ad14ec2abe75e94ba003bb61c681c0e43d4c58d2a" +dependencies = [ + "digest 0.10.7", + "ecdsa", + "ed25519-zebra", + "k256", + "rand_core 0.6.4", + "thiserror", +] + +[[package]] +name = "cosmwasm-derive" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e07de16c800ac82fd188d055ecdb923ead0cf33960d3350089260bb982c09f" +dependencies = [ + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-schema" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ae2e971fb831d0c4fa3c8c3d2291cdbdd73786a73d65196dbf983d9b2468af" +dependencies = [ + "cosmwasm-schema-derive", + "schemars", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cosmwasm-schema-derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cadc57fd0825b85bc2f9b972c17da718b9efb4bc17e5935cc2d6036324f853d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-std" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98e19fae6c3f468412f731274b0f9434602722009d6a77432d39c7c4bb09202" +dependencies = [ + "base64 0.21.7", + "bnum", + "cosmwasm-crypto", + "cosmwasm-derive", + "derivative", + "forward_ref", + "hex", + "schemars", + "serde", + "serde-json-wasm", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "serde", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + +[[package]] +name = "cw-controllers" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d8edce4b78785f36413f67387e4be7d0cb7d032b5d4164bcc024f9c3f3f2ea" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-storage-plus" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2", + "schemars", + "semver 1.0.23", + "serde", + "thiserror", +] + +[[package]] +name = "cw2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "semver 1.0.23", + "serde", + "thiserror", +] + +[[package]] +name = "cw20" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2967fbd073d4b626dd9e7148e05a84a3bebd9794e71342e12351110ffbb12395" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw20", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24754ff6e45f2a1c60adc409d9b2eb87666012c44021329141ffaab3388fccd2" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle 2.6.1", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect 0.2.0", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.8", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek 3.2.0", + "hashbrown 0.12.3", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9775b22bc152ad86a0cf23f0f348b884b26add12bf741e7ffc4d4ab2ab4d205" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.7", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "eyre", + "paste", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "handlebars" +version = "3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error 2.0.1", + "serde", + "serde_json", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error 1.2.3", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime 2.1.0", + "serde", +] + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.8", + "signature", +] + +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2", + "chacha", + "keystream", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "nym-api-requests" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bs58", + "cosmrs", + "cosmwasm-std", + "ecdsa", + "getset", + "nym-compact-ecash", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-mixnet-contract-common", + "nym-node-requests", + "nym-serde-helpers", + "schemars", + "serde", + "sha2 0.10.8", + "tendermint", + "thiserror", + "time", + "utoipa", +] + +[[package]] +name = "nym-bin-common" +version = "0.6.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "const-str", + "log", + "pretty_env_logger", + "schemars", + "semver 0.11.0", + "serde", + "tracing-subscriber", + "utoipa", + "vergen", +] + +[[package]] +name = "nym-coconut-bandwidth-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-multisig-contract-common", +] + +[[package]] +name = "nym-coconut-dkg-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw2", + "cw4", + "nym-contracts-common", + "nym-multisig-contract-common", +] + +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bincode", + "bls12_381", + "bs58", + "cfg-if", + "digest 0.9.0", + "ff", + "group", + "itertools 0.12.1", + "nym-network-defaults", + "nym-pemstore", + "rand", + "serde", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + +[[package]] +name = "nym-config" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "dirs", + "handlebars", + "log", + "nym-network-defaults", + "serde", + "toml 0.7.8", + "url", +] + +[[package]] +name = "nym-contracts-common" +version = "0.5.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", + "thiserror", + "vergen", +] + +[[package]] +name = "nym-credentials" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bincode", + "bls12_381", + "cosmrs", + "log", + "nym-api-requests", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", + "nym-network-defaults", + "nym-serde-helpers", + "nym-validator-client", + "serde", + "thiserror", + "time", + "zeroize", +] + +[[package]] +name = "nym-credentials-interface" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bls12_381", + "nym-compact-ecash", + "nym-ecash-time", + "nym-network-defaults", + "rand", + "serde", + "strum 0.25.0", + "thiserror", + "time", +] + +[[package]] +name = "nym-crypto" +version = "0.4.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bs58", + "ed25519-dalek", + "nym-pemstore", + "nym-sphinx-types", + "rand", + "serde", + "serde_bytes", + "subtle-encoding", + "thiserror", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common", + "thiserror", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "nym-compact-ecash", + "time", +] + +[[package]] +name = "nym-exit-policy" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-group-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "cosmwasm-schema", + "cw-controllers", + "cw4", + "schemars", + "serde", +] + +[[package]] +name = "nym-http-api-client" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "async-trait", + "http 1.1.0", + "nym-bin-common", + "reqwest 0.12.4", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "nym-http-api-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "axum", + "bytes", + "colored", + "mime", + "serde", + "serde_json", + "serde_yaml", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-mixnet-contract-common" +version = "0.6.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "humantime-serde", + "log", + "nym-contracts-common", + "schemars", + "serde", + "serde-json-wasm", + "serde_repr", + "thiserror", + "time", +] + +[[package]] +name = "nym-multisig-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw3", + "cw4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "nym-network-defaults" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "dotenvy", + "log", + "schemars", + "serde", + "url", + "utoipa", +] + +[[package]] +name = "nym-node-requests" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "base64 0.22.1", + "celes", + "humantime 2.1.0", + "humantime-serde", + "nym-bin-common", + "nym-crypto", + "nym-exit-policy", + "nym-wireguard-types", + "schemars", + "serde", + "serde_json", + "thiserror", + "time", + "utoipa", +] + +[[package]] +name = "nym-pemstore" +version = "0.3.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "pem", +] + +[[package]] +name = "nym-serde-helpers" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "base64 0.22.1", + "bs58", + "serde", + "time", +] + +[[package]] +name = "nym-sphinx-types" +version = "0.2.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "sphinx-packet", + "thiserror", +] + +[[package]] +name = "nym-validator-client" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bip32", + "bip39", + "colored", + "cosmrs", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "cw3", + "cw4", + "eyre", + "flate2", + "futures", + "itertools 0.13.0", + "log", + "nym-api-requests", + "nym-coconut-bandwidth-contract-common", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-http-api-client", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-network-defaults", + "nym-vesting-contract-common", + "prost", + "reqwest 0.12.4", + "serde", + "serde_json", + "sha2 0.9.9", + "tendermint-rpc", + "thiserror", + "time", + "tokio", + "url", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-vesting-contract-common" +version = "0.7.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-contracts-common", + "nym-mixnet-contract-common", + "serde", + "thiserror", +] + +[[package]] +name = "nym-vpn-api" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "bip39", + "bs58", + "cfg-if", + "clap", + "colored", + "dotenv", + "futures", + "humantime 2.1.0", + "nym-bin-common", + "nym-compact-ecash", + "nym-config", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-http-api-common", + "nym-network-defaults", + "nym-validator-client", + "nym-vpn-api-requests", + "rand", + "reqwest 0.12.4", + "serde", + "serde_json", + "sqlx", + "strum 0.26.3", + "strum_macros 0.26.4", + "tempfile", + "thiserror", + "time", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", + "url", + "utoipa", + "utoipa-swagger-ui", + "uuid", + "zeroize", +] + +[[package]] +name = "nym-vpn-api-lib-wasm" +version = "0.1.0" +dependencies = [ + "bs58", + "getrandom", + "js-sys", + "nym-bin-common", + "nym-compact-ecash", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-vpn-api-requests", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_json", + "thiserror", + "time", + "tsify", + "wasm-bindgen", + "wasm-utils", + "zeroize", +] + +[[package]] +name = "nym-vpn-api-requests" +version = "0.1.0" +dependencies = [ + "async-trait", + "nym-credentials", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-serde-helpers", + "reqwest 0.12.4", + "schemars", + "serde", + "serde_json", + "time", + "tsify", + "utoipa", + "uuid", + "wasm-bindgen", + "wasmtimer", +] + +[[package]] +name = "nym-wireguard-types" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "base64 0.22.1", + "log", + "nym-config", + "nym-crypto", + "nym-network-defaults", + "serde", + "serde_json", + "thiserror", + "utoipa", + "x25519-dalek", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peg" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" + +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.1", + "once_cell", + "regex", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "pest_meta" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + +[[package]] +name = "pin-project" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls 0.26.0", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.22.4", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.25.0", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.6", + "winreg 0.52.0", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle 2.6.1", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.79", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.23", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals 0.29.1", + "syn 2.0.79", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.7", + "pkcs8", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15bee9b04dd165c3f4e142628982ddde884c2022a89e8ddf99c4829bf2c3a58" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.6.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a504c8ee181e3e594d84052f983d60afe023f4d94d050900be18062bbbf7b58" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.1", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "sphinx-packet" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabeca95bf5fd0563d6be7ebcb1c6a9fcb135746a0ba9050c47dc68c8607e595" +dependencies = [ + "aes", + "arrayref", + "blake2", + "bs58", + "byteorder", + "chacha", + "ctr", + "curve25519-dalek 4.1.3", + "digest 0.10.7", + "hkdf", + "hmac", + "lioness", + "log", + "rand", + "rand_distr", + "sha2 0.10.8", + "subtle 2.6.1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash 0.8.11", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.6.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.25.4", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.6.0", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.6.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.79", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.79", +] + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendermint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954496fbc9716eb4446cdd6d00c071a3e2f22578d62aa03b40c7e5b4fda3ed42" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "prost-types", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle 2.6.1", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-config" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84b11b57d20ee4492a1452faff85f5c520adc36ca9fe5e701066935255bb89f" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint", + "toml 0.8.19", + "url", +] + +[[package]] +name = "tendermint-proto" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc87024548c7f3da479885201e3da20ef29e85a3b13d04606b380ac4c7120d87" +dependencies = [ + "bytes", + "flex-error", + "prost", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-rpc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdc2281e271277fda184d96d874a6fe59f569b130b634289257baacfc95aa85" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom", + "peg", + "pin-project", + "rand", + "reqwest 0.11.27", + "semver 1.0.23", + "serde", + "serde_bytes", + "serde_json", + "subtle 2.6.1", + "subtle-encoding", + "tendermint", + "tendermint-config", + "tendermint-proto", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "hashbrown 0.14.5", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.22", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.20", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals 0.28.0", + "syn 2.0.79", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utoipa" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.79", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943e0ff606c6d57d410fd5663a4d7c074ab2c5f14ab903b9514565e59fa1189e" +dependencies = [ + "axum", + "mime_guess", + "regex", + "reqwest 0.12.4", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +dependencies = [ + "anyhow", + "cargo_metadata", + "cfg-if", + "regex", + "rustc_version", + "rustversion", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9" + +[[package]] +name = "wasm-utils" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" +dependencies = [ + "futures", + "gloo-utils 0.2.0", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f656cd8858a5164932d8a90f936700860976ec21eb00e0fe2aa8cab13f6b4cf" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44188d185b5bdcae1052d08bcbcf9091a5524038d4572cc4f4f2bb9d5554ddd9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "zip" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.6.0", + "num_enum", + "thiserror", +] diff --git a/nym-vpn-api/Cargo.toml b/nym-vpn-api/Cargo.toml new file mode 100644 index 0000000000..c27ed1bcd6 --- /dev/null +++ b/nym-vpn-api/Cargo.toml @@ -0,0 +1,97 @@ +[profile.release] +panic = "abort" +opt-level = "s" +overflow-checks = true + +[profile.dev] +panic = "abort" + +[workspace] + +resolver = "2" +members = [ + "vpn-api-server", + "vpn-api-requests", + "vpn-api-lib-wasm" +] + +[workspace.package] +authors = ["Nym Technologies SA"] +repository = "https://github.com/nymtech/nym" +homepage = "https://nymtech.net" +documentation = "https://nymtech.net" +edition = "2021" +license = "GPL-3.0" + +[workspace.dependencies] +async-trait = "0.1.80" +axum = "0.7.5" +anyhow = "1.0.71" +bip39 = "2.0.0" +bs58 = "0.5.1" +colored = "2.1.0" +cfg-if = "1.0.0" +clap = "4.5.4" +dotenv = "0.15.0" +futures = "0.3.30" +humantime = "2.1.0" +thiserror = "1.0.59" +rand = "0.8.5" +reqwest = "0.12.4" +schemars = "0.8.17" +strum = "0.26.3" +strum_macros = "0.26.4" +serde = "1.0.200" +serde_json = "1.0.117" +sqlx = "0.7.4" +tempfile = "3.12.0" +time = "0.3.36" +tracing = "0.1.40" +tsify = "0.4.5" +tokio = "1.37.0" +tokio-util = "0.7.10" +tower = "0.5.0" +tower-http = "0.5.2" +uuid = "1.8.0" +url = "2.5.2" +utoipa = "4.2.0" +utoipa-swagger-ui = "7.0.1" +zeroize = "1.6.0" + +wasm-bindgen = "0.2.93" +wasmtimer = "0.2.0" + + +# point to the current head of `feature/ticketbook-utils` (as of 30.08.2024) +nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-crypto = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-config = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-http-api-common = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-http-api-client = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-credentials = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-credentials-interface = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-api-requests = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-serde-helpers = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +wasm-utils = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-compact-ecash = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } +nym-ecash-time = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } + + +## point to local files instead; makes it easier to debug issues +#[patch.'https://github.com/nymtech/nym'] +#nym-bin-common = { path = "../nym/common/bin-common" } +#nym-crypto = { path = "../nym/common/crypto" } +#nym-config = { path = "../nym/common/config" } +#nym-http-api-common = { path = "../nym/common/http-api-common" } +#nym-http-api-client = { path = "../nym/common/http-api-client" } +#nym-credentials = { path = "../nym/common/credentials" } +#nym-credentials-interface = { path = "../nym/common/credentials-interface" } +#nym-validator-client = { path = "../nym/common/client-libs/validator-client" } +#nym-network-defaults = { path = "../nym/common/network-defaults" } +#nym-api-requests = { path = "../nym/nym-api/nym-api-requests" } +#nym-serde-helpers = { path = "../nym/common/serde-helpers" } +#wasm-utils = { path = "../nym/common/wasm/utils" } +#nym-compact-ecash = { path = "../nym/common/nym_offline_compact_ecash" } +#nym-ecash-time = { path = "../nym/common/ecash-time" } From 3460ce70a3d93d0a56afcec3b676ceab769935c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 10 Oct 2024 16:22:10 +0200 Subject: [PATCH 068/207] Remove workspace deps for nym --- nym-vpn-api/Cargo.lock | 140 ++++-------------------- nym-vpn-api/Cargo.toml | 35 ------ nym-vpn-api/vpn-api-lib-wasm/Cargo.toml | 14 +-- nym-vpn-api/vpn-api-requests/Cargo.toml | 10 +- nym-vpn-api/vpn-api-server/Cargo.toml | 20 ++-- 5 files changed, 43 insertions(+), 176 deletions(-) diff --git a/nym-vpn-api/Cargo.lock b/nym-vpn-api/Cargo.lock index 207a02b456..3cbaa09188 100644 --- a/nym-vpn-api/Cargo.lock +++ b/nym-vpn-api/Cargo.lock @@ -465,7 +465,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", - "semver 1.0.23", + "semver", "serde", "serde_json", "thiserror", @@ -875,7 +875,7 @@ dependencies = [ "cosmwasm-std", "cw2", "schemars", - "semver 1.0.23", + "semver", "serde", "thiserror", ] @@ -890,7 +890,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "schemars", - "semver 1.0.23", + "semver", "serde", "thiserror", ] @@ -2243,7 +2243,6 @@ dependencies = [ [[package]] name = "nym-api-requests" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bs58", "cosmrs", @@ -2255,10 +2254,12 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", "schemars", "serde", + "serde_json", "sha2 0.10.8", "tendermint", "thiserror", @@ -2269,13 +2270,12 @@ dependencies = [ [[package]] name = "nym-bin-common" version = "0.6.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "const-str", "log", "pretty_env_logger", "schemars", - "semver 0.11.0", + "semver", "serde", "tracing-subscriber", "utoipa", @@ -2285,7 +2285,6 @@ dependencies = [ [[package]] name = "nym-coconut-bandwidth-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2295,7 +2294,6 @@ dependencies = [ [[package]] name = "nym-coconut-dkg-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2309,7 +2307,6 @@ dependencies = [ [[package]] name = "nym-compact-ecash" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bincode", "bls12_381", @@ -2331,21 +2328,19 @@ dependencies = [ [[package]] name = "nym-config" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "dirs", "handlebars", "log", "nym-network-defaults", "serde", - "toml 0.7.8", + "toml", "url", ] [[package]] name = "nym-contracts-common" version = "0.5.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bs58", "cosmwasm-schema", @@ -2360,7 +2355,6 @@ dependencies = [ [[package]] name = "nym-credentials" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bincode", "bls12_381", @@ -2383,7 +2377,6 @@ dependencies = [ [[package]] name = "nym-credentials-interface" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bls12_381", "nym-compact-ecash", @@ -2391,7 +2384,7 @@ dependencies = [ "nym-network-defaults", "rand", "serde", - "strum 0.25.0", + "strum", "thiserror", "time", ] @@ -2399,7 +2392,6 @@ dependencies = [ [[package]] name = "nym-crypto" version = "0.4.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bs58", "ed25519-dalek", @@ -2417,7 +2409,6 @@ dependencies = [ [[package]] name = "nym-ecash-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bs58", "cosmwasm-schema", @@ -2431,7 +2422,6 @@ dependencies = [ [[package]] name = "nym-ecash-time" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "nym-compact-ecash", "time", @@ -2440,7 +2430,6 @@ dependencies = [ [[package]] name = "nym-exit-policy" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "serde", "serde_json", @@ -2452,7 +2441,6 @@ dependencies = [ [[package]] name = "nym-group-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "cosmwasm-schema", "cw-controllers", @@ -2464,7 +2452,6 @@ dependencies = [ [[package]] name = "nym-http-api-client" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "async-trait", "http 1.1.0", @@ -2481,7 +2468,6 @@ dependencies = [ [[package]] name = "nym-http-api-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "axum", "bytes", @@ -2497,11 +2483,12 @@ dependencies = [ [[package]] name = "nym-mixnet-contract-common" version = "0.6.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "bs58", "cosmwasm-schema", "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", "humantime-serde", "log", "nym-contracts-common", @@ -2516,7 +2503,6 @@ dependencies = [ [[package]] name = "nym-multisig-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2532,7 +2518,6 @@ dependencies = [ [[package]] name = "nym-network-defaults" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "dotenvy", "log", @@ -2545,7 +2530,6 @@ dependencies = [ [[package]] name = "nym-node-requests" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "base64 0.22.1", "celes", @@ -2566,7 +2550,6 @@ dependencies = [ [[package]] name = "nym-pemstore" version = "0.3.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "pem", ] @@ -2574,10 +2557,10 @@ dependencies = [ [[package]] name = "nym-serde-helpers" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -2585,7 +2568,6 @@ dependencies = [ [[package]] name = "nym-sphinx-types" version = "0.2.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "sphinx-packet", "thiserror", @@ -2594,7 +2576,6 @@ dependencies = [ [[package]] name = "nym-validator-client" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "async-trait", "base64 0.22.1", @@ -2625,6 +2606,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.4", @@ -2643,7 +2625,6 @@ dependencies = [ [[package]] name = "nym-vesting-contract-common" version = "0.7.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2683,8 +2664,8 @@ dependencies = [ "serde", "serde_json", "sqlx", - "strum 0.26.3", - "strum_macros 0.26.4", + "strum", + "strum_macros", "tempfile", "thiserror", "time", @@ -2750,17 +2731,13 @@ dependencies = [ [[package]] name = "nym-wireguard-types" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "base64 0.22.1", "log", "nym-config", - "nym-crypto", "nym-network-defaults", "serde", - "serde_json", "thiserror", - "utoipa", "x25519-dalek", ] @@ -3073,7 +3050,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.22.22", + "toml_edit", ] [[package]] @@ -3483,7 +3460,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.23", + "semver", ] [[package]] @@ -3692,15 +3669,6 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.23" @@ -3710,15 +3678,6 @@ dependencies = [ "serde", ] -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "serde" version = "1.0.210" @@ -4254,35 +4213,13 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" -dependencies = [ - "strum_macros 0.25.3", -] - [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum_macros" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.79", + "strum_macros", ] [[package]] @@ -4434,7 +4371,7 @@ dependencies = [ "serde", "serde_json", "tendermint", - "toml 0.8.19", + "toml", "url", ] @@ -4469,7 +4406,7 @@ dependencies = [ "pin-project", "rand", "reqwest 0.11.27", - "semver 1.0.23", + "semver", "serde", "serde_bytes", "serde_json", @@ -4660,18 +4597,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.19.15", -] - [[package]] name = "toml" version = "0.8.19" @@ -4681,7 +4606,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.22", + "toml_edit", ] [[package]] @@ -4693,19 +4618,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.6.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.22.22" @@ -4716,7 +4628,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.20", + "winnow", ] [[package]] @@ -5139,7 +5051,6 @@ checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9" [[package]] name = "wasm-utils" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=6dee57fc46fa30420510ef7d8b7100607762fe3e#6dee57fc46fa30420510ef7d8b7100607762fe3e" dependencies = [ "futures", "gloo-utils 0.2.0", @@ -5377,15 +5288,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.6.20" diff --git a/nym-vpn-api/Cargo.toml b/nym-vpn-api/Cargo.toml index c27ed1bcd6..7b08ce1792 100644 --- a/nym-vpn-api/Cargo.toml +++ b/nym-vpn-api/Cargo.toml @@ -60,38 +60,3 @@ zeroize = "1.6.0" wasm-bindgen = "0.2.93" wasmtimer = "0.2.0" - - -# point to the current head of `feature/ticketbook-utils` (as of 30.08.2024) -nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-crypto = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-config = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-http-api-common = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-http-api-client = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-credentials = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-credentials-interface = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-api-requests = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-serde-helpers = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -wasm-utils = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-compact-ecash = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } -nym-ecash-time = { git = "https://github.com/nymtech/nym", rev = "6dee57fc46fa30420510ef7d8b7100607762fe3e" } - - -## point to local files instead; makes it easier to debug issues -#[patch.'https://github.com/nymtech/nym'] -#nym-bin-common = { path = "../nym/common/bin-common" } -#nym-crypto = { path = "../nym/common/crypto" } -#nym-config = { path = "../nym/common/config" } -#nym-http-api-common = { path = "../nym/common/http-api-common" } -#nym-http-api-client = { path = "../nym/common/http-api-client" } -#nym-credentials = { path = "../nym/common/credentials" } -#nym-credentials-interface = { path = "../nym/common/credentials-interface" } -#nym-validator-client = { path = "../nym/common/client-libs/validator-client" } -#nym-network-defaults = { path = "../nym/common/network-defaults" } -#nym-api-requests = { path = "../nym/nym-api/nym-api-requests" } -#nym-serde-helpers = { path = "../nym/common/serde-helpers" } -#wasm-utils = { path = "../nym/common/wasm/utils" } -#nym-compact-ecash = { path = "../nym/common/nym_offline_compact_ecash" } -#nym-ecash-time = { path = "../nym/common/ecash-time" } diff --git a/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml b/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml index bf6770b05a..b5c68c9b47 100644 --- a/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml +++ b/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml @@ -27,12 +27,12 @@ tsify = { workspace = true, features = ["js"] } serde-wasm-bindgen = "0.6.5" # NYM: -nym-bin-common = { workspace = true } -nym-crypto = { workspace = true, features = ["asymmetric"] } -nym-compact-ecash = { workspace = true } -nym-credentials = { workspace = true } -nym-credentials-interface = { workspace = true } -nym-ecash-time = { workspace = true, features = ["expiration"] } +nym-bin-common = { path = "../../common/bin-common" } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } +nym-credentials = { path = "../../common/credentials" } +nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-ecash-time = { path = "../../common/ecash-time", features = ["expiration"] } nym-vpn-api-requests = { path = "../vpn-api-requests", default-features = false, features = ["tsify"] } -wasm-utils = { workspace = true } +wasm-utils = { path = "../../common/wasm/utils" } diff --git a/nym-vpn-api/vpn-api-requests/Cargo.toml b/nym-vpn-api/vpn-api-requests/Cargo.toml index bf37ca2d3d..51bb324d07 100644 --- a/nym-vpn-api/vpn-api-requests/Cargo.toml +++ b/nym-vpn-api/vpn-api-requests/Cargo.toml @@ -24,11 +24,11 @@ wasm-bindgen = { workspace = true, optional = true } ## openapi: utoipa = { workspace = true, optional = true } -nym-credentials.workspace = true -nym-credentials-interface.workspace = true -nym-http-api-common = { workspace = true, optional = true } -nym-http-api-client = { workspace = true } -nym-serde-helpers = { workspace = true, features = ["bs58"] } +nym-credentials = { path = "../../common/credentials" } +nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-http-api-common = { path = "../../common/http-api-common", optional = true } +nym-http-api-client = { path = "../../common/http-api-client" } +nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] } [target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer] workspace = true diff --git a/nym-vpn-api/vpn-api-server/Cargo.toml b/nym-vpn-api/vpn-api-server/Cargo.toml index bca4bad573..d0697b3ced 100644 --- a/nym-vpn-api/vpn-api-server/Cargo.toml +++ b/nym-vpn-api/vpn-api-server/Cargo.toml @@ -42,15 +42,15 @@ utoipa = { workspace = true, features = ["axum_extras", "time"] } utoipa-swagger-ui = { workspace = true, features = ["axum"] } zeroize.workspace = true -nym-bin-common = { workspace = true, features = ["basic_tracing"] } -nym-compact-ecash = { workspace = true } -nym-config = { workspace = true } -nym-crypto = { workspace = true, features = ["asymmetric", "rand", "serde"] } -nym-credentials = { workspace = true } -nym-credentials-interface = { workspace = true } -nym-http-api-common = { workspace = true, features = ["utoipa"] } -nym-validator-client = { workspace = true } -nym-network-defaults = { workspace = true } +nym-bin-common = { path = "../../common/bin-common", features = ["basic_tracing"] } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } +nym-config = { path = "../../common/config" } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "serde"] } +nym-credentials = { path = "../../common/credentials" } +nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } +nym-validator-client = { path = "../../common/client-libs/validator-client" } +nym-network-defaults = { path = "../../common/network-defaults" } nym-vpn-api-requests = { path = "../vpn-api-requests", features = ["openapi"] } @@ -63,4 +63,4 @@ sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros [features] default = ["cors"] -cors = ["tower-http"] \ No newline at end of file +cors = ["tower-http"] From 8435cf91e547c75a074038d7c778049de8ac7970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 10 Oct 2024 15:28:39 +0200 Subject: [PATCH 069/207] Add CI workflows --- .github/workflows/ci-build-vpn-api-wasm.yml | 40 +++++++++++++++++++ .github/workflows/ci-build-vpn-api.yml | 44 +++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 .github/workflows/ci-build-vpn-api-wasm.yml create mode 100644 .github/workflows/ci-build-vpn-api.yml diff --git a/.github/workflows/ci-build-vpn-api-wasm.yml b/.github/workflows/ci-build-vpn-api-wasm.yml new file mode 100644 index 0000000000..56126da8db --- /dev/null +++ b/.github/workflows/ci-build-vpn-api-wasm.yml @@ -0,0 +1,40 @@ +name: ci-build-vpn-api-wasm + +on: + pull_request: + paths: + - 'nym-vpn-api/**' + - '.github/workflows/ci-build-vpn-api-wasm.yml' + +jobs: + wasm: + runs-on: arc-ubuntu-22.04 + env: + CARGO_TERM_COLOR: always + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: wasm32-unknown-unknown + override: true + components: rustfmt, clippy + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Install wasm-opt + uses: ./.github/actions/install-wasm-opt + with: + version: '116' + + - name: Install wasm-bindgen-cli + run: cargo install wasm-bindgen-cli + + - name: "Build" + run: make + working-directory: nym-vpn-api/vpn-api-lib-wasm diff --git a/.github/workflows/ci-build-vpn-api.yml b/.github/workflows/ci-build-vpn-api.yml new file mode 100644 index 0000000000..c6a71ea8a7 --- /dev/null +++ b/.github/workflows/ci-build-vpn-api.yml @@ -0,0 +1,44 @@ +name: ci-build-vpn-api + +on: + pull_request: + paths: + - 'nym-vpn-api/**' + - '.github/workspace/ci-build-vpn-api.yml' + workflow_dispatch: + +jobs: + build: + runs-on: arc-ubuntu-22.04 + env: + CARGO_TERM_COLOR: always + MANIFEST_PATH: "--manifest-path nym-vpn-api/Cargo.toml" + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: ${{ env.MANIFEST_PATH }} --all -- --check + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: ${{ env.MANIFEST_PATH }} --workspace --all-targets + + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings From 275cd9ff92f82e577977bdd9ef09a0ca1b9a5aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 10 Oct 2024 15:28:47 +0200 Subject: [PATCH 070/207] rustfmt --- nym-vpn-api/vpn-api-requests/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nym-vpn-api/vpn-api-requests/src/lib.rs b/nym-vpn-api/vpn-api-requests/src/lib.rs index fbbd38ee02..8415c35ba6 100644 --- a/nym-vpn-api/vpn-api-requests/src/lib.rs +++ b/nym-vpn-api/vpn-api-requests/src/lib.rs @@ -48,11 +48,7 @@ pub mod routes { pub const CURRENT_EPOCH: &str = "/current-epoch"; pub const SHARES: &str = "/shares"; - absolute_route!( - obtain_wallet_shares_absolute, - ticketbook_absolute(), - OBTAIN - ); + absolute_route!(obtain_wallet_shares_absolute, ticketbook_absolute(), OBTAIN); absolute_route!( obtain_async_wallet_shares_absolute, ticketbook_absolute(), @@ -76,7 +72,11 @@ pub mod routes { "/device/:device_id/credential/:credential_id"; absolute_route!(share_by_id_absolute, shares_absolute(), SHARE_BY_ID); - absolute_route!(share_by_device_and_credential_id_absolute, shares_absolute(), SHARE_BY_DEVICE_AND_CREDENTIAL_ID); + absolute_route!( + share_by_device_and_credential_id_absolute, + shares_absolute(), + SHARE_BY_DEVICE_AND_CREDENTIAL_ID + ); } } } From 435d9d31159996379d8e82a27490aade467c1da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Sat, 12 Oct 2024 11:29:15 +0200 Subject: [PATCH 071/207] Update to don't use removed default ticket type --- nym-vpn-api/vpn-api-lib-wasm/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs b/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs index 44603fdbc2..b4cf50b2b9 100644 --- a/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs +++ b/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs @@ -87,7 +87,7 @@ impl NymIssuanceTicketbook { }; let ticketbook_type = match opts.ticketbook_type { - None => TicketType::default(), + None => TicketType::V1MixnetEntry, Some(typ) => typ .parse() .map_err(|_| VpnApiLibError::MalformedTicketType)?, @@ -104,7 +104,7 @@ impl NymIssuanceTicketbook { Ok(NymIssuanceTicketbook { ecash_keypair, withdrawal_request, - ticketbook_type: Default::default(), + ticketbook_type, expiration_date, request_info: Zeroizing::new(request_info), }) From 9d8a686760ea17cf689aeda60dfb93ed5797248c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Sat, 12 Oct 2024 11:56:17 +0200 Subject: [PATCH 072/207] Add common to trigger paths for CI --- .github/workflows/ci-build-vpn-api-wasm.yml | 1 + .github/workflows/ci-build-vpn-api.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci-build-vpn-api-wasm.yml b/.github/workflows/ci-build-vpn-api-wasm.yml index 56126da8db..c8dae05cdb 100644 --- a/.github/workflows/ci-build-vpn-api-wasm.yml +++ b/.github/workflows/ci-build-vpn-api-wasm.yml @@ -3,6 +3,7 @@ name: ci-build-vpn-api-wasm on: pull_request: paths: + - 'common/**' - 'nym-vpn-api/**' - '.github/workflows/ci-build-vpn-api-wasm.yml' diff --git a/.github/workflows/ci-build-vpn-api.yml b/.github/workflows/ci-build-vpn-api.yml index c6a71ea8a7..1a31e7fc29 100644 --- a/.github/workflows/ci-build-vpn-api.yml +++ b/.github/workflows/ci-build-vpn-api.yml @@ -3,6 +3,7 @@ name: ci-build-vpn-api on: pull_request: paths: + - 'common/**' - 'nym-vpn-api/**' - '.github/workspace/ci-build-vpn-api.yml' workflow_dispatch: From 845b5df14c0e4e45964c86ccc3b90f4b320a7ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 10:27:59 +0100 Subject: [PATCH 073/207] chore: remove unused rocket code --- nym-api/src/circulating_supply_api/mod.rs | 10 - nym-api/src/circulating_supply_api/routes.rs | 77 --- nym-api/src/ecash/api_routes/aggregation.rs | 106 ++- .../src/ecash/api_routes/aggregation_axum.rs | 143 ---- nym-api/src/ecash/api_routes/handlers.rs | 8 +- nym-api/src/ecash/api_routes/issued.rs | 115 +++- nym-api/src/ecash/api_routes/issued_axum.rs | 147 ----- nym-api/src/ecash/api_routes/mod.rs | 8 +- .../src/ecash/api_routes/partial_signing.rs | 107 ++- .../ecash/api_routes/partial_signing_axum.rs | 179 ----- nym-api/src/ecash/api_routes/spending.rs | 106 ++- nym-api/src/ecash/api_routes/spending_axum.rs | 246 ------- nym-api/src/ecash/mod.rs | 24 - nym-api/src/network/mod.rs | 9 - nym-api/src/network/routes.rs | 61 -- nym-api/src/node_status_api/mod.rs | 47 -- .../src/node_status_api/routes_deprecated.rs | 615 ------------------ .../src/nym_contract_cache/legacy_helpers.rs | 2 - nym-api/src/nym_contract_cache/mod.rs | 19 - nym-api/src/nym_contract_cache/routes.rs | 158 ----- nym-api/src/nym_nodes/mod.rs | 31 - nym-api/src/nym_nodes/routes.rs | 135 ---- nym-api/src/nym_nodes/unstable_routes.rs | 403 ------------ nym-api/src/status/mod.rs | 10 - nym-api/src/status/routes.rs | 52 -- 25 files changed, 341 insertions(+), 2477 deletions(-) delete mode 100644 nym-api/src/circulating_supply_api/routes.rs delete mode 100644 nym-api/src/ecash/api_routes/aggregation_axum.rs delete mode 100644 nym-api/src/ecash/api_routes/issued_axum.rs delete mode 100644 nym-api/src/ecash/api_routes/partial_signing_axum.rs delete mode 100644 nym-api/src/ecash/api_routes/spending_axum.rs delete mode 100644 nym-api/src/network/routes.rs delete mode 100644 nym-api/src/node_status_api/routes_deprecated.rs delete mode 100644 nym-api/src/nym_contract_cache/legacy_helpers.rs delete mode 100644 nym-api/src/nym_contract_cache/routes.rs delete mode 100644 nym-api/src/nym_nodes/routes.rs delete mode 100644 nym-api/src/nym_nodes/unstable_routes.rs delete mode 100644 nym-api/src/status/routes.rs diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 9c125815cb..3253ccaaab 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -7,16 +7,6 @@ use nym_task::TaskManager; pub(crate) mod cache; pub(crate) mod handlers; -// pub(crate) mod routes; - -// /// Merges the routes with http information and returns it to Rocket for serving -// pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { -// openapi_get_routes_spec![ -// settings: routes::get_full_circulating_supply, -// routes::get_total_supply, -// routes::get_circulating_supply -// ] -// } /// Spawn the circulating supply cache refresher. pub(crate) fn start_cache_refresh( diff --git a/nym-api/src/circulating_supply_api/routes.rs b/nym-api/src/circulating_supply_api/routes.rs deleted file mode 100644 index b8b00cb82e..0000000000 --- a/nym-api/src/circulating_supply_api/routes.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::node_status_api::models::RocketErrorResponse; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::Coin; -use rocket::http::Status; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; - -// TODO: this is not the best place to put it, it should be more centralised, -// but for a quick fix, that's good enough for now... -// (for proper solution we should be managing `NymNetworkDetails` via rocket and grabbing display exponent -// value from the mix denom here. -const UNYM_RATIO: f64 = 1000000.; - -fn unym_coin_to_float_unym(coin: Coin) -> f64 { - // our total supply can't exceed 1B so an overflow here is impossible - // (if it happened, then we SHOULD crash) - coin.amount as f64 / UNYM_RATIO -} - -#[openapi(tag = "circulating-supply")] -#[get("/circulating-supply")] -pub(crate) async fn get_full_circulating_supply( - cache: &State, -) -> Result, RocketErrorResponse> { - match cache.get_circulating_supply().await { - Some(value) => Ok(Json(value)), - None => Err(RocketErrorResponse::new( - "unavailable", - Status::InternalServerError, - )), - } -} - -#[openapi(tag = "circulating-supply")] -#[get("/circulating-supply/total-supply-value")] -pub(crate) async fn get_total_supply( - cache: &State, -) -> Result, RocketErrorResponse> { - let full_circulating_supply = match cache.get_circulating_supply().await { - Some(res) => res, - None => { - return Err(RocketErrorResponse::new( - "unavailable", - Status::InternalServerError, - )) - } - }; - - Ok(Json(unym_coin_to_float_unym( - full_circulating_supply.total_supply.into(), - ))) -} - -#[openapi(tag = "circulating-supply")] -#[get("/circulating-supply/circulating-supply-value")] -pub(crate) async fn get_circulating_supply( - cache: &State, -) -> Result, RocketErrorResponse> { - let full_circulating_supply = match cache.get_circulating_supply().await { - Some(res) => res, - None => { - return Err(RocketErrorResponse::new( - "unavailable", - Status::InternalServerError, - )) - } - }; - - Ok(Json(unym_coin_to_float_unym( - full_circulating_supply.circulating_supply.into(), - ))) -} diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index 9e6018550c..d956f2897d 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -1,28 +1,66 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::ecash::error::{EcashError, Result}; +use crate::ecash::api_routes::helpers::EpochIdParam; +use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use axum::extract::Path; +use axum::{Json, Router}; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_ecash_time::{cred_exp_date, EcashTime}; use nym_validator_client::nym_api::rfc_3339_date; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; +use serde::Deserialize; +use std::sync::Arc; use time::Date; use tracing::trace; +use utoipa::IntoParams; -// routes with globally aggregated keys, signatures, etc. +/// routes with globally aggregated keys, signatures, etc. +pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/master-verification-key:epoch_id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| master_verification_key(epoch_id, ecash_state) + }), + ) + .route( + "/aggregated-expiration-date-signatures:expiration_date", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |expiration_date| expiration_date_signatures(expiration_date, ecash_state) + }), + ) + .route( + "/aggregated-coin-indices-signatures:epoch_id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| coin_indices_signatures(epoch_id, ecash_state) + }), + ) +} -#[openapi(tag = "Ecash Global Data")] -#[get("/master-verification-key?")] -pub async fn master_verification_key( - epoch_id: Option, - state: &RocketState, -) -> Result> { +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/master-verification-key/{epoch_id}", + responses( + (status = 200, body = VerificationKeyResponse) + ) +)] +async fn master_verification_key( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { trace!("aggregated_verification_key request"); // see if we're not in the middle of new dkg @@ -33,12 +71,27 @@ pub async fn master_verification_key( Ok(Json(VerificationKeyResponse::new(key.clone()))) } -#[openapi(tag = "Ecash Global Data")] -#[get("/aggregated-expiration-date-signatures?")] -pub async fn expiration_date_signatures( +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct ExpirationDateParam { expiration_date: Option, - state: &RocketState, -) -> Result> { +} + +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + ExpirationDateParam + ), + path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}", + responses( + (status = 200, body = AggregatedExpirationDateSignatureResponse) + ) +)] +async fn expiration_date_signatures( + Path(ExpirationDateParam { expiration_date }): Path, + state: Arc, +) -> AxumResult> { trace!("aggregated_expiration_date_signatures request"); let expiration_date = match expiration_date { @@ -61,12 +114,21 @@ pub async fn expiration_date_signatures( })) } -#[openapi(tag = "Ecash Global Data")] -#[get("/aggregated-coin-indices-signatures?")] -pub async fn coin_indices_signatures( - epoch_id: Option, - state: &RocketState, -) -> Result> { +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}", + responses( + (status = 200, body = AggregatedCoinIndicesSignatureResponse) + ) +)] +async fn coin_indices_signatures( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { trace!("aggregated_coin_indices_signatures request"); // see if we're not in the middle of new dkg diff --git a/nym-api/src/ecash/api_routes/aggregation_axum.rs b/nym-api/src/ecash/api_routes/aggregation_axum.rs deleted file mode 100644 index d956f2897d..0000000000 --- a/nym-api/src/ecash/api_routes/aggregation_axum.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::api_routes::helpers::EpochIdParam; -use crate::ecash::error::EcashError; -use crate::ecash::state::EcashState; -use crate::node_status_api::models::AxumResult; -use crate::support::http::state::AppState; -use axum::extract::Path; -use axum::{Json, Router}; -use nym_api_requests::ecash::models::{ - AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, -}; -use nym_api_requests::ecash::VerificationKeyResponse; -use nym_ecash_time::{cred_exp_date, EcashTime}; -use nym_validator_client::nym_api::rfc_3339_date; -use serde::Deserialize; -use std::sync::Arc; -use time::Date; -use tracing::trace; -use utoipa::IntoParams; - -/// routes with globally aggregated keys, signatures, etc. -pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/master-verification-key:epoch_id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| master_verification_key(epoch_id, ecash_state) - }), - ) - .route( - "/aggregated-expiration-date-signatures:expiration_date", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |expiration_date| expiration_date_signatures(expiration_date, ecash_state) - }), - ) - .route( - "/aggregated-coin-indices-signatures:epoch_id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| coin_indices_signatures(epoch_id, ecash_state) - }), - ) -} - -#[utoipa::path( - tag = "Ecash Global Data", - get, - params( - EpochIdParam - ), - path = "/v1/ecash/master-verification-key/{epoch_id}", - responses( - (status = 200, body = VerificationKeyResponse) - ) -)] -async fn master_verification_key( - Path(EpochIdParam { epoch_id }): Path, - state: Arc, -) -> AxumResult> { - trace!("aggregated_verification_key request"); - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let key = state.master_verification_key(epoch_id).await?; - - Ok(Json(VerificationKeyResponse::new(key.clone()))) -} - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct ExpirationDateParam { - expiration_date: Option, -} - -#[utoipa::path( - tag = "Ecash Global Data", - get, - params( - ExpirationDateParam - ), - path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}", - responses( - (status = 200, body = AggregatedExpirationDateSignatureResponse) - ) -)] -async fn expiration_date_signatures( - Path(ExpirationDateParam { expiration_date }): Path, - state: Arc, -) -> AxumResult> { - trace!("aggregated_expiration_date_signatures request"); - - let expiration_date = match expiration_date { - None => cred_exp_date().ecash_date(), - Some(raw) => Date::parse(&raw, &rfc_3339_date()) - .map_err(|_| EcashError::MalformedExpirationDate { raw })?, - }; - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let expiration_date_signatures = state - .master_expiration_date_signatures(expiration_date) - .await?; - - Ok(Json(AggregatedExpirationDateSignatureResponse { - epoch_id: expiration_date_signatures.epoch_id, - expiration_date, - signatures: expiration_date_signatures.signatures.clone(), - })) -} - -#[utoipa::path( - tag = "Ecash Global Data", - get, - params( - EpochIdParam - ), - path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}", - responses( - (status = 200, body = AggregatedCoinIndicesSignatureResponse) - ) -)] -async fn coin_indices_signatures( - Path(EpochIdParam { epoch_id }): Path, - state: Arc, -) -> AxumResult> { - trace!("aggregated_coin_indices_signatures request"); - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; - - Ok(Json(AggregatedCoinIndicesSignatureResponse { - epoch_id: coin_indices_signatures.epoch_id, - signatures: coin_indices_signatures.signatures.clone(), - })) -} diff --git a/nym-api/src/ecash/api_routes/handlers.rs b/nym-api/src/ecash/api_routes/handlers.rs index 5b9d6cee46..e9f97c48b0 100644 --- a/nym-api/src/ecash/api_routes/handlers.rs +++ b/nym-api/src/ecash/api_routes/handlers.rs @@ -1,10 +1,10 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::ecash::api_routes::aggregation_axum::aggregation_routes; -use crate::ecash::api_routes::issued_axum::issued_routes; -use crate::ecash::api_routes::partial_signing_axum::partial_signing_routes; -use crate::ecash::api_routes::spending_axum::spending_routes; +use crate::ecash::api_routes::aggregation::aggregation_routes; +use crate::ecash::api_routes::issued::issued_routes; +use crate::ecash::api_routes::partial_signing::partial_signing_routes; +use crate::ecash::api_routes::spending::spending_routes; use crate::ecash::state::EcashState; use crate::support::http::state::AppState; use axum::Router; diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs index 5b85e6b0cf..95c1439600 100644 --- a/nym-api/src/ecash/api_routes/issued.rs +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -5,21 +5,64 @@ use crate::ecash::api_routes::helpers::build_credentials_response; use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::ecash::storage::EcashStorageExt; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use axum::extract::Path; +use axum::{Json, Router}; use nym_api_requests::ecash::models::{ EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, }; use nym_api_requests::ecash::CredentialsRequestBody; -use nym_coconut_dkg_common::types::EpochId; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; +use serde::Deserialize; +use std::sync::Arc; +use utoipa::IntoParams; -#[openapi(tag = "Ecash")] -#[get("/epoch-credentials/")] -pub async fn epoch_credentials( - epoch: EpochId, - state: &RocketState, -) -> crate::ecash::error::Result> { +pub(crate) fn issued_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/epoch-credentials/:epoch", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch| epoch_credentials(epoch, ecash_state) + }), + ) + .route( + "/issued-credential/:id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |id| issued_credential(id, ecash_state) + }), + ) + .route( + "/issued-credentials", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| issued_credentials(body, ecash_state) + }), + ) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct EpochParam { + epoch: u64, +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + EpochParam + ), + path = "/v1/ecash/epoch-credentials/{epoch}", + responses( + (status = 200, body = EpochCredentialsResponse) + ) +)] +async fn epoch_credentials( + Path(EpochParam { epoch }): Path, + state: Arc, +) -> AxumResult> { let issued = state.aux.storage.get_epoch_credentials(epoch).await?; let response = if let Some(issued) = issued { @@ -35,12 +78,27 @@ pub async fn epoch_credentials( Ok(Json(response)) } -#[openapi(tag = "Ecash")] -#[get("/issued-credential/")] -pub async fn issued_credential( +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct IdParam { id: i64, - state: &RocketState, -) -> crate::ecash::error::Result> { +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + IdParam + ), + path = "/v1/ecash/issued-credential/{id}", + responses( + (status = 200, body = IssuedCredentialResponse) + ) +)] +async fn issued_credential( + Path(IdParam { id }): Path, + state: Arc, +) -> AxumResult> { let issued = state.aux.storage.get_issued_credential(id).await?; let credential = if let Some(issued) = issued { @@ -52,16 +110,21 @@ pub async fn issued_credential( Ok(Json(IssuedCredentialResponse { credential })) } -#[openapi(tag = "Ecash")] -#[post("/issued-credentials", data = "")] -pub async fn issued_credentials( - params: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { - let params = params.into_inner(); - +#[utoipa::path( + tag = "Ecash", + post, + request_body = CredentialsRequestBody, + path = "/v1/ecash/issued-credentials", + responses( + (status = 200, body = IssuedCredentialsResponse) + ) +)] +async fn issued_credentials( + Json(params): Json, + state: Arc, +) -> AxumResult> { if params.pagination.is_some() && !params.credential_ids.is_empty() { - return Err(EcashError::InvalidQueryArguments); + return Err(EcashError::InvalidQueryArguments.into()); } let credentials = if let Some(pagination) = params.pagination { @@ -78,5 +141,7 @@ pub async fn issued_credentials( .await? }; - build_credentials_response(credentials).map(Json) + build_credentials_response(credentials) + .map(Json) + .map_err(From::from) } diff --git a/nym-api/src/ecash/api_routes/issued_axum.rs b/nym-api/src/ecash/api_routes/issued_axum.rs deleted file mode 100644 index 95c1439600..0000000000 --- a/nym-api/src/ecash/api_routes/issued_axum.rs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::api_routes::helpers::build_credentials_response; -use crate::ecash::error::EcashError; -use crate::ecash::state::EcashState; -use crate::ecash::storage::EcashStorageExt; -use crate::node_status_api::models::AxumResult; -use crate::support::http::state::AppState; -use axum::extract::Path; -use axum::{Json, Router}; -use nym_api_requests::ecash::models::{ - EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, -}; -use nym_api_requests::ecash::CredentialsRequestBody; -use serde::Deserialize; -use std::sync::Arc; -use utoipa::IntoParams; - -pub(crate) fn issued_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/epoch-credentials/:epoch", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch| epoch_credentials(epoch, ecash_state) - }), - ) - .route( - "/issued-credential/:id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |id| issued_credential(id, ecash_state) - }), - ) - .route( - "/issued-credentials", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| issued_credentials(body, ecash_state) - }), - ) -} - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct EpochParam { - epoch: u64, -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - EpochParam - ), - path = "/v1/ecash/epoch-credentials/{epoch}", - responses( - (status = 200, body = EpochCredentialsResponse) - ) -)] -async fn epoch_credentials( - Path(EpochParam { epoch }): Path, - state: Arc, -) -> AxumResult> { - let issued = state.aux.storage.get_epoch_credentials(epoch).await?; - - let response = if let Some(issued) = issued { - issued.into() - } else { - EpochCredentialsResponse { - epoch_id: epoch, - first_epoch_credential_id: None, - total_issued: 0, - } - }; - - Ok(Json(response)) -} - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct IdParam { - id: i64, -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - IdParam - ), - path = "/v1/ecash/issued-credential/{id}", - responses( - (status = 200, body = IssuedCredentialResponse) - ) -)] -async fn issued_credential( - Path(IdParam { id }): Path, - state: Arc, -) -> AxumResult> { - let issued = state.aux.storage.get_issued_credential(id).await?; - - let credential = if let Some(issued) = issued { - Some(issued.try_into()?) - } else { - None - }; - - Ok(Json(IssuedCredentialResponse { credential })) -} - -#[utoipa::path( - tag = "Ecash", - post, - request_body = CredentialsRequestBody, - path = "/v1/ecash/issued-credentials", - responses( - (status = 200, body = IssuedCredentialsResponse) - ) -)] -async fn issued_credentials( - Json(params): Json, - state: Arc, -) -> AxumResult> { - if params.pagination.is_some() && !params.credential_ids.is_empty() { - return Err(EcashError::InvalidQueryArguments.into()); - } - - let credentials = if let Some(pagination) = params.pagination { - state - .aux - .storage - .get_issued_credentials_paged(pagination) - .await? - } else { - state - .aux - .storage - .get_issued_credentials(params.credential_ids) - .await? - }; - - build_credentials_response(credentials) - .map(Json) - .map_err(From::from) -} diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs index 9c0082748c..6e1db9ffca 100644 --- a/nym-api/src/ecash/api_routes/mod.rs +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -7,8 +7,8 @@ mod helpers; // pub(crate) mod partial_signing; // pub(crate) mod spending; -pub(crate) mod aggregation_axum; +pub(crate) mod aggregation; pub(crate) mod handlers; -pub(crate) mod issued_axum; -pub(crate) mod partial_signing_axum; -pub(crate) mod spending_axum; +pub(crate) mod issued; +pub(crate) mod partial_signing; +pub(crate) mod spending; diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index a0958a2a6b..e42ffcf6e0 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -1,27 +1,65 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::ecash::api_routes::helpers::EpochIdParam; use crate::ecash::error::EcashError; use crate::ecash::helpers::blind_sign; use crate::ecash::state::EcashState; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use axum::extract::Path; +use axum::{Json, Router}; use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, }; use nym_ecash_time::{cred_exp_date, EcashTime}; use nym_validator_client::nym_api::rfc_3339_date; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; +use serde::Deserialize; use std::ops::Deref; +use std::sync::Arc; use time::Date; +use tracing::{debug, trace}; +use utoipa::IntoParams; -#[openapi(tag = "Ecash")] -#[post("/blind-sign", data = "")] -pub async fn post_blind_sign( - blind_sign_request_body: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { +pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/blind-sign", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| post_blind_sign(body, ecash_state) + }), + ) + .route( + "/partial-expiration-date-signatures:expiration_date", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state) + }), + ) + .route( + "/partial-coin-indices-signatures:epoch_id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state) + }), + ) +} + +#[utoipa::path( + tag = "Ecash", + post, + request_body = BlindSignRequestBody, + path = "/v1/ecash/blind-sign", + responses( + (status = 200, body = BlindedSignatureResponse) + ) +)] +async fn post_blind_sign( + Json(blind_sign_request_body): Json, + state: Arc, +) -> AxumResult> { debug!("Received blind sign request"); trace!("body: {:?}", blind_sign_request_body); @@ -31,7 +69,7 @@ pub async fn post_blind_sign( // basic check of expiration date validity if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { - return Err(EcashError::ExpirationDateTooLate); + return Err(EcashError::ExpirationDateTooLate.into()); } // see if we're not in the middle of new dkg @@ -62,24 +100,38 @@ pub async fn post_blind_sign( // produce the partial signature debug!("producing the partial credential"); - let blinded_signature = blind_sign(blind_sign_request_body.deref(), signing_key.deref())?; + let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?; // store the information locally debug!("storing the issued credential in the database"); state - .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) + .store_issued_credential(blind_sign_request_body, &blinded_signature) .await?; // finally return the credential to the client Ok(Json(BlindedSignatureResponse { blinded_signature })) } -#[openapi(tag = "Ecash")] -#[get("/partial-expiration-date-signatures?")] -pub async fn partial_expiration_date_signatures( +#[derive(Deserialize, IntoParams)] +struct ExpirationDateParam { expiration_date: Option, - state: &RocketState, -) -> crate::ecash::error::Result> { +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + ExpirationDateParam + ), + path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}", + responses( + (status = 200, body = PartialExpirationDateSignatureResponse) + ) +)] +async fn partial_expiration_date_signatures( + Path(ExpirationDateParam { expiration_date }): Path, + state: Arc, +) -> AxumResult> { let expiration_date = match expiration_date { None => cred_exp_date().ecash_date(), Some(raw) => Date::parse(&raw, &rfc_3339_date()) @@ -100,12 +152,21 @@ pub async fn partial_expiration_date_signatures( })) } -#[openapi(tag = "Ecash")] -#[get("/partial-coin-indices-signatures?")] -pub async fn partial_coin_indices_signatures( - epoch_id: Option, - state: &RocketState, -) -> crate::ecash::error::Result> { +#[utoipa::path( + tag = "Ecash", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}", + responses( + (status = 200, body = PartialExpirationDateSignatureResponse) + ) +)] +async fn partial_coin_indices_signatures( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; diff --git a/nym-api/src/ecash/api_routes/partial_signing_axum.rs b/nym-api/src/ecash/api_routes/partial_signing_axum.rs deleted file mode 100644 index e42ffcf6e0..0000000000 --- a/nym-api/src/ecash/api_routes/partial_signing_axum.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::api_routes::helpers::EpochIdParam; -use crate::ecash::error::EcashError; -use crate::ecash::helpers::blind_sign; -use crate::ecash::state::EcashState; -use crate::node_status_api::models::AxumResult; -use crate::support::http::state::AppState; -use axum::extract::Path; -use axum::{Json, Router}; -use nym_api_requests::ecash::{ - BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, - PartialExpirationDateSignatureResponse, -}; -use nym_ecash_time::{cred_exp_date, EcashTime}; -use nym_validator_client::nym_api::rfc_3339_date; -use serde::Deserialize; -use std::ops::Deref; -use std::sync::Arc; -use time::Date; -use tracing::{debug, trace}; -use utoipa::IntoParams; - -pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/blind-sign", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| post_blind_sign(body, ecash_state) - }), - ) - .route( - "/partial-expiration-date-signatures:expiration_date", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state) - }), - ) - .route( - "/partial-coin-indices-signatures:epoch_id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state) - }), - ) -} - -#[utoipa::path( - tag = "Ecash", - post, - request_body = BlindSignRequestBody, - path = "/v1/ecash/blind-sign", - responses( - (status = 200, body = BlindedSignatureResponse) - ) -)] -async fn post_blind_sign( - Json(blind_sign_request_body): Json, - state: Arc, -) -> AxumResult> { - debug!("Received blind sign request"); - trace!("body: {:?}", blind_sign_request_body); - - // check if we have the signing key available - debug!("checking if we actually have ecash keys derived..."); - let signing_key = state.ecash_signing_key().await?; - - // basic check of expiration date validity - if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { - return Err(EcashError::ExpirationDateTooLate.into()); - } - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - // check if we already issued a credential for this deposit - let deposit_id = blind_sign_request_body.deposit_id; - debug!( - "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", - ); - if let Some(blinded_signature) = state.already_issued(deposit_id).await? { - return Ok(Json(BlindedSignatureResponse { blinded_signature })); - } - - //check if account was blacklisted - let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string(); - state.aux.ensure_not_blacklisted(&pub_key_bs58).await?; - - // get the deposit details of the claimed id - debug!("getting deposit details from the chain"); - let deposit = state.get_deposit(deposit_id).await?; - - // check validity of the request - debug!("fully validating received request"); - state - .validate_request(&blind_sign_request_body, deposit) - .await?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?; - - // store the information locally - debug!("storing the issued credential in the database"); - state - .store_issued_credential(blind_sign_request_body, &blinded_signature) - .await?; - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[derive(Deserialize, IntoParams)] -struct ExpirationDateParam { - expiration_date: Option, -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - ExpirationDateParam - ), - path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}", - responses( - (status = 200, body = PartialExpirationDateSignatureResponse) - ) -)] -async fn partial_expiration_date_signatures( - Path(ExpirationDateParam { expiration_date }): Path, - state: Arc, -) -> AxumResult> { - let expiration_date = match expiration_date { - None => cred_exp_date().ecash_date(), - Some(raw) => Date::parse(&raw, &rfc_3339_date()) - .map_err(|_| EcashError::MalformedExpirationDate { raw })?, - }; - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let expiration_date_signatures = state - .partial_expiration_date_signatures(expiration_date) - .await?; - - Ok(Json(PartialExpirationDateSignatureResponse { - epoch_id: expiration_date_signatures.epoch_id, - expiration_date, - signatures: expiration_date_signatures.signatures.clone(), - })) -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - EpochIdParam - ), - path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}", - responses( - (status = 200, body = PartialExpirationDateSignatureResponse) - ) -)] -async fn partial_coin_indices_signatures( - Path(EpochIdParam { epoch_id }): Path, - state: Arc, -) -> AxumResult> { - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; - - Ok(Json(PartialCoinIndicesSignatureResponse { - epoch_id: coin_indices_signatures.epoch_id, - signatures: coin_indices_signatures.signatures.clone(), - })) -} diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index 1ea24efde6..a5110a52fc 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -3,6 +3,9 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; use nym_api_requests::ecash::models::{ BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, @@ -10,31 +13,62 @@ use nym_api_requests::ecash::models::{ }; use nym_compact_ecash::identify::IdentifyResult; use nym_ecash_time::EcashTime; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; use std::collections::HashSet; use std::ops::Deref; +use std::sync::Arc; use time::macros::time; use time::{OffsetDateTime, Time}; +use tracing::{error, warn}; + +pub(crate) fn spending_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/verify-ecash-ticket", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| verify_ticket(body, ecash_state) + }), + ) + .route( + "/batch-redeem-ecash-tickets", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| batch_redeem_tickets(body, ecash_state) + }), + ) + .route( + "/double-spending-filter-v1", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + || double_spending_filter_v1(ecash_state) + }), + ) +} const ONE_AM: Time = time!(1:00); fn reject_ticket( reason: EcashTicketVerificationRejection, -) -> crate::ecash::error::Result> { +) -> AxumResult> { Ok(Json(EcashTicketVerificationResponse::reject(reason))) } // TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` // introduce bloomfilter checks without touching storage first, etc. -#[openapi(tag = "Ecash")] -#[post("/verify-ecash-ticket", data = "")] -pub async fn verify_ticket( +#[utoipa::path( + tag = "Ecash", + post, + request_body = VerifyEcashTicketBody, + path = "/v1/ecash/verify-ecash-ticket", + responses( + (status = 200, body = EcashTicketVerificationResponse) + ) +)] +async fn verify_ticket( // TODO in the future: make it send binary data rather than json - verify_ticket_body: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { + Json(verify_ticket_body): Json, + state: Arc, +) -> AxumResult> { let credential_data = &verify_ticket_body.credential; let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; @@ -117,26 +151,30 @@ pub async fn verify_ticket( } // // for particular SN returns what gateway has submitted it and whether it has been verified correctly -// pub async fn credential_status() -> ! { +// async fn credential_status() -> ! { // todo!() // } -#[openapi(tag = "Ecash")] -#[post( - "/batch-redeem-ecash-tickets", - data = "" +#[utoipa::path( + tag = "Ecash", + post, + request_body = BatchRedeemTicketsBody, + path = "/v1/ecash/batch-redeem-ecash-tickets", + responses( + (status = 200, body = EcashBatchTicketRedemptionResponse) + ) )] -pub async fn batch_redeem_tickets( +async fn batch_redeem_tickets( // TODO in the future: make it send binary data rather than json - batch_redeem_credentials_body: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { + Json(batch_redeem_credentials_body): Json, + state: Arc, +) -> AxumResult> { // 1. see if that gateway has even submitted any tickets let Some(provider_info) = state .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) .await? else { - return Err(EcashError::NotTicketsProvided); + return Err(EcashError::NotTicketsProvided.into()); }; // 2. check if the gateway is not trying to spam the redemption requests @@ -149,13 +187,14 @@ pub async fn batch_redeem_tickets( return Err(EcashError::TooFrequentRedemption { last_redemption, next_allowed, - }); + } + .into()); } } // 3. verify the request digest if !batch_redeem_credentials_body.verify_digest() { - return Err(EcashError::MismatchedRequestDigest); + return Err(EcashError::MismatchedRequestDigest.into()); } // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) @@ -164,9 +203,7 @@ pub async fn batch_redeem_tickets( .await?; let proposal_id = batch_redeem_credentials_body.proposal_id; - let received = batch_redeem_credentials_body - .into_inner() - .included_serial_numbers; + let received = batch_redeem_credentials_body.included_serial_numbers; // 5. check if **every** serial number included in the request has been verified by us // if we have more than requested, tough luck, they're going to lose them @@ -177,7 +214,8 @@ pub async fn batch_redeem_tickets( if !verified_tickets.contains(sn.deref()) { return Err(EcashError::TicketNotVerified { serial_number_bs58: bs58::encode(sn).into_string(), - }); + } + .into()); } } @@ -190,11 +228,17 @@ pub async fn batch_redeem_tickets( // explicitly mark it as v1 in the URL because the response type WILL change; // it will probably be compressed bincode or something -#[openapi(tag = "Ecash")] -#[get("/double-spending-filter-v1")] -pub async fn double_spending_filter_v1( - state: &RocketState, -) -> crate::ecash::error::Result> { +#[utoipa::path( + tag = "Ecash", + get, + path = "/v1/ecash/double-spending-filter-v1", + responses( + (status = 200, body = SpentCredentialsResponse) + ) +)] +async fn double_spending_filter_v1( + state: Arc, +) -> AxumResult> { let spent_credentials_export = state.get_bloomfilter_bytes().await; Ok(Json(SpentCredentialsResponse::new( spent_credentials_export, diff --git a/nym-api/src/ecash/api_routes/spending_axum.rs b/nym-api/src/ecash/api_routes/spending_axum.rs deleted file mode 100644 index a5110a52fc..0000000000 --- a/nym-api/src/ecash/api_routes/spending_axum.rs +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::error::EcashError; -use crate::ecash::state::EcashState; -use crate::node_status_api::models::AxumResult; -use crate::support::http::state::AppState; -use axum::{Json, Router}; -use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; -use nym_api_requests::ecash::models::{ - BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, - EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody, -}; -use nym_compact_ecash::identify::IdentifyResult; -use nym_ecash_time::EcashTime; -use std::collections::HashSet; -use std::ops::Deref; -use std::sync::Arc; -use time::macros::time; -use time::{OffsetDateTime, Time}; -use tracing::{error, warn}; - -pub(crate) fn spending_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/verify-ecash-ticket", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| verify_ticket(body, ecash_state) - }), - ) - .route( - "/batch-redeem-ecash-tickets", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| batch_redeem_tickets(body, ecash_state) - }), - ) - .route( - "/double-spending-filter-v1", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - || double_spending_filter_v1(ecash_state) - }), - ) -} - -const ONE_AM: Time = time!(1:00); - -fn reject_ticket( - reason: EcashTicketVerificationRejection, -) -> AxumResult> { - Ok(Json(EcashTicketVerificationResponse::reject(reason))) -} - -// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` -// introduce bloomfilter checks without touching storage first, etc. -#[utoipa::path( - tag = "Ecash", - post, - request_body = VerifyEcashTicketBody, - path = "/v1/ecash/verify-ecash-ticket", - responses( - (status = 200, body = EcashTicketVerificationResponse) - ) -)] -async fn verify_ticket( - // TODO in the future: make it send binary data rather than json - Json(verify_ticket_body): Json, - state: Arc, -) -> AxumResult> { - let credential_data = &verify_ticket_body.credential; - let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; - - // easy check: is there only a single payment attached? - if credential_data.payment.spend_value != 1 { - return reject_ticket(EcashTicketVerificationRejection::MultipleTickets); - } - - let sn = &credential_data.encoded_serial_number(); - let spend_date = credential_data.spend_date; - let epoch_id = credential_data.epoch_id; - - let now = OffsetDateTime::now_utc(); - let today_ecash = now.ecash_date(); - - #[allow(clippy::unwrap_used)] - let yesterday_ecash = today_ecash.previous_day().unwrap(); - - // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC - if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { - return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { - today: today_ecash, - yesterday: yesterday_ecash, - received: spend_date, - }); - } - - // check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification - // TODO: when blacklisting is implemented, this should get removed - if state.check_bloomfilter(sn).await { - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); - } - - // actual double spend detection with storage - if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? { - match nym_compact_ecash::identify::identify( - &credential_data.payment, - &previous_payment.payment, - credential_data.pay_info, - previous_payment.pay_info, - ) { - IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? - IdentifyResult::DuplicatePayInfo(_) => { - warn!("Identical payInfo"); - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); - } - IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { - //Actual double spending - warn!( - "Double spending attempt for key {}", - pub_key.to_base58_string() - ); - error!("UNIMPLEMENTED: blacklisting the double spend key"); - return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); - } - } - } - - let verification_key = state.master_verification_key(Some(epoch_id)).await?; - - // perform actual crypto verification - if credential_data.verify(&verification_key).is_err() { - return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); - } - - // finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN - let was_present = state - .update_bloomfilter(sn, spend_date, today_ecash) - .await?; - if was_present { - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); - } - - //store credential - state - .store_verified_ticket(credential_data, gateway_cosmos_addr) - .await?; - - Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) -} - -// // for particular SN returns what gateway has submitted it and whether it has been verified correctly -// async fn credential_status() -> ! { -// todo!() -// } - -#[utoipa::path( - tag = "Ecash", - post, - request_body = BatchRedeemTicketsBody, - path = "/v1/ecash/batch-redeem-ecash-tickets", - responses( - (status = 200, body = EcashBatchTicketRedemptionResponse) - ) -)] -async fn batch_redeem_tickets( - // TODO in the future: make it send binary data rather than json - Json(batch_redeem_credentials_body): Json, - state: Arc, -) -> AxumResult> { - // 1. see if that gateway has even submitted any tickets - let Some(provider_info) = state - .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) - .await? - else { - return Err(EcashError::NotTicketsProvided.into()); - }; - - // 2. check if the gateway is not trying to spam the redemption requests - // (we have to protect our poor chain) - if let Some(last_redemption) = provider_info.last_batch_verification { - let now = OffsetDateTime::now_utc(); - let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY; - - if next_allowed > now { - return Err(EcashError::TooFrequentRedemption { - last_redemption, - next_allowed, - } - .into()); - } - } - - // 3. verify the request digest - if !batch_redeem_credentials_body.verify_digest() { - return Err(EcashError::MismatchedRequestDigest.into()); - } - - // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) - state - .validate_redemption_proposal(&batch_redeem_credentials_body) - .await?; - - let proposal_id = batch_redeem_credentials_body.proposal_id; - let received = batch_redeem_credentials_body.included_serial_numbers; - - // 5. check if **every** serial number included in the request has been verified by us - // if we have more than requested, tough luck, they're going to lose them - let verified = state.get_redeemable_tickets(provider_info).await?; - let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect(); - - for sn in &received { - if !verified_tickets.contains(sn.deref()) { - return Err(EcashError::TicketNotVerified { - serial_number_bs58: bs58::encode(sn).into_string(), - } - .into()); - } - } - - // TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx - state.accept_proposal(proposal_id).await?; - Ok(Json(EcashBatchTicketRedemptionResponse { - proposal_accepted: true, - })) -} - -// explicitly mark it as v1 in the URL because the response type WILL change; -// it will probably be compressed bincode or something -#[utoipa::path( - tag = "Ecash", - get, - path = "/v1/ecash/double-spending-filter-v1", - responses( - (status = 200, body = SpentCredentialsResponse) - ) -)] -async fn double_spending_filter_v1( - state: Arc, -) -> AxumResult> { - let spent_credentials_export = state.get_bloomfilter_bytes().await; - Ok(Json(SpentCredentialsResponse::new( - spent_credentials_export, - ))) -} diff --git a/nym-api/src/ecash/mod.rs b/nym-api/src/ecash/mod.rs index 2855f8dd7a..6a1d04564a 100644 --- a/nym-api/src/ecash/mod.rs +++ b/nym-api/src/ecash/mod.rs @@ -16,27 +16,3 @@ pub(crate) mod tests; // equivalent of 100nym pub(crate) const MINIMUM_BALANCE: u128 = 100_000000; - -// pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { -// if enabled { -// openapi_get_routes_spec![ -// settings: -// api_routes::partial_signing::post_blind_sign, -// api_routes::partial_signing::partial_expiration_date_signatures, -// api_routes::partial_signing::partial_coin_indices_signatures, -// api_routes::spending::verify_ticket, -// api_routes::spending::batch_redeem_tickets, -// api_routes::spending::double_spending_filter_v1, -// api_routes::issued::epoch_credentials, -// api_routes::issued::issued_credential, -// api_routes::issued::issued_credentials, -// api_routes::aggregation::master_verification_key, -// api_routes::aggregation::coin_indices_signatures, -// api_routes::aggregation::expiration_date_signatures -// ] -// } else { -// openapi_get_routes_spec![ -// settings: -// ] -// } -// } diff --git a/nym-api/src/network/mod.rs b/nym-api/src/network/mod.rs index 8595827dd0..016fc441b5 100644 --- a/nym-api/src/network/mod.rs +++ b/nym-api/src/network/mod.rs @@ -3,12 +3,3 @@ pub(crate) mod handlers; pub(crate) mod models; -// mod routes; -// -// pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { -// openapi_get_routes_spec![ -// settings: routes::network_details, -// routes::nym_contracts, -// routes::nym_contracts_detailed -// ] -// } diff --git a/nym-api/src/network/routes.rs b/nym-api/src/network/routes.rs deleted file mode 100644 index 73625f8b88..0000000000 --- a/nym-api/src/network/routes.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::network::models::{ContractInformation, NetworkDetails}; -use crate::nym_contract_cache::cache::NymContractCache; -use nym_contracts_common::ContractBuildInformation; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; -use std::collections::HashMap; -use std::ops::Deref; - -#[openapi(tag = "network")] -#[get("/details")] -pub(crate) fn network_details(details: &State) -> Json { - Json(details.deref().clone()) -} - -// I agree, it feels weird to be pulling contract cache here, but I feel like it makes -// more sense to return this information here rather than in the generic cache route -#[openapi(tag = "network")] -#[get("/nym-contracts")] -pub(crate) async fn nym_contracts( - cache: &State, -) -> Json>> { - let info = cache.contract_details().await; - Json( - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.base.clone(), - }, - ) - }) - .collect(), - ) -} - -#[openapi(tag = "network")] -#[get("/nym-contracts-detailed")] -pub(crate) async fn nym_contracts_detailed( - cache: &State, -) -> Json>> { - let info = cache.contract_details().await; - Json( - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.detailed.clone(), - }, - ) - }) - .collect(), - ) -} diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 3f7ed14b16..942a476467 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -16,7 +16,6 @@ pub(crate) mod handlers; pub(crate) mod helpers; pub(crate) mod models; pub(crate) mod reward_estimate; -// pub(crate) mod routes_deprecated; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -24,52 +23,6 @@ pub(crate) const FIFTEEN_MINUTES: Duration = Duration::from_secs(900); pub(crate) const ONE_HOUR: Duration = Duration::from_secs(3600); pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); -// pub(crate) fn node_status_routes( -// settings: &OpenApiSettings, -// enabled: bool, -// ) -> (Vec, OpenApi) { -// if enabled { -// openapi_get_routes_spec![ -// settings: routes_deprecated::gateway_report, -// routes_deprecated::gateway_uptime_history, -// routes_deprecated::gateway_core_status_count, -// routes_deprecated::mixnode_report, -// routes_deprecated::mixnode_uptime_history, -// routes_deprecated::mixnode_core_status_count, -// routes_deprecated::get_mixnode_status, -// routes_deprecated::get_mixnode_reward_estimation, -// routes_deprecated::compute_mixnode_reward_estimation, -// routes_deprecated::get_mixnode_stake_saturation, -// routes_deprecated::get_mixnode_inclusion_probability, -// routes_deprecated::get_mixnode_avg_uptime, -// routes_deprecated::get_gateway_avg_uptime, -// routes_deprecated::get_mixnode_inclusion_probabilities, -// routes_deprecated::get_mixnodes_detailed, -// routes_deprecated::get_mixnodes_detailed_unfiltered, -// routes_deprecated::get_rewarded_set_detailed, -// routes_deprecated::get_active_set_detailed, -// routes_deprecated::get_gateways_detailed, -// routes_deprecated::get_gateways_detailed_unfiltered, -// routes_deprecated::unstable::mixnode_test_results, -// routes_deprecated::unstable::gateway_test_results, -// routes_deprecated::submit_gateway_monitoring_results, -// routes_deprecated::submit_node_monitoring_results, -// ] -// } else { -// // in the minimal variant we would not have access to endpoints relying on existence -// // of the network monitor and the associated storage -// openapi_get_routes_spec![ -// settings: routes_deprecated::get_mixnode_status, -// routes_deprecated::get_mixnode_stake_saturation, -// routes_deprecated::get_mixnode_inclusion_probability, -// routes_deprecated::get_mixnode_inclusion_probabilities, -// routes_deprecated::get_mixnodes_detailed, -// routes_deprecated::get_rewarded_set_detailed, -// routes_deprecated::get_active_set_detailed, -// ] -// } -// } - /// Spawn the node status cache refresher. /// /// It is primarily refreshed in-sync with the nym contract cache, however provide a fallback diff --git a/nym-api/src/node_status_api/routes_deprecated.rs b/nym-api/src/node_status_api/routes_deprecated.rs deleted file mode 100644 index fa49ad692a..0000000000 --- a/nym-api/src/node_status_api/routes_deprecated.rs +++ /dev/null @@ -1,615 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::NodeStatusCache; -use crate::node_status_api::helpers_deprecated::{ - _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, - _gateway_uptime_history, _get_gateway_avg_uptime, _get_mixnode_avg_uptime, - _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, - _get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status, - _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report, - _mixnode_uptime_history, -}; -use crate::node_status_api::models::RocketErrorResponse; -use crate::storage::NymApiStorage; -use crate::NymContractCache; -use nym_api_requests::models::{ - AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, - GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, - GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, - MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, - UptimeResponse, -}; -use nym_mixnet_contract_common::NodeId; -use nym_types::monitoring::MonitorMessage; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; - -#[openapi(tag = "status", deprecated = true)] -#[post("/submit-gateway-monitoring-results", data = "")] -pub(crate) async fn submit_gateway_monitoring_results( - message: Json, - storage: &State, -) -> Result<(), RocketErrorResponse> { - todo!("rebasing"); - // if !message.from_allowed() { - // return Err(RocketErrorResponse::new( - // "Monitor not registered to submit results".to_string(), - // rocket::http::Status::Forbidden, - // )); - // } - // - // if !message.timely() { - // return Err(RocketErrorResponse::new( - // "Message is too old".to_string(), - // rocket::http::Status::BadRequest, - // )); - // } - // - // if !message.verify() { - // return Err(RocketErrorResponse::new( - // "Invalid signature".to_string(), - // rocket::http::Status::BadRequest, - // )); - // } - // - // match storage - // .manager - // .submit_gateway_statuses_v2(message.results()) - // .await - // { - // Ok(_) => Ok(()), - // Err(err) => { - // error!("failed to submit gateway monitoring results: {}", err); - // Err(RocketErrorResponse::new( - // "failed to submit gateway monitoring results".to_string(), - // rocket::http::Status::InternalServerError, - // )) - // } - // } -} - -#[openapi(tag = "status")] -#[post("/submit-node-monitoring-results", data = "")] -pub(crate) async fn submit_node_monitoring_results( - message: Json, - storage: &State, -) -> Result<(), RocketErrorResponse> { - todo!("rebasing"); - - // if !message.from_allowed() { - // return Err(RocketErrorResponse::new( - // "Monitor not registered to submit results".to_string(), - // rocket::http::Status::Forbidden, - // )); - // } - // - // if !message.timely() { - // return Err(RocketErrorResponse::new( - // "Message is too old".to_string(), - // rocket::http::Status::BadRequest, - // )); - // } - // - // if !message.verify() { - // return Err(RocketErrorResponse::new( - // "Invalid signature".to_string(), - // rocket::http::Status::BadRequest, - // )); - // } - // - // match storage - // .manager - // .submit_mixnode_statuses_v2(message.results()) - // .await - // { - // Ok(_) => Ok(()), - // Err(err) => { - // error!("failed to submit node monitoring results: {}", err); - // Err(RocketErrorResponse::new( - // "failed to submit node monitoring results".to_string(), - // rocket::http::Status::InternalServerError, - // )) - // } - // } -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/gateway//report")] -pub(crate) async fn gateway_report( - cache: &State, - identity: &str, -) -> Result, RocketErrorResponse> { - todo!("rebasing"); - - // Ok(Json(_gateway_report(cache, identity).await?)) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/gateway//history")] -pub(crate) async fn gateway_uptime_history( - storage: &State, - nym_contract_cache: &State, - identity: &str, -) -> Result, RocketErrorResponse> { - todo!("rebasing"); - - // Ok(Json( - // _gateway_uptime_history(storage, nym_contract_cache, identity).await?, - // )) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/gateway//core-status-count?")] -pub(crate) async fn gateway_core_status_count( - storage: &State, - identity: &str, - since: Option, -) -> Result, RocketErrorResponse> { - Ok(Json( - _gateway_core_status_count(storage, identity, since).await?, - )) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//report")] -pub(crate) async fn mixnode_report( - cache: &State, - mix_id: NodeId, -) -> Result, RocketErrorResponse> { - Ok(Json(_mixnode_report(cache, mix_id).await?)) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//history")] -pub(crate) async fn mixnode_uptime_history( - storage: &State, - nym_contract_cache: &State, - mix_id: NodeId, -) -> Result, RocketErrorResponse> { - todo!("rebasing"); - - // Ok(Json( - // _mixnode_uptime_history(storage, nym_contract_cache, mix_id).await?, - // )) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//core-status-count?")] -pub(crate) async fn mixnode_core_status_count( - storage: &State, - mix_id: NodeId, - since: Option, -) -> Result, RocketErrorResponse> { - Ok(Json( - _mixnode_core_status_count(storage, mix_id, since).await?, - )) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//status")] -pub(crate) async fn get_mixnode_status( - cache: &State, - mix_id: NodeId, -) -> Json { - Json(_get_mixnode_status(cache, mix_id).await) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//reward-estimation")] -pub(crate) async fn get_mixnode_reward_estimation( - cache: &State, - validator_cache: &State, - mix_id: NodeId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?, - )) -} - -#[openapi(tag = "status", deprecated = true)] -#[post( - "/mixnode//compute-reward-estimation", - data = "" -)] -pub(crate) async fn compute_mixnode_reward_estimation( - user_reward_param: Json, - cache: &State, - validator_cache: &State, - mix_id: NodeId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _compute_mixnode_reward_estimation( - user_reward_param.into_inner(), - cache, - validator_cache, - mix_id, - ) - .await?, - )) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//stake-saturation")] -pub(crate) async fn get_mixnode_stake_saturation( - cache: &State, - validator_cache: &State, - mix_id: NodeId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?, - )) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//inclusion-probability")] -pub(crate) async fn get_mixnode_inclusion_probability( - cache: &State, - mix_id: NodeId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _get_mixnode_inclusion_probability(cache, mix_id).await?, - )) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnode//avg_uptime")] -pub(crate) async fn get_mixnode_avg_uptime( - cache: &State, - mix_id: NodeId, -) -> Result, RocketErrorResponse> { - Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?)) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/gateway//avg_uptime")] -pub(crate) async fn get_gateway_avg_uptime( - cache: &State, - identity: &str, -) -> Result, RocketErrorResponse> { - Ok(Json(_get_gateway_avg_uptime(cache, identity).await?)) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnodes/inclusion_probability")] -pub(crate) async fn get_mixnode_inclusion_probabilities( - cache: &State, -) -> Result, RocketErrorResponse> { - Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?)) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnodes/detailed")] -pub async fn get_mixnodes_detailed( - cache: &State, -) -> Json> { - todo!("rebasing"); - - // Json(_get_legacy_mixnodes_detailed(cache).await) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnodes/detailed-unfiltered")] -pub async fn get_mixnodes_detailed_unfiltered( - cache: &State, -) -> Json> { - Json(_get_mixnodes_detailed_unfiltered(cache).await) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnodes/rewarded/detailed")] -pub async fn get_rewarded_set_detailed( - status_cache: &State, - contract_cache: &State, -) -> Json> { - todo!("rebasing"); - - // Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/mixnodes/active/detailed")] -pub async fn get_active_set_detailed( - status_cache: &State, - contract_cache: &State, -) -> Json> { - todo!("rebasing"); - - // Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/gateways/detailed")] -pub async fn get_gateways_detailed( - cache: &State, -) -> Json> { - todo!("rebasing"); - - // Json(_get_legacy_gateways_detailed(cache).await) -} - -#[openapi(tag = "status", deprecated = true)] -#[get("/gateways/detailed-unfiltered")] -pub async fn get_gateways_detailed_unfiltered( - cache: &State, -) -> Json> { - todo!("rebasing"); - // Json(_get_legacy_gateways_detailed_unfiltered(cache).await) -} - -pub mod unstable { - use crate::node_status_api::models::RocketErrorResponse; - use crate::support::http::helpers::PaginationRequest; - use crate::support::storage::NymApiStorage; - use nym_api_requests::models::{ - GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, - TestRoute, - }; - use nym_api_requests::pagination::Pagination; - use nym_mixnet_contract_common::NodeId; - use rocket::http::Status; - use rocket::serde::json::Json; - use rocket::State; - use rocket_okapi::openapi; - use std::cmp::min; - use std::collections::HashMap; - use std::sync::Arc; - use tokio::sync::RwLock; - - pub type DbId = i64; - - // a simply in-memory cache of node details - #[derive(Debug, Default)] - pub struct NodeInfoCache { - inner: Arc>, - } - - impl NodeInfoCache { - async fn get_mix_node_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode { - { - let read_guard = self.inner.read().await; - if let Some(cached) = read_guard.mixnodes.get(&db_id) { - trace!("cache hit for mixnode {db_id}"); - return cached.clone(); - } - } - trace!("cache miss for mixnode {db_id}"); - - let mut write_guard = self.inner.write().await; - // double-check the cache in case somebody already updated it while we were waiting for the lock - if let Some(cached) = write_guard.mixnodes.get(&db_id) { - return cached.clone(); - } - - let details = match storage.get_mixnode_details_by_db_id(db_id).await { - Ok(Some(details)) => details.into(), - Ok(None) => { - error!("somebody has been messing with the database! details for mixnode with database id {db_id} have been removed!"); - TestNode::default() - } - Err(err) => { - // don't insert into the cache in case another request is successful - error!("failed to retrieve details for mixnode {db_id}: {err}"); - return TestNode::default(); - } - }; - - write_guard.mixnodes.insert(db_id, details.clone()); - details - } - - async fn get_gateway_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode { - { - let read_guard = self.inner.read().await; - if let Some(cached) = read_guard.gateways.get(&db_id) { - trace!("cache hit for gateway {db_id}"); - return cached.clone(); - } - } - trace!("cache miss for gateway {db_id}"); - - let mut write_guard = self.inner.write().await; - // double-check the cache in case somebody already updated it while we were waiting for the lock - if let Some(cached) = write_guard.gateways.get(&db_id) { - return cached.clone(); - } - - let details = match storage.get_gateway_details_by_db_id(db_id).await { - Ok(Some(details)) => details.into(), - Ok(None) => { - error!("somebody has been messing with the database! details for gateway with database id {db_id} have been removed!"); - TestNode::default() - } - Err(err) => { - // don't insert into the cache in case another request is successful - error!("failed to retrieve details for gateway {db_id}: {err}"); - return TestNode::default(); - } - }; - - write_guard.gateways.insert(db_id, details.clone()); - details - } - } - - #[derive(Debug, Default)] - struct NodeInfoCacheInner { - mixnodes: HashMap, - gateways: HashMap, - } - - const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100; - const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; - - async fn _mixnode_test_results( - mix_id: NodeId, - page: u32, - per_page: u32, - info_cache: &State, - storage: &State, - ) -> anyhow::Result { - // convert to db offset - // we're paging from page 0 like civilised people, - // so we have to skip (page * per_page) results - let offset = page * per_page; - let limit = per_page; - - let raw_results = storage - .get_mixnode_detailed_statuses(mix_id, limit, offset) - .await?; - let total = match raw_results.first() { - None => 0, - Some(r) => storage.get_mixnode_detailed_statuses_count(r.db_id).await?, - }; - - let mut partial_results = Vec::new(); - for result in raw_results { - let gateway = info_cache - .get_gateway_details(result.gateway_id, storage) - .await; - let layer1 = info_cache - .get_mix_node_details(result.layer1_mix_id, storage) - .await; - let layer2 = info_cache - .get_mix_node_details(result.layer2_mix_id, storage) - .await; - let layer3 = info_cache - .get_mix_node_details(result.layer3_mix_id, storage) - .await; - - partial_results.push(PartialTestResult { - monitor_run_id: result.monitor_run_id, - timestamp: result.timestamp, - overall_reliability_for_all_routes_in_monitor_run: result.reliability, - test_routes: TestRoute { - gateway, - layer1, - layer2, - layer3, - }, - }) - } - - Ok(MixnodeTestResultResponse { - pagination: Pagination { - total, - page, - size: partial_results.len(), - }, - data: partial_results, - }) - } - - #[openapi(tag = "UNSTABLE - DO **NOT** USE")] - #[get("/mixnodes/unstable//test-results?")] - pub async fn mixnode_test_results( - mix_id: NodeId, - pagination: PaginationRequest, - info_cache: &State, - storage: &State, - ) -> Result, RocketErrorResponse> { - let page = pagination.page.unwrap_or_default(); - let per_page = min( - pagination - .per_page - .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), - MAX_TEST_RESULTS_PAGE_SIZE, - ); - - match _mixnode_test_results(mix_id, page, per_page, info_cache, storage).await { - Ok(res) => Ok(Json(res)), - Err(err) => Err(RocketErrorResponse::new( - format!("failed to retrieve mixnode test results for node {mix_id}: {err}"), - Status::InternalServerError, - )), - } - } - - async fn _gateway_test_results( - gateway_identity: &str, - page: u32, - per_page: u32, - info_cache: &State, - storage: &State, - ) -> anyhow::Result { - // convert to db offset - // we're paging from page 0 like civilised people, - // so we have to skip (page * per_page) results - let offset = page * per_page; - let limit = per_page; - - let raw_results = storage - .get_gateway_detailed_statuses(gateway_identity, limit, offset) - .await?; - let total = match raw_results.first() { - None => 0, - Some(r) => storage.get_gateway_detailed_statuses_count(r.db_id).await?, - }; - - let mut partial_results = Vec::new(); - for result in raw_results { - let gateway = info_cache - .get_gateway_details(result.gateway_id, storage) - .await; - let layer1 = info_cache - .get_mix_node_details(result.layer1_mix_id, storage) - .await; - let layer2 = info_cache - .get_mix_node_details(result.layer2_mix_id, storage) - .await; - let layer3 = info_cache - .get_mix_node_details(result.layer3_mix_id, storage) - .await; - - partial_results.push(PartialTestResult { - monitor_run_id: result.monitor_run_id, - timestamp: result.timestamp, - overall_reliability_for_all_routes_in_monitor_run: result.reliability, - test_routes: TestRoute { - gateway, - layer1, - layer2, - layer3, - }, - }) - } - - Ok(GatewayTestResultResponse { - pagination: Pagination { - total, - page, - size: partial_results.len(), - }, - data: partial_results, - }) - } - - #[openapi(tag = "UNSTABLE - DO **NOT** USE")] - #[get("/gateways/unstable//test-results?")] - pub async fn gateway_test_results( - gateway_identity: &str, - pagination: PaginationRequest, - info_cache: &State, - storage: &State, - ) -> Result, RocketErrorResponse> { - let page = pagination.page.unwrap_or_default(); - let per_page = min( - pagination - .per_page - .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), - MAX_TEST_RESULTS_PAGE_SIZE, - ); - - match _gateway_test_results(gateway_identity, page, per_page, info_cache, storage).await { - Ok(res) => Ok(Json(res)), - Err(err) => Err(RocketErrorResponse::new( - format!( - "failed to retrieve mixnode test results for gateway {gateway_identity}: {err}" - ), - Status::InternalServerError, - )), - } - } -} diff --git a/nym-api/src/nym_contract_cache/legacy_helpers.rs b/nym-api/src/nym_contract_cache/legacy_helpers.rs deleted file mode 100644 index 939f19b3a9..0000000000 --- a/nym-api/src/nym_contract_cache/legacy_helpers.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/nym_contract_cache/mod.rs index 49f4147a69..6ca509ef1e 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/nym_contract_cache/mod.rs @@ -9,25 +9,6 @@ use self::cache::refresher::NymContractCacheRefresher; pub(crate) mod cache; pub(crate) mod handlers; -pub(crate) mod legacy_helpers; -// pub(crate) mod routes; -// -// pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { -// openapi_get_routes_spec![ -// settings: routes::get_mixnodes, -// routes::get_mixnodes_detailed, -// routes::get_gateways, -// routes::get_active_set, -// routes::get_active_set_detailed, -// routes::get_rewarded_set, -// routes::get_rewarded_set_detailed, -// routes::get_blacklisted_mixnodes, -// routes::get_blacklisted_gateways, -// routes::get_blacklisted_gateways_v2, -// routes::get_interval_reward_params, -// routes::get_current_epoch, -// ] -// } pub(crate) fn start_refresher( config: &config::NodeStatusAPI, diff --git a/nym-api/src/nym_contract_cache/routes.rs b/nym-api/src/nym_contract_cache/routes.rs deleted file mode 100644 index 52a9dd70e0..0000000000 --- a/nym-api/src/nym_contract_cache/routes.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::{node_status_api::NodeStatusCache, nym_contract_cache::cache::NymContractCache}; -use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; - -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use rocket::{serde::json::Json, State}; -use rocket_okapi::openapi; -use std::collections::HashSet; - -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/mixnodes")] -pub async fn get_mixnodes( - cache: &State, -) -> Json> { - Json(cache.legacy_mixnodes_filtered().await) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/detailed")) -// } -// ``` -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/mixnodes/detailed")] -pub async fn get_mixnodes_detailed( - cache: &State, -) -> Json> { - todo!("rebasing") - // Json(_get_legacy_mixnodes_detailed(cache).await) -} - -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/gateways")] -pub async fn get_gateways(cache: &State) -> Json> { - Json( - cache - .legacy_gateways_filtered() - .await - .into_iter() - .map(Into::into) - .collect(), - ) -} - -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/mixnodes/rewarded")] -pub async fn get_rewarded_set( - cache: &State, -) -> Json> { - Json(cache.legacy_v1_rewarded_set_mixnodes().await.clone()) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_set_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed")) -// } -// ``` -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/mixnodes/rewarded/detailed")] -pub async fn get_rewarded_set_detailed( - status_cache: &State, - contract_cache: &State, -) -> Json> { - todo!("rebasing") - // Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) -} - -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/mixnodes/active")] -pub async fn get_active_set( - cache: &State, -) -> Json> { - Json(cache.legacy_v1_active_set_mixnodes().await.clone()) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_active_set_detailed() -> Redirect { -// Redirect::to(uri!("/status/mixnodes/active/detailed")) -// } -// ``` -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/mixnodes/active/detailed")] -pub async fn get_active_set_detailed( - status_cache: &State, - contract_cache: &State, -) -> Json> { - todo!("rebasing") - // Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) -} - -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/mixnodes/blacklisted")] -pub async fn get_blacklisted_mixnodes( - cache: &State, -) -> Json>> { - let blacklist = cache.mixnodes_blacklist().await.clone(); - if blacklist.is_empty() { - Json(None) - } else { - Json(Some(blacklist)) - } -} - -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/gateways/blacklisted")] -pub async fn get_blacklisted_gateways( - cache: &State, -) -> Json>> { - let blacklist = cache.gateways_blacklist().await.clone(); - if blacklist.is_empty() { - Json(None) - } else { - let gateways = cache.legacy_gateways_all().await; - Json(Some( - gateways - .into_iter() - .filter(|g| blacklist.contains(&g.node_id)) - .map(|g| g.gateway.identity_key.clone()) - .collect(), - )) - } -} - -#[openapi(tag = "contract-cache", deprecated = true)] -#[get("/gateways/blacklisted_v2")] -pub async fn get_blacklisted_gateways_v2( - cache: &State, -) -> Json>> { - let blacklist = cache.gateways_blacklist().await.clone(); - if blacklist.is_empty() { - Json(None) - } else { - Json(Some(blacklist)) - } -} - -#[openapi(tag = "contract-cache")] -#[get("/epoch/reward_params")] -pub async fn get_interval_reward_params( - cache: &State, -) -> Json> { - Json(*cache.interval_reward_params().await) -} - -#[openapi(tag = "contract-cache")] -#[get("/epoch/current")] -pub async fn get_current_epoch(cache: &State) -> Json> { - Json(*cache.current_interval().await) -} diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs index 33a7ff2fd8..8e3725c7ec 100644 --- a/nym-api/src/nym_nodes/mod.rs +++ b/nym-api/src/nym_nodes/mod.rs @@ -2,34 +2,3 @@ // SPDX-License-Identifier: GPL-3.0-only pub(crate) mod handlers; - -// pub(crate) mod routes; -// mod unstable_routes; - -// /// Merges the routes with http information and returns it to Rocket for serving -// pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec, OpenApi) { -// openapi_get_routes_spec![ -// settings: -// routes::get_gateways_described, -// routes::get_mixnodes_described, -// ] -// } -// -// pub(crate) fn nym_node_routes_next(settings: &OpenApiSettings) -> (Vec, OpenApi) { -// openapi_get_routes_spec![ -// settings: -// unstable_routes::nodes_basic, -// unstable_routes::nodes_expanded, -// unstable_routes::nodes_detailed, -// unstable_routes::gateways_basic, -// unstable_routes::gateways_expanded, -// unstable_routes::gateways_detailed, -// unstable_routes::mixnodes_basic, -// unstable_routes::mixnodes_expanded, -// unstable_routes::mixnodes_detailed, -// routes::all_described_nodes, -// routes::node_description, -// routes::node_annotation_by_identity, -// routes::node_annotation -// ] -// } diff --git a/nym-api/src/nym_nodes/routes.rs b/nym-api/src/nym_nodes/routes.rs deleted file mode 100644 index f5574418cc..0000000000 --- a/nym-api/src/nym_nodes/routes.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::support::caching::cache::SharedCache; -use nym_api_requests::models::{ - AnnotationResponse, LegacyDescribedGateway, LegacyDescribedMixNode, NymNodeDescription, -}; -use nym_mixnet_contract_common::NodeId; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; -use std::ops::Deref; - -#[openapi(tag = "Nym Nodes")] -#[get("/all/described")] -pub async fn all_described_nodes( - describe_cache: &State>, -) -> Json> { - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(Vec::new()); - }; - - Json(self_descriptions.all_nodes().cloned().collect()) -} - -#[openapi(tag = "Nym Nodes")] -#[get("/all//described")] -pub async fn node_description( - node_id: NodeId, - describe_cache: &State>, -) -> Json> { - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(None); - }; - - Json(self_descriptions.get_node(&node_id).cloned()) -} - -#[openapi(tag = "Nym Nodes")] -#[get("/annotation-by-identity/")] -pub async fn node_annotation_by_identity( - identity: String, - status_cache: &State, -) -> Json { - let Some(node_id) = status_cache.map_identity_to_node_id(&identity).await else { - return Json(Default::default()); - }; - node_annotation(node_id, status_cache).await -} - -#[openapi(tag = "Nym Nodes")] -#[get("/annotation/")] -pub async fn node_annotation( - node_id: NodeId, - status_cache: &State, -) -> Json { - let Some(annotation) = status_cache.node_annotations().await else { - return Json(Default::default()); - }; - - Json(AnnotationResponse { - node_id: Some(node_id), - annotation: annotation.get(&node_id).cloned(), - }) -} - -/// This only returns descriptions of **legacy** gateways -#[openapi(tag = "Nym Nodes", deprecated = true)] -#[get("/gateways/described")] -pub async fn get_gateways_described( - contract_cache: &State, - describe_cache: &State>, -) -> Json> { - let gateways = contract_cache.legacy_gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); - }; - - Json( - gateways - .into_iter() - .map(|bond| LegacyDescribedGateway { - self_described: self_descriptions - .deref() - .get_description(&bond.node_id) - .cloned(), - bond, - }) - .collect(), - ) -} - -/// This only returns descriptions of **legacy** mixnodes -#[openapi(tag = "Nym Nodes", deprecated = true)] -#[get("/mixnodes/described")] -pub async fn get_mixnodes_described( - contract_cache: &State, - describe_cache: &State>, -) -> Json> { - let mixnodes = contract_cache - .legacy_mixnodes_filtered() - .await - .into_iter() - .map(|m| m.bond_information) - .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); - }; - - Json( - mixnodes - .into_iter() - .map(|bond| LegacyDescribedMixNode { - self_described: self_descriptions - .deref() - .get_description(&bond.mix_id) - .cloned(), - bond, - }) - .collect(), - ) -} diff --git a/nym-api/src/nym_nodes/unstable_routes.rs b/nym-api/src/nym_nodes/unstable_routes.rs deleted file mode 100644 index 0e178da389..0000000000 --- a/nym-api/src/nym_nodes/unstable_routes.rs +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::models::RocketErrorResponse; -use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::support::caching::cache::SharedCache; -use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRole, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, -}; -use nym_bin_common::version_checker; -use rocket::http::Status; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; -use std::collections::HashSet; - -/* - routes: - - // all routes/nodes are split into three tiers: - // /skimmed => [used by clients] returns the very basic information for routing purposes - // /semi-skimmed => [used by other nodes/VPN] returns more additional information such noise keys - // /full-fat => [used by explorers, et al.] returns almost everything there is about the nodes - - // there's also additional split based on the role: - ?role => filters based on the specific role (mixnode/gateway/(in the future: entry/exit)) - /mixnodes/ => only returns mixnode role data - /gateway/ => only returns (entry) gateway role data - - -*/ - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/skimmed?&")] -pub async fn nodes_basic( - status_cache: &State, - contract_cache: &State, - describe_cache: &State>, - role: Option, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_basic( - status_cache, - contract_cache, - describe_cache, - semver_compatibility, - ) - .await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_basic( - status_cache, - contract_cache, - describe_cache, - semver_compatibility, - ) - .await - } - _ => {} - } - } - - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/semi-skimmed?&")] -pub async fn nodes_expanded( - cache: &State, - role: Option, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_expanded(cache, semver_compatibility).await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_expanded(cache, semver_compatibility).await - } - _ => {} - } - } - - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/full-fat?&")] -pub async fn nodes_detailed( - cache: &State, - role: Option, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_detailed(cache, semver_compatibility).await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_detailed(cache, semver_compatibility).await - } - _ => {} - } - } - - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/gateways/skimmed?")] -pub async fn gateways_basic( - status_cache: &State, - contract_cache: &State, - describe_cache: &State>, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - // 1. get the rewarded set - let rewarded_set = contract_cache - .rewarded_set() - .await - .ok_or_else(RocketErrorResponse::internal_server_error)?; - - // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic - let active_gateways = rewarded_set.gateways().into_iter().collect::>(); - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = status_cache - .node_annotations() - .await - .ok_or_else(RocketErrorResponse::internal_server_error)?; - - // 3. grab all legacy gateways - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_gateways = status_cache - .annotated_legacy_gateways() - .await - .ok_or_else(RocketErrorResponse::internal_server_error)?; - - // 4. grab all relevant described nym-nodes - let describe_cache = describe_cache.get().await?; - let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); - - // 5. only return nodes that are present in the active set - let mut active_skimmed_gateways = Vec::new(); - - for (node_id, legacy) in annotated_legacy_gateways.iter() { - if !active_gateways.contains(node_id) { - continue; - } - - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = legacy.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - active_skimmed_gateways.push( - described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), - ) - } else { - active_skimmed_gateways.push(legacy.into()); - } - } - - for nym_node in gateway_capable_nym_nodes { - // if this node is not an active gateway, ignore it - if !active_gateways.contains(&nym_node.node_id) { - continue; - } - - // if we have wrong version, ignore - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = nym_node.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit - let role = if rewarded_set.is_exit(&nym_node.node_id) { - NodeRole::ExitGateway - } else { - NodeRole::EntryGateway - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations - .get(&nym_node.node_id) - .copied() - .unwrap_or_default(); - - active_skimmed_gateways - .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // min of all caches - let refreshed_at = [ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_gateways.timestamp(), - ] - .into_iter() - .min() - .unwrap() - .into(); - - Ok(Json(CachedNodesResponse { - refreshed_at, - nodes: active_skimmed_gateways, - })) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/gateways/semi-skimmed?")] -pub async fn gateways_expanded( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/gateways/full-fat?")] -pub async fn gateways_detailed( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/mixnodes/skimmed?")] -pub async fn mixnodes_basic( - status_cache: &State, - contract_cache: &State, - describe_cache: &State>, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - // 1. get the rewarded set - let rewarded_set = contract_cache - .rewarded_set() - .await - .ok_or_else(RocketErrorResponse::internal_server_error)?; - - // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic - let active_mixnodes = rewarded_set - .active_mixnodes() - .into_iter() - .collect::>(); - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = status_cache - .node_annotations() - .await - .ok_or_else(RocketErrorResponse::internal_server_error)?; - - // 3. grab all legacy mixnodes - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_mixnodes = status_cache - .annotated_legacy_mixnodes() - .await - .ok_or_else(RocketErrorResponse::internal_server_error)?; - - // 4. grab all relevant described nym-nodes - let describe_cache = describe_cache.get().await?; - let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); - - // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it - - // 5. only return nodes that are present in the active set - let mut active_skimmed_mixnodes = Vec::new(); - - for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { - if !active_mixnodes.contains(mix_id) { - continue; - } - - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = legacy.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(mix_id) { - active_skimmed_mixnodes.push(described.to_skimmed_node( - NodeRole::Mixnode { - layer: legacy.mixnode_details.bond_information.layer.into(), - }, - legacy.node_performance.last_24h, - )) - } else { - active_skimmed_mixnodes.push(legacy.into()); - } - } - - for nym_node in mixing_nym_nodes { - // if this node is not an active mixnode, ignore it - if !active_mixnodes.contains(&nym_node.node_id) { - continue; - } - - // if we have wrong version, ignore - if let Some(semver_compat) = semver_compatibility.as_ref() { - let version = nym_node.version(); - if !version_checker::is_minor_version_compatible(version, semver_compat) { - continue; - } - } - - // SAFETY: if we determined our node IS active, it MUST have a layer - // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data - #[allow(clippy::unwrap_used)] - let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations - .get(&nym_node.node_id) - .copied() - .unwrap_or_default(); - - active_skimmed_mixnodes.push( - nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), - ); - } - - // min of all caches - let refreshed_at = [ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_mixnodes.timestamp(), - ] - .into_iter() - .min() - .unwrap() - .into(); - - Ok(Json(CachedNodesResponse { - refreshed_at, - nodes: active_skimmed_mixnodes, - })) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/mixnodes/semi-skimmed?")] -pub async fn mixnodes_expanded( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/mixnodes/full-fat?")] -pub async fn mixnodes_detailed( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 5e74eeffb6..0ed94a1819 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -8,7 +8,6 @@ use nym_bin_common::build_information::BinaryBuildInformation; use tokio::time::Instant; pub(crate) mod handlers; -// pub(crate) mod routes; pub(crate) struct ApiStatusState { startup_time: Instant, @@ -40,12 +39,3 @@ impl ApiStatusState { self.signer_information = Some(signer_information) } } - -// pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { -// openapi_get_routes_spec![ -// settings: -// routes::health, -// routes::build_information, -// routes::signer_information -// ] -// } diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs deleted file mode 100644 index 911e9e2a9b..0000000000 --- a/nym-api/src/status/routes.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::models::RocketErrorResponse; -use crate::status::ApiStatusState; -use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; -use nym_bin_common::build_information::BinaryBuildInformationOwned; -use nym_compact_ecash::Base58; -use rocket::http::Status; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; - -#[openapi(tag = "Api Status")] -#[get("/health")] -pub(crate) async fn health(state: &State) -> Json { - let uptime = state.startup_time.elapsed(); - let health = ApiHealthResponse::new_healthy(uptime); - Json(health) -} - -#[openapi(tag = "Api Status")] -#[get("/build-information")] -pub(crate) async fn build_information( - state: &State, -) -> Json { - Json(state.build_information.to_owned()) -} - -#[openapi(tag = "Api Status")] -#[get("/signer-information")] -pub(crate) async fn signer_information( - state: &State, -) -> Result, RocketErrorResponse> { - let signer_state = state.signer_information.as_ref().ok_or_else(|| { - RocketErrorResponse::new( - "this api does not expose zk-nym signing functionalities", - Status::InternalServerError, - ) - })?; - - Ok(Json(SignerInformationResponse { - cosmos_address: signer_state.cosmos_address.clone(), - identity: signer_state.identity.clone(), - announce_address: signer_state.announce_address.clone(), - verification_key: signer_state - .ecash_keypair - .verification_key() - .await - .map(|maybe_vk| maybe_vk.to_bs58()), - })) -} From 8758bea17cdbd759ed4f5934f9bd6eb604b2fe3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 14 Oct 2024 11:39:12 +0200 Subject: [PATCH 074/207] Use ticket type when retrieving from storage (#4947) --- Cargo.lock | 1 + common/bandwidth-controller/src/lib.rs | 12 ++++++++---- common/client-libs/gateway-client/Cargo.toml | 1 + common/client-libs/gateway-client/src/client/mod.rs | 7 ++++++- common/commands/src/ecash/generate_ticket.rs | 7 ++++++- common/commands/src/ecash/issue_ticket_book.rs | 2 +- common/credential-storage/src/backends/memory.rs | 2 ++ common/credential-storage/src/backends/sqlite.rs | 3 +++ common/credential-storage/src/ephemeral_storage.rs | 5 +++-- .../credential-storage/src/persistent_storage/mod.rs | 5 ++++- common/credential-storage/src/storage.rs | 3 ++- 11 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e74311fa1..a35a6acaaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5251,6 +5251,7 @@ dependencies = [ "nym-bandwidth-controller", "nym-credential-storage", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", "nym-network-defaults", diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index a164d3df60..a9b15db1fb 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -16,7 +16,7 @@ use nym_credential_storage::models::RetrievedTicketbook; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::{ - AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth, + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, TicketType, VerificationKeyAuth, }; use nym_ecash_time::Date; use nym_validator_client::nym_api::EpochId; @@ -64,9 +64,10 @@ impl BandwidthController { BandwidthController { storage, client } } - /// Tries to retrieve one of the stored, unused credentials that hasn't yet expired. + /// Tries to retrieve one of the stored, unused credentials for the given type that hasn't yet expired. pub async fn get_next_usable_ticketbook( &self, + ticketbook_type: TicketType, tickets: u32, ) -> Result where @@ -74,7 +75,7 @@ impl BandwidthController { { let Some(ticketbook) = self .storage - .get_next_unspent_usable_ticketbook(tickets) + .get_next_unspent_usable_ticketbook(ticketbook_type.to_string(), tickets) .await .map_err(BandwidthControllerError::credential_storage_error)? else { @@ -181,6 +182,7 @@ impl BandwidthController { pub async fn prepare_ecash_ticket( &self, + ticketbook_type: TicketType, provider_pk: [u8; 32], tickets_to_spend: u32, ) -> Result @@ -188,7 +190,9 @@ impl BandwidthController { C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let retrieved_ticketbook = self.get_next_usable_ticketbook(tickets_to_spend).await?; + let retrieved_ticketbook = self + .get_next_usable_ticketbook(ticketbook_type, tickets_to_spend) + .await?; let ticketbook_id = retrieved_ticketbook.ticketbook_id; let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 418debb4a9..81b9b4d7f4 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -24,6 +24,7 @@ zeroize.workspace = true nym-bandwidth-controller = { path = "../../bandwidth-controller" } nym-credentials = { path = "../../credentials" } nym-credential-storage = { path = "../../credential-storage" } +nym-credentials-interface = { path = "../../credentials-interface" } nym-crypto = { path = "../../crypto" } nym-gateway-requests = { path = "../../gateway-requests" } nym-network-defaults = { path = "../../network-defaults" } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 2278c6fa47..58e061887e 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -16,6 +16,7 @@ use nym_bandwidth_controller::{BandwidthController, BandwidthStatusMessage}; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ @@ -748,7 +749,11 @@ impl GatewayClient { } let prepared_credential = self .unchecked_bandwidth_controller() - .prepare_ecash_ticket(self.gateway_identity.to_bytes(), TICKETS_TO_SPEND) + .prepare_ecash_ticket( + TicketType::V1MixnetEntry, + self.gateway_identity.to_bytes(), + TICKETS_TO_SPEND, + ) .await?; match self.claim_ecash_bandwidth(prepared_credential.data).await { diff --git a/common/commands/src/ecash/generate_ticket.rs b/common/commands/src/ecash/generate_ticket.rs index 67a85ab093..0da2a878c4 100644 --- a/common/commands/src/ecash/generate_ticket.rs +++ b/common/commands/src/ecash/generate_ticket.rs @@ -9,10 +9,15 @@ use comfy_table::Table; use nym_credential_storage::initialise_persistent_storage; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials_interface::TicketType; use std::path::PathBuf; #[derive(Debug, Parser)] pub struct Args { + /// Specify which type of ticketbook + #[clap(long, default_value_t = TicketType::V1MixnetEntry)] + pub(crate) ticketbook_type: TicketType, + /// Specify the index of the ticket to retrieve from the ticketbook. /// By default, the current unspent value is used. #[clap(long, group = "output")] @@ -62,7 +67,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { let persistent_storage = initialise_persistent_storage(&credentials_store).await; let Some(mut next_ticketbook) = persistent_storage - .get_next_unspent_usable_ticketbook(0) + .get_next_unspent_usable_ticketbook(args.ticketbook_type.to_string(), 0) .await? else { bail!( diff --git a/common/commands/src/ecash/issue_ticket_book.rs b/common/commands/src/ecash/issue_ticket_book.rs index 28e8c3eafc..0097f4676a 100644 --- a/common/commands/src/ecash/issue_ticket_book.rs +++ b/common/commands/src/ecash/issue_ticket_book.rs @@ -107,7 +107,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()> utils::issue_credential(&client, &credentials_store, &secret, args.ticketbook_type).await?; let ticketbook = credentials_store - .get_next_unspent_usable_ticketbook(0) + .get_next_unspent_usable_ticketbook(args.ticketbook_type.to_string(), 0) .await? .ok_or(anyhow!("we just issued a ticketbook, it must be present!"))? .ticketbook; diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index d107677897..91d197d632 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -73,6 +73,7 @@ impl MemoryEcachTicketbookManager { pub async fn get_next_unspent_ticketbook_and_update( &self, + ticketbook_type: String, tickets: u32, ) -> Option { let mut guard = self.inner.write().await; @@ -81,6 +82,7 @@ impl MemoryEcachTicketbookManager { if !t.ticketbook.expired() && t.ticketbook.spent_tickets() + tickets as u64 <= t.ticketbook.params_total_tickets() + && t.ticketbook.ticketbook_type().to_string() == ticketbook_type { t.ticketbook .update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64); diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 8c5b20c7d8..9267bbddb3 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -284,6 +284,7 @@ impl SqliteEcashTicketbookManager { pub(crate) async fn get_next_unspent_ticketbook<'a, E>( executor: E, + ticketbook_type: String, deadline: Date, tickets: u32, ) -> Result, sqlx::Error> @@ -296,12 +297,14 @@ where FROM ecash_ticketbook WHERE used_tickets + ? <= total_tickets AND expiration_date >= ? + AND ticketbook_type = ? ORDER BY expiration_date ASC LIMIT 1 "#, ) .bind(tickets) .bind(deadline) + .bind(ticketbook_type) .fetch_optional(executor) .await } diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index 4b3ef93660..91436d4d8c 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -85,17 +85,18 @@ impl Storage for EphemeralStorage { Ok(()) } - /// Tries to retrieve one of the stored ticketbook, + /// Tries to retrieve one of the stored ticketbook for the specified type, /// that has not yet expired and has required number of unspent tickets. /// it immediately updated the on-disk number of used tickets so that another task /// could obtain their own tickets at the same time async fn get_next_unspent_usable_ticketbook( &self, + ticketbook_type: String, tickets: u32, ) -> Result, Self::StorageError> { Ok(self .storage_manager - .get_next_unspent_ticketbook_and_update(tickets) + .get_next_unspent_ticketbook_and_update(ticketbook_type, tickets) .await) } diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index c5daceabd5..8f5cf7610d 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -171,13 +171,16 @@ impl Storage for PersistentStorage { /// could obtain their own tickets at the same time async fn get_next_unspent_usable_ticketbook( &self, + ticketbook_type: String, tickets: u32, ) -> Result, Self::StorageError> { let deadline = ecash_today().ecash_date(); let mut tx = self.storage_manager.begin_storage_tx().await?; // we don't want ticketbooks with expiration in the past - let Some(raw) = get_next_unspent_ticketbook(&mut tx, deadline, tickets).await? else { + let Some(raw) = + get_next_unspent_ticketbook(&mut tx, ticketbook_type, deadline, tickets).await? + else { // make sure to finish our tx tx.commit().await?; return Ok(None); diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index 7033cd6e1f..19ddc44e86 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -45,12 +45,13 @@ pub trait Storage: Send + Sync { async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError>; - /// Tries to retrieve one of the stored ticketbook, + /// Tries to retrieve one of the stored ticketbook for the specified type, /// that has not yet expired and has required number of unspent tickets. /// it immediately updated the on-disk number of used tickets so that another task /// could obtain their own tickets at the same time async fn get_next_unspent_usable_ticketbook( &self, + ticketbook_type: String, tickets: u32, ) -> Result, Self::StorageError>; From b68fca0efa4cbedd5233e46341002a64b41b6007 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Mon, 14 Oct 2024 12:32:29 +0200 Subject: [PATCH 075/207] feat: add clap and env vars through clap, add Dockerfile --- nym-data-observatory/Cargo.toml | 1 + nym-data-observatory/Dockerfile | 21 +++++++++++++++ nym-data-observatory/docker-compose.yml | 14 ++++++++-- nym-data-observatory/src/db/mod.rs | 4 +-- nym-data-observatory/src/main.rs | 34 ++++++++++++++++++------- 5 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 nym-data-observatory/Dockerfile diff --git a/nym-data-observatory/Cargo.toml b/nym-data-observatory/Cargo.toml index b4a1ea8619..0ef35d100f 100644 --- a/nym-data-observatory/Cargo.toml +++ b/nym-data-observatory/Cargo.toml @@ -17,6 +17,7 @@ readme.workspace = true anyhow = { workspace = true } axum = { workspace = true, features = ["tokio"] } chrono = { workspace = true } +clap = { workspace = true, features = ["derive", "env"] } nym-bin-common = { path = "../common/bin-common" } nym-network-defaults = { path = "../common/network-defaults" } nym-task = { path = "../common/task" } diff --git a/nym-data-observatory/Dockerfile b/nym-data-observatory/Dockerfile new file mode 100644 index 0000000000..2fb545f4d9 --- /dev/null +++ b/nym-data-observatory/Dockerfile @@ -0,0 +1,21 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-data-observatory + +RUN cargo build --release + +FROM debian:12-slim + +RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \ + echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ + echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom + +RUN apt update + +RUN apt install -yy curl + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-data-observatory ./ +CMD [ "/nym/nym-data-observatory" ] diff --git a/nym-data-observatory/docker-compose.yml b/nym-data-observatory/docker-compose.yml index 3d93d058f3..1877e1979e 100644 --- a/nym-data-observatory/docker-compose.yml +++ b/nym-data-observatory/docker-compose.yml @@ -2,12 +2,22 @@ services: postgres: image: postgres:13 container_name: nym-data-observatory-pg - env_file: - - .env + environment: + POSTGRES_PASSWORD: password ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data + data-observatory: + image: nym-data-observatory:latest + build: + context: ../ + dockerfile: nym-data-observatory/Dockerfile + container_name: nym-data-observatory + environment: + NYM_DATA_OBSERVATORY_CONNECTION_URL: "postgres://postgres:password@postgres:5432" + NYM_DATA_OBSERVATORY_HTTP_PORT: 8000 + volumes: pgdata: diff --git a/nym-data-observatory/src/db/mod.rs b/nym-data-observatory/src/db/mod.rs index 307d2a97b3..ef90996da8 100644 --- a/nym-data-observatory/src/db/mod.rs +++ b/nym-data-observatory/src/db/mod.rs @@ -16,8 +16,8 @@ pub(crate) struct Storage { } impl Storage { - pub async fn init() -> Result { - let connection_url = std::env::var(DATABASE_URL_ENV_VAR).map_err(anyhow::Error::from)?; + pub async fn init(connection_url: Option) -> Result { + let connection_url = connection_url.unwrap(); let connect_options = { let mut connect_options = PgConnectOptions::from_str(&connection_url)?; let connect_options = connect_options.disable_statement_logging(); diff --git a/nym-data-observatory/src/main.rs b/nym-data-observatory/src/main.rs index 074279d2ed..8fed033503 100644 --- a/nym-data-observatory/src/main.rs +++ b/nym-data-observatory/src/main.rs @@ -1,3 +1,4 @@ +use clap::Parser; use nym_network_defaults::setup_env; use nym_task::signal::wait_for_signal; @@ -8,29 +9,44 @@ mod db; mod http; mod logging; +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Port to listen on + #[arg(long, default_value_t = 8000, env = "NYM_DATA_OBSERVATORY_HTTP_PORT")] + http_port: u16, + + /// Path to the environment file + #[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_ENV_FILE")] + env_file: Option, + + /// DB connection url + #[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_URL")] + connection_url: Option, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { logging::setup_tracing_logger(); - // if dotenv file is present, load its values - // otherwise, default to mainnet - setup_env(Some(".env")); + let args = Args::parse(); + + setup_env(args.env_file); // Defaults to mainnet if empty let conf = config::Config::from_env(); tracing::debug!("Using config:\n{:?}", conf); - let storage = db::Storage::init().await?; + let storage = db::Storage::init(args.connection_url).await?; let db_pool = storage.pool_owned().await; tokio::spawn(async move { background_task::spawn_in_background(db_pool).await; tracing::info!("Started task"); }); - let shutdown_handles = - http::server::start_http_api(storage.pool_owned().await, conf.http_port()) - .await - .expect("Failed to start server"); - tracing::info!("Started HTTP server on port {}", conf.http_port()); + let shutdown_handles = http::server::start_http_api(storage.pool_owned().await, args.http_port) + .await + .expect("Failed to start server"); + tracing::info!("Started HTTP server on port {}", args.http_port); wait_for_signal().await; From 16edca21b04acdbae8d1d1de593e06d6b32cf341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 11:41:36 +0100 Subject: [PATCH 076/207] allow to optionally skip state migration --- .../mixnet-contract/src/msg.rs | 1 + contracts/mixnet/src/contract.rs | 12 ++++++++---- .../testnet-manager/src/manager/network_init.rs | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 06caec5376..0e728aaf63 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -830,4 +830,5 @@ pub enum QueryMsg { #[cw_serde] pub struct MigrateMsg { pub vesting_contract_address: Option, + pub unsafe_skip_state_updates: Option, } diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index b3e3cc5df2..25bca25919 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -576,11 +576,15 @@ pub fn migrate( set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - // remove all family-related things - crate::queued_migrations::families_purge(deps.branch())?; + let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false); - // prepare the ground for using nym-nodes rather than standalone mixnodes/gateways - migrate_to_nym_nodes_usage(deps.branch(), &msg)?; + if !skip_state_updates { + // remove all family-related things + crate::queued_migrations::families_purge(deps.branch())?; + + // prepare the ground for using nym-nodes rather than standalone mixnodes/gateways + migrate_to_nym_nodes_usage(deps.branch(), &msg)?; + } // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index bdbf39cf32..b298f9b981 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -109,6 +109,7 @@ impl NetworkManager { ) -> Result { Ok(nym_mixnet_contract_common::MigrateMsg { vesting_contract_address: Some(ctx.network.contracts.vesting.address()?.to_string()), + unsafe_skip_state_updates: Some(true), }) } From c14481bb777fc4433f1ac714c770435259b277b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 14:50:36 +0100 Subject: [PATCH 077/207] allow nym-api to control bind address with CLI --- nym-api/src/support/cli/init.rs | 6 ++++++ nym-api/src/support/cli/run.rs | 6 ++++++ nym-api/src/support/config/mod.rs | 3 +++ nym-api/src/support/config/override.rs | 7 +++++++ 4 files changed, 22 insertions(+) diff --git a/nym-api/src/support/cli/init.rs b/nym-api/src/support/cli/init.rs index 10ce41b6f9..a51933f150 100644 --- a/nym-api/src/support/cli/init.rs +++ b/nym-api/src/support/cli/init.rs @@ -4,6 +4,7 @@ use crate::support::config::default_config_filepath; use crate::support::config::helpers::initialise_new; use anyhow::bail; +use std::net::SocketAddr; #[derive(clap::Args, Debug)] pub(crate) struct Args { @@ -51,6 +52,11 @@ pub(crate) struct Args { /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement #[clap(long, requires = "enable_monitor")] pub(crate) monitor_credentials_mode: bool, + + /// Socket address this api will use for binding its http API. + /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` + #[clap(long)] + pub(crate) bind_address: Option, // #[clap(short, long, default_value_t = OutputFormat::default())] // output: OutputFormat, } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 08e8d49f94..c3fabc81e1 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -35,6 +35,7 @@ use nym_config::defaults::NymNetworkDetails; use nym_sphinx::receiver::SphinxMessageReceiver; use nym_task::TaskManager; use nym_validator_client::nyxd::Coin; +use std::net::SocketAddr; use std::sync::Arc; use tokio_util::sync::CancellationToken; use tracing::{error, info}; @@ -86,6 +87,11 @@ pub(crate) struct Args { /// default: None - config value will be used instead #[clap(long)] pub(crate) monitor_credentials_mode: Option, + + /// Socket address this api will use for binding its http API. + /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` + #[clap(long)] + pub(crate) bind_address: Option, } async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result { diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 235464dc0a..8d1809668f 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -169,6 +169,9 @@ impl Config { if let Some(monitor_credentials_mode) = args.monitor_credentials_mode { self.network_monitor.debug.disabled_credentials_mode = !monitor_credentials_mode } + if let Some(http_bind_address) = args.bind_address { + self.base.bind_address = http_bind_address + } self } diff --git a/nym-api/src/support/config/override.rs b/nym-api/src/support/config/override.rs index dea892d2a9..059d0f8136 100644 --- a/nym-api/src/support/config/override.rs +++ b/nym-api/src/support/config/override.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::cli::{init, run}; +use std::net::SocketAddr; // Configuration that can be overridden. pub(crate) struct OverrideConfig { @@ -26,6 +27,10 @@ pub(crate) struct OverrideConfig { /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement pub(crate) monitor_credentials_mode: Option, + + /// Socket address this api will use for binding its http API. + /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` + pub(crate) bind_address: Option, } impl From for OverrideConfig { @@ -38,6 +43,7 @@ impl From for OverrideConfig { enable_zk_nym: Some(args.enable_zk_nym), announce_address: args.announce_address, monitor_credentials_mode: Some(args.monitor_credentials_mode), + bind_address: args.bind_address, } } } @@ -52,6 +58,7 @@ impl From for OverrideConfig { enable_zk_nym: args.enable_zk_nym, announce_address: args.announce_address, monitor_credentials_mode: args.monitor_credentials_mode, + bind_address: args.bind_address, } } } From 11f6db5304e0a1ca2c2d9fa3823bd666cb70ce94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 14:51:02 +0100 Subject: [PATCH 078/207] fixed compatibility with 'skimmed' endpoints by making "no_legacy" argument optional --- nym-api/src/nym_nodes/handlers/unstable/mod.rs | 4 ++-- nym-api/src/nym_nodes/handlers/unstable/skimmed.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index add55ec427..a9110a599d 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -81,7 +81,7 @@ struct NodesParamsWithRole { role: Option, semver_compatibility: Option, - no_legacy: bool, + no_legacy: Option, page: Option, per_page: Option, } @@ -89,7 +89,7 @@ struct NodesParamsWithRole { #[derive(Debug, Deserialize, utoipa::IntoParams)] struct NodesParams { semver_compatibility: Option, - no_legacy: bool, + no_legacy: Option, page: Option, per_page: Option, } diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index df263d2e06..53dace0e09 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -143,7 +143,7 @@ where ); // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes - if query_params.no_legacy { + if let Some(true) = query_params.no_legacy { // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), From e87b00bce5fd56aadc1522876e2ba0314c95c299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 15:14:12 +0100 Subject: [PATCH 079/207] add the nym-nodes count to refresher log --- nym-api/src/nym_contract_cache/cache/refresher.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index a46ef92afd..0e9e6f45f9 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -180,9 +180,10 @@ impl NymContractCacheRefresher { let contract_info = self.get_nym_contracts_info().await?; info!( - "Updating validator cache. There are {} mixnodes and {} gateways", + "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", mixnodes.len(), gateways.len(), + nym_nodes.len(), ); self.cache From 1219dd9719d3f7e3cb9023b9636a06a26e6dbd85 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Mon, 14 Oct 2024 16:59:09 +0200 Subject: [PATCH 080/207] changed debian for ubuntu docker image --- nym-data-observatory/Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nym-data-observatory/Dockerfile b/nym-data-observatory/Dockerfile index 2fb545f4d9..33969aa14f 100644 --- a/nym-data-observatory/Dockerfile +++ b/nym-data-observatory/Dockerfile @@ -5,15 +5,13 @@ WORKDIR /usr/src/nym/nym-data-observatory RUN cargo build --release -FROM debian:12-slim +FROM ubuntu:24.04 RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \ echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom -RUN apt update - -RUN apt install -yy curl +RUN apt update && apt install -yy curl WORKDIR /nym From 67179510372143417772c3d70b07522e4d2ba6b7 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Mon, 14 Oct 2024 17:31:03 +0200 Subject: [PATCH 081/207] feat: added dockerfile for nym-credentials-proxy --- nym-vpn-api/vpn-api-server/Dockerfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 nym-vpn-api/vpn-api-server/Dockerfile diff --git a/nym-vpn-api/vpn-api-server/Dockerfile b/nym-vpn-api/vpn-api-server/Dockerfile new file mode 100644 index 0000000000..039f37c74e --- /dev/null +++ b/nym-vpn-api/vpn-api-server/Dockerfile @@ -0,0 +1,13 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-vpn-api/vpn-api-server + +RUN cargo build --release + +FROM ubuntu:24.04 + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/nym-vpn-api/target/release/nym-vpn-api ./ +CMD [ "/nym/nym-vpn-api" ] From 99cf7d1eec27884287ec6da9724a2d3b1351d885 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Mon, 14 Oct 2024 18:03:22 +0200 Subject: [PATCH 082/207] feat: add Dockerfile for nym node --- nym-node/Dockerfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 nym-node/Dockerfile diff --git a/nym-node/Dockerfile b/nym-node/Dockerfile new file mode 100644 index 0000000000..d7a47a28e9 --- /dev/null +++ b/nym-node/Dockerfile @@ -0,0 +1,13 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node + +RUN cargo build --release + +FROM ubuntu:24.04 + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node ./ +ENTRYPOINT [ "/nym/nym-node" ] From 62c94d58e89d990b928219e92b05dc1524360294 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Mon, 14 Oct 2024 18:04:05 +0200 Subject: [PATCH 083/207] Fix: CMD -> ENTRYPOINT --- nym-vpn-api/vpn-api-server/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-vpn-api/vpn-api-server/Dockerfile b/nym-vpn-api/vpn-api-server/Dockerfile index 039f37c74e..2942290a2a 100644 --- a/nym-vpn-api/vpn-api-server/Dockerfile +++ b/nym-vpn-api/vpn-api-server/Dockerfile @@ -10,4 +10,4 @@ FROM ubuntu:24.04 WORKDIR /nym COPY --from=builder /usr/src/nym/nym-vpn-api/target/release/nym-vpn-api ./ -CMD [ "/nym/nym-vpn-api" ] +ENTRYPOINT [ "/nym/nym-vpn-api" ] From a1c33bbae3060d26fbf95480ac269f9e85b2754f Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Mon, 14 Oct 2024 18:05:03 +0200 Subject: [PATCH 084/207] fix: CMD -> ENTRYPOINT --- nym-data-observatory/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-data-observatory/Dockerfile b/nym-data-observatory/Dockerfile index 33969aa14f..2293a8ca91 100644 --- a/nym-data-observatory/Dockerfile +++ b/nym-data-observatory/Dockerfile @@ -16,4 +16,4 @@ RUN apt update && apt install -yy curl WORKDIR /nym COPY --from=builder /usr/src/nym/target/release/nym-data-observatory ./ -CMD [ "/nym/nym-data-observatory" ] +ENTRYPOINT [ "/nym/nym-data-observatory" ] From 652f2db5c02ea16e8882c23e65086e45d537fe72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 17:55:00 +0100 Subject: [PATCH 085/207] exposed announce ports to nym-node CLI --- nym-node/src/cli/helpers.rs | 24 ++++++++++++++++++++++++ nym-node/src/env.rs | 2 ++ 2 files changed, 26 insertions(+) diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 02a2b9ce6c..cabbd6d621 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -188,6 +188,15 @@ pub(crate) struct MixnetArgs { )] pub(crate) mixnet_bind_address: Option, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[clap( + long, + env = NYMNODE_MIXNET_ANNOUNCE_PORT_ARG + )] + pub(crate) mixnet_announce_port: Option, + /// Addresses to nym APIs from which the node gets the view of the network. #[clap( long, @@ -223,6 +232,9 @@ impl MixnetArgs { if let Some(bind_address) = self.mixnet_bind_address { section.bind_address = bind_address } + if let Some(mixnet_announce_port) = self.mixnet_announce_port { + section.announce_port = Some(mixnet_announce_port) + } if let Some(nym_api_urls) = self.nym_api_urls { section.nym_api_urls = nym_api_urls } @@ -321,6 +333,15 @@ pub(crate) struct MixnodeArgs { env = NYMNODE_VERLOC_BIND_ADDRESS_ARG )] pub(crate) verloc_bind_address: Option, + + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[clap( + long, + env = NYMNODE_VERLOC_ANNOUNCE_PORT_ARG + )] + pub(crate) verloc_announce_port: Option, } impl MixnodeArgs { @@ -336,6 +357,9 @@ impl MixnodeArgs { if let Some(bind_address) = self.verloc_bind_address { section.verloc.bind_address = bind_address } + if let Some(announce_port) = self.verloc_announce_port { + section.verloc.announce_port = Some(announce_port) + } section } } diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 6067c94aa7..d9b03a4b3e 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -35,6 +35,7 @@ pub mod vars { // mixnet: pub const NYMNODE_MIXNET_BIND_ADDRESS_ARG: &str = "NYMNODE_MIXNET_BIND_ADDRESS"; + pub const NYMNODE_MIXNET_ANNOUNCE_PORT_ARG: &str = "NYMNODE_MIXNET_ANNOUNCE_PORT"; pub const NYMNODE_NYM_APIS_ARG: &str = "NYMNODE_NYM_APIS"; pub const NYMNODE_NYXD_URLS_ARG: &str = "NYMNODE_NYXD"; pub const NYMNODE_UNSAFE_DISABLE_NOISE: &str = "UNSAFE_DISABLE_NOISE"; @@ -48,6 +49,7 @@ pub mod vars { // mixnode: pub const NYMNODE_VERLOC_BIND_ADDRESS_ARG: &str = "NYMNODE_VERLOC_BIND_ADDRESS"; + pub const NYMNODE_VERLOC_ANNOUNCE_PORT_ARG: &str = "NYMNODE_VERLOC_ANNOUNCE_PORT"; // entry gateway: pub const NYMNODE_ENTRY_BIND_ADDRESS_ARG: &str = "NYMNODE_ENTRY_BIND_ADDRESS"; From 27775a29c480f250e5a03e30b78c95ebbc0cd670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 18:12:09 +0100 Subject: [PATCH 086/207] added additional logs when refreshing self-described cache --- nym-api/src/node_describe_cache/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index e92a3ebe5b..d8e0b3c32b 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -20,7 +20,7 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; -use tracing::{debug, error}; +use tracing::{debug, error, info}; mod query_helpers; @@ -389,6 +389,8 @@ impl CacheItemProvider for NodeDescriptionProvider { .collect::>() .await; + info!("refreshed self described data for {} nodes", nodes.len()); + Ok(DescribedNodes { nodes }) } } From 07138696667258c95bf2ac7db5515806f06edc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 18:12:27 +0100 Subject: [PATCH 087/207] fixed swagger route arguments for skimmed endpoints --- .../src/nym_nodes/handlers/unstable/skimmed.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index 53dace0e09..a4aba882cf 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -221,7 +221,7 @@ pub(super) async fn deprecated_gateways_basic( (status = 200, body = CachedNodesResponse) ) )] -#[deprecated(note = "use '/v1/unstable/nym-nodes/mixnodes/skimmed/active' instead")] +#[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")] pub(super) async fn deprecated_mixnodes_basic( state: State, query_params: Query, @@ -335,7 +335,7 @@ pub(super) async fn nodes_basic_all( #[utoipa::path( tag = "Unstable Nym Nodes", get, - params(NodesParamsWithRole), + params(NodesParams), path = "/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( @@ -387,7 +387,7 @@ async fn mixnodes_basic( #[utoipa::path( tag = "Unstable Nym Nodes", get, - params(NodesParamsWithRole), + params(NodesParams), path = "/mixnodes/all", context_path = "/v1/unstable/nym-nodes/skimmed", responses( @@ -406,7 +406,7 @@ pub(super) async fn mixnodes_basic_all( #[utoipa::path( tag = "Unstable Nym Nodes", get, - params(NodesParamsWithRole), + params(NodesParams), path = "/mixnodes/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( @@ -444,7 +444,7 @@ async fn entry_gateways_basic( #[utoipa::path( tag = "Unstable Nym Nodes", get, - params(NodesParamsWithRole), + params(NodesParams), path = "/entry-gateways/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( @@ -463,7 +463,7 @@ pub(super) async fn entry_gateways_basic_active( #[utoipa::path( tag = "Unstable Nym Nodes", get, - params(NodesParamsWithRole), + params(NodesParams), path = "/entry-gateways/all", context_path = "/v1/unstable/nym-nodes/skimmed", responses( @@ -501,7 +501,7 @@ async fn exit_gateways_basic( #[utoipa::path( tag = "Unstable Nym Nodes", get, - params(NodesParamsWithRole), + params(NodesParams), path = "/exit-gateways/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( @@ -520,7 +520,7 @@ pub(super) async fn exit_gateways_basic_active( #[utoipa::path( tag = "Unstable Nym Nodes", get, - params(NodesParamsWithRole), + params(NodesParams), path = "/exit-gateways/all", context_path = "/v1/unstable/nym-nodes/skimmed", responses( From d04331a5dfc542aac1ba167baac555ecae290dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 18:12:56 +0100 Subject: [PATCH 088/207] updated clients to use 'new' endpoints --- .../topology_control/geo_aware_provider.rs | 15 +++- .../topology_control/nym_api_provider.rs | 5 +- common/client-core/src/init/helpers.rs | 21 ++--- .../validator-client/src/client.rs | 66 +++++++++++++++ .../validator-client/src/nym_api/mod.rs | 83 +++++++++++++++++++ common/wasm/client-core/src/helpers.rs | 6 +- .../examples/custom_topology_provider.rs | 4 +- 7 files changed, 175 insertions(+), 25 deletions(-) diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 9967e1a705..660acdf749 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -110,7 +110,11 @@ impl GeoAwareTopologyProvider { } async fn get_topology(&self) -> Option { - let mixnodes = match self.validator_client.get_basic_mixnodes(None).await { + let mixnodes = match self + .validator_client + .get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) + .await + { Err(err) => { error!("failed to get network mixnodes - {err}"); return None; @@ -118,7 +122,11 @@ impl GeoAwareTopologyProvider { Ok(mixes) => mixes, }; - let gateways = match self.validator_client.get_basic_gateways(None).await { + let gateways = match self + .validator_client + .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) + .await + { Err(err) => { error!("failed to get network gateways - {err}"); return None; @@ -185,8 +193,7 @@ impl GeoAwareTopologyProvider { .filter(|m| filtered_mixnode_ids.contains(&m.node_id)) .collect::>(); - let topology = nym_topology_from_basic_info(&mixnodes, &gateways) - .filter_system_version(&self.client_version); + let topology = nym_topology_from_basic_info(&mixnodes, &gateways); // TODO: return real error type check_layer_integrity(topology.clone()).ok()?; diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 3a00f462bd..c2b2dd9003 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -98,7 +98,7 @@ impl NymApiTopologyProvider { async fn get_current_compatible_topology(&mut self) -> Option { let mixnodes = match self .validator_client - .get_basic_mixnodes(Some(self.client_version.clone())) + .get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) .await { Err(err) => { @@ -110,7 +110,7 @@ impl NymApiTopologyProvider { let gateways = match self .validator_client - .get_basic_gateways(Some(self.client_version.clone())) + .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) .await { Err(err) => { @@ -134,7 +134,6 @@ impl NymApiTopologyProvider { g.performance.round_to_integer() >= self.config.min_gateway_performance }), ); - if let Err(err) = self.check_layer_distribution(&topology) { warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used: {err}"); self.use_next_nym_api(); diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index ac2b01bf6f..8e90f30240 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -7,7 +7,7 @@ use futures::{SinkExt, StreamExt}; use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; use nym_gateway_client::GatewayClient; -use nym_topology::{filter::VersionFilterable, gateway, mix}; +use nym_topology::{gateway, mix}; use nym_validator_client::client::IdentityKeyRef; use nym_validator_client::UserAgent; use rand::{seq::SliceRandom, Rng}; @@ -94,7 +94,7 @@ pub async fn current_gateways( log::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_basic_gateways(None).await?; + let gateways = client.get_all_basic_entry_assigned_nodes(None).await?; log::debug!("Found {} gateways", gateways.len()); log::trace!("Gateways: {:#?}", gateways); @@ -102,17 +102,12 @@ pub async fn current_gateways( .iter() .filter_map(|gateway| gateway.try_into().ok()) .collect::>(); - log::debug!("Ater checking validity: {}", valid_gateways.len()); + log::debug!("After checking validity: {}", valid_gateways.len()); log::trace!("Valid gateways: {:#?}", valid_gateways); - // we were always filtering by version so I'm not removing that 'feature' - let filtered_gateways = valid_gateways.filter_by_version(env!("CARGO_PKG_VERSION")); - log::debug!("After filtering for version: {}", filtered_gateways.len()); - log::trace!("Filtered gateways: {:#?}", filtered_gateways); + log::info!("nym-api reports {} valid gateways", valid_gateways.len()); - log::info!("nym-api reports {} valid gateways", filtered_gateways.len()); - - Ok(filtered_gateways) + Ok(valid_gateways) } pub async fn current_mixnodes( @@ -126,15 +121,13 @@ pub async fn current_mixnodes( log::trace!("Fetching list of mixnodes from: {nym_api}"); - let mixnodes = client.get_basic_mixnodes(None).await?; + let mixnodes = client.get_basic_active_mixing_assigned_nodes(None).await?; let valid_mixnodes = mixnodes .iter() .filter_map(|mixnode| mixnode.try_into().ok()) .collect::>(); - // we were always filtering by version so I'm not removing that 'feature' - let filtered_mixnodes = valid_mixnodes.filter_by_version(env!("CARGO_PKG_VERSION")); - Ok(filtered_mixnodes) + Ok(valid_mixnodes) } #[cfg(not(target_arch = "wasm32"))] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index fc68ae748c..b8375abdbd 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -283,6 +283,7 @@ impl NymApiClient { self.nym_api.change_base_url(new_endpoint); } + #[deprecated(note = "use get_basic_active_mixing_assigned_nodes instead")] pub async fn get_basic_mixnodes( &self, semver_compatibility: Option, @@ -294,6 +295,7 @@ impl NymApiClient { .nodes) } + #[deprecated(note = "use get_all_basic_entry_assigned_nodes instead")] pub async fn get_basic_gateways( &self, semver_compatibility: Option, @@ -305,6 +307,70 @@ impl NymApiClient { .nodes) } + /// retrieve basic information for nodes are capable of operating as an entry gateway + /// this includes legacy gateways and nym-nodes + pub async fn get_all_basic_entry_assigned_nodes( + &self, + semver_compatibility: Option, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_all_basic_entry_assigned_nodes( + semver_compatibility.clone(), + false, + Some(page), + None, + ) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + pub async fn get_basic_active_mixing_assigned_nodes( + &self, + semver_compatibility: Option, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_basic_active_mixing_assigned_nodes( + semver_compatibility.clone(), + false, + Some(page), + None, + ) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + pub async fn get_cached_active_mixnodes( &self, ) -> Result, ValidatorClientError> { diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index a664a503ab..9660e470c7 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -13,6 +13,7 @@ use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, }; +use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse; pub use nym_api_requests::{ ecash::{ models::{ @@ -164,6 +165,88 @@ pub trait NymApiClientExt: ApiClient { .await } + /// retrieve basic information for nodes are capable of operating as an entry gateway + /// this includes legacy gateways and nym-nodes + async fn get_all_basic_entry_assigned_nodes( + &self, + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(arg) = &semver_compatibility { + params.push(("semver_compatibility", arg.clone())) + } + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + "nym-nodes", + "skimmed", + "entry-gateways", + "all", + ], + ¶ms, + ) + .await + } + + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + async fn get_basic_active_mixing_assigned_nodes( + &self, + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(arg) = &semver_compatibility { + params.push(("semver_compatibility", arg.clone())) + } + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + "nym-nodes", + "skimmed", + "mixnodes", + "active", + ], + ¶ms, + ) + .await + } + async fn get_active_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 3eafcbd623..41b203d1b5 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -67,8 +67,10 @@ pub async fn current_network_topology_async( }; let api_client = NymApiClient::new(url); - let mixnodes = api_client.get_basic_mixnodes(None).await?; - let gateways = api_client.get_basic_gateways(None).await?; + let mixnodes = api_client + .get_basic_active_mixing_assigned_nodes(None) + .await?; + let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?; Ok(NymTopology::from_basic(&mixnodes, &gateways).into()) } diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 6737c2dcae..7b22a4f584 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -21,7 +21,7 @@ impl MyTopologyProvider { async fn get_topology(&self) -> NymTopology { let mixnodes = self .validator_client - .get_basic_mixnodes(None) + .get_basic_active_mixing_assigned_nodes(None) .await .unwrap(); @@ -35,7 +35,7 @@ impl MyTopologyProvider { let gateways = self .validator_client - .get_basic_gateways(None) + .get_all_basic_entry_assigned_nodes(None) .await .unwrap(); From 3320da206065552e94ecc474025522f00bedaf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 18:13:10 +0100 Subject: [PATCH 089/207] fixed testnet-manager tool to work with the updated binaries --- .../testnet-manager/src/manager/dkg_skip.rs | 6 --- .../testnet-manager/src/manager/local_apis.rs | 5 +- .../src/manager/local_nodes.rs | 53 ++++++++++--------- .../testnet-manager/src/manager/node.rs | 43 +++------------ 4 files changed, 38 insertions(+), 69 deletions(-) diff --git a/tools/internal/testnet-manager/src/manager/dkg_skip.rs b/tools/internal/testnet-manager/src/manager/dkg_skip.rs index d09e219228..a132ea6950 100644 --- a/tools/internal/testnet-manager/src/manager/dkg_skip.rs +++ b/tools/internal/testnet-manager/src/manager/dkg_skip.rs @@ -77,12 +77,6 @@ impl<'a> PemStorableKey for FakeDkgKey<'a> { } } -impl EcashSignerWithPaths { - pub(crate) fn api_port(&self) -> u16 { - self.data.endpoint.port().unwrap() - } -} - struct DkgSkipCtx<'a> { progress: ProgressTracker, network: &'a LoadedNetwork, diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs index 0351ce4d2b..631f6117f2 100644 --- a/tools/internal/testnet-manager/src/manager/local_apis.rs +++ b/tools/internal/testnet-manager/src/manager/local_apis.rs @@ -91,6 +91,8 @@ impl NetworkManager { "--enable-zk-nym", "--announce-address", info.data.endpoint.as_ref(), + "--bind-address", + &format!("0.0.0.0:{}", info.data.endpoint.port().unwrap()), ]) .stdin(Stdio::null()) .stderr(Stdio::null()) @@ -163,11 +165,10 @@ impl NetworkManager { let mut cmds = Vec::new(); for signer in &ctx.signers { - let port = signer.api_port(); let id = ctx.signer_id(signer); cmds.push(format!( - "ROCKET_PORT={port} {bin_canon_display} -c {env_canon_display} run --id {id}" + "{bin_canon_display} -c {env_canon_display} run --id {id}" )); } Ok(RunCommands(cmds)) diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index 9492de53c8..dc19d44436 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -161,6 +161,10 @@ impl NetworkManager { &format!("127.0.0.1:{verloc_port}"), "--entry-bind-address", &format!("127.0.0.1:{clients_port}"), + "--mixnet-announce-port", + &mix_port.to_string(), + "--verloc-announce-port", + &verloc_port.to_string(), "--mnemonic", &Zeroizing::new(node.owner.mnemonic.to_string()), "--local", @@ -203,11 +207,7 @@ impl NetworkManager { ctx.set_pb_message(format!("generating bonding signature for node {id}...")); - let msg = if is_gateway { - node.gateway_bonding_payload() - } else { - node.mixnode_bonding_payload() - }; + let msg = node.bonding_payload(); let child = Command::new(&ctx.nym_node_binary) .args([ @@ -318,28 +318,20 @@ impl NetworkManager { let owner = ctx.signing_node_owner(node)?; - let (bonding_fut, typ) = if is_gateway { - ( - owner.bond_gateway( - node.gateway(), - node.bonding_signature(), - node.pledge().into(), - None, - ), - "gateway", - ) + let typ = if is_gateway { + "gateway [as nym-node]" } else { - ( - owner.bond_mixnode( - node.mixnode(), - node.cost_params(), - node.bonding_signature(), - node.pledge().into(), - None, - ), - "mixnode", - ) + "mixnode [as nym-node]" }; + + let bonding_fut = owner.bond_nymnode( + node.bonding_nym_node(), + node.cost_params(), + node.bonding_signature(), + node.pledge().into(), + None, + ); + let res = ctx.async_with_progress(bonding_fut).await?; ctx.println(format!( "\t{id} ({typ}) bonded in transaction: {}", @@ -376,6 +368,7 @@ impl NetworkManager { style("[4/5]").bold().dim() )); + // this could be batched in a single tx, but that's too much effort for now let rewarder = ctx.signing_rewarder()?; ctx.set_pb_message("starting epoch transition..."); @@ -386,6 +379,16 @@ impl NetworkManager { let fut = rewarder.reconcile_epoch_events(None, None); ctx.async_with_progress(fut).await?; + ctx.set_pb_message("finally assigning the active set... exit..."); + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![], + }, + None, + ); + ctx.async_with_progress(fut).await?; + ctx.set_pb_message("finally assigning the active set... entry..."); let fut = rewarder.assign_roles( RoleAssignment { diff --git a/tools/internal/testnet-manager/src/manager/node.rs b/tools/internal/testnet-manager/src/manager/node.rs index 3cf3443c24..d744057f32 100644 --- a/tools/internal/testnet-manager/src/manager/node.rs +++ b/tools/internal/testnet-manager/src/manager/node.rs @@ -5,10 +5,7 @@ use crate::manager::contract::Account; use nym_coconut_dkg_common::types::Addr; use nym_contracts_common::signing::MessageSignature; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{ - construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Gateway, - MixNode, NodeCostParams, -}; +use nym_mixnet_contract_common::{construct_nym_node_bonding_sign_payload, NodeCostParams}; use nym_validator_client::nyxd::CosmWasmCoin; pub(crate) struct NymNode { @@ -44,27 +41,11 @@ impl NymNode { CosmWasmCoin::new(100_000000, "unym") } - pub(crate) fn gateway(&self) -> Gateway { - Gateway { + pub(crate) fn bonding_nym_node(&self) -> nym_mixnet_contract_common::NymNode { + nym_mixnet_contract_common::NymNode { host: "127.0.0.1".to_string(), - mix_port: self.mix_port, - clients_port: self.clients_port, - location: "foomp".to_string(), - sphinx_key: self.sphinx_key.clone(), + custom_http_port: Some(self.http_port), identity_key: self.identity_key.clone(), - version: self.version.clone(), - } - } - - pub(crate) fn mixnode(&self) -> MixNode { - MixNode { - host: "127.0.0.1".to_string(), - mix_port: self.mix_port, - verloc_port: self.verloc_port, - http_api_port: self.http_port, - sphinx_key: self.sphinx_key.clone(), - identity_key: self.identity_key.clone(), - version: self.version.clone(), } } @@ -80,24 +61,14 @@ impl NymNode { self.bonding_signature.parse().unwrap() } - pub(crate) fn mixnode_bonding_payload(&self) -> String { - let payload = construct_mixnode_bonding_sign_payload( + pub(crate) fn bonding_payload(&self) -> String { + let payload = construct_nym_node_bonding_sign_payload( 0, Addr::unchecked(self.owner.address.to_string()), self.pledge(), - self.mixnode(), + self.bonding_nym_node(), self.cost_params(), ); payload.to_base58_string().unwrap() } - - pub(crate) fn gateway_bonding_payload(&self) -> String { - let payload = construct_gateway_bonding_sign_payload( - 0, - Addr::unchecked(self.owner.address.to_string()), - self.pledge(), - self.gateway(), - ); - payload.to_base58_string().unwrap() - } } From 01db51e4925613bea9c8baf2647bc892f4021324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 18:52:19 +0100 Subject: [PATCH 090/207] updated mixnet schema --- contracts/mixnet/schema/nym-mixnet-contract.json | 6 ++++++ contracts/mixnet/schema/raw/migrate.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index bf25ebfd76..7d43c71228 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -3212,6 +3212,12 @@ "title": "MigrateMsg", "type": "object", "properties": { + "unsafe_skip_state_updates": { + "type": [ + "boolean", + "null" + ] + }, "vesting_contract_address": { "type": [ "string", diff --git a/contracts/mixnet/schema/raw/migrate.json b/contracts/mixnet/schema/raw/migrate.json index e9962aaf92..57f0d2acdb 100644 --- a/contracts/mixnet/schema/raw/migrate.json +++ b/contracts/mixnet/schema/raw/migrate.json @@ -3,6 +3,12 @@ "title": "MigrateMsg", "type": "object", "properties": { + "unsafe_skip_state_updates": { + "type": [ + "boolean", + "null" + ] + }, "vesting_contract_address": { "type": [ "string", From ca4523025e9d21b95d6587d104ce80d8e6e22b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 14 Oct 2024 19:16:13 +0100 Subject: [PATCH 091/207] missing update to the integration test --- contracts/mixnet-vesting-integration-tests/src/support/setup.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index e360777a94..5c4b08e1d4 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -464,6 +464,7 @@ pub fn instantiate_contracts( mixnet_contract_address.clone(), &nym_mixnet_contract_common::MigrateMsg { vesting_contract_address: Some(vesting_contract_address.to_string()), + unsafe_skip_state_updates: None, }, mixnet_code_id, ) From 435f236812ec8ab4a2eae0753ba0c7440eef6c38 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 15 Oct 2024 09:18:02 +0200 Subject: [PATCH 092/207] [Product Data] First step in gateway usage data collection (#4963) * add stats model * add stats collection * add stats route * propagate stuff and run stuff * cargo stuff * sqlx unused what? * add sessions started stat * session durations in miliseconds * apply Jon's comments * [Product Data] Second step in gateway usage data collection (#4964) * turn stats collection into event based * move events into a common crate for future use elsewhere * apply Jon's comments --- Cargo.lock | 11 +++ Cargo.toml | 1 + common/statistics/Cargo.toml | 16 ++++ common/statistics/src/events.rs | 39 +++++++++ common/statistics/src/lib.rs | 4 + gateway/Cargo.toml | 9 +- .../node/client_handling/active_clients.rs | 18 +++- gateway/src/node/mod.rs | 32 ++++++- gateway/src/node/statistics/mod.rs | 63 ++++++++++++++ gateway/src/node/statistics/sessions.rs | 86 +++++++++++++++++++ .../src/router/api/v1/metrics/mod.rs | 3 + .../src/router/api/v1/metrics/sessions.rs | 33 +++++++ .../nym-node-http-api/src/state/metrics.rs | 59 ++++++++++++- nym-node/nym-node-http-api/src/state/mod.rs | 10 ++- .../src/api/v1/metrics/models.rs | 15 ++++ nym-node/nym-node-requests/src/lib.rs | 2 + nym-node/src/node/mod.rs | 6 +- 17 files changed, 394 insertions(+), 13 deletions(-) create mode 100644 common/statistics/Cargo.toml create mode 100644 common/statistics/src/events.rs create mode 100644 common/statistics/src/lib.rs create mode 100644 gateway/src/node/statistics/mod.rs create mode 100644 gateway/src/node/statistics/sessions.rs create mode 100644 nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs diff --git a/Cargo.lock b/Cargo.lock index a35a6acaaf..f2864c0227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5219,6 +5219,7 @@ dependencies = [ "nym-node-http-api", "nym-pemstore", "nym-sphinx", + "nym-statistics-common", "nym-task", "nym-types", "nym-validator-client", @@ -5232,6 +5233,7 @@ dependencies = [ "sqlx", "subtle-encoding", "thiserror", + "time", "tokio", "tokio-stream", "tokio-tungstenite", @@ -6283,6 +6285,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-statistics-common" +version = "0.1.0" +dependencies = [ + "futures", + "nym-sphinx", + "time", +] + [[package]] name = "nym-store-cipher" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 04e5f5c8e8..f7fdcd1716 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ members = [ "common/socks5-client-core", "common/socks5/proxy-helpers", "common/socks5/requests", + "common/statistics", "common/store-cipher", "common/task", "common/topology", diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml new file mode 100644 index 0000000000..e07c863065 --- /dev/null +++ b/common/statistics/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-statistics-common" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = { workspace = true } +time = { workspace = true } + +nym-sphinx = { path = "../nymsphinx" } diff --git a/common/statistics/src/events.rs b/common/statistics/src/events.rs new file mode 100644 index 0000000000..12fc0b209c --- /dev/null +++ b/common/statistics/src/events.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use futures::channel::mpsc; +use nym_sphinx::DestinationAddressBytes; +use time::OffsetDateTime; + +pub type StatsEventSender = mpsc::UnboundedSender; +pub type StatsEventReceiver = mpsc::UnboundedReceiver; +pub enum StatsEvent { + SessionStatsEvent(SessionEvent), +} + +impl StatsEvent { + pub fn new_session_start(client: DestinationAddressBytes) -> StatsEvent { + StatsEvent::SessionStatsEvent(SessionEvent::SessionStart { + start_time: OffsetDateTime::now_utc(), + client, + }) + } + + pub fn new_session_stop(client: DestinationAddressBytes) -> StatsEvent { + StatsEvent::SessionStatsEvent(SessionEvent::SessionStop { + stop_time: OffsetDateTime::now_utc(), + client, + }) + } +} + +pub enum SessionEvent { + SessionStart { + start_time: OffsetDateTime, + client: DestinationAddressBytes, + }, + SessionStop { + stop_time: OffsetDateTime, + client: DestinationAddressBytes, + }, +} diff --git a/common/statistics/src/lib.rs b/common/statistics/src/lib.rs new file mode 100644 index 0000000000..222251db6c --- /dev/null +++ b/common/statistics/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod events; diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index cbdcfc1ad2..f910f13b6e 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -41,15 +41,9 @@ rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } si-scale = { workspace = true } -sqlx = { workspace = true, features = [ - "runtime-tokio-rustls", - "sqlite", - "macros", - "migrate", - "time", -] } subtle-encoding = { workspace = true, features = ["bech32-preview"] } thiserror = { workspace = true } +time = { workspace = true } tokio = { workspace = true, features = [ "rt-multi-thread", "net", @@ -83,6 +77,7 @@ nym-network-requester = { path = "../service-providers/network-requester" } nym-node-http-api = { path = "../nym-node/nym-node-http-api" } nym-pemstore = { path = "../common/pemstore" } nym-sphinx = { path = "../common/nymsphinx" } +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" } diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index 962765067e..82cdf98dc3 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -5,6 +5,8 @@ use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; use nym_sphinx::DestinationAddressBytes; +use nym_statistics_common::events; +use nym_statistics_common::events::StatsEventSender; use std::sync::Arc; use tracing::warn; @@ -35,6 +37,7 @@ impl ActiveClient { #[derive(Clone)] pub(crate) struct ActiveClientsStore { inner: Arc>, + stats_event_sender: StatsEventSender, } #[derive(Clone)] @@ -48,9 +51,10 @@ pub(crate) struct ClientIncomingChannels { impl ActiveClientsStore { /// Creates new instance of `ActiveClientsStore` to store in-memory handles to all currently connected clients. - pub(crate) fn new() -> Self { + pub(crate) fn new(stats_event_sender: StatsEventSender) -> Self { ActiveClientsStore { inner: Arc::new(DashMap::new()), + stats_event_sender, } } @@ -126,6 +130,12 @@ impl ActiveClientsStore { /// * `client`: address of the client for which to remove the handle. pub(crate) fn disconnect(&self, client: DestinationAddressBytes) { self.inner.remove(&client); + if let Err(e) = self + .stats_event_sender + .unbounded_send(events::StatsEvent::new_session_stop(client)) + { + warn!("Failed to send session stop event to collector : {e}") + }; } /// Insert new client handle into the store. @@ -147,6 +157,12 @@ impl ActiveClientsStore { if self.inner.insert(client, entry).is_some() { panic!("inserted a duplicate remote client") } + if let Err(e) = self + .stats_event_sender + .unbounded_send(events::StatsEvent::new_session_start(client)) + { + warn!("Failed to send session start event to collector : {e}") + }; } /// Inserts a handle to the embedded client diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index bf5c3f0ab0..b06878c42f 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -22,12 +22,15 @@ use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilter}; +use nym_node_http_api::state::metrics::SharedSessionStats; +use nym_statistics_common::events; use nym_task::{TaskClient, TaskHandle, TaskManager}; use nym_types::gateway::GatewayNodeDetailsResponse; use nym_validator_client::nyxd::{Coin, CosmWasmClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use rand::seq::SliceRandom; use rand::thread_rng; +use statistics::GatewayStatisticsCollector; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; @@ -36,6 +39,7 @@ use tracing::*; pub(crate) mod client_handling; pub(crate) mod helpers; pub(crate) mod mixnet_handling; +pub(crate) mod statistics; pub use nym_gateway_storage::{PersistentStorage, Storage}; @@ -147,6 +151,8 @@ pub struct Gateway { wireguard_data: Option, + session_stats: Option, + run_http_server: bool, task_client: Option, } @@ -168,6 +174,7 @@ impl Gateway { ip_packet_router_opts, authenticator_opts: None, wireguard_data: None, + session_stats: None, run_http_server: true, task_client: None, }) @@ -191,6 +198,7 @@ impl Gateway { sphinx_keypair, storage, wireguard_data: None, + session_stats: None, run_http_server: true, task_client: None, } @@ -204,6 +212,10 @@ impl Gateway { self.task_client = Some(task_client) } + pub fn set_session_stats(&mut self, session_stats: SharedSessionStats) { + self.session_stats = Some(session_stats); + } + pub fn set_wireguard_data(&mut self, wireguard_data: nym_wireguard::WireguardData) { self.wireguard_data = Some(wireguard_data) } @@ -393,6 +405,19 @@ impl Gateway { packet_sender } + fn start_stats_collector( + &self, + shared_session_stats: SharedSessionStats, + shutdown: TaskClient, + ) -> events::StatsEventSender { + info!("Starting gateway stats collector..."); + + let (mut stats_collector, stats_event_sender) = + GatewayStatisticsCollector::new(shared_session_stats); + tokio::spawn(async move { stats_collector.run(shutdown).await }); + stats_event_sender + } + // TODO: rethink the logic in this function... async fn start_network_requester( &self, @@ -599,6 +624,11 @@ impl Gateway { return Err(GatewayError::InsufficientNodeBalance { account, balance }); } } + let shared_session_stats = self.session_stats.take().unwrap_or_default(); + let stats_event_sender = self.start_stats_collector( + shared_session_stats, + shutdown.fork("statistics::GatewayStatisticsCollector"), + ); let handler_config = CredentialHandlerConfig { revocation_bandwidth_penalty: self @@ -629,7 +659,7 @@ impl Gateway { let mix_forwarding_channel = self.start_packet_forwarder(shutdown.fork("PacketForwarder")); - let active_clients_store = ActiveClientsStore::new(); + let active_clients_store = ActiveClientsStore::new(stats_event_sender.clone()); self.start_mix_socket_listener( mix_forwarding_channel.clone(), active_clients_store.clone(), diff --git a/gateway/src/node/statistics/mod.rs b/gateway/src/node/statistics/mod.rs new file mode 100644 index 0000000000..53ea277db5 --- /dev/null +++ b/gateway/src/node/statistics/mod.rs @@ -0,0 +1,63 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use futures::{channel::mpsc, StreamExt}; +use nym_node_http_api::state::metrics::SharedSessionStats; +use nym_statistics_common::events::{StatsEvent, StatsEventReceiver, StatsEventSender}; +use nym_task::TaskClient; +use sessions::SessionStatsHandler; +use std::time::Duration; +use time::OffsetDateTime; +use tracing::trace; + +pub mod sessions; + +const STATISTICS_UPDATE_TIMER_INTERVAL: Duration = Duration::from_secs(3600); //update timer, no need to check everytime + +pub(crate) struct GatewayStatisticsCollector { + stats_event_rx: StatsEventReceiver, + session_stats: SessionStatsHandler, + //here goes additionnal stats handler +} + +impl GatewayStatisticsCollector { + pub fn new( + shared_session_stats: SharedSessionStats, + ) -> (GatewayStatisticsCollector, StatsEventSender) { + let (stats_event_tx, stats_event_rx) = mpsc::unbounded(); + let collector = GatewayStatisticsCollector { + stats_event_rx, + session_stats: SessionStatsHandler::new(shared_session_stats), + }; + (collector, stats_event_tx) + } + + async fn update_shared_state(&mut self, update_time: OffsetDateTime) { + self.session_stats.update_shared_state(update_time).await; + //here goes additionnal stats handler update + } + + pub async fn run(&mut self, mut shutdown: TaskClient) { + let mut update_interval = tokio::time::interval(STATISTICS_UPDATE_TIMER_INTERVAL); + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("StatisticsCollector: Received shutdown"); + }, + _ = update_interval.tick() => { + let now = OffsetDateTime::now_utc(); + self.update_shared_state(now).await; + }, + + Some(stat_event) = self.stats_event_rx.next() => { + //dispatching event to proper handler + match stat_event { + StatsEvent::SessionStatsEvent(event) => self.session_stats.handle_event(event), + } + }, + + } + } + } +} diff --git a/gateway/src/node/statistics/sessions.rs b/gateway/src/node/statistics/sessions.rs new file mode 100644 index 0000000000..1f58bc1223 --- /dev/null +++ b/gateway/src/node/statistics/sessions.rs @@ -0,0 +1,86 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_node_http_api::state::metrics::SharedSessionStats; +use nym_sphinx::DestinationAddressBytes; +use std::collections::{HashMap, HashSet}; +use time::{Date, OffsetDateTime}; + +use nym_statistics_common::events::SessionEvent; + +type SessionDuration = u64; //in miliseconds + +pub(crate) struct SessionStatsHandler { + last_update_day: Date, + + shared_session_stats: SharedSessionStats, + active_sessions: HashMap, + unique_users: HashSet, + sessions_started: u32, + finished_sessions: Vec, +} + +impl SessionStatsHandler { + pub fn new(shared_session_stats: SharedSessionStats) -> Self { + SessionStatsHandler { + last_update_day: OffsetDateTime::now_utc().date(), + shared_session_stats, + active_sessions: Default::default(), + unique_users: Default::default(), + sessions_started: 0, + finished_sessions: Default::default(), + } + } + + pub(crate) fn handle_event(&mut self, event: SessionEvent) { + match event { + SessionEvent::SessionStart { start_time, client } => { + self.handle_session_start(start_time, client); + } + SessionEvent::SessionStop { stop_time, client } => { + self.handle_session_stop(stop_time, client); + } + } + } + fn handle_session_start( + &mut self, + start_time: OffsetDateTime, + client: DestinationAddressBytes, + ) { + self.sessions_started += 1; + self.unique_users.insert(client); + self.active_sessions.insert(client, start_time); + } + fn handle_session_stop(&mut self, stop_time: OffsetDateTime, client: DestinationAddressBytes) { + if let Some(session_start) = self.active_sessions.remove(&client) { + let session_duration = (stop_time - session_start).whole_milliseconds(); + + //this should always happen because it should always be positive and u64::max milliseconds is 500k millenia, but anyway + if let Ok(duration_u64) = session_duration.try_into() { + self.finished_sessions.push(duration_u64); + } + } + } + + //update shared state once a day has passed, with data from the previous day + pub(crate) async fn update_shared_state(&mut self, update_time: OffsetDateTime) { + let update_date = update_time.date(); + if update_date != self.last_update_day { + { + let mut shared_state = self.shared_session_stats.write().await; + shared_state.update_time = self.last_update_day; + shared_state.unique_active_users = self.unique_users.len() as u32; + shared_state.session_started = self.sessions_started; + shared_state.session_durations = self.finished_sessions.clone(); + } + self.reset_stats(update_date); + } + } + + fn reset_stats(&mut self, reset_day: Date) { + self.last_update_day = reset_day; + self.unique_users = self.active_sessions.keys().copied().collect(); + self.finished_sessions = Default::default(); + self.sessions_started = 0; + } +} diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs b/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs index 7f3dc9151e..cabf4aedf5 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs +++ b/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs @@ -3,6 +3,7 @@ use crate::api::v1::metrics::mixing::mixing_stats; use crate::api::v1::metrics::prometheus::prometheus_metrics; +use crate::api::v1::metrics::sessions::sessions_stats; use crate::api::v1::metrics::verloc::verloc_stats; use crate::state::metrics::MetricsAppState; use axum::extract::FromRef; @@ -12,6 +13,7 @@ use nym_node_requests::routes::api::v1::metrics; pub mod mixing; pub mod prometheus; +pub mod sessions; pub mod verloc; #[derive(Debug, Clone, Default)] @@ -26,6 +28,7 @@ where { Router::new() .route(metrics::MIXING, get(mixing_stats)) + .route(metrics::SESSIONS, get(sessions_stats)) .route(metrics::VERLOC, get(verloc_stats)) .route(metrics::PROMETHEUS, get(prometheus_metrics)) } diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs b/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs new file mode 100644 index 0000000000..59eceb8f89 --- /dev/null +++ b/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::metrics::MetricsAppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_requests::api::v1::metrics::models::SessionStats; + +/// If applicable, returns sessions statistics information of this node. +/// This information is **PURELY** self-reported and in no way validated. +#[utoipa::path( + get, + path = "/sessions", + context_path = "/api/v1/metrics", + tag = "Metrics", + responses( + (status = 200, content( + ("application/json" = SessionStats), + ("application/yaml" = SessionStats) + )) + ), + params(OutputParams), +)] +pub(crate) async fn sessions_stats( + Query(output): Query, + State(metrics_state): State, +) -> SessionStatsResponse { + let output = output.output.unwrap_or_default(); + let response = metrics_state.session_stats.read().await.as_response(); + output.to_response(response) +} + +pub type SessionStatsResponse = FormattedResponse; diff --git a/nym-node/nym-node-http-api/src/state/metrics.rs b/nym-node/nym-node-http-api/src/state/metrics.rs index d4691727d9..75616cf885 100644 --- a/nym-node/nym-node-http-api/src/state/metrics.rs +++ b/nym-node/nym-node-http-api/src/state/metrics.rs @@ -4,11 +4,12 @@ use crate::state::AppState; use axum::extract::FromRef; use nym_node_requests::api::v1::metrics::models::{ - MixingStats, VerlocResult, VerlocResultData, VerlocStats, + MixingStats, SessionStats, VerlocResult, VerlocResultData, VerlocStats, }; use std::collections::HashMap; use std::sync::Arc; -use time::OffsetDateTime; +use time::macros::time; +use time::{Date, OffsetDateTime}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; pub use nym_node_requests::api::v1::metrics::models::{VerlocMeasurement, VerlocNodeResult}; @@ -132,12 +133,66 @@ impl VerlocStatsState { } } +#[derive(Clone, Debug, Default)] +pub struct SharedSessionStats { + inner: Arc>, +} + +impl SharedSessionStats { + pub fn new() -> SharedSessionStats { + SharedSessionStats { + inner: Arc::new(RwLock::new(Default::default())), + } + } + + pub async fn read(&self) -> RwLockReadGuard<'_, SessionStatsState> { + self.inner.read().await + } + + pub async fn write(&self) -> RwLockWriteGuard<'_, SessionStatsState> { + self.inner.write().await + } +} + +#[derive(Debug, Clone)] +pub struct SessionStatsState { + pub update_time: Date, + pub unique_active_users: u32, + pub session_started: u32, + pub session_durations: Vec, +} + +impl SessionStatsState { + pub fn as_response(&self) -> SessionStats { + SessionStats { + update_time: self.update_time.with_time(time!(0:00)).assume_utc(), + unique_active_users: self.unique_active_users, + session_durations: self.session_durations.clone(), + sessions_started: self.session_started, + sessions_finished: self.session_durations.len() as u32, + } + } +} + +impl Default for SessionStatsState { + fn default() -> Self { + SessionStatsState { + update_time: OffsetDateTime::UNIX_EPOCH.date(), + unique_active_users: 0, + session_started: 0, + session_durations: Default::default(), + } + } +} + #[derive(Debug, Clone, Default)] pub struct MetricsAppState { pub(crate) prometheus_access_token: Option, pub(crate) mixing_stats: SharedMixingStats, + pub(crate) session_stats: SharedSessionStats, + pub(crate) verloc: SharedVerlocStats, } diff --git a/nym-node/nym-node-http-api/src/state/mod.rs b/nym-node/nym-node-http-api/src/state/mod.rs index 077ca782b6..67f16d1bd3 100644 --- a/nym-node/nym-node-http-api/src/state/mod.rs +++ b/nym-node/nym-node-http-api/src/state/mod.rs @@ -1,7 +1,9 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::state::metrics::{MetricsAppState, SharedMixingStats, SharedVerlocStats}; +use crate::state::metrics::{ + MetricsAppState, SharedMixingStats, SharedSessionStats, SharedVerlocStats, +}; use tokio::time::Instant; pub mod metrics; @@ -32,6 +34,12 @@ impl AppState { self } + #[must_use] + pub fn with_sessions_stats(mut self, session_stats: SharedSessionStats) -> Self { + self.metrics.session_stats = session_stats; + self + } + #[must_use] pub fn with_verloc_stats(mut self, verloc_stats: SharedVerlocStats) -> Self { self.metrics.verloc = verloc_stats; diff --git a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs index f0051e81e1..502e223c24 100644 --- a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs @@ -35,6 +35,21 @@ pub struct MixingStats { pub dropped_since_last_update: u64, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct SessionStats { + #[serde(with = "time::serde::rfc3339")] + pub update_time: OffsetDateTime, + + pub unique_active_users: u32, + + pub session_durations: Vec, + + pub sessions_started: u32, + + pub sessions_finished: u32, +} + #[derive(Serialize, Deserialize, Default, Debug, Clone)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct VerlocStats { diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 7b4e0ee9b7..983b1a6aec 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -65,10 +65,12 @@ pub mod routes { use super::*; pub const MIXING: &str = "/mixing"; + pub const SESSIONS: &str = "/sessions"; pub const VERLOC: &str = "/verloc"; pub const PROMETHEUS: &str = "/prometheus"; absolute_route!(mixing_absolute, metrics_absolute(), MIXING); + absolute_route!(sessions_absolute, metrics_absolute(), SESSIONS); absolute_route!(verloc_absolute, metrics_absolute(), VERLOC); absolute_route!(prometheus_absolute, metrics_absolute(), PROMETHEUS); } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index d3e756c6e0..5a7bc0c419 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -26,7 +26,7 @@ use nym_node::config::{ use nym_node::error::{EntryGatewayError, ExitGatewayError, MixnodeError, NymNodeError}; use nym_node_http_api::api::api_requests; use nym_node_http_api::api::api_requests::v1::node::models::{AnnouncePorts, NodeDescription}; -use nym_node_http_api::state::metrics::{SharedMixingStats, SharedVerlocStats}; +use nym_node_http_api::state::metrics::{SharedMixingStats, SharedSessionStats, SharedVerlocStats}; use nym_node_http_api::state::AppState; use nym_node_http_api::{NymNodeHTTPServer, NymNodeRouter}; use nym_sphinx_acknowledgements::AckKey; @@ -67,6 +67,7 @@ impl MixnodeData { pub struct EntryGatewayData { mnemonic: Zeroizing, client_storage: nym_gateway::node::PersistentStorage, + sessions_stats: SharedSessionStats, } impl EntryGatewayData { @@ -93,6 +94,7 @@ impl EntryGatewayData { ) .await .map_err(nym_gateway::GatewayError::from)?, + sessions_stats: SharedSessionStats::new(), }) } } @@ -581,6 +583,7 @@ impl NymNode { ); entry_gateway.disable_http_server(); entry_gateway.set_task_client(task_client); + entry_gateway.set_session_stats(self.entry_gateway.sessions_stats.clone()); if self.config.wireguard.enabled { entry_gateway.set_wireguard_data(self.wireguard.into()); } @@ -728,6 +731,7 @@ impl NymNode { let app_state = AppState::new() .with_mixing_stats(self.mixnode.mixing_stats.clone()) + .with_sessions_stats(self.entry_gateway.sessions_stats.clone()) .with_verloc_stats(self.verloc_stats.clone()) .with_metrics_key(self.config.http.access_token.clone()); From 5d445d6b47a7879c97a37984d2a6516e4ee493c8 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Wed, 16 Oct 2024 13:10:01 +0200 Subject: [PATCH 093/207] fix: comments on PR, removing stuff that's not needed --- nym-data-observatory/docker-compose.yml | 2 ++ nym-data-observatory/src/db/mod.rs | 5 ++--- nym-data-observatory/src/http/config.rs | 23 ----------------------- nym-data-observatory/src/http/mod.rs | 1 - nym-data-observatory/src/main.rs | 17 +---------------- 5 files changed, 5 insertions(+), 43 deletions(-) delete mode 100644 nym-data-observatory/src/http/config.rs diff --git a/nym-data-observatory/docker-compose.yml b/nym-data-observatory/docker-compose.yml index 1877e1979e..15da0609b7 100644 --- a/nym-data-observatory/docker-compose.yml +++ b/nym-data-observatory/docker-compose.yml @@ -10,6 +10,8 @@ services: - pgdata:/var/lib/postgresql/data data-observatory: + depends_on: + - postgres image: nym-data-observatory:latest build: context: ../ diff --git a/nym-data-observatory/src/db/mod.rs b/nym-data-observatory/src/db/mod.rs index ef90996da8..3e35fe6c61 100644 --- a/nym-data-observatory/src/db/mod.rs +++ b/nym-data-observatory/src/db/mod.rs @@ -5,8 +5,6 @@ use std::str::FromStr; pub(crate) mod models; pub(crate) mod queries; -pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; - static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); pub(crate) type DbPool = PgPool; @@ -17,7 +15,8 @@ pub(crate) struct Storage { impl Storage { pub async fn init(connection_url: Option) -> Result { - let connection_url = connection_url.unwrap(); + let connection_url = + connection_url.ok_or_else(|| anyhow!("Missing the connection url for database!"))?; let connect_options = { let mut connect_options = PgConnectOptions::from_str(&connection_url)?; let connect_options = connect_options.disable_statement_logging(); diff --git a/nym-data-observatory/src/http/config.rs b/nym-data-observatory/src/http/config.rs deleted file mode 100644 index e2088eb32d..0000000000 --- a/nym-data-observatory/src/http/config.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::read_env_var; - -#[derive(Debug)] -pub(crate) struct Config { - http_port: u16, -} - -const HTTP_PORT_DEFAULT: u16 = 8000; - -impl Config { - pub(crate) fn from_env() -> Self { - Self { - http_port: read_env_var("HTTP_PORT") - .unwrap_or(HTTP_PORT_DEFAULT.to_string()) - .parse() - .unwrap_or(HTTP_PORT_DEFAULT), - } - } - - pub(crate) fn http_port(&self) -> u16 { - self.http_port - } -} diff --git a/nym-data-observatory/src/http/mod.rs b/nym-data-observatory/src/http/mod.rs index 683391c839..1506514f0f 100644 --- a/nym-data-observatory/src/http/mod.rs +++ b/nym-data-observatory/src/http/mod.rs @@ -1,6 +1,5 @@ pub(crate) mod api; pub(crate) mod api_docs; -pub(crate) mod config; pub(crate) mod error; pub(crate) mod server; pub(crate) mod state; diff --git a/nym-data-observatory/src/main.rs b/nym-data-observatory/src/main.rs index 8fed033503..4f902f4b46 100644 --- a/nym-data-observatory/src/main.rs +++ b/nym-data-observatory/src/main.rs @@ -2,8 +2,6 @@ use clap::Parser; use nym_network_defaults::setup_env; use nym_task::signal::wait_for_signal; -use crate::http::config; - mod background_task; mod db; mod http; @@ -16,7 +14,7 @@ struct Args { #[arg(long, default_value_t = 8000, env = "NYM_DATA_OBSERVATORY_HTTP_PORT")] http_port: u16, - /// Path to the environment file + /// Path to the environment variables file. If you don't provide one, variables for the mainnet will be used. #[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_ENV_FILE")] env_file: Option, @@ -33,9 +31,6 @@ async fn main() -> anyhow::Result<()> { setup_env(args.env_file); // Defaults to mainnet if empty - let conf = config::Config::from_env(); - tracing::debug!("Using config:\n{:?}", conf); - let storage = db::Storage::init(args.connection_url).await?; let db_pool = storage.pool_owned().await; tokio::spawn(async move { @@ -56,13 +51,3 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - -// TODO dz move this to common -fn read_env_var(env_var: &str) -> anyhow::Result { - std::env::var(env_var) - .map_err(|_| anyhow::anyhow!("You need to set {}", env_var)) - .map(|value| { - tracing::trace!("{}={}", env_var, value); - value - }) -} From a26ba3d9f1c0ff023f755e21bfbddebff7e6223b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 16 Oct 2024 15:02:16 +0100 Subject: [PATCH 094/207] feature: require reporting using nym-node binary for rewarded set selection --- .../rewarded_set_assignment.rs | 96 +++++++++++++++++-- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 9ca5b5fb00..0947e86d67 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -199,11 +199,34 @@ impl EpochAdvancer { legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], ) -> Vec { - let mut with_performance = - Vec::with_capacity(legacy_mixnodes.len() + legacy_gateways.len() + nym_nodes.len()); + let mut with_performance = Vec::new(); + + // SAFETY: the cache MUST HAVE been initialised before now + let described_cache = self.described_cache.get().await.unwrap(); + + let mut no_self_described_mixnodes = 0; + let mut not_nym_node_bin_mixnodes = 0; + + let mut no_self_described_gateways = 0; + let mut not_nym_node_bin_gateways = 0; + + let mut no_self_described_nym_nodes = 0; + let mut not_nym_node_bin_nym_nodes = 0; for mix in legacy_mixnodes { + let node_id = mix.mix_id(); let total_stake = mix.total_stake(); + + let Some(self_described) = described_cache.get_description(&node_id) else { + no_self_described_mixnodes += 1; + continue; + }; + + if self_described.build_information.binary_name != "nym-node" { + not_nym_node_bin_mixnodes += 1; + continue; + } + let performance = self .load_mixnode_performance(&interval, mix.mix_id()) .await @@ -221,12 +244,24 @@ impl EpochAdvancer { }) } for gateway in legacy_gateways { + let node_id = gateway.node_id; let total_stake = gateway .bond .pledge_amount .amount .into_base_decimal() .unwrap_or_default(); + + let Some(self_described) = described_cache.get_description(&node_id) else { + no_self_described_gateways += 1; + continue; + }; + + if self_described.build_information.binary_name != "nym-node" { + not_nym_node_bin_gateways += 1; + continue; + } + let performance = self .load_gateway_performance(&interval, gateway.node_id) .await @@ -244,23 +279,26 @@ impl EpochAdvancer { }) } - // SAFETY: the cache MUST HAVE been initialised before now - let described_cache = self.described_cache.get().await.unwrap(); - for nym_node in nym_nodes { let node_id = nym_node.node_id(); let total_stake = nym_node.total_stake(); + + let Some(self_described) = described_cache.get_description(&node_id) else { + no_self_described_nym_nodes += 1; + continue; + }; + + if self_described.build_information.binary_name != "nym-node" { + not_nym_node_bin_nym_nodes += 1; + continue; + } + let performance = self .load_any_performance(&interval, nym_node.node_id()) .await .performance; debug!("nym-node {node_id}: stake: {total_stake}, performance: {performance}",); - let Some(self_described) = described_cache.get_description(&node_id) else { - warn!("we don't have self described information for nym-node {node_id}. it is not going to get considered for rewarded set selection."); - continue; - }; - let mut available_roles = Vec::new(); if self_described.declared_role.mixnode { available_roles.push(AvailableRole::Mix) @@ -285,6 +323,44 @@ impl EpochAdvancer { }) } + if no_self_described_mixnodes != 0 + || not_nym_node_bin_mixnodes != 0 + || no_self_described_gateways != 0 + || not_nym_node_bin_gateways != 0 + || no_self_described_nym_nodes != 0 + || not_nym_node_bin_nym_nodes != 0 + { + warn!("not every bonded node is being considered for rewarded set selection") + } + + if no_self_described_mixnodes != 0 { + warn!("{no_self_described_mixnodes} legacy mixnodes don't expose their self-described API") + } + + if not_nym_node_bin_mixnodes != 0 { + warn!( + "{not_nym_node_bin_mixnodes} legacy mixnodes are not running the 'nym-node' binary" + ) + } + + if no_self_described_gateways != 0 { + warn!("{no_self_described_gateways} legacy gateways don't expose their self-described API") + } + + if not_nym_node_bin_gateways != 0 { + warn!( + "{not_nym_node_bin_gateways} legacy gateways are not running the 'nym-node' binary" + ) + } + + if no_self_described_nym_nodes != 0 { + warn!("{no_self_described_nym_nodes} nym-nodes don't expose their self-described API") + } + + if not_nym_node_bin_nym_nodes != 0 { + warn!("{not_nym_node_bin_nym_nodes} migrated nym-nodes are not running the 'nym-node' binary") + } + with_performance } From 973d51eeecf439dbd1a03ee03264767c5cb784a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 16 Oct 2024 16:16:22 +0100 Subject: [PATCH 095/207] resolved beta clippy issues in DKG contract --- .../src/epoch_state/transactions/advance_epoch_state.rs | 3 +-- contracts/coconut-dkg/src/verification_key_shares/storage.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs index 7ca350120a..6eda195180 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs @@ -57,10 +57,9 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result { pub(crate) epoch_id: MultiIndex<'a, EpochId, ContractVKShare, VKShareKey<'a>>, } -impl<'a> IndexList for VkShareIndex<'a> { +impl IndexList for VkShareIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.epoch_id]; Box::new(v.into_iter()) From 56fb46cd69aaf5670a9d49c757d62382a325df59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 16 Oct 2024 16:24:58 +0100 Subject: [PATCH 096/207] removed old coconut-bandwidth contract from the workspace --- contracts/Cargo.toml | 2 +- contracts/Makefile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 104d1357e4..b5e53222d6 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" members = [ - "coconut-bandwidth", + # "coconut-bandwidth", "coconut-dkg", "coconut-test", "ecash", diff --git a/contracts/Makefile b/contracts/Makefile index e9ff461be0..62b4abcf18 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -1,7 +1,7 @@ -schema: coconut-bandwidth-schema coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema ecash-schema +schema: coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema ecash-schema -coconut-bandwidth-schema: - $(MAKE) -C coconut-bandwidth generate-schema +#coconut-bandwidth-schema: +# $(MAKE) -C coconut-bandwidth generate-schema coconut-dkg-schema: $(MAKE) -C coconut-dkg generate-schema From 7964e6204b916877b6efb9fd01587da324dc1f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 16 Oct 2024 16:26:50 +0100 Subject: [PATCH 097/207] elided extra lifetimes --- contracts/coconut-bandwidth/src/storage.rs | 2 +- contracts/mixnet/src/delegations/storage.rs | 2 +- contracts/mixnet/src/gateways/storage.rs | 2 +- contracts/mixnet/src/mixnodes/storage.rs | 4 ++-- contracts/mixnet/src/nodes/storage/mod.rs | 4 ++-- contracts/mixnet/src/queued_migrations.rs | 2 +- contracts/vesting/src/vesting/account/vesting_account.rs | 1 - 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/coconut-bandwidth/src/storage.rs b/contracts/coconut-bandwidth/src/storage.rs index f18bc6fa28..aa671bbbcb 100644 --- a/contracts/coconut-bandwidth/src/storage.rs +++ b/contracts/coconut-bandwidth/src/storage.rs @@ -18,7 +18,7 @@ pub(crate) struct SpendCredentialIndex<'a> { // IndexList is just boilerplate code for fetching a struct's indexes // note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for SpendCredentialIndex<'a> { +impl IndexList for SpendCredentialIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.blinded_serial_number]; Box::new(v.into_iter()) diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 3fa2931f64..25cd6844c9 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -17,7 +17,7 @@ pub(crate) struct DelegationIndex<'a> { pub(crate) mixnode: MultiIndex<'a, NodeId, Delegation, PrimaryKey>, } -impl<'a> IndexList for DelegationIndex<'a> { +impl IndexList for DelegationIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.mixnode]; Box::new(v.into_iter()) diff --git a/contracts/mixnet/src/gateways/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 95bf11dcc9..9a530fec8d 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -18,7 +18,7 @@ pub(crate) struct GatewayBondIndex<'a> { // IndexList is just boilerplate code for fetching a struct's indexes // note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for GatewayBondIndex<'a> { +impl IndexList for GatewayBondIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner]; Box::new(v.into_iter()) diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index 1a68bbebfb..8344926b7e 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -24,7 +24,7 @@ pub(crate) struct UnbondedMixnodeIndex<'a> { pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, NodeId>, } -impl<'a> IndexList for UnbondedMixnodeIndex<'a> { +impl IndexList for UnbondedMixnodeIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; Box::new(v.into_iter()) @@ -58,7 +58,7 @@ pub(crate) struct MixnodeBondIndex<'a> { // IndexList is just boilerplate code for fetching a struct's indexes // note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for MixnodeBondIndex<'a> { +impl IndexList for MixnodeBondIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key, &self.sphinx_key]; diff --git a/contracts/mixnet/src/nodes/storage/mod.rs b/contracts/mixnet/src/nodes/storage/mod.rs index c431e4af08..63584010a6 100644 --- a/contracts/mixnet/src/nodes/storage/mod.rs +++ b/contracts/mixnet/src/nodes/storage/mod.rs @@ -75,7 +75,7 @@ pub(crate) struct NymNodeBondIndex<'a> { pub(crate) identity_key: UniqueIndex<'a, IdentityKey, NymNodeBond>, } -impl<'a> IndexList for NymNodeBondIndex<'a> { +impl IndexList for NymNodeBondIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; Box::new(v.into_iter()) @@ -103,7 +103,7 @@ pub(crate) struct UnbondedNymNodeIndex<'a> { pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedNymNode, NodeId>, } -impl<'a> IndexList for UnbondedNymNodeIndex<'a> { +impl IndexList for UnbondedNymNodeIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; Box::new(v.into_iter()) diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index 9521172bcb..15cb9a6d43 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -52,7 +52,7 @@ mod families_purge { pub label: UniqueIndex<'a, FamilyHeadKey, Family>, } - impl<'a> IndexList for FamilyIndex<'a> { + impl IndexList for FamilyIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.label]; Box::new(v.into_iter()) diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index 6406df460d..3c2beb142e 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -14,7 +14,6 @@ impl VestingAccount for Account { /// See [VestingAccount::locked_coins] for documentation. /// Returns 0 in case of underflow. Which is fine, as the amount of pledged and delegated tokens can be larger then vesting_coins due to rewards and vesting periods expiring - // TODO: rename. it's no longer 'locked'... or is it? fn locked_coins( &self, From 14fd7c77d0172396b43569a9d0578c0eac37bd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 11:46:36 +0100 Subject: [PATCH 098/207] added unit tests for vested migrations and fixed additional issues --- Cargo.lock | 1 + .../mixnet-contract/src/helpers.rs | 1 + contracts/Cargo.lock | 6 +- contracts/mixnet/Cargo.toml | 1 + contracts/mixnet/src/contract.rs | 2 +- contracts/mixnet/src/support/tests/mod.rs | 138 +++- contracts/mixnet/src/vesting_migration.rs | 635 +++++++++++++++++- 7 files changed, 764 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e882a3dad..1db9eb26e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5055,6 +5055,7 @@ dependencies = [ "anyhow", "axum 0.7.7", "chrono", + "clap 4.5.18", "nym-bin-common", "nym-network-defaults", "nym-node-requests", diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs index 31e8e820d4..887c23fb8d 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -10,6 +10,7 @@ use crate::{ use contracts_common::IdentityKeyRef; use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; +#[track_caller] pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option) { let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128)); if a > b { diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index e15bd4a459..44f70d3fc4 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1284,6 +1284,7 @@ dependencies = [ "nym-crypto", "nym-mixnet-contract-common", "nym-vesting-contract-common", + "rand", "rand_chacha", "serde", "thiserror", @@ -1750,11 +1751,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 67f63d1007..796c6afb3f 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -46,6 +46,7 @@ time = { version = "0.3", features = ["macros"] } [dev-dependencies] anyhow.workspace = true rand_chacha = "0.3" +rand = "0.8.5" nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } [features] diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 25bca25919..694eb256a0 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -269,7 +269,7 @@ pub fn execute( crate::vesting_migration::try_migrate_vested_mixnode(deps, info) } ExecuteMsg::MigrateVestedDelegation { mix_id } => { - crate::vesting_migration::try_migrate_vested_delegation(deps, info, mix_id) + crate::vesting_migration::try_migrate_vested_delegation(deps, env, info, mix_id) } // legacy vesting diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index ba0eecaabb..26547ff6b0 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -31,7 +31,9 @@ pub mod test_helpers { use crate::nodes::storage as nymnodes_storage; use crate::nodes::storage::helpers::RoleStorageBucket; use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; - use crate::nodes::storage::{read_assigned_roles, save_assignment, swap_active_role_bucket}; + use crate::nodes::storage::{ + next_nymnode_id_counter, read_assigned_roles, save_assignment, swap_active_role_bucket, + }; use crate::nodes::transactions::{try_add_nym_node, try_remove_nym_node}; use crate::rewards::helpers::expensive_role_lookup; use crate::rewards::queries::{ @@ -52,7 +54,7 @@ pub mod test_helpers { use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage}; use cosmwasm_std::{Coin, Order}; use cosmwasm_std::{Decimal, Empty, MemoryStorage}; use cosmwasm_std::{Deps, OwnedDeps}; @@ -180,6 +182,10 @@ pub mod test_helpers { } } + pub fn random_address(&mut self) -> String { + format!("n1foomp{}", self.rng.next_u64()) + } + pub fn deps(&self) -> Deps<'_> { self.deps.as_ref() } @@ -326,6 +332,13 @@ pub mod test_helpers { self.owner.clone() } + pub fn vesting_contract(&self) -> Addr { + mixnet_params_storage::CONTRACT_STATE + .load(self.deps().storage) + .unwrap() + .vesting_contract_address + } + pub fn coin(&self, amount: u128) -> Coin { coin(amount, rewarding_denom(self.deps().storage).unwrap()) } @@ -565,6 +578,70 @@ pub mod test_helpers { node_id } + pub fn add_legacy_mixnode_with_proxy_and_keypair( + &mut self, + owner: &str, + stake: Option, + ) -> (NodeId, identity::KeyPair) { + let pledge = self.make_mix_pledge(stake).pop().unwrap(); + + let proxy = self.vesting_contract(); + + let keypair = identity::KeyPair::new(&mut self.rng); + let identity_key = keypair.public_key().to_base58_string(); + let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); + + let mixnode = MixNode { + identity_key, + sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), + ..tests::fixtures::mix_node_fixture() + }; + + let height = self.env.block.height; + let storage = self.deps_mut().storage; + + // manually unroll `save_new_mixnode` to allow for proxy usage + let mix_id = next_nymnode_id_counter(storage).unwrap(); + + let current_epoch = interval_storage::current_interval(storage) + .unwrap() + .current_epoch_absolute_id(); + + let mixnode_rewarding = NodeRewarding::initialise_new( + tests::fixtures::node_cost_params_fixture(), + &pledge, + current_epoch, + ) + .unwrap(); + let mixnode_bond = MixNodeBond { + mix_id, + owner: Addr::unchecked(owner), + original_pledge: pledge, + mix_node: mixnode, + proxy: Some(proxy), + bonding_height: height, + is_unbonding: false, + }; + + mixnode_bonds() + .save(storage, mix_id, &mixnode_bond) + .unwrap(); + rewards_storage::MIXNODE_REWARDING + .save(storage, mix_id, &mixnode_rewarding) + .unwrap(); + + (mix_id, keypair) + } + + pub fn add_legacy_mixnode_with_legal_proxy( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + self.add_legacy_mixnode_with_proxy_and_keypair(owner, stake) + .0 + } + pub fn add_rewarded_legacy_mixnode( &mut self, owner: &str, @@ -832,6 +909,55 @@ pub mod test_helpers { .unwrap(); } + pub fn add_immediate_delegation_with_legal_proxy( + &mut self, + delegator: &str, + amount: impl Into, + target: NodeId, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + let proxy = self.vesting_contract(); + + let owner = self.deps.api.addr_validate(delegator).unwrap(); + let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy)); + + let mut mix_rewarding = self.mix_rewarding(target); + + let mut stored_delegation_amount = amount; + + if let Some(existing_delegation) = delegations_storage::delegations() + .may_load(&self.deps.storage, storage_key.clone()) + .unwrap() + { + let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap(); + stored_delegation_amount.amount += og_with_reward.amount; + } + + mix_rewarding + .add_base_delegation(stored_delegation_amount.amount) + .unwrap(); + + let delegation = Delegation { + owner, + node_id: target, + cumulative_reward_ratio: mix_rewarding.total_unit_reward, + amount: stored_delegation_amount, + height: self.env.block.height, + proxy: Some(proxy), + }; + + delegations_storage::delegations() + .save(&mut self.deps.storage, storage_key, &delegation) + .unwrap(); + rewards_storage::MIXNODE_REWARDING + .save(&mut self.deps.storage, target, &mix_rewarding) + .unwrap(); + } + #[allow(unused)] pub fn add_delegation( &mut self, @@ -1170,6 +1296,13 @@ pub mod test_helpers { let res = try_reward_node(self.deps_mut(), env, sender, node_id, rewarding_params).unwrap(); + + if rewarding_params.is_zero() { + return RewardDistribution { + operator: Decimal::zero(), + delegates: Decimal::zero(), + }; + } let operator: Decimal = find_attribute( Some(MixnetEventType::NodeRewarding.to_string()), OPERATOR_REWARD_KEY, @@ -1286,6 +1419,7 @@ pub mod test_helpers { None } + #[track_caller] pub fn find_attribute>( event_type: Option, attribute: &str, diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs index cfdc18fcf6..6bf6f2a688 100644 --- a/contracts/mixnet/src/vesting_migration.rs +++ b/contracts/mixnet/src/vesting_migration.rs @@ -5,10 +5,11 @@ use crate::delegations::storage as delegations_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::helpers::get_mixnode_details_by_owner; use crate::mixnodes::storage as mixnodes_storage; +use crate::rewards::storage as rewards_storage; use crate::support::helpers::{ ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes, }; -use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response}; +use cosmwasm_std::{wasm_execute, DepsMut, Env, Event, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{Delegation, NodeId}; use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; @@ -49,42 +50,159 @@ pub(crate) fn try_migrate_vested_mixnode( Some(&mix_details.bond_information), )?; - Ok(Response::new().add_message(wasm_execute( - vesting_contract, - &VestingExecuteMsg::TrackMigratedMixnode { - owner: info.sender.into_string(), - }, - vec![], - )?)) + Ok(Response::new() + .add_event(Event::new("migrate-vested-mixnode").add_attribute("mix_id", mix_id.to_string())) + .add_message(wasm_execute( + vesting_contract, + &VestingExecuteMsg::TrackMigratedMixnode { + owner: info.sender.into_string(), + }, + vec![], + )?)) } pub(crate) fn try_migrate_vested_delegation( deps: DepsMut<'_>, + env: Env, info: MessageInfo, mix_id: NodeId, ) -> Result { + let mut response = Response::new(); + ensure_epoch_in_progress_state(deps.storage)?; let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract)); - let Some(mut delegation) = + let Some(vested_delegation) = delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? else { return Err(MixnetContractError::NotAVestingDelegation); }; // sanity check that's meant to blow up the contract - assert_eq!(delegation.proxy, Some(vesting_contract.clone())); + assert_eq!(vested_delegation.proxy, Some(vesting_contract.clone())); // update the delegation and save it under the correct storage key - delegation.proxy = None; - let updated_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); - delegations_storage::delegations().remove(deps.storage, storage_key)?; - delegations_storage::delegations().save(deps.storage, updated_storage_key, &delegation)?; + let mut updated_delegation = vested_delegation.clone(); + updated_delegation.proxy = None; - Ok(Response::new().add_message(wasm_execute( + let new_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + + // remove the old (vested) delegation + delegations_storage::delegations().remove(deps.storage, storage_key)?; + + // check if there was already a delegation present under that key (i.e. an old liquid one) + if let Some(existing_liquid_delegation) = + delegations_storage::delegations().may_load(deps.storage, new_storage_key.clone())? + { + // treat it as adding extra stake to the existing delegation, so we need to update the unit reward value + // as well as retrieve any pending rewards + // it replicates part of code from `pending_events::delegate`, + // but without some checks that'd be redundant in this instance + let mut mix_rewarding = + rewards_storage::MIXNODE_REWARDING.load(deps.storage, vested_delegation.node_id)?; + + // calculate rewards separately for the purposes of emitting those in events + let pending_liquid_reward = + mix_rewarding.determine_delegation_reward(&existing_liquid_delegation)?; + let pending_vested_reward = + mix_rewarding.determine_delegation_reward(&vested_delegation)?; + + // the calls to 'undelegate' followed by artificial delegate are performed + // to keep the internal `.delegates` field in sync + // (this is due to the fact delegation only holds values up in `Uint128` and lacks the precision of a `Decimal` + // which has to be used for reward accounting) + let liquid_delegation_with_reward = + mix_rewarding.undelegate(&existing_liquid_delegation)?; + let vested_delegation_with_reward = mix_rewarding.undelegate(&vested_delegation)?; + + // updated delegation amount consists of: + // - delegated vested tokens + // - delegated liquid tokens + // - pending rewards earned by the delegated vested tokens + // - pending rewards earned by the delegated liquid tokens + let mut updated_total = liquid_delegation_with_reward.clone(); + updated_total.amount += vested_delegation_with_reward.amount; + mix_rewarding.add_base_delegation(updated_total.amount)?; + + updated_delegation.amount = updated_total; + updated_delegation.height = env.block.height; + updated_delegation.cumulative_reward_ratio = mix_rewarding.total_unit_reward; + + rewards_storage::MIXNODE_REWARDING.save( + deps.storage, + vested_delegation.node_id, + &mix_rewarding, + )?; + + // replace the old delegation with the new one + delegations_storage::delegations().replace( + deps.storage, + new_storage_key, + Some(&updated_delegation), + Some(&existing_liquid_delegation), + )?; + + // just emit EVERYTHING we can. just in case + response.events.push( + Event::new("migrate-vested-delegation") + .add_attribute("mix_id", mix_id.to_string()) + .add_attribute("existing_liquid", "true") + .add_attribute( + "old_vested_unit_reward", + vested_delegation.cumulative_reward_ratio.to_string(), + ) + .add_attribute( + "old_vested_delegation_amount", + vested_delegation.amount.to_string(), + ) + .add_attribute( + "old_liquid_unit_reward", + existing_liquid_delegation + .cumulative_reward_ratio + .to_string(), + ) + .add_attribute( + "old_liquid_delegation_amount", + existing_liquid_delegation.amount.to_string(), + ) + .add_attribute( + "new_unit_reward", + updated_delegation.cumulative_reward_ratio.to_string(), + ) + .add_attribute( + "new_delegation_amount", + updated_delegation.amount.to_string(), + ) + .add_attribute("applied_liquid_reward", pending_liquid_reward.to_string()) + .add_attribute("applied_vested_reward", pending_vested_reward.to_string()), + ) + } else { + // otherwise, this is as simple as resaving the updated value under the new key + delegations_storage::delegations().save( + deps.storage, + new_storage_key, + &updated_delegation, + )?; + + response.events.push( + Event::new("migrate-vested-delegation") + .add_attribute("mix_id", mix_id.to_string()) + .add_attribute("existing_liquid", "false") + .add_attribute( + "old_vested_unit_reward", + vested_delegation.cumulative_reward_ratio.to_string(), + ) + .add_attribute( + "old_vested_delegation_amount", + vested_delegation.amount.to_string(), + ), + ) + } + + Ok(response.add_message(wasm_execute( vesting_contract, &VestingExecuteMsg::TrackMigratedDelegation { owner: info.sender.into_string(), @@ -93,3 +211,490 @@ pub(crate) fn try_migrate_vested_delegation( vec![], )?)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod migrating_vested_mixnode { + use super::*; + use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{from_binary, Addr, CosmosMsg, WasmMsg}; + + #[test] + fn with_no_bonded_nodes() { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let deps = test.deps_mut(); + + // nothing happens + let res = try_migrate_vested_mixnode(deps, sender).unwrap_err(); + assert_eq!( + res, + MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked("owner") + } + ) + } + + #[test] + fn with_liquid_node_bonded() { + let mut test = TestSetup::new(); + test.add_legacy_mixnode("owner", None); + + let sender = mock_info("owner", &[]); + let deps = test.deps_mut(); + + // nothing happens + let res = try_migrate_vested_mixnode(deps, sender).unwrap_err(); + assert_eq!(res, MixnetContractError::NotAVestingMixnode) + } + + #[test] + fn with_vested_node_bonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode_with_legal_proxy("owner", None); + + let sender = mock_info("owner", &[]); + let deps = test.deps_mut(); + + let existing_node = get_mixnode_details_by_id(deps.storage, mix_id) + .unwrap() + .unwrap(); + assert!(existing_node.bond_information.proxy.is_some()); + + let mut expected = existing_node.clone(); + expected.bond_information.proxy = None; + + // node is simply resaved with proxy data removed and a track message is sent into the vesting contract + let res = try_migrate_vested_mixnode(deps, sender).unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else { + panic!("no track message present") + }; + + assert_eq!( + from_binary::(msg).unwrap(), + VestingExecuteMsg::TrackMigratedMixnode { + owner: "owner".to_string() + } + ); + } + } + + #[cfg(test)] + mod migrating_vested_delegation { + use super::*; + use crate::delegations::storage::delegations; + use crate::mixnodes::storage::mixnode_bonds; + use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{from_binary, Addr, CosmosMsg, Decimal, Order, Uint128, WasmMsg}; + use mixnet_contract_common::helpers::compare_decimals; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance}; + use mixnet_contract_common::rewarding::helpers::truncate_reward; + use mixnet_contract_common::RoleAssignment; + use rand::distributions::Distribution; + use rand::distributions::WeightedIndex; + use rand::prelude::SliceRandom; + use rand::RngCore; + + fn setup_state() -> TestSetup { + let mut test = TestSetup::new(); + + let mut nodes = Vec::new(); + + let problematic_delegator = "n1unhappydelegator"; + let problematic_delegator_twin = "n1anotherunhappydelegator"; + + let choices = [true, false]; + + // every epoch there's a 2% chance of somebody bonding a node + let bonding_weights = [2, 98]; + + // and 15% of making a delegation + let delegation_weights = [15, 85]; + + // and 1% of making a VESTED delegation + let vested_delegation_weights = [1, 99]; + + let bonding_dist = WeightedIndex::new(bonding_weights).unwrap(); + let delegation_dist = WeightedIndex::new(delegation_weights).unwrap(); + let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap(); + + // make sure we have at least a single node at the beginning + let owner = test.random_address(); + let mix_id = test.add_legacy_mixnode(&owner, None); + nodes.push(mix_id); + + // create a bunch of nodes and delegations and progress through epochs + for epoch_id in 0..1000 { + // go through 1000 epochs + + let owner = test.random_address(); + let min_stake = 100_000_000; + // u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate + let variance = test.rng.next_u32(); + let stake = Uint128::new(min_stake as u128 + variance as u128); + + if choices[bonding_dist.sample(&mut test.rng)] { + // bond + let mix_id = test.add_legacy_mixnode(&owner, Some(stake)); + nodes.push(mix_id); + } + + if choices[delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to delegate to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation(&owner, stake, *node) + } + + if choices[vested_delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to make vested delegation to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node) + } + + // make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node + if epoch_id == 123 { + test.add_immediate_delegation(problematic_delegator, stake, 4); + test.add_immediate_delegation(problematic_delegator_twin, stake, 4); + } + + if epoch_id == 666 { + test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4); + test.add_immediate_delegation_with_legal_proxy( + problematic_delegator_twin, + stake, + 4, + ); + } + + test.skip_to_next_epoch_end(); + // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded + test.force_assign_rewarded_set(vec![RoleAssignment { + role: Role::Layer1, + nodes: nodes.clone(), + }]); + test.start_epoch_transition(); + + // reward each node + for node in &nodes { + let performance = test.rng.next_u64() % 100; + let work_factor = test.active_node_work(); + test.reward_with_distribution( + *node, + NodeRewardingParameters { + performance: Performance::from_percentage_value(performance).unwrap(), + work_factor, + }, + ); + } + + test.set_epoch_in_progress_state(); + } + + test + } + + #[test] + fn with_no_delegation() { + let mut test = setup_state(); + let env = test.env(); + + let sender = mock_info("owner-without-any-delegations", &[]); + + // it simply fails for there is nothing to migrate + let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap_err(); + assert_eq!(res, MixnetContractError::NotAVestingDelegation); + } + + #[test] + fn with_just_liquid_delegation() { + let mut test = setup_state(); + let env = test.env(); + + // find a valid delegation + let delegation = delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .find(|d| d.proxy.is_none()) + .unwrap(); + + // make sure we haven't chosen somebody that also has a vested delegation because that would have invalidated the test + assert!(!delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .any(|d| d.proxy.is_some() && d.owner.as_str() == delegation.owner.as_str())); + + let sender = mock_info(delegation.owner.as_str(), &[]); + let mix_id = delegation.node_id; + + // it also fails because the method is only allowed for vested delegations + let res = + try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap_err(); + assert_eq!(res, MixnetContractError::NotAVestingDelegation); + } + + #[test] + fn with_just_vested_delegation() { + let mut test = setup_state(); + let env = test.env(); + + // find a valid delegation + let delegation = delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .find(|d| d.proxy.is_some()) + .unwrap(); + + // make sure we haven't chosen somebody that also has a liquid delegation because that would have invalidated the test + assert!(!delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .any(|d| d.proxy.is_none() && d.owner.as_str() == delegation.owner.as_str())); + + let storage_key = delegation.storage_key(); + let mut expected_liquid = delegation.clone(); + expected_liquid.proxy = None; + let expected_new_storage_key = expected_liquid.storage_key(); + + let sender = mock_info(delegation.owner.as_str(), &[]); + let mix_id = delegation.node_id; + + // a track message is sent into the vesting contract + let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else { + panic!("no track message present") + }; + + assert_eq!( + from_binary::(msg).unwrap(), + VestingExecuteMsg::TrackMigratedDelegation { + owner: delegation.owner.to_string(), + mix_id, + } + ); + + // the entry is gone from the old storage key + assert!(delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and is resaved (without proxy) under the new key + assert_eq!( + expected_liquid, + delegations() + .load(test.deps().storage, expected_new_storage_key) + .unwrap() + ); + } + + #[test] + fn with_both_liquid_and_vested_delegation() { + #[track_caller] + fn ensure_delegation_sync(test: &TestSetup, mix_id: NodeId) { + let mix_info = test.mix_rewarding(mix_id); + let epsilon = "0.001".parse().unwrap(); + + let subtotal: Decimal = delegations() + .prefix(mix_id) + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| { + d.map(|(_, del)| { + let pending_rewards = + mix_info.determine_delegation_reward(&del).unwrap(); + pending_rewards + del.dec_amount().unwrap() + }) + .ok() + }) + .sum(); + + compare_decimals(mix_info.delegates, subtotal, Some(epsilon)) + } + + let mut test = setup_state(); + let env = test.env(); + + let problematic_delegator = "n1unhappydelegator"; + let problematic_delegator_twin = "n1anotherunhappydelegator"; + let mix_id = 4; + + let liquid_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator), + None, + ); + let vested_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator), + Some(&test.vesting_contract()), + ); + + let liquid_delegation = delegations() + .load(test.deps().storage, liquid_storage_key.clone()) + .unwrap(); + let vested_delegation = delegations() + .load(test.deps().storage, vested_storage_key.clone()) + .unwrap(); + let mix_info = test.mix_rewarding(mix_id); + let unclaimed_liquid_reward = mix_info + .determine_delegation_reward(&liquid_delegation) + .unwrap(); + let unclaimed_vested_reward = mix_info + .determine_delegation_reward(&vested_delegation) + .unwrap(); + + // sanity check before doing anything + ensure_delegation_sync(&test, mix_id); + + // a track message is sent into the vesting contract + let sender = mock_info(problematic_delegator, &[]); + let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else { + panic!("no track message present") + }; + + assert_eq!( + from_binary::(msg).unwrap(), + VestingExecuteMsg::TrackMigratedDelegation { + owner: problematic_delegator.to_string(), + mix_id, + } + ); + + let updated_mix_info = test.mix_rewarding(mix_id); + assert_eq!( + mix_info.unique_delegations - 1, + updated_mix_info.unique_delegations + ); + + // the vested delegation is gone + assert!(delegations() + .may_load(test.deps().storage, vested_storage_key) + .unwrap() + .is_none()); + + let updated_liquid_delegation = delegations() + .load(test.deps().storage, liquid_storage_key.clone()) + .unwrap(); + + assert!(updated_liquid_delegation.proxy.is_none()); + assert_eq!( + updated_liquid_delegation.cumulative_reward_ratio, + updated_mix_info.total_unit_reward + ); + + let expected_amount = truncate_reward( + vested_delegation.dec_amount().unwrap() + + liquid_delegation.dec_amount().unwrap() + + unclaimed_liquid_reward + + unclaimed_vested_reward, + "unym", + ); + // due to rounding we can expect and tolerate a single token of difference + assert_eq_with_leeway( + updated_liquid_delegation.amount.amount, + expected_amount.amount, + Uint128::one(), + ); + + // this assertion must still hold + ensure_delegation_sync(&test, mix_id); + + // go through few more rewarding epochs to make sure the rewards and accounting + // would be the same as if the delegations remained separate + let all_nodes = mixnode_bonds() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|m| m.map(|(_, node)| node.mix_id).ok()) + .collect::>(); + + let twin_liquid_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator_twin), + None, + ); + let twin_vested_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator_twin), + Some(&test.vesting_contract()), + ); + + let twin_liquid_delegation = delegations() + .load(test.deps().storage, twin_liquid_storage_key.clone()) + .unwrap(); + let twin_vested_delegation = delegations() + .load(test.deps().storage, twin_vested_storage_key.clone()) + .unwrap(); + + let info = test.mix_rewarding(mix_id); + + let unclaimed_rewards_twin_liquid = info + .determine_delegation_reward(&twin_liquid_delegation) + .unwrap(); + let unclaimed_rewards_twin_vested = info + .determine_delegation_reward(&twin_vested_delegation) + .unwrap(); + + for _ in 0..100 { + test.skip_to_next_epoch_end(); + // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded + test.force_assign_rewarded_set(vec![RoleAssignment { + role: Role::Layer1, + nodes: all_nodes.clone(), + }]); + test.start_epoch_transition(); + + // reward each node + for node in &all_nodes { + let performance = test.rng.next_u64() % 100; + let work_factor = test.active_node_work(); + test.reward_with_distribution( + *node, + NodeRewardingParameters { + performance: Performance::from_percentage_value(performance).unwrap(), + work_factor, + }, + ); + } + + test.set_epoch_in_progress_state(); + } + + // this assertion must still hold + ensure_delegation_sync(&test, mix_id); + + let info = test.mix_rewarding(mix_id); + + let current_liquid = delegations() + .load(test.deps().storage, liquid_storage_key) + .unwrap(); + let rewards = info.determine_delegation_reward(¤t_liquid).unwrap(); + + let twin_liquid_delegation = delegations() + .load(test.deps().storage, twin_liquid_storage_key.clone()) + .unwrap(); + let twin_vested_delegation = delegations() + .load(test.deps().storage, twin_vested_storage_key.clone()) + .unwrap(); + + let rewards_twin_liquid = info + .determine_delegation_reward(&twin_liquid_delegation) + .unwrap(); + let rewards_twin_vested = info + .determine_delegation_reward(&twin_vested_delegation) + .unwrap(); + + let new_rewards_twin = rewards_twin_liquid + rewards_twin_vested + - unclaimed_rewards_twin_liquid + - unclaimed_rewards_twin_vested; + + compare_decimals(rewards, new_rewards_twin, Some("0.01".parse().unwrap())) + } + } +} From 02dd16bcc68cb56a586be9e7fb39360cd59ff98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 14:52:39 +0100 Subject: [PATCH 099/207] added unit tests for the migration and fixed rounding errors --- .../mixnet-contract/src/helpers.rs | 4 +- contracts/mixnet/src/mixnodes/helpers.rs | 9 +- contracts/mixnet/src/mixnodes/queries.rs | 12 +- contracts/mixnet/src/support/tests/mod.rs | 218 ++++++++++++------ contracts/mixnet/src/vesting_migration.rs | 148 +----------- 5 files changed, 172 insertions(+), 219 deletions(-) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs index 887c23fb8d..092f35e80c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -14,9 +14,9 @@ use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option) { let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128)); if a > b { - assert!(a - b < epsilon, "{a} != {b}") + assert!(a - b < epsilon, "{a} != {b}, delta: {}", a - b) } else { - assert!(b - a < epsilon, "{a} != {b}") + assert!(b - a < epsilon, "{a} != {b}, delta: {}", b - a) } } diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index fc2d56254f..ef78ffb741 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -156,28 +156,29 @@ pub(crate) mod tests { test: &mut TestSetup, stake: Option, ) -> Vec { - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_EXISTS, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_EXISTS, stake); let mix_exists = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_EXISTS), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDING, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_UNBONDING, stake); let mix_unbonding = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDING), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_UNBONDED, stake); let mix_unbonded = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDED), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED_LEFTOVER, stake); + let (mix_id, keypair) = + test.add_legacy_mixnode_with_keypair(OWNER_UNBONDED_LEFTOVER, stake); let mix_unbonded_leftover = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDED_LEFTOVER), diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index 8300edf5f1..8f127ed9f5 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -266,7 +266,7 @@ pub(crate) mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); let limit = 2; let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(limit)).unwrap(); @@ -276,7 +276,7 @@ pub(crate) mod tests { #[test] fn has_default_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query without explicitly setting a limit let page1 = query_mixnode_bonds_paged(test.deps(), None, None).unwrap(); @@ -290,7 +290,7 @@ pub(crate) mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; @@ -353,7 +353,7 @@ pub(crate) mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); let limit = 2; let page1 = query_mixnodes_details_paged(test.deps(), None, Some(limit)).unwrap(); @@ -363,7 +363,7 @@ pub(crate) mod tests { #[test] fn has_default_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query without explicitly setting a limit let page1 = query_mixnodes_details_paged(test.deps(), None, None).unwrap(); @@ -377,7 +377,7 @@ pub(crate) mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 26547ff6b0..ed59e8a19c 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -11,6 +11,7 @@ pub mod test_helpers { use crate::contract::{execute, instantiate}; use crate::delegations::queries::query_node_delegations_paged; use crate::delegations::storage as delegations_storage; + use crate::delegations::storage::delegations; use crate::delegations::transactions::try_delegate_to_node; use crate::interval::transactions::{ perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, @@ -64,6 +65,7 @@ pub mod test_helpers { use mixnet_contract_common::events::{ may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, }; + use mixnet_contract_common::helpers::compare_decimals; use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode}; use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; @@ -86,6 +88,8 @@ pub mod test_helpers { }; use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity::KeyPair; + use rand::distributions::WeightedIndex; + use rand::prelude::*; use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use serde::Serialize; @@ -135,14 +139,16 @@ pub mod test_helpers { } } + #[track_caller] pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { if a > b { - assert!(a - b <= leeway) + assert!(a - b <= leeway, "{} != {}", a, b) } else { - assert!(b - a <= leeway) + assert!(b - a <= leeway, "{} != {}", a, b) } } + #[track_caller] pub fn assert_decimals(a: Decimal, b: Decimal) { let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); if a > b { @@ -182,6 +188,137 @@ pub mod test_helpers { } } + pub fn new_complex() -> Self { + let mut test = TestSetup::new(); + + let mut nodes = Vec::new(); + + let problematic_delegator = "n1foomp"; + let problematic_delegator_twin = "n1bar"; + let problematic_delegator_alt_twin = "n1whatever"; + + let choices = [true, false]; + + // every epoch there's a 2% chance of somebody bonding a node + let bonding_weights = [2, 98]; + + // and 15% of making a delegation + let delegation_weights = [15, 85]; + + // and 1% of making a VESTED delegation + let vested_delegation_weights = [1, 99]; + + let bonding_dist = WeightedIndex::new(bonding_weights).unwrap(); + let delegation_dist = WeightedIndex::new(delegation_weights).unwrap(); + let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap(); + + // make sure we have at least a single node at the beginning + let owner = test.random_address(); + let mix_id = test.add_legacy_mixnode(&owner, None); + nodes.push(mix_id); + + // create a bunch of nodes and delegations and progress through epochs + for epoch_id in 0..1000 { + // go through 1000 epochs + + let owner = test.random_address(); + let min_stake = 100_000_000; + // u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate + let variance = test.rng.next_u32(); + let stake = Uint128::new(min_stake as u128 + variance as u128); + + if choices[bonding_dist.sample(&mut test.rng)] { + // bond + let mix_id = test.add_legacy_mixnode(&owner, Some(stake)); + nodes.push(mix_id); + } + + if choices[delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to delegate to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation(&owner, stake, *node) + } + + if choices[vested_delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to make vested delegation to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node) + } + + // make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node + if epoch_id == 123 { + test.add_immediate_delegation(problematic_delegator, stake, 4); + test.add_immediate_delegation(problematic_delegator_twin, stake, 4); + } + + if epoch_id == 666 { + test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4); + test.add_immediate_delegation_with_legal_proxy( + problematic_delegator_twin, + stake, + 4, + ); + } + + if epoch_id == 234 { + test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 4); + } + + if epoch_id == 420 { + test.add_immediate_delegation_with_legal_proxy( + problematic_delegator_alt_twin, + stake, + 4, + ); + } + + test.skip_to_next_epoch_end(); + // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded + test.force_assign_rewarded_set(vec![RoleAssignment { + role: Role::Layer1, + nodes: nodes.clone(), + }]); + test.start_epoch_transition(); + + // reward each node + for node in &nodes { + let performance = test.rng.next_u64() % 100; + let work_factor = test.active_node_work(); + test.reward_with_distribution( + *node, + NodeRewardingParameters { + performance: Performance::from_percentage_value(performance).unwrap(), + work_factor, + }, + ); + } + + test.set_epoch_in_progress_state(); + } + + test + } + + #[track_caller] + pub fn ensure_delegation_sync(&self, mix_id: NodeId) { + let mix_info = self.mix_rewarding(mix_id); + let epsilon = "0.001".parse().unwrap(); + + let subtotal: Decimal = delegations() + .prefix(mix_id) + .range(self.deps().storage, None, None, Order::Ascending) + .filter_map(|d| { + d.map(|(_, del)| { + let pending_rewards = mix_info.determine_delegation_reward(&del).unwrap(); + pending_rewards + del.dec_amount().unwrap() + }) + .ok() + }) + .sum(); + + compare_decimals(mix_info.delegates, subtotal, Some(epsilon)) + } + pub fn random_address(&mut self) -> String { format!("n1foomp{}", self.rng.next_u64()) } @@ -339,6 +476,13 @@ pub mod test_helpers { .vesting_contract_address } + pub fn all_mixnodes(&self) -> Vec { + mixnode_bonds() + .range(self.deps().storage, None, None, Order::Ascending) + .filter_map(|m| m.map(|(_, node)| node.mix_id).ok()) + .collect::>() + } + pub fn coin(&self, amount: u128) -> Coin { coin(amount, rewarding_denom(self.deps().storage).unwrap()) } @@ -687,7 +831,7 @@ pub mod test_helpers { .unwrap(); } - pub fn add_dummy_mixnodes(&mut self, n: usize) { + pub fn add_legacy_mixnodes(&mut self, n: usize) { for i in 0..n { self.add_legacy_mixnode(&format!("owner{i}"), None); } @@ -738,7 +882,7 @@ pub mod test_helpers { ed25519_sign_message(msg, key) } - pub fn add_dummy_mixnode_with_keypair( + pub fn add_legacy_mixnode_with_keypair( &mut self, owner: &str, stake: Option, @@ -1520,55 +1664,6 @@ pub mod test_helpers { perform_pending_interval_actions(deps.branch(), &env, None).unwrap(); } - // pub fn mixnode_with_signature( - // mut rng: impl RngCore + CryptoRng, - // deps: Deps<'_>, - // sender: &str, - // stake: Option>, - // ) -> (MixNode, MessageSignature, KeyPair) { - // // hehe stupid workaround for bypassing the immutable borrow and removing duplicate code - // - // let stake = stake.unwrap_or(good_mixnode_pledge()); - // - // let keypair = identity::KeyPair::new(&mut rng); - // let identity_key = keypair.public_key().to_base58_string(); - // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - // - // let mixnode = MixNode { - // identity_key, - // sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - // ..tests::fixtures::mix_node_fixture() - // }; - // let msg = mixnode_bonding_sign_payload(deps, sender, None, mixnode.clone(), stake.clone()); - // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - // - // (mixnode, owner_signature, keypair) - // } - - // pub fn gateway_with_signature( - // mut rng: impl RngCore + CryptoRng, - // deps: Deps<'_>, - // sender: &str, - // stake: Option>, - // ) -> (Gateway, MessageSignature) { - // let stake = stake.unwrap_or(good_gateway_pledge()); - // - // let keypair = identity::KeyPair::new(&mut rng); - // let identity_key = keypair.public_key().to_base58_string(); - // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - // - // let gateway = Gateway { - // identity_key, - // sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - // ..tests::fixtures::gateway_fixture() - // }; - // - // let msg = gateway_bonding_sign_payload(deps, sender, None, gateway.clone(), stake.clone()); - // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - // - // (gateway, owner_signature) - // } - pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) { for i in 0..n { pending_events::delegate( @@ -1583,23 +1678,6 @@ pub mod test_helpers { } } - // pub fn add_dummy_mixnodes( - // mut rng: impl RngCore + CryptoRng, - // mut deps: DepsMut<'_>, - // env: Env, - // n: usize, - // ) { - // for i in 0..n { - // add_mixnode( - // &mut rng, - // deps.branch(), - // env.clone(), - // &format!("owner{}", i), - // tests::fixtures::good_mixnode_pledge(), - // ); - // } - // } - pub fn add_dummy_unbonded_mixnodes( mut rng: impl RngCore + CryptoRng, mut deps: DepsMut<'_>, diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs index 6bf6f2a688..c67a87518e 100644 --- a/contracts/mixnet/src/vesting_migration.rs +++ b/contracts/mixnet/src/vesting_migration.rs @@ -289,121 +289,19 @@ mod tests { mod migrating_vested_delegation { use super::*; use crate::delegations::storage::delegations; - use crate::mixnodes::storage::mixnode_bonds; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{from_binary, Addr, CosmosMsg, Decimal, Order, Uint128, WasmMsg}; + use cosmwasm_std::{from_binary, Addr, CosmosMsg, Order, Uint128, WasmMsg}; use mixnet_contract_common::helpers::compare_decimals; use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance}; use mixnet_contract_common::rewarding::helpers::truncate_reward; use mixnet_contract_common::RoleAssignment; - use rand::distributions::Distribution; - use rand::distributions::WeightedIndex; - use rand::prelude::SliceRandom; use rand::RngCore; - fn setup_state() -> TestSetup { - let mut test = TestSetup::new(); - - let mut nodes = Vec::new(); - - let problematic_delegator = "n1unhappydelegator"; - let problematic_delegator_twin = "n1anotherunhappydelegator"; - - let choices = [true, false]; - - // every epoch there's a 2% chance of somebody bonding a node - let bonding_weights = [2, 98]; - - // and 15% of making a delegation - let delegation_weights = [15, 85]; - - // and 1% of making a VESTED delegation - let vested_delegation_weights = [1, 99]; - - let bonding_dist = WeightedIndex::new(bonding_weights).unwrap(); - let delegation_dist = WeightedIndex::new(delegation_weights).unwrap(); - let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap(); - - // make sure we have at least a single node at the beginning - let owner = test.random_address(); - let mix_id = test.add_legacy_mixnode(&owner, None); - nodes.push(mix_id); - - // create a bunch of nodes and delegations and progress through epochs - for epoch_id in 0..1000 { - // go through 1000 epochs - - let owner = test.random_address(); - let min_stake = 100_000_000; - // u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate - let variance = test.rng.next_u32(); - let stake = Uint128::new(min_stake as u128 + variance as u128); - - if choices[bonding_dist.sample(&mut test.rng)] { - // bond - let mix_id = test.add_legacy_mixnode(&owner, Some(stake)); - nodes.push(mix_id); - } - - if choices[delegation_dist.sample(&mut test.rng)] { - // uniformly choose a random node to delegate to - let node = nodes.choose(&mut test.rng).unwrap(); - test.add_immediate_delegation(&owner, stake, *node) - } - - if choices[vested_delegation_dist.sample(&mut test.rng)] { - // uniformly choose a random node to make vested delegation to - let node = nodes.choose(&mut test.rng).unwrap(); - test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node) - } - - // make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node - if epoch_id == 123 { - test.add_immediate_delegation(problematic_delegator, stake, 4); - test.add_immediate_delegation(problematic_delegator_twin, stake, 4); - } - - if epoch_id == 666 { - test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4); - test.add_immediate_delegation_with_legal_proxy( - problematic_delegator_twin, - stake, - 4, - ); - } - - test.skip_to_next_epoch_end(); - // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded - test.force_assign_rewarded_set(vec![RoleAssignment { - role: Role::Layer1, - nodes: nodes.clone(), - }]); - test.start_epoch_transition(); - - // reward each node - for node in &nodes { - let performance = test.rng.next_u64() % 100; - let work_factor = test.active_node_work(); - test.reward_with_distribution( - *node, - NodeRewardingParameters { - performance: Performance::from_percentage_value(performance).unwrap(), - work_factor, - }, - ); - } - - test.set_epoch_in_progress_state(); - } - - test - } - #[test] fn with_no_delegation() { - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); let sender = mock_info("owner-without-any-delegations", &[]); @@ -415,7 +313,7 @@ mod tests { #[test] fn with_just_liquid_delegation() { - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); // find a valid delegation @@ -442,7 +340,7 @@ mod tests { #[test] fn with_just_vested_delegation() { - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); // find a valid delegation @@ -497,32 +395,11 @@ mod tests { #[test] fn with_both_liquid_and_vested_delegation() { - #[track_caller] - fn ensure_delegation_sync(test: &TestSetup, mix_id: NodeId) { - let mix_info = test.mix_rewarding(mix_id); - let epsilon = "0.001".parse().unwrap(); - - let subtotal: Decimal = delegations() - .prefix(mix_id) - .range(test.deps().storage, None, None, Order::Ascending) - .filter_map(|d| { - d.map(|(_, del)| { - let pending_rewards = - mix_info.determine_delegation_reward(&del).unwrap(); - pending_rewards + del.dec_amount().unwrap() - }) - .ok() - }) - .sum(); - - compare_decimals(mix_info.delegates, subtotal, Some(epsilon)) - } - - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); - let problematic_delegator = "n1unhappydelegator"; - let problematic_delegator_twin = "n1anotherunhappydelegator"; + let problematic_delegator = "n1foomp"; + let problematic_delegator_twin = "n1bar"; let mix_id = 4; let liquid_storage_key = Delegation::generate_storage_key( @@ -551,7 +428,7 @@ mod tests { .unwrap(); // sanity check before doing anything - ensure_delegation_sync(&test, mix_id); + test.ensure_delegation_sync(mix_id); // a track message is sent into the vesting contract let sender = mock_info(problematic_delegator, &[]); @@ -605,14 +482,11 @@ mod tests { ); // this assertion must still hold - ensure_delegation_sync(&test, mix_id); + test.ensure_delegation_sync(mix_id); // go through few more rewarding epochs to make sure the rewards and accounting // would be the same as if the delegations remained separate - let all_nodes = mixnode_bonds() - .range(test.deps().storage, None, None, Order::Ascending) - .filter_map(|m| m.map(|(_, node)| node.mix_id).ok()) - .collect::>(); + let all_nodes = test.all_mixnodes(); let twin_liquid_storage_key = Delegation::generate_storage_key( mix_id, @@ -667,7 +541,7 @@ mod tests { } // this assertion must still hold - ensure_delegation_sync(&test, mix_id); + test.ensure_delegation_sync(mix_id); let info = test.mix_rewarding(mix_id); From 86eb06048a3bc2e071172c889d45149abcc31f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 15 Oct 2024 15:08:52 +0100 Subject: [PATCH 100/207] ci: update 'publish-nym-contracts' runner --- .github/workflows/ci-contracts-upload-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index 4873d46885..9cb7116dca 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - platform: arc-ubuntu-20.04 + platform: [ arc-ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: From 9bcf48f7dc86455103d3ccf7fae1b81b665ac52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 16 Oct 2024 17:09:17 +0100 Subject: [PATCH 101/207] expose all ecash routes but return error if nym-api is not a signer --- Cargo.lock | 1 + nym-api/src/ecash/api_routes/issued.rs | 15 +++++-- .../src/ecash/api_routes/partial_signing.rs | 16 ++++++-- nym-api/src/ecash/api_routes/spending.rs | 15 +++++-- nym-api/src/ecash/error.rs | 3 ++ nym-api/src/ecash/state/local.rs | 4 ++ nym-api/src/ecash/state/mod.rs | 24 +++++++++++ nym-api/src/support/cli/run.rs | 40 ++++++++++--------- 8 files changed, 90 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e882a3dad..1db9eb26e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5055,6 +5055,7 @@ dependencies = [ "anyhow", "axum 0.7.7", "chrono", + "clap 4.5.18", "nym-bin-common", "nym-network-defaults", "nym-node-requests", diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs index 95c1439600..1593e5e9e1 100644 --- a/nym-api/src/ecash/api_routes/issued.rs +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -56,13 +56,16 @@ struct EpochParam { ), path = "/v1/ecash/epoch-credentials/{epoch}", responses( - (status = 200, body = EpochCredentialsResponse) + (status = 200, body = EpochCredentialsResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn epoch_credentials( Path(EpochParam { epoch }): Path, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + let issued = state.aux.storage.get_epoch_credentials(epoch).await?; let response = if let Some(issued) = issued { @@ -92,13 +95,16 @@ struct IdParam { ), path = "/v1/ecash/issued-credential/{id}", responses( - (status = 200, body = IssuedCredentialResponse) + (status = 200, body = IssuedCredentialResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_credential( Path(IdParam { id }): Path, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + let issued = state.aux.storage.get_issued_credential(id).await?; let credential = if let Some(issued) = issued { @@ -116,13 +122,16 @@ async fn issued_credential( request_body = CredentialsRequestBody, path = "/v1/ecash/issued-credentials", responses( - (status = 200, body = IssuedCredentialsResponse) + (status = 200, body = IssuedCredentialsResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_credentials( Json(params): Json, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + if params.pagination.is_some() && !params.credential_ids.is_empty() { return Err(EcashError::InvalidQueryArguments.into()); } diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index e42ffcf6e0..72e6b50fec 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -53,13 +53,17 @@ pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + debug!("Received blind sign request"); trace!("body: {:?}", blind_sign_request_body); @@ -125,13 +129,16 @@ struct ExpirationDateParam { ), path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}", responses( - (status = 200, body = PartialExpirationDateSignatureResponse) + (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn partial_expiration_date_signatures( Path(ExpirationDateParam { expiration_date }): Path, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + let expiration_date = match expiration_date { None => cred_exp_date().ecash_date(), Some(raw) => Date::parse(&raw, &rfc_3339_date()) @@ -160,13 +167,16 @@ async fn partial_expiration_date_signatures( ), path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}", responses( - (status = 200, body = PartialExpirationDateSignatureResponse) + (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn partial_coin_indices_signatures( Path(EpochIdParam { epoch_id }): Path, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index a5110a52fc..fdbb694c92 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -61,7 +61,8 @@ fn reject_ticket( request_body = VerifyEcashTicketBody, path = "/v1/ecash/verify-ecash-ticket", responses( - (status = 200, body = EcashTicketVerificationResponse) + (status = 200, body = EcashTicketVerificationResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn verify_ticket( @@ -69,6 +70,8 @@ async fn verify_ticket( Json(verify_ticket_body): Json, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + let credential_data = &verify_ticket_body.credential; let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; @@ -161,7 +164,8 @@ async fn verify_ticket( request_body = BatchRedeemTicketsBody, path = "/v1/ecash/batch-redeem-ecash-tickets", responses( - (status = 200, body = EcashBatchTicketRedemptionResponse) + (status = 200, body = EcashBatchTicketRedemptionResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn batch_redeem_tickets( @@ -169,6 +173,8 @@ async fn batch_redeem_tickets( Json(batch_redeem_credentials_body): Json, state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + // 1. see if that gateway has even submitted any tickets let Some(provider_info) = state .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) @@ -233,12 +239,15 @@ async fn batch_redeem_tickets( get, path = "/v1/ecash/double-spending-filter-v1", responses( - (status = 200, body = SpentCredentialsResponse) + (status = 200, body = SpentCredentialsResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn double_spending_filter_v1( state: Arc, ) -> AxumResult> { + state.ensure_signer().await?; + let spent_credentials_export = state.get_bloomfilter_bytes().await; Ok(Json(SpentCredentialsResponse::new( spent_credentials_export, diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index e5c7a9fd89..066d62ea8b 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -27,6 +27,9 @@ pub enum EcashError { #[error(transparent)] IOError(#[from] std::io::Error), + #[error("this operation couldn't be completed as this nym-api is not an active ecash signer")] + NotASigner, + #[error("the address of the bandwidth contract hasn't been set")] MissingBandwidthContractAddress, diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index 52faecbf3f..9da29dc4c9 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -97,6 +97,9 @@ pub(crate) struct LocalEcashState { pub(crate) ecash_keypair: KeyPair, pub(crate) identity_keypair: identity::KeyPair, + /// Specifies whether this api is a signer in given epoch + pub(crate) active_signer: CachedImmutableEpochItem, + pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem, pub(crate) partial_expiration_date_signatures: CachedImmutableItems, @@ -117,6 +120,7 @@ impl LocalEcashState { LocalEcashState { ecash_keypair, identity_keypair, + active_signer: Default::default(), partial_coin_index_signatures: Default::default(), partial_expiration_date_signatures: Default::default(), exported_double_spending_filter: ExportedDoubleSpendingFilter { diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index 27a878f061..c33ca61442 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -44,6 +44,7 @@ use nym_ecash_double_spending::DoubleSpendingFilter; use nym_ecash_time::cred_exp_date; use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; +use std::ops::Deref; use time::ext::NumericalDuration; use time::{Date, Duration, OffsetDateTime}; use tokio::sync::RwLockReadGuard; @@ -88,6 +89,29 @@ impl EcashState { }) } + /// Ensures that this nym-api is one of ecash signers for the current epoch + pub(crate) async fn ensure_signer(&self) -> Result<()> { + let epoch_id = self.aux.current_epoch().await?; + + let is_epoch_signer = self + .local + .active_signer + .get_or_init(epoch_id, || async { + let address = self.aux.client.address().await; + let ecash_signers = self.aux.comm_channel.ecash_clients(epoch_id).await?; + + // check if any ecash signers for this epoch has the same cosmos address as this api + Ok(ecash_signers.iter().any(|c| c.cosmos_address == address)) + }) + .await?; + + if !is_epoch_signer.deref() { + return Err(EcashError::NotASigner); + } + + Ok(()) + } + pub(crate) async fn ecash_signing_key(&self) -> Result> { self.local.ecash_keypair.signing_key().await } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index c3fabc81e1..119043d230 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -131,7 +131,26 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result anyhow::Result Date: Wed, 16 Oct 2024 17:17:48 +0100 Subject: [PATCH 102/207] re-disabled bloomfilters --- nym-api/src/ecash/api_routes/spending.rs | 14 ++----- nym-api/src/ecash/state/local.rs | 53 +----------------------- nym-api/src/ecash/state/mod.rs | 15 +------ 3 files changed, 6 insertions(+), 76 deletions(-) diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index fdbb694c92..6f0a94d4f9 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -3,7 +3,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; -use crate::node_status_api::models::AxumResult; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; @@ -239,17 +239,11 @@ async fn batch_redeem_tickets( get, path = "/v1/ecash/double-spending-filter-v1", responses( - (status = 200, body = SpentCredentialsResponse), - (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + (status = 500, body = ErrorResponse, description = "bloomfilters got disabled"), ) )] async fn double_spending_filter_v1( - state: Arc, + _state: Arc, ) -> AxumResult> { - state.ensure_signer().await?; - - let spent_credentials_export = state.get_bloomfilter_bytes().await; - Ok(Json(SpentCredentialsResponse::new( - spent_credentials_export, - ))) + AxumResult::Err(AxumErrorResponse::internal_msg("permanently restricted")) } diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index 9da29dc4c9..64eef0ce07 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -9,11 +9,9 @@ use crate::ecash::keys::KeyPair; use nym_config::defaults::BloomfilterParameters; use nym_crypto::asymmetric::identity; use nym_ecash_double_spending::DoubleSpendingFilter; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use time::{Date, OffsetDateTime}; +use time::Date; use tokio::sync::RwLock; -use tracing::debug; pub(crate) struct TicketDoubleSpendingFilter { built_on: Date, @@ -71,10 +69,6 @@ impl TicketDoubleSpendingFilter { self.today_filter.dump_bitmap() } - pub(crate) fn export_global_bitmap(&self) -> Vec { - self.global_filter.dump_bitmap() - } - pub(crate) fn advance_day(&mut self, date: Date, new_global: DoubleSpendingFilter) { self.built_on = date; self.global_filter = new_global; @@ -82,17 +76,6 @@ impl TicketDoubleSpendingFilter { } } -pub(crate) struct ExportedDoubleSpendingFilterData { - pub(crate) last_exported_at: OffsetDateTime, - pub(crate) bytes: Vec, -} - -#[derive(Clone)] -pub(crate) struct ExportedDoubleSpendingFilter { - pub(crate) being_exported: Arc, - pub(crate) data: Arc>, -} - pub(crate) struct LocalEcashState { pub(crate) ecash_keypair: KeyPair, pub(crate) identity_keypair: identity::KeyPair, @@ -106,9 +89,6 @@ pub(crate) struct LocalEcashState { // the actual, up to date, bloomfilter pub(crate) double_spending_filter: Arc>, - - // the cached byte representation of the bloomfilter to be used by the clients - pub(crate) exported_double_spending_filter: ExportedDoubleSpendingFilter, } impl LocalEcashState { @@ -123,38 +103,7 @@ impl LocalEcashState { active_signer: Default::default(), partial_coin_index_signatures: Default::default(), partial_expiration_date_signatures: Default::default(), - exported_double_spending_filter: ExportedDoubleSpendingFilter { - being_exported: Arc::new(Default::default()), - data: Arc::new(RwLock::new(ExportedDoubleSpendingFilterData { - last_exported_at: OffsetDateTime::now_utc(), - bytes: double_spending_filter.export_global_bitmap(), - })), - }, double_spending_filter: Arc::new(RwLock::new(double_spending_filter)), } } - - pub(crate) fn maybe_background_update_exported_bloomfilter(&self) { - // make sure another query hasn't already spawned an exporting task - let Ok(should_export) = self - .exported_double_spending_filter - .being_exported - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) - else { - return; - }; - - let filter = self.double_spending_filter.clone(); - let exported = self.exported_double_spending_filter.clone(); - - if should_export { - tokio::spawn(async move { - debug!("exporting bloomfilter bitmap"); - let new = filter.read().await.export_global_bitmap(); - let mut exported_guard = exported.data.write().await; - exported_guard.last_exported_at = OffsetDateTime::now_utc(); - exported_guard.bytes = new; - }); - } - } } diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index c33ca61442..e1a3908510 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -46,7 +46,7 @@ use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; use std::ops::Deref; use time::ext::NumericalDuration; -use time::{Date, Duration, OffsetDateTime}; +use time::{Date, OffsetDateTime}; use tokio::sync::RwLockReadGuard; use tracing::{debug, error, info, warn}; @@ -864,17 +864,4 @@ impl EcashState { res } - - pub async fn get_bloomfilter_bytes(&self) -> Vec { - let guard = self.local.exported_double_spending_filter.data.read().await; - - let bytes = guard.bytes.clone(); - - // see if it's been > 5min since last export (that value is arbitrary) - if guard.last_exported_at + Duration::minutes(5) < OffsetDateTime::now_utc() { - self.local.maybe_background_update_exported_bloomfilter(); - } - - bytes - } } From ed2fbc588ff5a90d41c37825a986effe9501b125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 16 Oct 2024 17:34:34 +0100 Subject: [PATCH 103/207] allow to overwrite contract state with explicit config flag --- nym-api/src/ecash/state/local.rs | 4 ++ nym-api/src/ecash/state/mod.rs | 12 +++- nym-api/src/ecash/tests/issued_credentials.rs | 4 +- nym-api/src/ecash/tests/mod.rs | 55 ++++++++++++------- nym-api/src/support/cli/run.rs | 1 + 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index 64eef0ce07..b840bab394 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -80,6 +80,8 @@ pub(crate) struct LocalEcashState { pub(crate) ecash_keypair: KeyPair, pub(crate) identity_keypair: identity::KeyPair, + pub(crate) explicitly_disabled: bool, + /// Specifies whether this api is a signer in given epoch pub(crate) active_signer: CachedImmutableEpochItem, @@ -96,10 +98,12 @@ impl LocalEcashState { ecash_keypair: KeyPair, identity_keypair: identity::KeyPair, double_spending_filter: TicketDoubleSpendingFilter, + explicitly_disabled: bool, ) -> Self { LocalEcashState { ecash_keypair, identity_keypair, + explicitly_disabled, active_signer: Default::default(), partial_coin_index_signatures: Default::default(), partial_expiration_date_signatures: Default::default(), diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index e1a3908510..11ed0d3ff7 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -75,6 +75,7 @@ impl EcashState { key_pair: KeyPair, comm_channel: D, storage: NymApiStorage, + signer_disabled: bool, ) -> Result where C: LocalClient + Send + Sync + 'static, @@ -84,13 +85,22 @@ impl EcashState { Ok(Self { global: GlobalEcachState::new(contract_address), - local: LocalEcashState::new(key_pair, identity_keypair, double_spending_filter), + local: LocalEcashState::new( + key_pair, + identity_keypair, + double_spending_filter, + signer_disabled, + ), aux: AuxiliaryEcashState::new(client, comm_channel, storage), }) } /// Ensures that this nym-api is one of ecash signers for the current epoch pub(crate) async fn ensure_signer(&self) -> Result<()> { + if self.local.explicitly_disabled { + return Err(EcashError::NotASigner); + } + let epoch_id = self.aux.current_epoch().await?; let is_epoch_signer = self diff --git a/nym-api/src/ecash/tests/issued_credentials.rs b/nym-api/src/ecash/tests/issued_credentials.rs index 877e974d05..00dac0d525 100644 --- a/nym-api/src/ecash/tests/issued_credentials.rs +++ b/nym-api/src/ecash/tests/issued_credentials.rs @@ -53,7 +53,7 @@ async fn epoch_credentials() { assert_eq!(parsed_response.total_issued, 2); assert_eq!(parsed_response.first_epoch_credential_id, Some(1)); - test_fixture.set_epoch(2); + test_fixture.set_epoch(2).await; let response = test_fixture.axum.get(&route_epoch2).await; assert_eq!(response.status_code(), StatusCode::OK); @@ -115,7 +115,7 @@ async fn issued_credential() { let cred1 = test_fixture.issue_credential(request1.clone()).await; - test_fixture.set_epoch(3); + test_fixture.set_epoch(3).await; let cred2 = test_fixture.issue_credential(request2.clone()).await; let response = test_fixture.axum.get(&route(1)).await; diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index aba83c5ec4..123c0a7930 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1133,18 +1133,23 @@ impl DummyCommunicationChannel { } } - pub fn new_single_dummy(aggregated_verification_key: VerificationKeyAuth) -> Self { + pub fn new_single_dummy( + aggregated_verification_key: VerificationKeyAuth, + cosmos_address: AccountId, + ) -> Self { let client = EcashApiClient { api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()), verification_key: aggregated_verification_key, node_id: 1, - cosmos_address: "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" - .parse() - .unwrap(), + cosmos_address, }; Self::new(vec![client]) } + pub fn clients_arc(&self) -> Arc>>> { + Arc::clone(&self.ecash_clients) + } + pub fn with_epoch(mut self, current_epoch: Arc) -> Self { self.current_epoch = current_epoch; self @@ -1248,6 +1253,7 @@ struct TestFixture { storage: NymApiStorage, chain_state: SharedFakeChain, epoch: Arc, + ecash_clients: Arc>>>, _tmp_dir: TempDir, } @@ -1273,9 +1279,13 @@ impl TestFixture { let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0); let identity = identity::KeyPair::new(&mut rng); let epoch = Arc::new(AtomicU64::new(1)); - let comm_channel = - DummyCommunicationChannel::new_single_dummy(coconut_keypair.verification_key().clone()) - .with_epoch(epoch.clone()); + let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); + let comm_channel = DummyCommunicationChannel::new_single_dummy( + coconut_keypair.verification_key().clone(), + address.clone(), + ) + .with_epoch(epoch.clone()); + let ecash_clients = comm_channel.clients_arc(); // TODO: it's AWFUL to test with actual storage, we should somehow abstract it away let tmp_dir = tempdir().unwrap(); @@ -1293,10 +1303,7 @@ impl TestFixture { staged_key_pair.validate(); let chain_state = SharedFakeChain::default(); - let nyxd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - chain_state.clone(), - ); + let nyxd_client = DummyClient::new(address, chain_state.clone()); let ecash_contract = chain_state .lock() @@ -1315,6 +1322,7 @@ impl TestFixture { staged_key_pair, comm_channel, storage.clone(), + false, ) .await .unwrap(); @@ -1329,12 +1337,20 @@ impl TestFixture { storage, chain_state, epoch, + ecash_clients, _tmp_dir: tmp_dir, } } - fn set_epoch(&self, epoch: u64) { - self.epoch.store(epoch, Ordering::Relaxed) + async fn set_epoch(&self, epoch: u64) { + let current_epoch = self.epoch.load(Ordering::Relaxed); + self.epoch.store(epoch, Ordering::Relaxed); + + // copy the same epoch_signers as we had initially + let existing = self.ecash_clients.read().await.get(¤t_epoch).cloned(); + if let Some(clients) = existing { + self.ecash_clients.write().await.insert(epoch, clients); + } } #[allow(dead_code)] @@ -1455,19 +1471,19 @@ mod credential_tests { async fn state_functions() { let mut rng = OsRng; let identity = identity::KeyPair::new(&mut rng); + let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); - let nyxd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - Default::default(), - ); + let nyxd_client = DummyClient::new(address.clone(), Default::default()); let key_pair = ttp_keygen(1, 1).unwrap().remove(0); let tmp_dir = tempdir().unwrap(); let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) .await .unwrap(); - let comm_channel = - DummyCommunicationChannel::new_single_dummy(key_pair.verification_key().clone()); + let comm_channel = DummyCommunicationChannel::new_single_dummy( + key_pair.verification_key().clone(), + address, + ); let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { @@ -1486,6 +1502,7 @@ mod credential_tests { staged_key_pair, comm_channel, storage.clone(), + false, ) .await .unwrap(); diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 119043d230..cef94c86ce 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -146,6 +146,7 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result Date: Wed, 16 Oct 2024 22:19:34 +0200 Subject: [PATCH 104/207] Rename crate to nym-credential-proxy --- nym-vpn-api/Cargo.lock | 140 ++++++++++++------------ nym-vpn-api/vpn-api-lib-wasm/Cargo.toml | 2 +- nym-vpn-api/vpn-api-requests/Cargo.toml | 2 +- nym-vpn-api/vpn-api-server/Cargo.toml | 4 +- 4 files changed, 74 insertions(+), 74 deletions(-) diff --git a/nym-vpn-api/Cargo.lock b/nym-vpn-api/Cargo.lock index 3cbaa09188..e112b27dbb 100644 --- a/nym-vpn-api/Cargo.lock +++ b/nym-vpn-api/Cargo.lock @@ -2352,6 +2352,75 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-credential-proxy" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "bip39", + "bs58", + "cfg-if", + "clap", + "colored", + "dotenv", + "futures", + "humantime 2.1.0", + "nym-bin-common", + "nym-compact-ecash", + "nym-config", + "nym-credential-proxy-requests", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-http-api-common", + "nym-network-defaults", + "nym-validator-client", + "rand", + "reqwest 0.12.4", + "serde", + "serde_json", + "sqlx", + "strum", + "strum_macros", + "tempfile", + "thiserror", + "time", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", + "url", + "utoipa", + "utoipa-swagger-ui", + "uuid", + "zeroize", +] + +[[package]] +name = "nym-credential-proxy-requests" +version = "0.1.0" +dependencies = [ + "async-trait", + "nym-credentials", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-serde-helpers", + "reqwest 0.12.4", + "schemars", + "serde", + "serde_json", + "time", + "tsify", + "utoipa", + "uuid", + "wasm-bindgen", + "wasmtimer", +] + [[package]] name = "nym-credentials" version = "0.1.0" @@ -2634,53 +2703,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "nym-vpn-api" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "axum", - "bip39", - "bs58", - "cfg-if", - "clap", - "colored", - "dotenv", - "futures", - "humantime 2.1.0", - "nym-bin-common", - "nym-compact-ecash", - "nym-config", - "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-http-api-common", - "nym-network-defaults", - "nym-validator-client", - "nym-vpn-api-requests", - "rand", - "reqwest 0.12.4", - "serde", - "serde_json", - "sqlx", - "strum", - "strum_macros", - "tempfile", - "thiserror", - "time", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tracing", - "url", - "utoipa", - "utoipa-swagger-ui", - "uuid", - "zeroize", -] - [[package]] name = "nym-vpn-api-lib-wasm" version = "0.1.0" @@ -2690,11 +2712,11 @@ dependencies = [ "js-sys", "nym-bin-common", "nym-compact-ecash", + "nym-credential-proxy-requests", "nym-credentials", "nym-credentials-interface", "nym-crypto", "nym-ecash-time", - "nym-vpn-api-requests", "serde", "serde-wasm-bindgen 0.6.5", "serde_json", @@ -2706,28 +2728,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "nym-vpn-api-requests" -version = "0.1.0" -dependencies = [ - "async-trait", - "nym-credentials", - "nym-credentials-interface", - "nym-http-api-client", - "nym-http-api-common", - "nym-serde-helpers", - "reqwest 0.12.4", - "schemars", - "serde", - "serde_json", - "time", - "tsify", - "utoipa", - "uuid", - "wasm-bindgen", - "wasmtimer", -] - [[package]] name = "nym-wireguard-types" version = "0.1.0" diff --git a/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml b/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml index b5c68c9b47..aeab69dcec 100644 --- a/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml +++ b/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml @@ -33,6 +33,6 @@ nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-ecash-time = { path = "../../common/ecash-time", features = ["expiration"] } -nym-vpn-api-requests = { path = "../vpn-api-requests", default-features = false, features = ["tsify"] } +nym-credential-proxy-requests = { path = "../vpn-api-requests", default-features = false, features = ["tsify"] } wasm-utils = { path = "../../common/wasm/utils" } diff --git a/nym-vpn-api/vpn-api-requests/Cargo.toml b/nym-vpn-api/vpn-api-requests/Cargo.toml index 51bb324d07..5f1d6a49a0 100644 --- a/nym-vpn-api/vpn-api-requests/Cargo.toml +++ b/nym-vpn-api/vpn-api-requests/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "nym-vpn-api-requests" +name = "nym-credential-proxy-requests" version = "0.1.0" authors.workspace = true repository.workspace = true diff --git a/nym-vpn-api/vpn-api-server/Cargo.toml b/nym-vpn-api/vpn-api-server/Cargo.toml index d0697b3ced..fea8b65fa2 100644 --- a/nym-vpn-api/vpn-api-server/Cargo.toml +++ b/nym-vpn-api/vpn-api-server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "nym-vpn-api" +name = "nym-credential-proxy" version = "0.1.0" authors.workspace = true repository.workspace = true @@ -52,7 +52,7 @@ nym-http-api-common = { path = "../../common/http-api-common", features = ["utoi nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } -nym-vpn-api-requests = { path = "../vpn-api-requests", features = ["openapi"] } +nym-credential-proxy-requests = { path = "../vpn-api-requests", features = ["openapi"] } [dev-dependencies] tempfile = { workspace = true } From b6ab25a7aa9d23e4833966e34aa432a78af71ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Oct 2024 22:22:56 +0200 Subject: [PATCH 105/207] Update crate references --- nym-vpn-api/vpn-api-lib-wasm/src/lib.rs | 8 ++++---- .../vpn-api-server/src/credentials/ticketbook/mod.rs | 8 ++++---- nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs | 2 +- nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs | 2 +- .../vpn-api-server/src/http/router/api/v1/openapi.rs | 4 ++-- .../src/http/router/api/v1/ticketbook/mod.rs | 6 +++--- .../src/http/router/api/v1/ticketbook/shares.rs | 8 ++++---- nym-vpn-api/vpn-api-server/src/http/router/mod.rs | 2 +- nym-vpn-api/vpn-api-server/src/http/state/mod.rs | 8 ++++---- nym-vpn-api/vpn-api-server/src/http/types.rs | 2 +- nym-vpn-api/vpn-api-server/src/storage/models.rs | 6 ++++-- 11 files changed, 29 insertions(+), 27 deletions(-) diff --git a/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs b/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs index b4cf50b2b9..02c3b757f3 100644 --- a/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs +++ b/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs @@ -8,6 +8,10 @@ use nym_compact_ecash::{ aggregate_wallets, issue_verify, withdrawal_request, Base58, BlindedSignature, VerificationKeyAuth, WithdrawalRequest, }; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, +}; use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, IssuedTicketBook, @@ -15,10 +19,6 @@ use nym_credentials::{ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::ed25519; use nym_ecash_time::{ecash_default_expiration_date, EcashTime}; -use nym_vpn_api_requests::api::v1::ticketbook::models::{ - MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookRequest, - TicketbookWalletSharesResponse, WalletShare, -}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use time::Date; diff --git a/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs b/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs index 14624dc7c1..217aa6e89b 100644 --- a/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs +++ b/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs @@ -4,16 +4,16 @@ use crate::error::VpnApiError; use crate::http::state::ApiState; use futures::{stream, StreamExt}; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, +}; use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::Base58; use nym_crypto::asymmetric::ed25519; use nym_validator_client::ecash::BlindSignRequestBody; use nym_validator_client::nyxd::contract_traits::EcashSigningClient; use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; -use nym_vpn_api_requests::api::v1::ticketbook::models::{ - TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, - TicketbookWalletSharesResponse, WalletShare, -}; use rand::rngs::OsRng; use std::collections::HashMap; use std::sync::Arc; diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs index 59b9f9bcea..597887fb30 100644 --- a/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs @@ -3,7 +3,7 @@ use crate::http::state::ApiState; use axum::Router; -use nym_vpn_api_requests::routes; +use nym_credential_proxy_requests::routes; use crate::http::middleware::auth::AuthLayer; pub(crate) use nym_http_api_common::{Output, OutputParams}; diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs index a6929d3591..59ee7b4df9 100644 --- a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs @@ -4,7 +4,7 @@ use crate::http::middleware::auth::AuthLayer; use crate::http::state::ApiState; use axum::Router; -use nym_vpn_api_requests::routes::api::v1; +use nym_credential_proxy_requests::routes::api::v1; // pub mod bandwidth_voucher; // pub mod freepass; diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs index ed215b6c3b..8332c1c450 100644 --- a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs @@ -4,8 +4,8 @@ use crate::http::router::api; use crate::http::types::RequestError; use axum::Router; -use nym_vpn_api_requests::api as api_requests; -use nym_vpn_api_requests::routes::api::{v1, v1_absolute}; +use nym_credential_proxy_requests::api as api_requests; +use nym_credential_proxy_requests::routes::api::{v1, v1_absolute}; use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme}; use utoipa::{Modify, OpenApi}; use utoipa_swagger_ui::SwaggerUi; diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs index 9c6a1932cf..670b6aaba9 100644 --- a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs @@ -13,13 +13,13 @@ use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Json, Router}; use nym_compact_ecash::Base58; -use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_vpn_api_requests::api::v1::ticketbook::models::{ +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse, PartialVerificationKey, PartialVerificationKeysResponse, TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse, }; -use nym_vpn_api_requests::routes::api::v1::ticketbook; +use nym_credential_proxy_requests::routes::api::v1::ticketbook; +use nym_http_api_common::{FormattedResponse, OutputParams}; use time::OffsetDateTime; use tracing::{error, info, span, warn, Level}; diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs index 2f23dbc3aa..174a419694 100644 --- a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs +++ b/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs @@ -11,12 +11,12 @@ use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::routing::get; use axum::Router; -use nym_http_api_common::OutputParams; -use nym_validator_client::nym_api::EpochId; -use nym_vpn_api_requests::api::v1::ticketbook::models::{ +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ SharesQueryParams, TicketbookWalletSharesResponse, }; -use nym_vpn_api_requests::routes::api::v1::ticketbook::shares; +use nym_credential_proxy_requests::routes::api::v1::ticketbook::shares; +use nym_http_api_common::OutputParams; +use nym_validator_client::nym_api::EpochId; use tracing::{debug, span, warn, Level}; use uuid::Uuid; diff --git a/nym-vpn-api/vpn-api-server/src/http/router/mod.rs b/nym-vpn-api/vpn-api-server/src/http/router/mod.rs index 6e2ac305c6..fd0ecf267a 100644 --- a/nym-vpn-api/vpn-api-server/src/http/router/mod.rs +++ b/nym-vpn-api/vpn-api-server/src/http/router/mod.rs @@ -7,7 +7,7 @@ use crate::http::state::ApiState; use axum::response::Redirect; use axum::routing::{get, MethodRouter}; use axum::Router; -use nym_vpn_api_requests::routes; +use nym_credential_proxy_requests::routes; use std::sync::Arc; use zeroize::Zeroizing; diff --git a/nym-vpn-api/vpn-api-server/src/http/state/mod.rs b/nym-vpn-api/vpn-api-server/src/http/state/mod.rs index 8ffd1bcc94..c4c4a8ebe3 100644 --- a/nym-vpn-api/vpn-api-server/src/http/state/mod.rs +++ b/nym-vpn-api/vpn-api-server/src/http/state/mod.rs @@ -19,6 +19,10 @@ use nym_compact_ecash::scheme::expiration_date_signatures::{ aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, }; use nym_compact_ecash::Base58; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, + MasterVerificationKeyResponse, +}; use nym_credentials::ecash::utils::{ecash_today, EcashTime}; use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, @@ -32,10 +36,6 @@ use nym_validator_client::nyxd::contract_traits::{ }; use nym_validator_client::nyxd::{Coin, NyxdClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient}; -use nym_vpn_api_requests::api::v1::ticketbook::models::{ - AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, - MasterVerificationKeyResponse, -}; use std::future::Future; use std::ops::Deref; use std::sync::Arc; diff --git a/nym-vpn-api/vpn-api-server/src/http/types.rs b/nym-vpn-api/vpn-api-server/src/http/types.rs index 34406480da..8602103456 100644 --- a/nym-vpn-api/vpn-api-server/src/http/types.rs +++ b/nym-vpn-api/vpn-api-server/src/http/types.rs @@ -5,7 +5,7 @@ use crate::error::VpnApiError; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; -use nym_vpn_api_requests::api::v1::ErrorResponse; +use nym_credential_proxy_requests::api::v1::ErrorResponse; use utoipa::ToResponse; use uuid::Uuid; diff --git a/nym-vpn-api/vpn-api-server/src/storage/models.rs b/nym-vpn-api/vpn-api-server/src/storage/models.rs index 191db6d250..5bc443bf45 100644 --- a/nym-vpn-api/vpn-api-server/src/storage/models.rs +++ b/nym-vpn-api/vpn-api-server/src/storage/models.rs @@ -76,9 +76,11 @@ pub struct MinimalWalletShare { pub blinded_signature: Vec, } -impl From for nym_vpn_api_requests::api::v1::ticketbook::models::WalletShare { +impl From + for nym_credential_proxy_requests::api::v1::ticketbook::models::WalletShare +{ fn from(value: MinimalWalletShare) -> Self { - nym_vpn_api_requests::api::v1::ticketbook::models::WalletShare { + nym_credential_proxy_requests::api::v1::ticketbook::models::WalletShare { node_index: value.node_id as u64, bs58_encoded_share: bs58::encode(&value.blinded_signature).into_string(), } From fa879baeaba27e1e3132d11f98cd30ef9a9a159b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Oct 2024 22:23:53 +0200 Subject: [PATCH 106/207] Rename directory to nym-credential-proxy --- {nym-vpn-api => nym-credential-proxy}/Cargo.lock | 0 {nym-vpn-api => nym-credential-proxy}/Cargo.toml | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/Cargo.toml | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/Makefile | 0 .../vpn-api-lib-wasm/internal-dev/bootstrap.js | 0 .../vpn-api-lib-wasm/internal-dev/index.html | 0 .../vpn-api-lib-wasm/internal-dev/index.js | 0 .../vpn-api-lib-wasm/internal-dev/package.json | 0 .../vpn-api-lib-wasm/internal-dev/webpack.config.js | 0 .../vpn-api-lib-wasm/internal-dev/yarn.lock | 0 .../vpn-api-lib-wasm/src/error.rs | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/src/lib.rs | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/Cargo.toml | 0 .../vpn-api-requests/src/api/mod.rs | 0 .../vpn-api-requests/src/api/v1/mod.rs | 0 .../vpn-api-requests/src/api/v1/ticketbook/mod.rs | 0 .../vpn-api-requests/src/api/v1/ticketbook/models.rs | 0 .../vpn-api-requests/src/client.rs | 0 .../vpn-api-requests/src/helpers.rs | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/lib.rs | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/.gitignore | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/Cargo.toml | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/Dockerfile | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/build.rs | 0 .../vpn-api-server/migrations/01_initial.sql | 0 .../vpn-api-server/migrations/02_cherry_picking_chaos.sql | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/cli.rs | 0 .../vpn-api-server/src/config/mod.rs | 0 .../vpn-api-server/src/credentials/mod.rs | 0 .../vpn-api-server/src/credentials/ticketbook/mod.rs | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/error.rs | 0 .../vpn-api-server/src/helpers.rs | 0 .../vpn-api-server/src/http/helpers.rs | 0 .../vpn-api-server/src/http/middleware/auth.rs | 0 .../vpn-api-server/src/http/middleware/logging.rs | 0 .../vpn-api-server/src/http/middleware/mod.rs | 0 .../vpn-api-server/src/http/mod.rs | 0 .../vpn-api-server/src/http/router/api/mod.rs | 0 .../vpn-api-server/src/http/router/api/v1/mod.rs | 0 .../vpn-api-server/src/http/router/api/v1/openapi.rs | 0 .../vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs | 0 .../vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs | 0 .../vpn-api-server/src/http/router/mod.rs | 0 .../vpn-api-server/src/http/state/mod.rs | 0 .../vpn-api-server/src/http/types.rs | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/main.rs | 0 .../vpn-api-server/src/nym_api_helpers.rs | 0 .../vpn-api-server/src/storage/manager.rs | 0 .../vpn-api-server/src/storage/mod.rs | 0 .../vpn-api-server/src/storage/models.rs | 0 {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/tasks.rs | 0 .../vpn-api-server/src/webhook.rs | 0 52 files changed, 0 insertions(+), 0 deletions(-) rename {nym-vpn-api => nym-credential-proxy}/Cargo.lock (100%) rename {nym-vpn-api => nym-credential-proxy}/Cargo.toml (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/Cargo.toml (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/Makefile (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/internal-dev/bootstrap.js (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/internal-dev/index.html (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/internal-dev/index.js (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/internal-dev/package.json (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/internal-dev/webpack.config.js (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/internal-dev/yarn.lock (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/src/error.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-lib-wasm/src/lib.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/Cargo.toml (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/api/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/api/v1/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/api/v1/ticketbook/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/api/v1/ticketbook/models.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/client.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/helpers.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-requests/src/lib.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/.gitignore (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/Cargo.toml (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/Dockerfile (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/build.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/migrations/01_initial.sql (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/migrations/02_cherry_picking_chaos.sql (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/cli.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/config/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/credentials/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/credentials/ticketbook/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/error.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/helpers.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/helpers.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/middleware/auth.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/middleware/logging.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/middleware/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/router/api/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/router/api/v1/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/router/api/v1/openapi.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/router/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/state/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/http/types.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/main.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/nym_api_helpers.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/storage/manager.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/storage/mod.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/storage/models.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/tasks.rs (100%) rename {nym-vpn-api => nym-credential-proxy}/vpn-api-server/src/webhook.rs (100%) diff --git a/nym-vpn-api/Cargo.lock b/nym-credential-proxy/Cargo.lock similarity index 100% rename from nym-vpn-api/Cargo.lock rename to nym-credential-proxy/Cargo.lock diff --git a/nym-vpn-api/Cargo.toml b/nym-credential-proxy/Cargo.toml similarity index 100% rename from nym-vpn-api/Cargo.toml rename to nym-credential-proxy/Cargo.toml diff --git a/nym-vpn-api/vpn-api-lib-wasm/Cargo.toml b/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/Cargo.toml rename to nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml diff --git a/nym-vpn-api/vpn-api-lib-wasm/Makefile b/nym-credential-proxy/vpn-api-lib-wasm/Makefile similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/Makefile rename to nym-credential-proxy/vpn-api-lib-wasm/Makefile diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/bootstrap.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/internal-dev/bootstrap.js rename to nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.html b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.html rename to nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/internal-dev/index.js rename to nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/package.json b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/internal-dev/package.json rename to nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/webpack.config.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/internal-dev/webpack.config.js rename to nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js diff --git a/nym-vpn-api/vpn-api-lib-wasm/internal-dev/yarn.lock b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/internal-dev/yarn.lock rename to nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock diff --git a/nym-vpn-api/vpn-api-lib-wasm/src/error.rs b/nym-credential-proxy/vpn-api-lib-wasm/src/error.rs similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/src/error.rs rename to nym-credential-proxy/vpn-api-lib-wasm/src/error.rs diff --git a/nym-vpn-api/vpn-api-lib-wasm/src/lib.rs b/nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs similarity index 100% rename from nym-vpn-api/vpn-api-lib-wasm/src/lib.rs rename to nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs diff --git a/nym-vpn-api/vpn-api-requests/Cargo.toml b/nym-credential-proxy/vpn-api-requests/Cargo.toml similarity index 100% rename from nym-vpn-api/vpn-api-requests/Cargo.toml rename to nym-credential-proxy/vpn-api-requests/Cargo.toml diff --git a/nym-vpn-api/vpn-api-requests/src/api/mod.rs b/nym-credential-proxy/vpn-api-requests/src/api/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-requests/src/api/mod.rs rename to nym-credential-proxy/vpn-api-requests/src/api/mod.rs diff --git a/nym-vpn-api/vpn-api-requests/src/api/v1/mod.rs b/nym-credential-proxy/vpn-api-requests/src/api/v1/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-requests/src/api/v1/mod.rs rename to nym-credential-proxy/vpn-api-requests/src/api/v1/mod.rs diff --git a/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/mod.rs b/nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/mod.rs rename to nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/mod.rs diff --git a/nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/models.rs similarity index 100% rename from nym-vpn-api/vpn-api-requests/src/api/v1/ticketbook/models.rs rename to nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/models.rs diff --git a/nym-vpn-api/vpn-api-requests/src/client.rs b/nym-credential-proxy/vpn-api-requests/src/client.rs similarity index 100% rename from nym-vpn-api/vpn-api-requests/src/client.rs rename to nym-credential-proxy/vpn-api-requests/src/client.rs diff --git a/nym-vpn-api/vpn-api-requests/src/helpers.rs b/nym-credential-proxy/vpn-api-requests/src/helpers.rs similarity index 100% rename from nym-vpn-api/vpn-api-requests/src/helpers.rs rename to nym-credential-proxy/vpn-api-requests/src/helpers.rs diff --git a/nym-vpn-api/vpn-api-requests/src/lib.rs b/nym-credential-proxy/vpn-api-requests/src/lib.rs similarity index 100% rename from nym-vpn-api/vpn-api-requests/src/lib.rs rename to nym-credential-proxy/vpn-api-requests/src/lib.rs diff --git a/nym-vpn-api/vpn-api-server/.gitignore b/nym-credential-proxy/vpn-api-server/.gitignore similarity index 100% rename from nym-vpn-api/vpn-api-server/.gitignore rename to nym-credential-proxy/vpn-api-server/.gitignore diff --git a/nym-vpn-api/vpn-api-server/Cargo.toml b/nym-credential-proxy/vpn-api-server/Cargo.toml similarity index 100% rename from nym-vpn-api/vpn-api-server/Cargo.toml rename to nym-credential-proxy/vpn-api-server/Cargo.toml diff --git a/nym-vpn-api/vpn-api-server/Dockerfile b/nym-credential-proxy/vpn-api-server/Dockerfile similarity index 100% rename from nym-vpn-api/vpn-api-server/Dockerfile rename to nym-credential-proxy/vpn-api-server/Dockerfile diff --git a/nym-vpn-api/vpn-api-server/build.rs b/nym-credential-proxy/vpn-api-server/build.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/build.rs rename to nym-credential-proxy/vpn-api-server/build.rs diff --git a/nym-vpn-api/vpn-api-server/migrations/01_initial.sql b/nym-credential-proxy/vpn-api-server/migrations/01_initial.sql similarity index 100% rename from nym-vpn-api/vpn-api-server/migrations/01_initial.sql rename to nym-credential-proxy/vpn-api-server/migrations/01_initial.sql diff --git a/nym-vpn-api/vpn-api-server/migrations/02_cherry_picking_chaos.sql b/nym-credential-proxy/vpn-api-server/migrations/02_cherry_picking_chaos.sql similarity index 100% rename from nym-vpn-api/vpn-api-server/migrations/02_cherry_picking_chaos.sql rename to nym-credential-proxy/vpn-api-server/migrations/02_cherry_picking_chaos.sql diff --git a/nym-vpn-api/vpn-api-server/src/cli.rs b/nym-credential-proxy/vpn-api-server/src/cli.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/cli.rs rename to nym-credential-proxy/vpn-api-server/src/cli.rs diff --git a/nym-vpn-api/vpn-api-server/src/config/mod.rs b/nym-credential-proxy/vpn-api-server/src/config/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/config/mod.rs rename to nym-credential-proxy/vpn-api-server/src/config/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/credentials/mod.rs b/nym-credential-proxy/vpn-api-server/src/credentials/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/credentials/mod.rs rename to nym-credential-proxy/vpn-api-server/src/credentials/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/vpn-api-server/src/credentials/ticketbook/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/credentials/ticketbook/mod.rs rename to nym-credential-proxy/vpn-api-server/src/credentials/ticketbook/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/error.rs b/nym-credential-proxy/vpn-api-server/src/error.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/error.rs rename to nym-credential-proxy/vpn-api-server/src/error.rs diff --git a/nym-vpn-api/vpn-api-server/src/helpers.rs b/nym-credential-proxy/vpn-api-server/src/helpers.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/helpers.rs rename to nym-credential-proxy/vpn-api-server/src/helpers.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/helpers.rs b/nym-credential-proxy/vpn-api-server/src/http/helpers.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/helpers.rs rename to nym-credential-proxy/vpn-api-server/src/http/helpers.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/middleware/auth.rs b/nym-credential-proxy/vpn-api-server/src/http/middleware/auth.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/middleware/auth.rs rename to nym-credential-proxy/vpn-api-server/src/http/middleware/auth.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/middleware/logging.rs b/nym-credential-proxy/vpn-api-server/src/http/middleware/logging.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/middleware/logging.rs rename to nym-credential-proxy/vpn-api-server/src/http/middleware/logging.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/middleware/mod.rs b/nym-credential-proxy/vpn-api-server/src/http/middleware/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/middleware/mod.rs rename to nym-credential-proxy/vpn-api-server/src/http/middleware/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/mod.rs b/nym-credential-proxy/vpn-api-server/src/http/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/mod.rs rename to nym-credential-proxy/vpn-api-server/src/http/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs b/nym-credential-proxy/vpn-api-server/src/http/router/api/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/router/api/mod.rs rename to nym-credential-proxy/vpn-api-server/src/http/router/api/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs b/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/router/api/v1/mod.rs rename to nym-credential-proxy/vpn-api-server/src/http/router/api/v1/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs b/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/openapi.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/router/api/v1/openapi.rs rename to nym-credential-proxy/vpn-api-server/src/http/router/api/v1/openapi.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs b/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs rename to nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs rename to nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/router/mod.rs b/nym-credential-proxy/vpn-api-server/src/http/router/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/router/mod.rs rename to nym-credential-proxy/vpn-api-server/src/http/router/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/state/mod.rs b/nym-credential-proxy/vpn-api-server/src/http/state/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/state/mod.rs rename to nym-credential-proxy/vpn-api-server/src/http/state/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/http/types.rs b/nym-credential-proxy/vpn-api-server/src/http/types.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/http/types.rs rename to nym-credential-proxy/vpn-api-server/src/http/types.rs diff --git a/nym-vpn-api/vpn-api-server/src/main.rs b/nym-credential-proxy/vpn-api-server/src/main.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/main.rs rename to nym-credential-proxy/vpn-api-server/src/main.rs diff --git a/nym-vpn-api/vpn-api-server/src/nym_api_helpers.rs b/nym-credential-proxy/vpn-api-server/src/nym_api_helpers.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/nym_api_helpers.rs rename to nym-credential-proxy/vpn-api-server/src/nym_api_helpers.rs diff --git a/nym-vpn-api/vpn-api-server/src/storage/manager.rs b/nym-credential-proxy/vpn-api-server/src/storage/manager.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/storage/manager.rs rename to nym-credential-proxy/vpn-api-server/src/storage/manager.rs diff --git a/nym-vpn-api/vpn-api-server/src/storage/mod.rs b/nym-credential-proxy/vpn-api-server/src/storage/mod.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/storage/mod.rs rename to nym-credential-proxy/vpn-api-server/src/storage/mod.rs diff --git a/nym-vpn-api/vpn-api-server/src/storage/models.rs b/nym-credential-proxy/vpn-api-server/src/storage/models.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/storage/models.rs rename to nym-credential-proxy/vpn-api-server/src/storage/models.rs diff --git a/nym-vpn-api/vpn-api-server/src/tasks.rs b/nym-credential-proxy/vpn-api-server/src/tasks.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/tasks.rs rename to nym-credential-proxy/vpn-api-server/src/tasks.rs diff --git a/nym-vpn-api/vpn-api-server/src/webhook.rs b/nym-credential-proxy/vpn-api-server/src/webhook.rs similarity index 100% rename from nym-vpn-api/vpn-api-server/src/webhook.rs rename to nym-credential-proxy/vpn-api-server/src/webhook.rs From 8f45649daa167fb0b0180dd957da8206e2411140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Oct 2024 22:26:57 +0200 Subject: [PATCH 107/207] Rename crate directories --- .../Cargo.toml | 0 .../src/api/mod.rs | 0 .../src/api/v1/mod.rs | 0 .../src/api/v1/ticketbook/mod.rs | 0 .../src/api/v1/ticketbook/models.rs | 0 .../src/client.rs | 0 .../src/helpers.rs | 0 .../src/lib.rs | 0 .../{vpn-api-server => nym-credential-proxy}/.gitignore | 0 .../{vpn-api-server => nym-credential-proxy}/Cargo.toml | 0 .../{vpn-api-server => nym-credential-proxy}/Dockerfile | 0 .../{vpn-api-server => nym-credential-proxy}/build.rs | 0 .../migrations/01_initial.sql | 0 .../migrations/02_cherry_picking_chaos.sql | 0 .../{vpn-api-server => nym-credential-proxy}/src/cli.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/config/mod.rs | 0 .../src/credentials/mod.rs | 0 .../src/credentials/ticketbook/mod.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/error.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/helpers.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/http/helpers.rs | 0 .../src/http/middleware/auth.rs | 0 .../src/http/middleware/logging.rs | 0 .../src/http/middleware/mod.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/http/mod.rs | 0 .../src/http/router/api/mod.rs | 0 .../src/http/router/api/v1/mod.rs | 0 .../src/http/router/api/v1/openapi.rs | 0 .../src/http/router/api/v1/ticketbook/mod.rs | 0 .../src/http/router/api/v1/ticketbook/shares.rs | 0 .../src/http/router/mod.rs | 0 .../src/http/state/mod.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/http/types.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/main.rs | 0 .../src/nym_api_helpers.rs | 0 .../src/storage/manager.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/storage/mod.rs | 0 .../src/storage/models.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/tasks.rs | 0 .../{vpn-api-server => nym-credential-proxy}/src/webhook.rs | 0 40 files changed, 0 insertions(+), 0 deletions(-) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/Cargo.toml (100%) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/src/api/mod.rs (100%) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/src/api/v1/mod.rs (100%) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/src/api/v1/ticketbook/mod.rs (100%) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/src/api/v1/ticketbook/models.rs (100%) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/src/client.rs (100%) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/src/helpers.rs (100%) rename nym-credential-proxy/{vpn-api-requests => nym-credential-proxy-requests}/src/lib.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/.gitignore (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/Cargo.toml (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/Dockerfile (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/build.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/migrations/01_initial.sql (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/migrations/02_cherry_picking_chaos.sql (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/cli.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/config/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/credentials/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/credentials/ticketbook/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/error.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/helpers.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/helpers.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/middleware/auth.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/middleware/logging.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/middleware/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/router/api/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/router/api/v1/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/router/api/v1/openapi.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/router/api/v1/ticketbook/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/router/api/v1/ticketbook/shares.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/router/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/state/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/http/types.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/main.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/nym_api_helpers.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/storage/manager.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/storage/mod.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/storage/models.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/tasks.rs (100%) rename nym-credential-proxy/{vpn-api-server => nym-credential-proxy}/src/webhook.rs (100%) diff --git a/nym-credential-proxy/vpn-api-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml similarity index 100% rename from nym-credential-proxy/vpn-api-requests/Cargo.toml rename to nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml diff --git a/nym-credential-proxy/vpn-api-requests/src/api/mod.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-requests/src/api/mod.rs rename to nym-credential-proxy/nym-credential-proxy-requests/src/api/mod.rs diff --git a/nym-credential-proxy/vpn-api-requests/src/api/v1/mod.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-requests/src/api/v1/mod.rs rename to nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/mod.rs diff --git a/nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/mod.rs rename to nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/mod.rs diff --git a/nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs similarity index 100% rename from nym-credential-proxy/vpn-api-requests/src/api/v1/ticketbook/models.rs rename to nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs diff --git a/nym-credential-proxy/vpn-api-requests/src/client.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs similarity index 100% rename from nym-credential-proxy/vpn-api-requests/src/client.rs rename to nym-credential-proxy/nym-credential-proxy-requests/src/client.rs diff --git a/nym-credential-proxy/vpn-api-requests/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/helpers.rs similarity index 100% rename from nym-credential-proxy/vpn-api-requests/src/helpers.rs rename to nym-credential-proxy/nym-credential-proxy-requests/src/helpers.rs diff --git a/nym-credential-proxy/vpn-api-requests/src/lib.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs similarity index 100% rename from nym-credential-proxy/vpn-api-requests/src/lib.rs rename to nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs diff --git a/nym-credential-proxy/vpn-api-server/.gitignore b/nym-credential-proxy/nym-credential-proxy/.gitignore similarity index 100% rename from nym-credential-proxy/vpn-api-server/.gitignore rename to nym-credential-proxy/nym-credential-proxy/.gitignore diff --git a/nym-credential-proxy/vpn-api-server/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml similarity index 100% rename from nym-credential-proxy/vpn-api-server/Cargo.toml rename to nym-credential-proxy/nym-credential-proxy/Cargo.toml diff --git a/nym-credential-proxy/vpn-api-server/Dockerfile b/nym-credential-proxy/nym-credential-proxy/Dockerfile similarity index 100% rename from nym-credential-proxy/vpn-api-server/Dockerfile rename to nym-credential-proxy/nym-credential-proxy/Dockerfile diff --git a/nym-credential-proxy/vpn-api-server/build.rs b/nym-credential-proxy/nym-credential-proxy/build.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/build.rs rename to nym-credential-proxy/nym-credential-proxy/build.rs diff --git a/nym-credential-proxy/vpn-api-server/migrations/01_initial.sql b/nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql similarity index 100% rename from nym-credential-proxy/vpn-api-server/migrations/01_initial.sql rename to nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql diff --git a/nym-credential-proxy/vpn-api-server/migrations/02_cherry_picking_chaos.sql b/nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql similarity index 100% rename from nym-credential-proxy/vpn-api-server/migrations/02_cherry_picking_chaos.sql rename to nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql diff --git a/nym-credential-proxy/vpn-api-server/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/cli.rs rename to nym-credential-proxy/nym-credential-proxy/src/cli.rs diff --git a/nym-credential-proxy/vpn-api-server/src/config/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/config/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/config/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/credentials/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/credentials/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/credentials/ticketbook/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/error.rs b/nym-credential-proxy/nym-credential-proxy/src/error.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/error.rs rename to nym-credential-proxy/nym-credential-proxy/src/error.rs diff --git a/nym-credential-proxy/vpn-api-server/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/helpers.rs rename to nym-credential-proxy/nym-credential-proxy/src/helpers.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/helpers.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/middleware/auth.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/middleware/auth.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/middleware/logging.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/middleware/logging.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/middleware/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/middleware/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/router/api/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/router/api/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/router/api/v1/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/openapi.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/router/api/v1/openapi.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/router/api/v1/ticketbook/shares.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/router/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/router/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/state/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/http/types.rs b/nym-credential-proxy/nym-credential-proxy/src/http/types.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/http/types.rs rename to nym-credential-proxy/nym-credential-proxy/src/http/types.rs diff --git a/nym-credential-proxy/vpn-api-server/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/main.rs rename to nym-credential-proxy/nym-credential-proxy/src/main.rs diff --git a/nym-credential-proxy/vpn-api-server/src/nym_api_helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/nym_api_helpers.rs rename to nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs diff --git a/nym-credential-proxy/vpn-api-server/src/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/storage/manager.rs rename to nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs diff --git a/nym-credential-proxy/vpn-api-server/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/storage/mod.rs rename to nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs diff --git a/nym-credential-proxy/vpn-api-server/src/storage/models.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/storage/models.rs rename to nym-credential-proxy/nym-credential-proxy/src/storage/models.rs diff --git a/nym-credential-proxy/vpn-api-server/src/tasks.rs b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/tasks.rs rename to nym-credential-proxy/nym-credential-proxy/src/tasks.rs diff --git a/nym-credential-proxy/vpn-api-server/src/webhook.rs b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs similarity index 100% rename from nym-credential-proxy/vpn-api-server/src/webhook.rs rename to nym-credential-proxy/nym-credential-proxy/src/webhook.rs From 561203572dcb671c8c869026f46855313f97689b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Oct 2024 22:27:56 +0200 Subject: [PATCH 108/207] Update Cargo.toml --- nym-credential-proxy/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/Cargo.toml index 7b08ce1792..b3ebb7f459 100644 --- a/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/Cargo.toml @@ -10,8 +10,8 @@ panic = "abort" resolver = "2" members = [ - "vpn-api-server", - "vpn-api-requests", + "nym-credential-proxy", + "nym-credential-proxy-requests", "vpn-api-lib-wasm" ] From 06b53472004599f22cc388f082b8d21d5adb445c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Oct 2024 22:30:23 +0200 Subject: [PATCH 109/207] Update paths --- nym-credential-proxy/nym-credential-proxy/Cargo.toml | 2 +- nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index fea8b65fa2..b9ded341ca 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -52,7 +52,7 @@ nym-http-api-common = { path = "../../common/http-api-common", features = ["utoi nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } -nym-credential-proxy-requests = { path = "../vpn-api-requests", features = ["openapi"] } +nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = ["openapi"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml b/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml index aeab69dcec..f3437ae951 100644 --- a/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml +++ b/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml @@ -33,6 +33,6 @@ nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-ecash-time = { path = "../../common/ecash-time", features = ["expiration"] } -nym-credential-proxy-requests = { path = "../vpn-api-requests", default-features = false, features = ["tsify"] } +nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", default-features = false, features = ["tsify"] } wasm-utils = { path = "../../common/wasm/utils" } From 9a94e40b36386d1d05529cd4c8591dd5e52b0f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Oct 2024 22:35:59 +0200 Subject: [PATCH 110/207] Update paths in various build and deployment files --- .../nym-credential-proxy-requests/src/client.rs | 2 +- nym-credential-proxy/nym-credential-proxy/Dockerfile | 6 +++--- nym-credential-proxy/nym-credential-proxy/build.rs | 2 +- nym-credential-proxy/nym-credential-proxy/src/config/mod.rs | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs index 301656db79..87b486e677 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs @@ -27,7 +27,7 @@ pub fn new_client( Ok(VpnApiClient { inner: Client::builder(base_url)? .with_user_agent(format!( - "nym-vpn-api-requests/{}", + "nym-credential-proxy-requests/{}", env!("CARGO_PKG_VERSION") )) .build()?, diff --git a/nym-credential-proxy/nym-credential-proxy/Dockerfile b/nym-credential-proxy/nym-credential-proxy/Dockerfile index 2942290a2a..0db4eb79d9 100644 --- a/nym-credential-proxy/nym-credential-proxy/Dockerfile +++ b/nym-credential-proxy/nym-credential-proxy/Dockerfile @@ -1,7 +1,7 @@ FROM rust:latest AS builder COPY ./ /usr/src/nym -WORKDIR /usr/src/nym/nym-vpn-api/vpn-api-server +WORKDIR /usr/src/nym/nym-credential-proxy/nym-credential-proxy RUN cargo build --release @@ -9,5 +9,5 @@ FROM ubuntu:24.04 WORKDIR /nym -COPY --from=builder /usr/src/nym/nym-vpn-api/target/release/nym-vpn-api ./ -ENTRYPOINT [ "/nym/nym-vpn-api" ] +COPY --from=builder /usr/src/nym/nym-credential-proxy/target/release/nym-credential-proxy ./ +ENTRYPOINT [ "/nym/nym-credential-proxy" ] diff --git a/nym-credential-proxy/nym-credential-proxy/build.rs b/nym-credential-proxy/nym-credential-proxy/build.rs index 9e5c18ec6c..bdbdcf0d22 100644 --- a/nym-credential-proxy/nym-credential-proxy/build.rs +++ b/nym-credential-proxy/nym-credential-proxy/build.rs @@ -7,7 +7,7 @@ async fn main() { use std::env; let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{out_dir}/nym-vpn-api-example.sqlite"); + let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite"); let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await diff --git a/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs index 128d6f53dc..eeafe05203 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs @@ -4,14 +4,14 @@ use nym_config::{must_get_home, DEFAULT_DATA_DIR, NYM_DIR}; use std::path::PathBuf; -pub const DEFAULT_NYM_VPN_API_DIR: &str = "nym-vpn-api"; +pub const DEFAULT_NYM_CREDENTIAL_PROXY_DIR: &str = "nym-credential-proxy"; -pub const DEFAULT_DB_FILENAME: &str = "nym-vpn-api.sqlite"; +pub const DEFAULT_DB_FILENAME: &str = "nym-credential-proxy.sqlite"; pub fn default_data_directory() -> PathBuf { must_get_home() .join(NYM_DIR) - .join(DEFAULT_NYM_VPN_API_DIR) + .join(DEFAULT_NYM_CREDENTIAL_PROXY_DIR) .join(DEFAULT_DATA_DIR) } From 97fbd7db1774e9217cfa51db63ce040f9e1fd980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Oct 2024 22:42:49 +0200 Subject: [PATCH 111/207] Update gh workflows --- .github/workflows/ci-build-vpn-api-wasm.yml | 4 ++-- .../{ci-build-vpn-api.yml => ci-nym-credential-proxy.yml} | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename .github/workflows/{ci-build-vpn-api.yml => ci-nym-credential-proxy.yml} (83%) diff --git a/.github/workflows/ci-build-vpn-api-wasm.yml b/.github/workflows/ci-build-vpn-api-wasm.yml index c8dae05cdb..a4abd8fa3e 100644 --- a/.github/workflows/ci-build-vpn-api-wasm.yml +++ b/.github/workflows/ci-build-vpn-api-wasm.yml @@ -4,7 +4,7 @@ on: pull_request: paths: - 'common/**' - - 'nym-vpn-api/**' + - 'nym-credential-proxy/**' - '.github/workflows/ci-build-vpn-api-wasm.yml' jobs: @@ -38,4 +38,4 @@ jobs: - name: "Build" run: make - working-directory: nym-vpn-api/vpn-api-lib-wasm + working-directory: nym-credential-proxy/vpn-api-lib-wasm diff --git a/.github/workflows/ci-build-vpn-api.yml b/.github/workflows/ci-nym-credential-proxy.yml similarity index 83% rename from .github/workflows/ci-build-vpn-api.yml rename to .github/workflows/ci-nym-credential-proxy.yml index 1a31e7fc29..8b70a1a73c 100644 --- a/.github/workflows/ci-build-vpn-api.yml +++ b/.github/workflows/ci-nym-credential-proxy.yml @@ -1,11 +1,11 @@ -name: ci-build-vpn-api +name: ci-nym-credential-proxy on: pull_request: paths: - 'common/**' - - 'nym-vpn-api/**' - - '.github/workspace/ci-build-vpn-api.yml' + - 'nym-credential-proxy/**' + - '.github/workspace/ci-nym-credential-proxy.yml' workflow_dispatch: jobs: @@ -13,7 +13,7 @@ jobs: runs-on: arc-ubuntu-22.04 env: CARGO_TERM_COLOR: always - MANIFEST_PATH: "--manifest-path nym-vpn-api/Cargo.toml" + MANIFEST_PATH: "--manifest-path nym-credential-proxy/Cargo.toml" steps: - name: Check out repository code uses: actions/checkout@v4 From f455b7c720bd20f871eea33dece6ae650f04e19b Mon Sep 17 00:00:00 2001 From: Gala Date: Thu, 17 Oct 2024 11:08:05 +0200 Subject: [PATCH 112/207] Migrate Legacy Node (Frontend) (#4826) * refactor bonding requests * use migrate node modal * disable node settings for legacy nodes * refine bonded node types * start migration and bonding work * update types and requests * clean up bonding context * move old forms to legacy directory * create nymnode bonding flow --------- Co-authored-by: fmtabbara --- .../components/Balance/VestingTimeline.tsx | 2 +- .../Balance/cards/VestingSchedule.tsx | 2 +- nym-wallet/src/components/Bonding/Bond.tsx | 2 +- .../src/components/Bonding/BondUpdateCard.tsx | 44 ++ .../src/components/Bonding/BondedGateway.tsx | 35 +- .../src/components/Bonding/BondedMixnode.tsx | 31 +- .../src/components/Bonding/BondedNymNode.tsx | 206 +++++++ .../Bonding/BondedNymNodeActions.tsx | 49 ++ .../src/components/Bonding/NodeStats.tsx | 17 +- .../Bonding/forms/BondMixnodeForm.tsx | 50 -- .../Bonding/forms/GatewaySignatureForm.tsx | 94 --- .../Bonding/forms/MixnodeSignatureForm.tsx | 95 --- .../{ => legacyForms}/BondGatewayForm.tsx | 6 +- .../{ => legacyForms}/GatewayAmountForm.tsx | 6 +- .../{ => legacyForms}/GatewayInitForm.tsx | 2 +- .../{ => legacyForms}/MixnodeAmountForm.tsx | 10 +- .../{ => legacyForms}/MixnodeInitForm.tsx | 2 +- .../gatewayValidationSchema.ts | 0 .../mixnodeValidationSchema.ts | 2 +- .../Bonding/forms/nym-node/FormContext.tsx | 98 +++ .../Bonding/forms/nym-node/NymNodeAmount.tsx | 120 ++++ .../Bonding/forms/nym-node/NymNodeData.tsx | 110 ++++ .../forms/nym-node/NymNodeSignature.tsx | 111 ++++ .../forms/nym-node/amountValidationSchema.ts | 58 ++ .../Bonding/modals/BondGatewayModal.tsx | 41 +- .../Bonding/modals/BondMixnodeModal.tsx | 163 ----- .../Bonding/modals/BondNymNodeModal.tsx | 88 +++ .../Bonding/modals/MigrateLegacyNode.tsx | 28 + .../Bonding/modals/NodeSettingsModal.tsx | 2 +- .../Bonding/modals/RedeemRewardsModal.tsx | 3 +- .../components/Bonding/modals/UnbondModal.tsx | 3 +- .../Bonding/modals/UpdateBondAmountModal.tsx | 3 +- .../modals/UpdateBondAmountNymNode.tsx | 153 +++++ nym-wallet/src/components/Bonding/utils.ts | 4 +- nym-wallet/src/components/NodeStatus.tsx | 1 - nym-wallet/src/components/NymCard.tsx | 2 +- nym-wallet/src/context/bonding.tsx | 576 +++--------------- nym-wallet/src/context/mocks/bonding.tsx | 29 +- nym-wallet/src/context/mocks/delegations.tsx | 7 +- nym-wallet/src/context/mocks/rewards.tsx | 4 +- nym-wallet/src/hooks/useGetNodeDetails.ts | 69 +++ nym-wallet/src/pages/Admin/index.tsx | 20 +- nym-wallet/src/pages/bonding/Bonding.tsx | 172 ++++-- .../bonding/node-settings/NodeSettings.tsx | 11 +- .../node-settings/apy-playground/index.tsx | 2 +- .../node-settings/node-settings.constant.tsx | 12 +- .../bonding/node-settings/node-test/index.tsx | 5 +- .../settings-pages/NodeUnbondPage.tsx | 15 +- .../GeneralGatewaySettings.tsx | 7 +- .../GeneralMixnodeSettings.tsx | 6 +- .../GeneralNymNodeSettings.tsx | 182 ++++++ .../general-settings/ParametersSettings.tsx | 16 +- .../settings-pages/general-settings/index.tsx | 11 +- nym-wallet/src/pages/bonding/types.ts | 4 +- .../pages/node-settings/system-variables.tsx | 17 +- nym-wallet/src/requests/actions.ts | 34 +- nym-wallet/src/requests/bond.ts | 56 ++ nym-wallet/src/requests/gatewayDetails.ts | 71 +++ nym-wallet/src/requests/index.ts | 1 + nym-wallet/src/requests/mixnodeDetails.ts | 199 ++++++ nym-wallet/src/requests/nymNodeDetails.ts | 145 +++++ nym-wallet/src/requests/queries.ts | 26 +- nym-wallet/src/requests/simulate.ts | 6 +- nym-wallet/src/requests/utils.ts | 6 +- nym-wallet/src/requests/vesting.ts | 6 +- nym-wallet/src/types/global.ts | 42 +- nym-wallet/src/utils/common.ts | 18 +- .../packages/validator-client/src/index.ts | 4 +- .../validator-client/src/signing-client.ts | 6 +- .../types/src/types/rust/MixNodeRewarding.ts | 4 +- ts-packages/types/src/types/rust/index.ts | 3 +- 71 files changed, 2206 insertions(+), 1229 deletions(-) create mode 100644 nym-wallet/src/components/Bonding/BondUpdateCard.tsx create mode 100644 nym-wallet/src/components/Bonding/BondedNymNode.tsx create mode 100644 nym-wallet/src/components/Bonding/BondedNymNodeActions.tsx delete mode 100644 nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx delete mode 100644 nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx delete mode 100644 nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx rename nym-wallet/src/components/Bonding/forms/{ => legacyForms}/BondGatewayForm.tsx (78%) rename nym-wallet/src/components/Bonding/forms/{ => legacyForms}/GatewayAmountForm.tsx (94%) rename nym-wallet/src/components/Bonding/forms/{ => legacyForms}/GatewayInitForm.tsx (98%) rename nym-wallet/src/components/Bonding/forms/{ => legacyForms}/MixnodeAmountForm.tsx (93%) rename nym-wallet/src/components/Bonding/forms/{ => legacyForms}/MixnodeInitForm.tsx (98%) rename nym-wallet/src/components/Bonding/forms/{ => legacyForms}/gatewayValidationSchema.ts (100%) rename nym-wallet/src/components/Bonding/forms/{ => legacyForms}/mixnodeValidationSchema.ts (98%) create mode 100644 nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts delete mode 100644 nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx create mode 100644 nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx create mode 100644 nym-wallet/src/components/Bonding/modals/MigrateLegacyNode.tsx create mode 100644 nym-wallet/src/components/Bonding/modals/UpdateBondAmountNymNode.tsx create mode 100644 nym-wallet/src/hooks/useGetNodeDetails.ts create mode 100644 nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx create mode 100644 nym-wallet/src/requests/bond.ts create mode 100644 nym-wallet/src/requests/gatewayDetails.ts create mode 100644 nym-wallet/src/requests/mixnodeDetails.ts create mode 100644 nym-wallet/src/requests/nymNodeDetails.ts diff --git a/nym-wallet/src/components/Balance/VestingTimeline.tsx b/nym-wallet/src/components/Balance/VestingTimeline.tsx index 52e137f4c8..cfb489056d 100644 --- a/nym-wallet/src/components/Balance/VestingTimeline.tsx +++ b/nym-wallet/src/components/Balance/VestingTimeline.tsx @@ -26,7 +26,7 @@ export const VestingTimeline: FCWithChildren<{ percentageComplete: number }> = ( const nextPeriod = typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods - ? Number(vestingAccountInfo?.periods[currentVestingPeriod.in + 1]?.start_time) + ? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time) : undefined; return ( diff --git a/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx b/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx index e713565680..b26b999970 100644 --- a/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx +++ b/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx @@ -23,7 +23,7 @@ const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> = const vestingPeriod = (current?: Period, original?: number) => { if (current === 'After') return 'Complete'; - if (typeof current === 'object' && typeof original === 'number') return `${current.in + 1}/${original}`; + if (typeof current === 'object' && typeof original === 'number') return `${current.In + 1}/${original}`; return 'N/A'; }; diff --git a/nym-wallet/src/components/Bonding/Bond.tsx b/nym-wallet/src/components/Bonding/Bond.tsx index d240b31035..79a19c3396 100644 --- a/nym-wallet/src/components/Bonding/Bond.tsx +++ b/nym-wallet/src/components/Bonding/Bond.tsx @@ -20,7 +20,7 @@ export const Bond = ({ }} > - Bond a mix node or a gateway. Learn how to set up and run a node{' '} + Bond a nym node. Learn how to set up and run a Nym node{' '} here diff --git a/nym-wallet/src/components/Bonding/BondUpdateCard.tsx b/nym-wallet/src/components/Bonding/BondUpdateCard.tsx new file mode 100644 index 0000000000..9cd24d0af8 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondUpdateCard.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Box, Button, Stack, Tooltip, Typography } from '@mui/material'; +import { NymCard } from 'src/components'; + +export const BondUpdateCard = ({ setSuccesfullUpdate }: { setSuccesfullUpdate: (staus: boolean) => void }) => ( + + + Upgrade your node! + + } + subheader={ + + + It seems like your node is running outdated binaries. + + Update to the latest stable Nym node binary now* + The update takes less than a minute! + + *Without updating, legacy node settings can be changed in the Nym CLI. + + + } + Action={ + + + + + + + + } + /> + +); diff --git a/nym-wallet/src/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index 2c1c5f6127..ab8ee84209 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { Box, Button, Stack, Typography } from '@mui/material'; +import { Box, Button, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; -import { TBondedGateway, urls } from 'src/context'; +import { urls } from 'src/context'; import { NymCard } from 'src/components'; import { Network } from 'src/types'; import { IdentityKey } from 'src/components/IdentityKey'; import { useNavigate } from 'react-router-dom'; +import { UpgradeRounded } from '@mui/icons-material'; +import { TBondedGateway } from 'src/requests/gatewayDetails'; import { Node as NodeIcon } from '../../svg-icons/node'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction'; @@ -39,10 +41,12 @@ const headers: Header[] = [ export const BondedGateway = ({ gateway, network, + onShowMigrateToNymNodeModal, onActionSelect, }: { gateway: TBondedGateway; network?: Network; + onShowMigrateToNymNodeModal: () => void; onActionSelect: (action: TBondedGatwayActions) => void; }) => { const { name, bond, ip, identityKey, routingScore } = gateway; @@ -91,16 +95,29 @@ export const BondedGateway = ({ } Action={ - + + + + + + - + } > diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index e569cd7aec..5d0fc68e64 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -2,12 +2,14 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; -import { isMixnode, Network } from 'src/types'; -import { TBondedMixnode, urls } from 'src/context'; +import { Network } from 'src/types'; +import { urls } from 'src/context'; import { NymCard } from 'src/components'; import { IdentityKey } from 'src/components/IdentityKey'; import { NodeStatus } from 'src/components/NodeStatus'; import { getIntervalAsDate } from 'src/utils'; +import { UpgradeRounded } from '@mui/icons-material'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { Node as NodeIcon } from '../../svg-icons/node'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions'; @@ -60,10 +62,12 @@ const headers: Header[] = [ export const BondedMixnode = ({ mixnode, network, + onShowMigrateToNymNodeModal, onActionSelect, }: { mixnode: TBondedMixnode; network?: Network; + onShowMigrateToNymNodeModal: () => void; onActionSelect: (action: TBondedMixnodeActions) => void; }) => { const [nextEpoch, setNextEpoch] = useState(); @@ -81,6 +85,7 @@ export const BondedMixnode = ({ status, identityKey, host, + isUnbonding, } = mixnode; const getNextInterval = async () => { @@ -165,9 +170,13 @@ export const BondedMixnode = ({ } Action={ - {isMixnode(mixnode) && ( + - )} + + + {nextEpoch instanceof Error ? null : ( Next epoch starts at {nextEpoch} diff --git a/nym-wallet/src/components/Bonding/BondedNymNode.tsx b/nym-wallet/src/components/Bonding/BondedNymNode.tsx new file mode 100644 index 0000000000..6186891e7f --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedNymNode.tsx @@ -0,0 +1,206 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material'; +import { Link } from '@nymproject/react/link/Link'; +import { Network } from 'src/types'; +import { urls } from 'src/context'; +import { NymCard } from 'src/components'; +import { IdentityKey } from 'src/components/IdentityKey'; +import { getIntervalAsDate } from 'src/utils'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; +import { Node as NodeIcon } from '../../svg-icons/node'; +import { Cell, Header, NodeTable } from './NodeTable'; +import { BondedNymNodeActions, TBondedNymNodeActions } from './BondedNymNodeActions'; + +const textWhenNotName = 'This node has not yet set a name'; + +const headers: Header[] = [ + { + header: 'Stake', + id: 'stake', + sx: { pl: 0 }, + }, + { + header: 'Bond', + id: 'bond', + }, + { + header: 'Stake saturation', + id: 'stake-saturation', + }, + { + header: 'PM', + id: 'profit-margin', + tooltipText: + 'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.', + }, + { + header: 'Operating cost', + id: 'operator-cost', + tooltipText: + 'Monthly operational costs of running your node. The cost also influences how the rewards are split between you and your delegators. ', + }, + { + header: 'Operator rewards', + id: 'operator-rewards', + tooltipText: + 'This is your (operator) rewards including the PM and cost. Rewards are automatically compounded every epoch. You can redeem your rewards at any time.', + }, + { + header: 'No. delegators', + id: 'delegators', + }, + { + id: 'menu-button', + sx: { width: 34, maxWidth: 34 }, + }, +]; + +export const BondedNymNode = ({ + nymnode, + network, + onActionSelect, +}: { + nymnode: TBondedNymNode; + network?: Network; + onActionSelect: (action: TBondedNymNodeActions) => void; +}) => { + const [nextEpoch, setNextEpoch] = useState(); + const navigate = useNavigate(); + const { + name, + nodeId, + stake, + bond, + stakeSaturation, + profitMargin, + operatorRewards, + operatorCost, + delegators, + identityKey, + host, + } = nymnode; + + const getNextInterval = async () => { + try { + const { nextEpoch: newNextEpoch } = await getIntervalAsDate(); + setNextEpoch(newNextEpoch); + } catch { + setNextEpoch(Error()); + } + }; + const cells: Cell[] = [ + { + cell: `${stake.amount} ${stake.denom}`, + id: 'stake-cell', + }, + { + cell: `${bond.amount} ${bond.denom}`, + id: 'bond-cell', + }, + { + cell: `${stakeSaturation}%`, + id: 'stake-saturation-cell', + }, + { + cell: `${profitMargin}%`, + id: 'pm-cell', + }, + { + cell: operatorCost ? `${operatorCost.amount} ${operatorCost.denom}` : '-', + id: 'operator-cost-cell', + }, + { + cell: operatorRewards ? `${operatorRewards.amount} ${operatorRewards.denom}` : '-', + id: 'operator-rewards-cell', + }, + { + cell: delegators, + id: 'delegators-cell', + }, + { + cell: nymnode.isUnbonding ? ( + + ) : ( + + ), + id: 'actions-cell', + align: 'right', + }, + ]; + + useEffect(() => { + getNextInterval(); + }, []); + + return ( + + + + + Nym node + + + {name?.includes(textWhenNotName) ? null : ( + + {name} + + )} + + + + + + + } + Action={ + + + + + + + + + + {nextEpoch instanceof Error ? null : ( + + Next epoch starts at {nextEpoch} + + )} + + } + > + + {network && ( + + Check more stats of your node on the{' '} + + explorer + + + )} + + {/* */} + + ); +}; diff --git a/nym-wallet/src/components/Bonding/BondedNymNodeActions.tsx b/nym-wallet/src/components/Bonding/BondedNymNodeActions.tsx new file mode 100644 index 0000000000..3125bbd2e1 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedNymNodeActions.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { Typography } from '@mui/material'; +import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu'; +import { Unbond as UnbondIcon, Bond as BondIcon } from '../../svg-icons'; + +export type TBondedNymNodeActions = 'nodeSettings' | 'updateBond' | 'unbond' | 'redeem'; + +export const BondedNymNodeActions = ({ + onActionSelect, + disabledRedeemAndCompound, + disabledUpdateBond, +}: { + onActionSelect: (action: TBondedNymNodeActions) => void; + disabledRedeemAndCompound: boolean; + disabledUpdateBond?: boolean; +}) => { + const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => setIsOpen(true); + const handleClose = () => setIsOpen(false); + + const handleActionClick = (action: TBondedNymNodeActions) => { + onActionSelect(action); + handleClose(); + }; + + return ( + + {!disabledUpdateBond && ( + } + onClick={() => handleActionClick('updateBond')} + /> + )} + R} + onClick={() => handleActionClick('redeem')} + disabled={disabledRedeemAndCompound} + /> + } + onClick={() => handleActionClick('unbond')} + /> + + ); +}; diff --git a/nym-wallet/src/components/Bonding/NodeStats.tsx b/nym-wallet/src/components/Bonding/NodeStats.tsx index eb22b50727..8c492968c7 100644 --- a/nym-wallet/src/components/Bonding/NodeStats.tsx +++ b/nym-wallet/src/components/Bonding/NodeStats.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps, Button } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import { TBondedMixnode } from 'src/context'; +import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps } from '@mui/material'; import { Cell, Pie, PieChart, Legend, ResponsiveContainer } from 'recharts'; import { SelectionChance } from '@nymproject/types'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { NymCard } from '../NymCard'; import { InfoTooltip } from '../InfoToolTip'; @@ -50,10 +49,9 @@ const StatRow = ({ export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => { const { activeSetProbability, routingScore } = mixnode; const theme = useTheme(); - const navigate = useNavigate(); // clamp routing score to [0-100] - const score = Math.min(Math.max(routingScore, 0), 100); + const score = Math.min(Math.max(routingScore || 0, 0), 100); const data = [ { key: 'routingScore', value: score }, @@ -74,10 +72,6 @@ export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => { } }; - const handleGoToTestNode = () => { - navigate('/bonding/node-settings', { state: 'test-node' }); - }; - const renderLegend = () => ( { Node stats } - Action={ - - } > diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx deleted file mode 100644 index 274878310f..0000000000 --- a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { NodeTypeSelector } from 'src/components'; -import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types'; -import MixnodeInitForm from './MixnodeInitForm'; -import MixnodeAmountForm from './MixnodeAmountForm'; -import MixnodeSignatureForm from './MixnodeSignatureForm'; - -export const BondMixnodeForm = ({ - step, - denom, - mixnodeData, - amountData, - hasVestingTokens, - onSelectNodeType, - onValidateMixnodeData, - onValidateAmountData, - onValidateSignature, -}: { - step: 1 | 2 | 3 | 4; - mixnodeData: MixnodeData; - amountData: MixnodeAmount; - denom: CurrencyDenom; - hasVestingTokens: boolean; - onSelectNodeType: (nodeType: TNodeType) => void; - onValidateMixnodeData: (data: MixnodeData) => void; - onValidateAmountData: (data: MixnodeAmount) => Promise; - onValidateSignature: (signature: Signature) => void; -}) => ( - <> - {step === 1 && ( - <> - - - - - - )} - {step === 2 && ( - - )} - {step === 3 && } - -); diff --git a/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx b/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx deleted file mode 100644 index fd7c25fcea..0000000000 --- a/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Stack, TextField, Typography } from '@mui/material'; -import { useForm } from 'react-hook-form'; -import { gatewayToTauri } from '../utils'; -import { CopyToClipboard } from '../../CopyToClipboard'; -import { useBondingContext } from '../../../context'; -import { Console } from '../../../utils/console'; -import { ErrorModal } from '../../Modals/ErrorModal'; -import { GatewayData, GatewayAmount, Signature } from '../../../pages/bonding/types'; - -const GatewaySignatureForm = ({ - gateway, - amount, - onNext, -}: { - gateway: GatewayData; - amount: GatewayAmount; - onNext: (data: Signature) => void; -}) => { - const [message, setMessage] = useState(); - const [error, setError] = useState(); - const { generateGatewayMsgPayload } = useBondingContext(); - - const { register, handleSubmit } = useForm(); - - const handleOnNext = (event: { detail: { step: number } }) => { - if (event.detail.step === 3) { - handleSubmit(onNext)(); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_gateway_step' as any, handleOnNext); - return () => window.removeEventListener('validate_bond_gateway_step' as any, handleOnNext); - }, []); - - const generateMessage = async () => { - try { - setMessage( - await generateGatewayMsgPayload({ - pledge: amount.amount, - gateway: gatewayToTauri(gateway), - tokenPool: amount.tokenPool as 'balance' | 'locked', - }), - ); - } catch (e) { - Console.error(e); - setError('Something went wrong while generating the payload signature'); - } - }; - - useEffect(() => { - generateMessage(); - }, [gateway, amount]); - - if (error) { - return {}} />; - } - - return ( - - - Copy the message below and sign it: -
- If you are using a nym-gateway: -
- nym-gateway sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- If you are using a nym-node: -
- nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- Then paste the signature in the next field. -
- - - Copy Message - {message && } - - -
- ); -}; - -export default GatewaySignatureForm; diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx b/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx deleted file mode 100644 index 1cce8d2ccd..0000000000 --- a/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Stack, TextField, Typography } from '@mui/material'; -import { useForm } from 'react-hook-form'; -import { costParamsToTauri, mixnodeToTauri } from '../utils'; -import { CopyToClipboard } from '../../CopyToClipboard'; -import { useBondingContext } from '../../../context'; -import { Console } from '../../../utils/console'; -import { ErrorModal } from '../../Modals/ErrorModal'; -import { MixnodeAmount, MixnodeData, Signature } from '../../../pages/bonding/types'; - -const MixnodeSignatureForm = ({ - mixnode, - amount, - onNext, -}: { - mixnode: MixnodeData; - amount: MixnodeAmount; - onNext: (data: Signature) => void; -}) => { - const [message, setMessage] = useState(''); - const [error, setError] = useState(); - const { generateMixnodeMsgPayload } = useBondingContext(); - - const { register, handleSubmit } = useForm(); - - const handleOnNext = (event: { detail: { step: number } }) => { - if (event.detail.step === 3) { - handleSubmit(onNext)(); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_mixnode_step' as any, handleOnNext); - return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleOnNext); - }, []); - - const generateMessage = async () => { - try { - setMessage( - (await generateMixnodeMsgPayload({ - pledge: amount.amount, - mixnode: mixnodeToTauri(mixnode), - costParams: costParamsToTauri(amount), - tokenPool: amount.tokenPool as 'balance' | 'locked', - })) as string, - ); - } catch (e) { - Console.error(e); - setError('Something went wrong while generating the payload signature'); - } - }; - - useEffect(() => { - generateMessage(); - }, [mixnode, amount]); - - if (error) { - return {}} />; - } - - return ( - - - Copy the message below and sign it: -
- If you are using a nym-mixnode: -
- nym-mixnode sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- If you are using a nym-node: -
- nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- Then paste the signature in the next field. -
- - - Copy Message - {message && } - - -
- ); -}; - -export default MixnodeSignatureForm; diff --git a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/BondGatewayForm.tsx similarity index 78% rename from nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/BondGatewayForm.tsx index efb812d969..843d604053 100644 --- a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/BondGatewayForm.tsx @@ -2,10 +2,9 @@ import React from 'react'; import { Box } from '@mui/material'; import { NodeTypeSelector } from 'src/components'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types'; +import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; import GatewayInitForm from './GatewayInitForm'; import GatewayAmountForm from './GatewayAmountForm'; -import GatewaySignatureForm from './GatewaySignatureForm'; export const BondGatewayForm = ({ step, @@ -16,7 +15,6 @@ export const BondGatewayForm = ({ onSelectNodeType, onValidateGatewayData, onValidateAmountData, - onValidateSignature, }: { step: 1 | 2 | 3 | 4; gatewayData: GatewayData; @@ -26,7 +24,6 @@ export const BondGatewayForm = ({ onSelectNodeType: (nodeType: TNodeType) => void; onValidateGatewayData: (data: GatewayData) => void; onValidateAmountData: (data: GatewayAmount) => Promise; - onValidateSignature: (signature: Signature) => void; }) => ( <> {step === 1 && ( @@ -45,6 +42,5 @@ export const BondGatewayForm = ({ onNext={onValidateAmountData} /> )} - {step === 3 && } ); diff --git a/nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayAmountForm.tsx similarity index 94% rename from nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/GatewayAmountForm.tsx index 2cef9a59da..7135d2b154 100644 --- a/nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayAmountForm.tsx @@ -5,9 +5,9 @@ import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { Box, Stack } from '@mui/material'; import { amountSchema } from './gatewayValidationSchema'; -import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils'; -import { GatewayAmount } from '../../../pages/bonding/types'; -import { TokenPoolSelector } from '../../TokenPoolSelector'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils'; +import { GatewayAmount } from '../../../../pages/bonding/types'; +import { TokenPoolSelector } from '../../../TokenPoolSelector'; const GatewayAmountForm = ({ denom, diff --git a/nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayInitForm.tsx similarity index 98% rename from nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/GatewayInitForm.tsx index 670c5a9e0a..5a891700c9 100644 --- a/nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayInitForm.tsx @@ -4,7 +4,7 @@ import { clean } from 'semver'; import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; -import { GatewayData } from '../../../pages/bonding/types'; +import { GatewayData } from '../../../../pages/bonding/types'; import { gatewayValidationSchema } from './gatewayValidationSchema'; const GatewayInitForm = ({ diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeAmountForm.tsx similarity index 93% rename from nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeAmountForm.tsx index f24f9138b7..13176dadf6 100644 --- a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeAmountForm.tsx @@ -5,11 +5,11 @@ import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { CurrencyDenom } from '@nymproject/types'; import { amountSchema } from './mixnodeValidationSchema'; -import { MixnodeAmount } from '../../../pages/bonding/types'; -import { AppContext } from '../../../context'; -import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils'; -import { TokenPoolSelector } from '../../TokenPoolSelector'; -import { ModalListItem } from '../../Modals/ModalListItem'; +import { MixnodeAmount } from '../../../../pages/bonding/types'; +import { AppContext } from '../../../../context'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils'; +import { TokenPoolSelector } from '../../../TokenPoolSelector'; +import { ModalListItem } from '../../../Modals/ModalListItem'; const MixnodeAmountForm = ({ amountData, diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeInitForm.tsx similarity index 98% rename from nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeInitForm.tsx index 3c97bb22ce..0032bb3364 100644 --- a/nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeInitForm.tsx @@ -5,7 +5,7 @@ import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; import { mixnodeValidationSchema } from './mixnodeValidationSchema'; -import { MixnodeData } from '../../../pages/bonding/types'; +import { MixnodeData } from '../../../../pages/bonding/types'; const MixnodeInitForm = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => { const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); diff --git a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/legacyForms/gatewayValidationSchema.ts similarity index 100% rename from nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts rename to nym-wallet/src/components/Bonding/forms/legacyForms/gatewayValidationSchema.ts diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/legacyForms/mixnodeValidationSchema.ts similarity index 98% rename from nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts rename to nym-wallet/src/components/Bonding/forms/legacyForms/mixnodeValidationSchema.ts index a72364377c..c11324457c 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/mixnodeValidationSchema.ts @@ -8,7 +8,7 @@ import { validateRawPort, validateVersion, } from 'src/utils'; -import { TauriContractStateParams } from '../../../types'; +import { TauriContractStateParams } from '../../../../types'; export const mixnodeValidationSchema = Yup.object().shape({ identityKey: Yup.string() diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx new file mode 100644 index 0000000000..6ad0bcadbc --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx @@ -0,0 +1,98 @@ +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { CurrencyDenom } from '@nymproject/types'; +import { TBondNymNodeArgs, TBondMixNodeArgs } from 'src/types'; + +const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = { + identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9', + custom_http_port: 1, + host: '1.1.1.1', +}; + +const defaultCostParams = (denom: CurrencyDenom): TBondNymNodeArgs['costParams'] => ({ + interval_operating_cost: { amount: '40', denom }, + profit_margin_percent: '10', +}); + +const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({ + amount: '100', + denom, +}); + +interface FormContextType { + step: 1 | 2 | 3 | 4; + setStep: React.Dispatch>; + nymNodeData: TBondNymNodeArgs['nymNode']; + setNymNodeData: React.Dispatch>; + costParams: TBondNymNodeArgs['costParams']; + setCostParams: React.Dispatch>; + amountData: TBondMixNodeArgs['pledge']; + setAmountData: React.Dispatch>; + signature?: string; + setSignature: React.Dispatch>; + onError: (e: string) => void; +} + +const FormContext = createContext({ + step: 1, + setStep: () => {}, + nymNodeData: defaultNymNodeValues, + setNymNodeData: () => {}, + costParams: defaultCostParams('nym'), + setCostParams: () => {}, + amountData: defaultAmount('nym'), + setAmountData: () => {}, + signature: undefined, + setSignature: () => {}, + + onError: () => {}, +}); + +const FormContextProvider = ({ children }: { children: React.ReactNode }) => { + // TODO - Make denom dynamic + const denom = 'nym'; + + const [step, setStep] = useState<1 | 2 | 3 | 4>(1); + const [nymNodeData, setNymNodeData] = useState(defaultNymNodeValues); + const [costParams, setCostParams] = useState(defaultCostParams(denom)); + const [amountData, setAmountData] = useState(defaultAmount(denom)); + const [signature, setSignature] = useState(); + + const onError = (e: string) => { + console.error(e); + }; + + const value = useMemo( + () => ({ + step, + setStep, + nymNodeData, + setNymNodeData, + costParams, + setCostParams, + amountData, + setAmountData, + signature, + setSignature, + onError, + }), + [ + step, + setStep, + nymNodeData, + setNymNodeData, + costParams, + setCostParams, + amountData, + setAmountData, + signature, + setSignature, + onError, + ], + ); + + return {children}; +}; + +export const useFormContext = () => useContext(FormContext); + +export default FormContextProvider; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx new file mode 100644 index 0000000000..550dd639ad --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { Stack, TextField, Box, FormHelperText } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { TBondNymNodeArgs } from 'src/types'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { checkHasEnoughFunds } from 'src/utils'; +import { nymNodeAmountSchema } from './amountValidationSchema'; + +const defaultNymNodeCostParamValues: TBondNymNodeArgs['costParams'] = { + profit_margin_percent: '10', + interval_operating_cost: { amount: '40', denom: 'nym' }, +}; + +const defaultNymNodePledgeValue: TBondNymNodeArgs['pledge'] = { + amount: '100', + denom: 'nym', +}; + +type NymNodeDataProps = { + onClose: () => void; + onBack: () => void; + onNext: () => Promise; + step: number; +}; + +const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => { + const { + formState: { errors }, + register, + getValues, + setValue, + setError, + handleSubmit, + } = useForm({ + mode: 'all', + defaultValues: { + pledge: defaultNymNodePledgeValue, + ...defaultNymNodeCostParamValues, + }, + resolver: yupResolver(nymNodeAmountSchema()), + }); + + console.log(errors, 'errors'); + + const handleRequestValidation = async () => { + const values = getValues(); + + const hasSufficientTokens = await checkHasEnoughFunds(values.pledge.amount); + + if (hasSufficientTokens) { + handleSubmit(onNext)(); + } else { + setError('pledge.amount', { message: 'Not enough tokens' }); + } + }; + + return ( + 0} + > + + { + setValue('pledge.amount', newValue.amount, { shouldValidate: true }); + }} + validationError={errors.pledge?.amount?.message} + denom={defaultNymNodePledgeValue.denom} + initialValue={defaultNymNodePledgeValue.amount} + /> + + + { + setValue('interval_operating_cost', newValue, { shouldValidate: true }); + }} + validationError={errors.interval_operating_cost?.amount?.message} + denom={defaultNymNodeCostParamValues.interval_operating_cost.denom} + initialValue={defaultNymNodeCostParamValues.interval_operating_cost.amount} + /> + + Monthly operational costs of running your node. If your node is in the active set the amount will be paid + back to you from the rewards. + + + + + + The percentage of node rewards that you as the node operator take before rewards are distributed to operator + and delegators. + + + + + ); +}; + +export default NymNodeAmount; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx new file mode 100644 index 0000000000..e32068a9c6 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { TBondNymNodeArgs } from 'src/types'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import { isValidHostname, validateRawPort } from 'src/utils'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; + +const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = { + identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9', + custom_http_port: 1, + host: '1.1.1.1', +}; + +const yupValidationSchema = yup.object().shape({ + identity_key: yup.string().required('Identity key is required'), + host: yup + .string() + .required('A host is required') + .test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || '')) + .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), + + custom_http_port: yup + .number() + .required('A custom http port is required') + .test('valid-http', 'A valid http port is required', (value) => (value ? validateRawPort(value) : false)), +}); + +type NymNodeDataProps = { + onClose: () => void; + onBack: () => void; + onNext: () => Promise; + step: number; +}; + +const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => { + const { + formState: { errors }, + register, + setValue, + handleSubmit, + } = useForm({ + mode: 'all', + defaultValues: defaultNymNodeValues, + resolver: yupResolver(yupValidationSchema), + }); + + const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false); + + const handleNext = async () => { + handleSubmit(onNext)(); + }; + + return ( + 0} + > + + setValue('identity_key', value, { shouldValidate: true })} + showTickOnValid={false} + /> + + + + setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} + label="Show advanced options" + /> + {showAdvancedOptions && ( + + + + )} + + + ); +}; + +export default NymNodeData; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx new file mode 100644 index 0000000000..e1e5814d55 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from 'react'; +import { Stack, TextField, Typography } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { CopyToClipboard } from 'src/components/CopyToClipboard'; +import { ErrorModal } from 'src/components/Modals/ErrorModal'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { useBondingContext } from 'src/context'; +import { TBondNymNodeArgs } from 'src/types'; +import { Signature } from 'src/pages/bonding/types'; + +const NymNodeSignature = ({ + nymNode, + pledge, + costParams, + step, + onNext, + onClose, + onBack, +}: { + nymNode: TBondNymNodeArgs['nymNode']; + pledge: TBondNymNodeArgs['pledge']; + costParams: TBondNymNodeArgs['costParams']; + step: number; + onNext: (data: Signature) => void; + onClose: () => void; + onBack: () => void; +}) => { + const [message, setMessage] = useState(''); + const [error, setError] = useState(); + const { generateNymNodeMsgPayload } = useBondingContext(); + + const { + register, + formState: { errors }, + } = useForm(); + + const generateMessage = async () => { + try { + const msg = await generateNymNodeMsgPayload({ + nymNode, + pledge, + costParams, + }); + + if (msg) { + setMessage(msg); + } + } catch (e) { + console.error(e); + setError('Something went wrong while generating the payload signature'); + } + }; + + useEffect(() => { + generateMessage(); + }, [nymNode, pledge, costParams]); + + const onSubmit = async (data: Signature) => { + onNext(data); + }; + + if (error) { + return {}} />; + } + + return ( + + onSubmit({ + signature: 'signature', + }) + } + onClose={onClose} + header="Bond Nym Node" + subHeader={`Step ${step}/3`} + okLabel="Next" + onBack={onBack} + okDisabled={Object.keys(errors).length > 0} + > + + + Copy the message below and sign it: +
+ If you are using a nym-node: +
+ nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> +
+ Then paste the signature in the next field. +
+ + + Copy Message + {message && } + + +
+
+ ); +}; + +export default NymNodeSignature; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts new file mode 100644 index 0000000000..1868a4f2e2 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts @@ -0,0 +1,58 @@ +import * as Yup from 'yup'; +import { TauriContractStateParams } from 'src/types'; +import { isLessThan, isGreaterThan, validateAmount } from 'src/utils'; + +const operatingCostAndPmValidation = (params?: TauriContractStateParams) => { + const defaultParams = { + profit_margin_percent: { + minimum: parseFloat(params?.profit_margin.minimum || '0%'), + maximum: parseFloat(params?.profit_margin.maximum || '100%'), + }, + + interval_operating_cost: { + minimum: parseFloat(params?.operating_cost.minimum.amount || '0'), + maximum: parseFloat(params?.operating_cost.maximum.amount || '1000000000'), + }, + }; + + return { + profit_margin_percent: Yup.number() + .required('Profit Percentage is required') + .min(defaultParams.profit_margin_percent.minimum) + .max(defaultParams.profit_margin_percent.maximum), + interval_operating_cost: Yup.object().shape({ + amount: Yup.string() + .required('An operating cost is required') + // eslint-disable-next-line prefer-arrow-callback + .test('valid-operating-cost', 'A valid amount is required', async function isValidAmount(this, value) { + if ( + value && + (!Number(value) || + isLessThan(+value, defaultParams.interval_operating_cost.minimum) || + isGreaterThan(+value, Number(defaultParams.interval_operating_cost.maximum))) + ) { + return this.createError({ + message: `A valid amount is required (min ${defaultParams?.interval_operating_cost.minimum} - max ${defaultParams?.interval_operating_cost.maximum})`, + }); + } + return true; + }), + }), + }; +}; + +export const nymNodeAmountSchema = (params?: TauriContractStateParams) => + Yup.object().shape({ + pledge: Yup.object().shape({ + amount: Yup.string() + .required('An amount is required') + .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { + const isValid = await validateAmount(value || '', '100'); + if (!isValid) { + return this.createError({ message: 'A valid amount is required (min 100)' }); + } + return true; + }), + }), + ...operatingCostAndPmValidation(params), + }); diff --git a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx index defdac3f95..1060b7c533 100644 --- a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx @@ -4,15 +4,11 @@ import { CurrencyDenom, TNodeType } from '@nymproject/types'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { TPoolOption } from 'src/components/TokenPoolSelector'; import { useGetFee } from 'src/hooks/useGetFee'; -import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types'; -import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests'; -import { TBondGatewayArgs } from 'src/types'; +import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; import { BalanceWarning } from 'src/components/FeeWarning'; import { AppContext } from 'src/context'; -import { BondGatewayForm } from '../forms/BondGatewayForm'; -import { gatewayToTauri } from '../utils'; +import { BondGatewayForm } from '../forms/legacyForms/BondGatewayForm'; const defaultGatewayValues: GatewayData = { identityKey: '', @@ -34,14 +30,12 @@ const defaultAmountValues = (denom: CurrencyDenom) => ({ export const BondGatewayModal = ({ denom, hasVestingTokens, - onBondGateway, onSelectNodeType, onClose, onError, }: { denom: CurrencyDenom; hasVestingTokens: boolean; - onBondGateway: (data: TBondGatewayArgs, tokenPool: TPoolOption) => void; onSelectNodeType: (type: TNodeType) => void; onClose: () => void; onError: (e: string) => void; @@ -49,9 +43,8 @@ export const BondGatewayModal = ({ const [step, setStep] = useState<1 | 2 | 3>(1); const [gatewayData, setGatewayData] = useState(defaultGatewayValues); const [amountData, setAmountData] = useState(defaultAmountValues(denom)); - const [signature, setSignature] = useState(); - const { fee, getFee, resetFeeState, feeError } = useGetFee(); + const { fee, resetFeeState, feeError } = useGetFee(); const { userBalance } = useContext(AppContext); useEffect(() => { @@ -83,32 +76,7 @@ export const BondGatewayModal = ({ setStep(3); }; - const handleUpdateSignature = async (data: Signature) => { - setSignature(data.signature); - - const payload = { - pledge: amountData.amount, - msgSignature: data.signature, - gateway: gatewayToTauri(gatewayData), - }; - - if (amountData.tokenPool === 'balance') { - await getFee(simulateBondGateway, payload); - } else { - await getFee(simulateVestingBondGateway, payload); - } - }; - - const handleConfirm = async () => { - await onBondGateway( - { - pledge: amountData.amount, - msgSignature: signature as string, - gateway: gatewayToTauri(gatewayData), - }, - amountData.tokenPool as TPoolOption, - ); - }; + const handleConfirm = async () => {}; if (fee) { return ( @@ -154,7 +122,6 @@ export const BondGatewayModal = ({ hasVestingTokens={hasVestingTokens} onValidateGatewayData={handleUpdateGatwayData} onValidateAmountData={handleUpdateAmountData} - onValidateSignature={handleUpdateSignature} onSelectNodeType={onSelectNodeType} />
diff --git a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx deleted file mode 100644 index 4e4c39f9cf..0000000000 --- a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { ConfirmTx } from 'src/components/ConfirmTX'; -import { ModalListItem } from 'src/components/Modals/ModalListItem'; -import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { TPoolOption } from 'src/components/TokenPoolSelector'; -import { useGetFee } from 'src/hooks/useGetFee'; -import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types'; -import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests'; -import { TBondMixNodeArgs } from 'src/types'; -import { BalanceWarning } from 'src/components/FeeWarning'; -import { AppContext } from 'src/context'; -import { BondMixnodeForm } from '../forms/BondMixnodeForm'; -import { costParamsToTauri, mixnodeToTauri } from '../utils'; - -const defaultMixnodeValues: MixnodeData = { - identityKey: '', - sphinxKey: '', - ownerSignature: '', - host: '', - version: '', - mixPort: 1789, - verlocPort: 1790, - httpApiPort: 8000, -}; - -const defaultAmountValues = (denom: CurrencyDenom) => ({ - amount: { amount: '100', denom }, - operatorCost: { amount: '40', denom }, - profitMargin: '10', - tokenPool: 'balance', -}); - -export const BondMixnodeModal = ({ - denom, - hasVestingTokens, - onBondMixnode, - onSelectNodeType, - onClose, - onError, -}: { - denom: CurrencyDenom; - hasVestingTokens: boolean; - onBondMixnode: (data: TBondMixNodeArgs, tokenPool: TPoolOption) => void; - onSelectNodeType: (type: TNodeType) => void; - onClose: () => void; - onError: (e: string) => void; -}) => { - const [step, setStep] = useState<1 | 2 | 3 | 4>(1); - const [mixnodeData, setMixnodeData] = useState(defaultMixnodeValues); - const [amountData, setAmountData] = useState(defaultAmountValues(denom)); - const [signature, setSignature] = useState(); - - const { fee, getFee, resetFeeState, feeError } = useGetFee(); - const { userBalance } = useContext(AppContext); - - useEffect(() => { - if (feeError) { - onError(feeError); - } - }, [feeError]); - - const validateStep = async (s: number) => { - const event = new CustomEvent('validate_bond_mixnode_step', { detail: { step: s } }); - window.dispatchEvent(event); - }; - - const handleBack = () => { - if (step === 2) { - setStep(1); - } else if (step === 3) { - setStep(2); - } - }; - - const handleUpdateMixnodeData = (data: MixnodeData) => { - setMixnodeData(data); - setStep(2); - }; - - const handleUpdateAmountData = async (data: MixnodeAmount) => { - setAmountData({ ...data }); - setStep(3); - }; - - const handleUpdateSignature = async (data: Signature) => { - setSignature(data.signature); - - const payload = { - pledge: amountData.amount, - msgSignature: data.signature, - mixnode: mixnodeToTauri(mixnodeData), - costParams: costParamsToTauri(amountData), - }; - - if (amountData.tokenPool === 'balance') { - await getFee(simulateBondMixnode, payload); - } else { - await getFee(simulateVestingBondMixnode, payload); - } - }; - - const handleConfirm = async () => { - await onBondMixnode( - { - pledge: amountData.amount, - msgSignature: signature as string, - mixnode: mixnodeToTauri(mixnodeData), - costParams: costParamsToTauri(amountData), - }, - amountData.tokenPool as TPoolOption, - ); - }; - - if (fee) { - return ( - - - - {fee.amount?.amount && userBalance.balance && ( - - )} - - ); - } - - return ( - { - await validateStep(step); - }} - onBack={step === 2 || step === 3 ? handleBack : undefined} - onClose={onClose} - header="Bond mixnode" - subHeader={`Step ${step}/3`} - okLabel="Next" - > - - - ); -}; diff --git a/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx new file mode 100644 index 0000000000..bfed31e765 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx @@ -0,0 +1,88 @@ +import React, { useContext, useEffect } from 'react'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { Signature } from 'src/pages/bonding/types'; +import { BalanceWarning } from 'src/components/FeeWarning'; +import { AppContext } from 'src/context'; +import FormContextProvider, { useFormContext } from '../forms/nym-node/FormContext'; +import NymNodeData from '../forms/nym-node/NymNodeData'; +import NymNodeAmount from '../forms/nym-node/NymNodeAmount'; +import NymNodeSignature from '../forms/nym-node/NymNodeSignature'; + +export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => { + const { fee, resetFeeState, feeError } = useGetFee(); + const { userBalance } = useContext(AppContext); + const { setStep, step, onError, setSignature, amountData, costParams, nymNodeData } = useFormContext(); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + const handleUpdateMixnodeData = async () => { + setStep(2); + }; + + const handleUpdateSignature = async (data: Signature) => { + setSignature(data.signature); + }; + + const handleConfirm = async () => {}; + + if (fee) { + return ( + + + + {fee.amount?.amount && userBalance.balance && ( + + )} + + ); + } + + if (step === 1) { + return ; + } + + if (step === 2) { + return setStep(1)} onNext={async () => setStep(3)} step={step} />; + } + + if (step === 3) { + return ( + setStep(2)} + step={step} + /> + ); + } + + return null; +}; + +export const BondNymNodeModalWithState = ({ open, onClose }: { open: boolean; onClose: () => void }) => { + if (!open) { + return null; + } + + return ( + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/modals/MigrateLegacyNode.tsx b/nym-wallet/src/components/Bonding/modals/MigrateLegacyNode.tsx new file mode 100644 index 0000000000..1a04601200 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/MigrateLegacyNode.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; + +const MigrateLegacyNode = ({ + open, + onClose, + handleMigrate, +}: { + open: boolean; + onClose: () => void; + handleMigrate: () => Promise; +}) => ( + Migrate Legacy Node} + onOk={handleMigrate} + okLabel="Migrate" + onClose={onClose} + sx={{ maxWidth: 500 }} + > + + You have a legacy node that needs to be migrated to the new system. + + +); + +export default MigrateLegacyNode; diff --git a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx index 0aa53e7368..8d87405d13 100644 --- a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import { Box, Button, FormHelperText, TextField, Typography } from '@mui/material'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { Node as NodeIcon } from 'src/svg-icons/node'; -import { TBondedMixnode } from 'src/context'; import { Tabs } from 'src/components/Tabs'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { attachDefaultOperatingCost, isDecimal, toPercentFloatString } from 'src/utils'; @@ -11,6 +10,7 @@ import { ConfirmTx } from 'src/components/ConfirmTX'; import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams } from 'src/requests'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { FeeDetails } from '@nymproject/types'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; // Now we are using the node setting page instead of this modal export const NodeSettings = ({ diff --git a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx index 11279278f6..8e00ddbfa5 100644 --- a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx @@ -5,9 +5,10 @@ import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { ModalFee } from 'src/components/Modals/ModalFee'; import { useGetFee } from 'src/hooks/useGetFee'; import { simulateClaimOperatorReward, simulateVestingClaimOperatorReward } from 'src/requests'; -import { AppContext, TBondedMixnode } from 'src/context'; +import { AppContext } from 'src/context'; import { BalanceWarning } from 'src/components/FeeWarning'; import { Box } from '@mui/material'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; export const RedeemRewardsModal = ({ node, diff --git a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx index 41eeb45211..51e779a563 100644 --- a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { useEffect } from 'react'; import { Typography } from '@mui/material'; -import { TBondedGateway, TBondedMixnode } from 'src/context'; import { useGetFee } from 'src/hooks/useGetFee'; import { isGateway, isMixnode } from 'src/types'; +import { TBondedGateway } from 'src/requests/gatewayDetails'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { ModalFee } from '../../Modals/ModalFee'; import { ModalListItem } from '../../Modals/ModalListItem'; import { SimpleModal } from '../../Modals/SimpleModal'; diff --git a/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx index 2148313b70..978675bd9b 100644 --- a/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx @@ -10,8 +10,9 @@ import { useGetFee } from 'src/hooks/useGetFee'; import { decCoinToDisplay, validateAmount } from 'src/utils'; import { simulateUpdateBond, simulateVestingUpdateBond } from 'src/requests'; import { TSimulateUpdateBondArgs, TUpdateBondArgs } from 'src/types'; -import { AppContext, TBondedMixnode } from 'src/context'; +import { AppContext } from 'src/context'; import { BalanceWarning } from 'src/components/FeeWarning'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { TPoolOption } from '../../TokenPoolSelector'; export const UpdateBondAmountModal = ({ diff --git a/nym-wallet/src/components/Bonding/modals/UpdateBondAmountNymNode.tsx b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountNymNode.tsx new file mode 100644 index 0000000000..e634b1c354 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountNymNode.tsx @@ -0,0 +1,153 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Box, Stack } from '@mui/material'; +import Big from 'big.js'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { DecCoin } from '@nymproject/types'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { decCoinToDisplay, validateAmount } from 'src/utils'; +import { simulateUpdateBond } from 'src/requests'; +import { TSimulateUpdateBondArgs, TUpdateBondArgs } from 'src/types'; +import { AppContext } from 'src/context'; +import { BalanceWarning } from 'src/components/FeeWarning'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; +import { TPoolOption } from '../../TokenPoolSelector'; + +export const UpdateBondAmountNymNode = ({ + node, + onUpdateBond, + onClose, + onError, +}: { + node: TBondedNymNode; + onUpdateBond: (data: TUpdateBondArgs, tokenPool: TPoolOption) => Promise; + onClose: () => void; + onError: (e: string) => void; +}) => { + const { bond: currentBond, stakeSaturation, uncappedStakeSaturation } = node; + + const { fee, getFee, resetFeeState, feeError } = useGetFee(); + const [newBond, setNewBond] = useState(); + const [errorAmount, setErrorAmount] = useState(false); + + const { printBalance, userBalance } = useContext(AppContext); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + const handleConfirm = async () => { + if (!newBond) { + return; + } + + await onUpdateBond( + { + currentPledge: currentBond, + newPledge: newBond, + fee: fee?.fee, + }, + 'balance', + ); + }; + + const handleAmountChanged = async (value: DecCoin) => { + const { amount } = value; + setNewBond(value); + if (!amount || !Number(amount)) { + setErrorAmount(true); + } else if (Big(amount).eq(currentBond.amount)) { + setErrorAmount(true); + } else { + const validAmount = await validateAmount(amount, '1'); + if (!validAmount) { + setErrorAmount(true); + return; + } + setErrorAmount(false); + } + }; + + const handleOnOk = async () => { + if (!newBond) { + return; + } + + await getFee(simulateUpdateBond, { + currentPledge: currentBond, + newPledge: newBond, + }); + }; + + const newBondToDisplay = () => { + const coin = decCoinToDisplay(newBond as DecCoin); + return `${coin.amount} ${coin.denom}`; + }; + + if (fee) + return ( + + + + {userBalance.balance?.amount.amount && fee?.amount?.amount && ( + + + + )} + + ); + + return ( + + + + { + handleAmountChanged(value); + }} + fullWidth + validationError={errorAmount ? 'Please enter a valid amount' : undefined} + /> + + + + + + {uncappedStakeSaturation ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/utils.ts b/nym-wallet/src/components/Bonding/utils.ts index 6e7b05a651..bb3be55e60 100644 --- a/nym-wallet/src/components/Bonding/utils.ts +++ b/nym-wallet/src/components/Bonding/utils.ts @@ -1,4 +1,4 @@ -import { Gateway, MixNode, MixNodeCostParams } from '@nymproject/types'; +import { Gateway, MixNode, NodeCostParams } from '@nymproject/types'; import { GatewayData, MixnodeAmount, MixnodeData } from '../../pages/bonding/types'; import { toPercentFloatString } from '../../utils'; @@ -14,7 +14,7 @@ export function mixnodeToTauri(data: MixnodeData): MixNode { }; } -export function costParamsToTauri(data: MixnodeAmount): MixNodeCostParams { +export function costParamsToTauri(data: MixnodeAmount): NodeCostParams { return { profit_margin_percent: toPercentFloatString(data.profitMargin), interval_operating_cost: { diff --git a/nym-wallet/src/components/NodeStatus.tsx b/nym-wallet/src/components/NodeStatus.tsx index 00ec32197d..99f92bcd18 100644 --- a/nym-wallet/src/components/NodeStatus.tsx +++ b/nym-wallet/src/components/NodeStatus.tsx @@ -32,5 +32,4 @@ export const NodeStatus = ({ status }: { status: MixnodeStatus }) => { default: return null; } - return null; }; diff --git a/nym-wallet/src/components/NymCard.tsx b/nym-wallet/src/components/NymCard.tsx index 768f0811ab..ae3d84fe23 100644 --- a/nym-wallet/src/components/NymCard.tsx +++ b/nym-wallet/src/components/NymCard.tsx @@ -20,7 +20,7 @@ export const NymCard: FCWithChildren<{ dataTestid?: string; sx?: SxProps; sxTitle?: SxProps; - children: React.ReactNode; + children?: React.ReactNode; }> = ({ title, subheader, Action, Icon, noPadding, borderless, children, dataTestid, sx, sxTitle }) => ( Promise; - bondMixnode: (data: TBondMixNodeArgs, tokenPool: TokenPool) => Promise; - bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise; - unbond: (fee?: FeeDetails) => Promise; - updateBondAmount: (data: TUpdateBondArgs, tokenPool: TokenPool) => Promise; - redeemRewards: (fee?: FeeDetails) => Promise; - updateMixnode: (pm: string, fee?: FeeDetails) => Promise; - generateMixnodeMsgPayload: (data: TBondMixnodeSignatureArgs) => Promise; - generateGatewayMsgPayload: (data: TBondGatewaySignatureArgs) => Promise; + bondedNode?: TBondedNode | null; isVestingAccount: boolean; + refresh: () => void; + unbond: (fee?: FeeDetails) => Promise; + updateBondAmount: (data: TUpdateBondArgs) => Promise; + redeemRewards: (fee?: FeeDetails) => Promise; + generateNymNodeMsgPayload: (data: TNymNodeSignatureArgs) => Promise; migrateVestedMixnode: () => Promise; + migrateLegacyNode: () => Promise; }; export const BondingContext = createContext({ isLoading: true, refresh: async () => undefined, - bondMixnode: async () => { - throw new Error('Not implemented'); - }, - bondGateway: async () => { - throw new Error('Not implemented'); - }, unbond: async () => { throw new Error('Not implemented'); }, @@ -150,29 +50,27 @@ export const BondingContext = createContext({ redeemRewards: async () => { throw new Error('Not implemented'); }, - updateMixnode: async () => { - throw new Error('Not implemented'); - }, - generateMixnodeMsgPayload: async () => { - throw new Error('Not implemented'); - }, - generateGatewayMsgPayload: async () => { + generateNymNodeMsgPayload: async () => { throw new Error('Not implemented'); }, migrateVestedMixnode: async () => { throw new Error('Not implemented'); }, + migrateLegacyNode: async () => { + throw new Error('Not implemented'); + }, isVestingAccount: false, }); export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Element => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); - const [bondedNode, setBondedNode] = useState(); + const [isVestingAccount, setIsVestingAccount] = useState(false); - const { userBalance, clientDetails } = useContext(AppContext); - const { ownership, isLoading: isOwnershipLoading } = useCheckOwnership(); + const { userBalance, clientDetails, network } = useContext(AppContext); + + const { bondedNode, isLoading: isBondedNodeLoading } = useGetNodeDetails(clientDetails?.client_address, network); useEffect(() => { userBalance.fetchBalance(); @@ -186,317 +84,19 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen const resetState = () => { setError(undefined); - setBondedNode(undefined); - }; - - /** - * Fetch mixnode **optional** data. - * ⚠ The underlying queries are allowed to fail. - */ - const fetchMixnodeDetails = async (mixId: number, host: string, port: number) => { - const details: { - status: MixnodeStatus; - stakeSaturation: string; - estimatedRewards?: DecCoin; - uptime: number; - averageUptime?: number; - setProbability?: InclusionProbabilityResponse; - nodeDescription?: TNodeDescription | undefined; - operatorRewards?: DecCoin; - uncappedSaturation?: number; - } = { - status: 'not_found', - stakeSaturation: '0', - uptime: 0, - }; - - const statusReq: TauriReq = { - name: 'getMixnodeStatus', - request: () => getMixnodeStatus(mixId), - onFulfilled: (value) => { - details.status = value.status; - }, - }; - - const uptimeReq: TauriReq = { - name: 'getMixnodeUptime', - request: () => getMixnodeUptime(mixId), - onFulfilled: (value) => { - details.uptime = value; - }, - }; - - const stakeSaturationReq: TauriReq = { - name: 'getMixnodeStakeSaturation', - request: () => getMixnodeStakeSaturation(mixId), - onFulfilled: (value) => { - details.stakeSaturation = decimalToPercentage(value.saturation); - const rawUncappedSaturation = decimalToFloatApproximation(value.uncapped_saturation); - if (rawUncappedSaturation && rawUncappedSaturation > 1) { - details.uncappedSaturation = Math.round(rawUncappedSaturation * 100); - } - }, - }; - - const rewardReq: TauriReq = { - name: 'getMixnodeRewardEstimation', - request: () => getMixnodeRewardEstimation(mixId), - onFulfilled: (value) => { - const estimatedRewards = unymToNym(value.estimation.total_node_reward); - if (estimatedRewards) { - details.estimatedRewards = { - amount: estimatedRewards, - denom: 'nym', - }; - } - }, - }; - - const inclusionReq: TauriReq = { - name: 'getInclusionProbability', - request: () => getInclusionProbability(mixId), - onFulfilled: (value) => { - details.setProbability = value; - }, - }; - - const avgUptimeReq: TauriReq = { - name: 'getMixnodeAvgUptime', - request: () => getMixnodeAvgUptime(), - onFulfilled: (value) => { - details.averageUptime = value as number | undefined; - }, - }; - - const nodeDescReq: TauriReq = { - name: 'getNodeDescription', - request: () => getNodeDescriptionRequest(host, port), - onFulfilled: (value) => { - details.nodeDescription = value; - }, - }; - - const operatorRewardsReq: TauriReq = { - name: 'getPendingOperatorRewards', - request: () => getPendingOperatorRewards(clientDetails?.client_address || ''), - onFulfilled: (value) => { - details.operatorRewards = decCoinToDisplay(value); - }, - }; - - await fireRequests([ - statusReq, - uptimeReq, - stakeSaturationReq, - rewardReq, - inclusionReq, - avgUptimeReq, - nodeDescReq, - operatorRewardsReq, - ]); - - return details; - }; - - /** - * Fetch gateway **optional** data. - * ⚠ The underlying queries are allowed to fail. - */ - const fetchGatewayDetails = async (identityKey: string, host: string, port: number) => { - const details: { - routingScore?: { current: number; average: number } | undefined; - nodeDescription?: TNodeDescription | undefined; - } = {}; - - const reportReq: TauriReq = { - name: 'getGatewayReport', - request: () => getGatewayReport(identityKey), - onFulfilled: (value) => { - details.routingScore = { current: value.most_recent, average: value.last_day }; - }, - }; - - const nodeDescReq: TauriReq = { - name: 'getNodeDescription', - request: () => getNodeDescriptionRequest(host, port), - onFulfilled: (value) => { - details.nodeDescription = value; - }, - }; - - await fireRequests([reportReq, nodeDescReq]); - - return details; - }; - - const calculateStake = (pledge: string, delegations: string) => { - let stake; - try { - stake = unymToNym(Big(pledge).plus(delegations)); - } catch (e: any) { - Console.warn(`not a valid decimal number: ${e}`); - } - return stake; - }; - - const refresh = useCallback(async () => { - setIsLoading(true); - setError(undefined); - - if (ownership.hasOwnership && ownership.nodeType === EnumNodeType.mixnode && clientDetails) { - try { - const data = await getMixnodeBondDetails(); - if (data) { - const { - bond_information, - rewarding_details, - bond_information: { mix_id }, - } = data; - - const { - status, - stakeSaturation, - uncappedSaturation: uncappedStakeSaturation, - estimatedRewards, - uptime, - operatorRewards, - averageUptime, - nodeDescription, - setProbability, - } = await fetchMixnodeDetails( - mix_id, - bond_information.mix_node.host, - bond_information.mix_node.http_api_port, - ); - - setBondedNode({ - id: data.bond_information.mix_id, - name: nodeDescription?.name, - mixId: mix_id, - identityKey: bond_information.mix_node.identity_key, - stake: { - amount: calculateStake(rewarding_details.operator, rewarding_details.delegates), - denom: bond_information.original_pledge.denom, - }, - bond: decCoinToDisplay(bond_information.original_pledge), - profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent), - delegators: rewarding_details.unique_delegations, - proxy: bond_information.proxy, - operatorRewards, - uptime, - status, - stakeSaturation, - uncappedStakeSaturation, - operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost), - host: bond_information.mix_node.host.replace(/\s/g, ''), - routingScore: averageUptime, - activeSetProbability: setProbability?.in_active, - standbySetProbability: setProbability?.in_reserve, - estimatedRewards, - httpApiPort: bond_information.mix_node.http_api_port, - mixPort: bond_information.mix_node.mix_port, - verlocPort: bond_information.mix_node.verloc_port, - version: bond_information.mix_node.version, - isUnbonding: bond_information.is_unbonding, - } as TBondedMixnode); - } - } catch (e: any) { - Console.warn(e); - setError(`While fetching current bond state, an error occurred: ${e}`); - } - } - - if (ownership.hasOwnership && ownership.nodeType === EnumNodeType.gateway) { - try { - const data = await getGatewayBondDetails(); - if (data) { - const { gateway, proxy } = data; - const { nodeDescription, routingScore } = await fetchGatewayDetails( - gateway.identity_key, - data.gateway.host, - data.gateway.clients_port, - ); - setBondedNode({ - name: nodeDescription?.name, - identityKey: gateway.identity_key, - mixPort: gateway.mix_port, - httpApiPort: gateway.clients_port, - host: gateway.host, - ip: gateway.host, - location: gateway.location, - bond: decCoinToDisplay(data.pledge_amount), - proxy, - routingScore, - version: gateway.version, - } as TBondedGateway); - } - } catch (e: any) { - Console.warn(e); - setError(`While fetching current bond state, an error occurred: ${e}`); - } - } - - if (!ownership.hasOwnership) { - resetState(); - } setIsLoading(false); - }, [ownership]); - - useEffect(() => { - refresh(); - }, [ownership, refresh]); - - const bondMixnode = async (data: TBondMixNodeArgs, tokenPool: TokenPool) => { - let tx: TransactionExecuteResult | undefined; - setIsLoading(true); - try { - if (tokenPool === 'balance') { - tx = await bondMixNodeRequest(data); - await userBalance.fetchBalance(); - } - if (tokenPool === 'locked') { - tx = await vestingBondMixNode(data); - await userBalance.fetchTokenAllocation(); - } - return tx; - } catch (e: any) { - Console.warn(e); - setError(`an error occurred: ${e}`); - } finally { - setIsLoading(false); - } - return undefined; }; - const bondGateway = async (data: TBondGatewayArgs, tokenPool: TokenPool) => { - let tx: TransactionExecuteResult | undefined; - setIsLoading(true); - try { - if (tokenPool === 'balance') { - tx = await bondGatewayRequest(data); - await userBalance.fetchBalance(); - } - if (tokenPool === 'locked') { - tx = await vestingBondGateway(data); - await userBalance.fetchTokenAllocation(); - } - return tx; - } catch (e: any) { - Console.warn(e); - setError(`an error occurred: ${e}`); - } finally { - setIsLoading(false); - } - return undefined; + const refresh = () => { + resetState(); }; const unbond = async (fee?: FeeDetails) => { let tx; setIsLoading(true); try { - if (bondedNode && isMixnode(bondedNode) && bondedNode.proxy) tx = await vestingUnbondMixnode(fee?.fee); + if (bondedNode && isNymNode(bondedNode)) tx = await unbondNymNodeRequest(fee?.fee); if (bondedNode && isMixnode(bondedNode) && !bondedNode.proxy) tx = await unbondMixnodeRequest(fee?.fee); - if (bondedNode && isGateway(bondedNode) && bondedNode.proxy) tx = await vestingUnbondGateway(fee?.fee); if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee); } catch (e) { Console.warn(e); @@ -507,35 +107,11 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; - const updateMixnode = async (pm: string, fee?: FeeDetails) => { - let tx; - setIsLoading(true); - - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm)); - - try { - // JS: this check is not entirely valid. you can have proxy field set whilst not using the vesting contract, - // you have to check if proxy exists AND if it matches the known vesting contract address! - if (bondedNode?.proxy) { - tx = await updateMixnodeVestingCostParamsRequest(defaultCostParams, fee?.fee); - } else { - tx = await updateMixnodeCostParamsRequest(defaultCostParams, fee?.fee); - } - } catch (e: any) { - Console.warn(e); - setError(`an error occurred: ${e}`); - } finally { - setIsLoading(false); - } - return tx; - }; - const redeemRewards = async (fee?: FeeDetails) => { let tx; setIsLoading(true); try { - if (bondedNode?.proxy) tx = await vestingClaimOperatorReward(fee?.fee); + if (bondedNode && !isNymNode(bondedNode)) tx = await vestingClaimOperatorReward(fee?.fee); else tx = await claimOperatorReward(fee?.fee); } catch (e: any) { setError(`an error occurred: ${e}`); @@ -545,18 +121,12 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; - const updateBondAmount = async (data: TUpdateBondArgs, tokenPool: TokenPool) => { + const updateBondAmount = async (data: TUpdateBondArgs) => { let tx: TransactionExecuteResult | undefined; setIsLoading(true); try { - if (tokenPool === 'balance') { - tx = await updateBondReq(data); - await userBalance.fetchBalance(); - } - if (tokenPool === 'locked') { - tx = await vestingUpdateBondReq(data); - await userBalance.fetchTokenAllocation(); - } + tx = await updateBondReq(data); + await userBalance.fetchBalance(); return tx; } catch (e: any) { @@ -568,40 +138,26 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return undefined; }; - const generateMixnodeMsgPayload = async (data: TBondMixnodeSignatureArgs) => { - let message; + const generateNymNodeMsgPayload = async (data: TNymNodeSignatureArgs) => { setIsLoading(true); - try { - if (data.tokenPool === 'locked') { - message = await vestingGenerateMixnodeMsgPayloadReq(data); - } else { - message = await generateMixnodeMsgPayloadReq(data); - } - } catch (e) { - Console.warn(e); - setError(`an error occurred: ${e}`); - } finally { - setIsLoading(false); - } - return message; - }; - const generateGatewayMsgPayload = async (data: TBondGatewaySignatureArgs) => { - let message; - setIsLoading(true); try { - if (data.tokenPool === 'locked') { - message = await vestingGenerateGatewayMsgPayloadReq(data); - } else { - message = await generateGatewayMsgPayloadReq(data); - } + const message = await generateNymNodeMsgPayloadReq({ + nymNode: data.nymNode, + pledge: data.pledge, + costParams: { + ...data.costParams, + profit_margin_percent: toPercentFloatString(data.costParams.profit_margin_percent), + }, + }); + return message; } catch (e) { Console.warn(e); setError(`an error occurred: ${e}`); } finally { setIsLoading(false); } - return message; + return undefined; }; const migrateVestedMixnode = async () => { @@ -611,24 +167,36 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; + const migrateLegacyNode = async () => { + setIsLoading(true); + let tx: TransactionExecuteResult | undefined; + + if (bondedNode && isMixnode(bondedNode)) { + tx = await migrateLegacyMixnodeReq(); + } + if (bondedNode && isGateway(bondedNode)) { + tx = await migrateLegacyGatewayReq(); + } + + setIsLoading(false); + return tx; + }; + const memoizedValue = useMemo( () => ({ - isLoading: isLoading || isOwnershipLoading, + isLoading: isLoading || isBondedNodeLoading, error, - bondMixnode, bondedNode, - bondGateway, unbond, - updateMixnode, refresh, redeemRewards, updateBondAmount, - generateMixnodeMsgPayload, - generateGatewayMsgPayload, + generateNymNodeMsgPayload, migrateVestedMixnode, + migrateLegacyNode, isVestingAccount, }), - [isLoading, isOwnershipLoading, error, bondedNode, isVestingAccount], + [isLoading, error, bondedNode, isVestingAccount, isBondedNodeLoading], ); return {children}; diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index e961272c66..2ef46237f9 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -1,9 +1,10 @@ import { FeeDetails, TransactionExecuteResult } from '@nymproject/types'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import type { Network } from 'src/types'; -import { BondingContext, TBondedGateway, TBondedMixnode } from '../bonding'; +import type { Network, TNymNodeSignatureArgs } from 'src/types'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; +import { TBondedGateway } from 'src/requests/gatewayDetails'; +import { BondingContext } from '../bonding'; import { mockSleep } from './utils'; -import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs } from '../../types'; const SLEEP_MS = 1000; @@ -30,10 +31,11 @@ const bondedMixnodeMock: TBondedMixnode = { version: '1.0.2', isUnbonding: false, uptime: 1, + proxy: null, + uncappedStakeSaturation: 100, }; const bondedGatewayMock: TBondedGateway = { - id: 1, name: 'Monster node', identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F', ip: '112.43.234.57', @@ -41,17 +43,18 @@ const bondedGatewayMock: TBondedGateway = { host: '1.2.34.5 ', httpApiPort: 8000, mixPort: 1789, - verlocPort: 1790, version: '1.0.2', routingScore: { average: 100, current: 100, }, + location: 'Germany', + proxy: null, }; const TxResultMock: TransactionExecuteResult = { logs_json: '', - data_json: '', + msg_responses_json: '', transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354', gas_info: { gas_wanted: { gas_units: BigInt(1) }, @@ -170,15 +173,7 @@ export const MockBondingContextProvider = ({ return feeMock; }; - const generateMixnodeMsgPayload = async (_data: TBondMixnodeSignatureArgs) => { - setIsLoading(true); - await mockSleep(SLEEP_MS); - triggerStateUpdate(); - setIsLoading(false); - return '77dcaba7f41409984f4ebce4a386f59b10f1e65ed5514d1acdccae30174bd84b'; - }; - - const generateGatewayMsgPayload = async (_data: TBondGatewaySignatureArgs) => { + const generateNymNodeMsgPayload = async (_data: TNymNodeSignatureArgs) => { setIsLoading(true); await mockSleep(SLEEP_MS); triggerStateUpdate(); @@ -204,10 +199,10 @@ export const MockBondingContextProvider = ({ updateMixnode, updateBondAmount, checkOwnership, - generateMixnodeMsgPayload, - generateGatewayMsgPayload, + generateNymNodeMsgPayload, isVestingAccount: false, migrateVestedMixnode: async () => undefined, + migrateLegacyNode: async () => undefined, }), [isLoading, error, bondedMixnode, bondedGateway, trigger, fee], ); diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx index 0c984d7369..f2aea600bc 100644 --- a/nym-wallet/src/context/mocks/delegations.tsx +++ b/nym-wallet/src/context/mocks/delegations.tsx @@ -108,7 +108,7 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { return { logs_json: '', - data_json: '', + msg_responses_json: '', gas_info: { gas_wanted: { gas_units: BigInt(1) }, gas_used: { gas_units: BigInt(1) }, @@ -183,7 +183,7 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { return { logs_json: '', - data_json: '', + msg_responses_json: '', transaction_hash: '', gas_info: { gas_wanted: { gas_units: BigInt(1) }, @@ -195,8 +195,9 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const undelegateVesting = async (mix_id: number, _fee?: FeeDetails) => ({ - logs_json: '', + msg_responses_json: '', data_json: '', + logs_json: '', transaction_hash: '', gas_info: { gas_wanted: { gas_units: BigInt(1) }, diff --git a/nym-wallet/src/context/mocks/rewards.tsx b/nym-wallet/src/context/mocks/rewards.tsx index c89fdb00b3..fe84e5cfe2 100644 --- a/nym-wallet/src/context/mocks/rewards.tsx +++ b/nym-wallet/src/context/mocks/rewards.tsx @@ -65,8 +65,8 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => { amount: '1', denom: 'nym', }, - data_json: '[]', logs_json: '[]', + msg_responses_json: '[]', gas_info: { gas_wanted: { gas_units: BigInt(1) }, gas_used: { gas_units: BigInt(1) }, @@ -78,7 +78,7 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => { amount: '1', denom: 'nym', }, - data_json: '[]', + msg_responses_json: '[]', logs_json: '[]', gas_info: { gas_wanted: { gas_units: BigInt(1) }, diff --git a/nym-wallet/src/hooks/useGetNodeDetails.ts b/nym-wallet/src/hooks/useGetNodeDetails.ts new file mode 100644 index 0000000000..9049b87ce5 --- /dev/null +++ b/nym-wallet/src/hooks/useGetNodeDetails.ts @@ -0,0 +1,69 @@ +import { useEffect, useState } from 'react'; +import { TBondedNode } from 'src/context'; +import { getGatewayDetails } from 'src/requests/gatewayDetails'; +import { getMixnodeDetails } from 'src/requests/mixnodeDetails'; +import { getNymNodeDetails } from 'src/requests/nymNodeDetails'; +import { fireRequests, TauriReq } from 'src/utils'; + +const useGetNodeDetails = (clientAddress?: string, network?: string) => { + const [bondedNode, setBondedNode] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getNodeDetails = async (address: string) => { + setIsError(false); + setBondedNode(null); + setIsLoading(true); + + // Check if the address has a Nym node bonded + const nymnode: TauriReq = { + name: 'getNymNodeBondDetails', + request: () => getNymNodeDetails(address), + onFulfilled: (value) => { + if (value) { + setBondedNode(value); + } + }, + }; + + // Check if the address has a Mix node bonded + const mixnode: TauriReq = { + name: 'getMixnodeDetails', + request: () => getMixnodeDetails(address), + onFulfilled: (value) => { + if (value) { + setBondedNode(value); + } + }, + }; + + // Check if the address has a Gateway bonded + const gateway: TauriReq = { + name: 'getGatewayDetails', + request: () => getGatewayDetails(), + onFulfilled: (value) => { + if (value) { + setBondedNode(value); + } + }, + }; + + await fireRequests([nymnode, mixnode, gateway]); + + setIsLoading(false); + }; + + useEffect(() => { + if (clientAddress) { + getNodeDetails(clientAddress); + } + }, [clientAddress, network]); + + return { + bondedNode, + isLoading, + isError, + }; +}; + +export default useGetNodeDetails; diff --git a/nym-wallet/src/pages/Admin/index.tsx b/nym-wallet/src/pages/Admin/index.tsx index 978360f254..7a0876c5a3 100644 --- a/nym-wallet/src/pages/Admin/index.tsx +++ b/nym-wallet/src/pages/Admin/index.tsx @@ -26,29 +26,15 @@ const AdminForm: FCWithChildren<{ - - - diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index 3ab472d1e4..843ada1f5a 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -2,55 +2,76 @@ import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FeeDetails } from '@nymproject/types'; import { Alert, AlertTitle, Box, Button, Typography } from '@mui/material'; -import { TPoolOption } from 'src/components'; import { Bond } from 'src/components/Bonding/Bond'; import { BondedMixnode } from 'src/components/Bonding/BondedMixnode'; import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions'; -import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal'; -import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal'; import { UpdateBondAmountModal } from 'src/components/Bonding/modals/UpdateBondAmountModal'; import { BondOversaturatedModal } from 'src/components/Bonding/modals/BondOversaturatedModal'; import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal'; import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { AppContext, urls } from 'src/context/main'; -import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TUpdateBondArgs } from 'src/types'; +import { isGateway, isMixnode, isNymNode, TUpdateBondArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; import { VestingWarningModal } from 'src/components/VestingWarningModal'; +import MigrateLegacyNode from 'src/components/Bonding/modals/MigrateLegacyNode'; +import { BondedNymNode } from 'src/components/Bonding/BondedNymNode'; +import { UpdateBondAmountNymNode } from 'src/components/Bonding/modals/UpdateBondAmountNymNode'; +import { BondNymNodeModalWithState } from 'src/components/Bonding/modals/BondNymNodeModal'; import { BondingContextProvider, useBondingContext } from '../../context'; export const Bonding = () => { const [showModal, setShowModal] = useState< - 'bond-mixnode' | 'bond-gateway' | 'update-bond' | 'update-bond-oversaturated' | 'unbond' | 'redeem' + | 'bond-mixnode' + | 'bond-nymnode' + | 'bond-gateway' + | 'update-bond' + | 'update-bond-oversaturated' + | 'unbond' + | 'redeem' + | 'update-bond-nymnode' >(); - const [confirmationDetails, setConfirmationDetails] = useState(); - const [uncappedSaturation, setUncappedSaturation] = useState(); - const [showMigrationModal, setShowMigrationModal] = useState(false); - const { - network, - clientDetails, - userBalance: { originalVesting }, - } = useContext(AppContext); + + const { network } = useContext(AppContext); const navigate = useNavigate(); const { bondedNode, - bondMixnode, - bondGateway, - redeemRewards, isLoading, - updateBondAmount, error, + redeemRewards, + updateBondAmount, refresh, migrateVestedMixnode, + migrateLegacyNode, } = useBondingContext(); + const shouldShowMigrateLegacyNodeModal = () => { + if (!bondedNode) { + return false; + } + if (isMixnode(bondedNode) && !bondedNode.isUnbonding) { + return true; + } + if (isGateway(bondedNode)) { + return true; + } + return false; + }; + + const [confirmationDetails, setConfirmationDetails] = useState(); + const [uncappedSaturation, setUncappedSaturation] = useState(); + const [showMigrationModal, setShowMigrationModal] = useState(false); + const [showMigrateLegacyNodeModal, setShowMigrateLegacyNodeModal] = useState(false); + useEffect(() => { if (bondedNode && isMixnode(bondedNode) && bondedNode.uncappedStakeSaturation) { setUncappedSaturation(bondedNode.uncappedStakeSaturation); } + + setShowMigrateLegacyNodeModal(shouldShowMigrateLegacyNodeModal()); }, [bondedNode]); const handleMigrateVestedMixnode = async () => { @@ -65,6 +86,18 @@ export const Bonding = () => { } }; + const handleMigrateLegacyNode = async () => { + setShowMigrateLegacyNodeModal(false); + const tx = await migrateLegacyNode(); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Migration successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } + }; + const handleCloseModal = async () => { setShowModal(undefined); refresh(); @@ -79,35 +112,10 @@ export const Bonding = () => { }); }; - const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => { - setShowModal(undefined); - const tx = await bondMixnode(data, tokenPool); - if (tx) { - setConfirmationDetails({ - status: 'success', - title: 'Bond successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); - } - return undefined; - }; - - const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => { - setShowModal(undefined); - const tx = await bondGateway(data, tokenPool); - if (tx) { - setConfirmationDetails({ - status: 'success', - title: 'Bond successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); - } - }; - - const handleUpdateBond = async (data: TUpdateBondArgs, tokenPool: TPoolOption) => { + const handleUpdateBond = async (data: TUpdateBondArgs) => { setShowModal(undefined); - const tx = await updateBondAmount(data, tokenPool); + const tx = await updateBondAmount(data); if (tx) { setConfirmationDetails({ status: 'success', @@ -154,13 +162,34 @@ export const Bonding = () => { return undefined; }; + const handleBondedNymNodeAction = async (action: TBondedMixnodeActions) => { + switch (action) { + case 'unbond': { + navigate('/bonding/node-settings', { state: 'unbond' }); + break; + } + case 'updateBond': { + setShowModal('update-bond-nymnode'); + break; + } + case 'redeem': { + setShowModal('redeem'); + break; + } + default: { + return undefined; + } + } + return undefined; + }; + if (error) { return refresh()} />; } return ( - {bondedNode?.proxy && ( + {bondedNode && !isNymNode(bondedNode) && bondedNode?.proxy && ( Your bonded node is using tokens from the vesting contract! @@ -187,41 +216,41 @@ export const Bonding = () => { }} /> - {!bondedNode && setShowModal('bond-mixnode')} />} + setShowMigrateLegacyNodeModal(false)} + handleMigrate={handleMigrateLegacyNode} + /> + + {!bondedNode && setShowModal('bond-nymnode')} />} + + {bondedNode && isNymNode(bondedNode) && ( + handleBondedNymNodeAction(action)} + /> + )} {bondedNode && isMixnode(bondedNode) && ( setShowMigrateLegacyNodeModal(true)} onActionSelect={(action) => handleBondedMixnodeAction(action)} /> )} {bondedNode && isGateway(bondedNode) && ( - - )} - - {showModal === 'bond-mixnode' && ( - setShowModal('bond-gateway')} - onClose={() => setShowModal(undefined)} - onError={handleError} + setShowMigrateLegacyNodeModal(true)} + onActionSelect={handleBondedMixnodeAction} /> )} - {showModal === 'bond-gateway' && ( - setShowModal('bond-mixnode')} - onClose={() => setShowModal(undefined)} - onError={handleError} - /> - )} + {showModal === 'update-bond-oversaturated' && uncappedSaturation && ( { /> )} + {showModal === 'update-bond-nymnode' && bondedNode && isNymNode(bondedNode) && ( + setShowModal(undefined)} + onError={handleError} + /> + )} + {showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && ( { - {isMixnode(bondedNode) ? 'Node' : 'Gateway'} Settings + Nym Node Settings { }} > - You should NOT shutdown your {isMixnode(bondedNode) ? 'mix node' : 'gateway'} until the unbond process is - complete + You should NOT shutdown your node until the unbond process is complete )} diff --git a/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx b/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx index 65edb35bc5..c288dc96e7 100644 --- a/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx @@ -4,10 +4,10 @@ import { ResultsTable } from 'src/components/RewardsPlayground/ResultsTable'; import { getDelegationSummary } from 'src/requests'; import { NodeDetails } from 'src/components/RewardsPlayground/NodeDetail'; import { CalculateArgs, Inputs } from 'src/components/RewardsPlayground/Inputs'; -import { TBondedMixnode } from 'src/context'; import { useSnackbar } from 'notistack'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { Console } from 'src/utils/console'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { computeEstimate, computeStakeSaturation, handleCalculatePeriodRewards } from './utils'; export type DefaultInputValues = { diff --git a/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx b/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx index 74e8d7cf5b..b308c03716 100644 --- a/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx @@ -1,7 +1,13 @@ -export const makeNavItems = (isMixnode: boolean) => { - const navItems: NavItems[] = ['General', 'Unbond']; +import { TBondedNode } from 'src/context'; +import { isNymNode } from 'src/types'; - if (isMixnode) navItems.splice(1, 0, 'Test my node', 'Playground'); +export const makeNavItems = (bondedNode: TBondedNode) => { + const navItems: NavItems[] = ['Unbond']; + + if (isNymNode(bondedNode)) { + // add these items to the beginning of the array "General", "Test my node", "Playground" + navItems.unshift('General', 'Test my node', 'Playground'); + } return navItems; }; diff --git a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx index ba9bf522f3..2e1dc533f8 100644 --- a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx @@ -10,6 +10,7 @@ import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { PrintResults } from 'src/components/TestNode/PrintResults'; import { MAINNET_VALIDATOR_URL, QA_VALIDATOR_URL } from 'src/constants'; import { TestStatus } from 'src/components/TestNode/types'; +import { isMixnode } from 'src/types'; export const NodeTestPage = () => { const [nodeTestClient, setNodeTestClient] = useState(); @@ -37,7 +38,7 @@ export const NodeTestPage = () => { }; const handleTestNode = async () => { - if (nodeTestClient && bondedNode) { + if (nodeTestClient && bondedNode && isMixnode(bondedNode)) { setResults(undefined); setTestDate(format(new Date(), 'dd/MM/yyyy HH:mm')); setIsLoading(true); @@ -87,7 +88,7 @@ export const NodeTestPage = () => { {isLoading && } {error && setError(undefined)} />} - {printResults && results && ( + {printResults && results && bondedNode && isMixnode(bondedNode) && ( Promise; onError: (e: string) => void; @@ -15,6 +15,9 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { const [confirmField, setConfirmField] = useState(''); const [isConfirmed, setIsConfirmed] = useState(false); // TODO: Check what happens with a gateway + + const shouldDisplayWarning = isMixnode(bondedNode) || isNymNode(bondedNode); + return ( @@ -24,7 +27,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { Unbond - {isMixnode(bondedNode) && ( + {shouldDisplayWarning && ( theme.palette.nym.text.muted }}> Remember you should only unbond if you want to remove your node from the network for good. @@ -35,7 +38,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { - {isMixnode(bondedNode) && ( + {shouldDisplayWarning && ( )} @@ -68,7 +71,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { - {isConfirmed && ( + {isConfirmed && !isNymNode(bondedNode) && ( { diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx index 892c1e245c..7f8bdb6d08 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx @@ -10,16 +10,17 @@ import { updateGatewayConfig, vestingUpdateGatewayConfig, } from 'src/requests'; -import { TBondedGateway, useBondingContext } from 'src/context/bonding'; +import { useBondingContext } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { useGetFee } from 'src/hooks/useGetFee'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; -import { updateGatewayValidationSchema } from 'src/components/Bonding/forms/gatewayValidationSchema'; +import { updateGatewayValidationSchema } from 'src/components/Bonding/forms/legacyForms/gatewayValidationSchema'; import { BalanceWarning } from 'src/components/FeeWarning'; import { AppContext } from 'src/context'; +import { TBondedGateway } from 'src/requests/gatewayDetails'; export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGateway }) => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); @@ -56,7 +57,6 @@ export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGate location, version: clean(version) as string, clients_port: httpApiPort, - verloc_port: bondedNode.verlocPort, }; if (bondedNode.proxy) { @@ -206,7 +206,6 @@ export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGate clients_port: data.httpApiPort, location: bondedNode.location!, version: data.version, - verloc_port: bondedNode.verlocPort, }), )} sx={{ m: 3 }} diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx index 509c605174..08304622c7 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx @@ -6,9 +6,8 @@ import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/m import { useTheme } from '@mui/material/styles'; import { isMixnode } from 'src/types'; import { simulateUpdateMixnodeConfig, simulateVestingUpdateMixnodeConfig, updateMixnodeConfig } from 'src/requests'; -import { TBondedGateway, TBondedMixnode } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; +import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; import { vestingUpdateMixnodeConfig } from 'src/requests/vesting'; @@ -17,8 +16,9 @@ import { useGetFee } from 'src/hooks/useGetFee'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { BalanceWarning } from 'src/components/FeeWarning'; import { AppContext } from 'src/context'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; -export const GeneralMixnodeSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => { +export const GeneralMixnodeSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }) => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); const { getFee, fee, resetFeeState } = useGetFee(); const { userBalance } = useContext(AppContext); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx new file mode 100644 index 0000000000..76721d9937 --- /dev/null +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx @@ -0,0 +1,182 @@ +import React, { useContext, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { updateNymNodeConfig } from 'src/requests'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema'; +import { Console } from 'src/utils/console'; +import { Alert } from 'src/components/Alert'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { LoadingModal } from 'src/components/Modals/LoadingModal'; +import { BalanceWarning } from 'src/components/FeeWarning'; +import { AppContext } from 'src/context'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; + +export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymNode }) => { + const [openConfirmationModal, setOpenConfirmationModal] = useState(false); + const { fee, resetFeeState } = useGetFee(); + const { userBalance } = useContext(AppContext); + + const theme = useTheme(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting, isDirty, isValid }, + } = useForm({ + resolver: yupResolver(bondedInfoParametersValidationSchema), + mode: 'onChange', + defaultValues: { + host: bondedNode.host, + customHttpPort: bondedNode.customHttpPort, + }, + }); + + const onSubmit = async (data: { host?: string; customHttpPort?: number | null }) => { + resetFeeState(); + const { host, customHttpPort } = data; + if (host && customHttpPort) { + const NymNodeConfigParams = { + host, + custom_http_port: customHttpPort, + }; + try { + await updateNymNodeConfig(NymNodeConfigParams); + setOpenConfirmationModal(true); + } catch (error) { + Console.error(error); + } + } + }; + + return ( + + {fee && ( + onSubmit(d))} + onPrev={resetFeeState} + onClose={resetFeeState} + > + {fee.amount?.amount && userBalance?.balance?.amount.amount && ( + + + + )} + + )} + {isSubmitting && } + + + Changing these values will ONLY change the data about your node on the blockchain. + + Remember to change your node’s config file with the same values too. + + } + bgColor={`${theme.palette.nym.nymWallet.text.blue}0D !important`} + dismissable + /> + + + + + Port + + + + + + + + + + + + + Host + + + + + + + + + + + + + + + + + + { + await setOpenConfirmationModal(false); + }} + buttonFullWidth + sx={{ + width: '450px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }} + headerStyles={{ + width: '100%', + mb: 1, + textAlign: 'center', + color: theme.palette.nym.nymWallet.text.blue, + fontSize: 16, + }} + subHeaderStyles={{ + width: '100%', + mb: 1, + textAlign: 'center', + color: 'main', + fontSize: 14, + }} + /> + + ); +}; diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx index 4f2032ac30..6f8ed79ec9 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx @@ -14,7 +14,7 @@ import { Typography, } from '@mui/material'; import { useTheme } from '@mui/material/styles'; -import { CurrencyDenom, MixNodeCostParams } from '@nymproject/types'; +import { CurrencyDenom, NodeCostParams } from '@nymproject/types'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { isMixnode } from 'src/types'; import { @@ -22,11 +22,9 @@ import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams, updateMixnodeCostParams, - vestingUpdateMixnodeCostParams, } from 'src/requests'; -import { TBondedMixnode } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; +import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { getIntervalAsDate } from 'src/utils'; import { Alert } from 'src/components/Alert'; @@ -36,6 +34,7 @@ import { useGetFee } from 'src/hooks/useGetFee'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { InfoOutlined } from '@mui/icons-material'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; const operatorCostHint = `This is your (operator) rewards including the PM and cost. Rewards are automatically compounded every epoch.You can redeem your rewards at any time. `; @@ -45,7 +44,7 @@ const profitMarginHint = export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }): JSX.Element => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); const [intervalTime, setIntervalTime] = useState(); - const [pendingUpdates, setPendingUpdates] = useState(); + const [pendingUpdates, setPendingUpdates] = useState(); const { clientDetails } = useContext(AppContext); const theme = useTheme(); @@ -115,11 +114,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }, }; try { - if (bondedNode.proxy) { - await vestingUpdateMixnodeCostParams(mixNodeCostParams); - } else { - await updateMixnodeCostParams(mixNodeCostParams); - } + await updateMixnodeCostParams(mixNodeCostParams); + await getPendingEvents(); reset(); setOpenConfirmationModal(true); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx index b49958ae42..7c1f4568b4 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx @@ -1,12 +1,13 @@ import React, { useState } from 'react'; import { Box, Button, Divider, Grid } from '@mui/material'; -import { isGateway, isMixnode } from 'src/types'; -import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; +import { isGateway, isMixnode, isNymNode } from 'src/types'; +import { TBondedNode } from 'src/context/bonding'; import { GeneralMixnodeSettings } from './GeneralMixnodeSettings'; import { ParametersSettings } from './ParametersSettings'; import { GeneralGatewaySettings } from './GeneralGatewaySettings'; +import { GeneralNymNodeSettings } from './GeneralNymNodeSettings'; -const makeGeneralNav = (bondedNode: TBondedMixnode | TBondedGateway) => { +const makeGeneralNav = (bondedNode: TBondedNode) => { const navItems = ['Info']; if (isMixnode(bondedNode)) { navItems.push('Parameters'); @@ -15,7 +16,7 @@ const makeGeneralNav = (bondedNode: TBondedMixnode | TBondedGateway) => { return navItems; }; -export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => { +export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedNode }) => { const [navSelection, setNavSelection] = useState(0); const getSettings = () => { @@ -23,10 +24,12 @@ export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode case 0: { if (isMixnode(bondedNode)) return ; if (isGateway(bondedNode)) return ; + if (isNymNode(bondedNode)) return ; break; } case 1: { if (isMixnode(bondedNode)) return ; + if (isNymNode(bondedNode)) return null; break; } default: diff --git a/nym-wallet/src/pages/bonding/types.ts b/nym-wallet/src/pages/bonding/types.ts index 0e2b4a6c8d..d2c8d3ae9f 100644 --- a/nym-wallet/src/pages/bonding/types.ts +++ b/nym-wallet/src/pages/bonding/types.ts @@ -1,4 +1,4 @@ -import { DecCoin, MixNodeCostParams, TNodeType, TransactionExecuteResult } from '@nymproject/types'; +import { DecCoin, NodeCostParams, TNodeType, TransactionExecuteResult } from '@nymproject/types'; import { TPoolOption } from 'src/components'; export type FormStep = 1 | 2 | 3 | 4; @@ -61,5 +61,5 @@ export interface BondState { export interface ChangeMixCostParams { mix_id: number; - new_costs: MixNodeCostParams; + new_costs: NodeCostParams; } diff --git a/nym-wallet/src/pages/node-settings/system-variables.tsx b/nym-wallet/src/pages/node-settings/system-variables.tsx index 761d98444c..13fb9a60c6 100644 --- a/nym-wallet/src/pages/node-settings/system-variables.tsx +++ b/nym-wallet/src/pages/node-settings/system-variables.tsx @@ -7,8 +7,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { InclusionProbabilityResponse, SelectionChance } from '@nymproject/types'; import { validationSchema } from './validationSchema'; import { InfoTooltip } from '../../components'; -import { useCheckOwnership } from '../../hooks/useCheckOwnership'; -import { updateMixnodeCostParams, vestingUpdateMixnodeCostParams } from '../../requests'; +import { updateMixnodeCostParams } from '../../requests'; import { AppContext } from '../../context'; import { Console } from '../../utils/console'; import { attachDefaultOperatingCost, toPercentFloatString } from '../../utils'; @@ -74,7 +73,6 @@ export const SystemVariables = ({ }) => { const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>(); const { mixnodeDetails } = useContext(AppContext); - const { ownership } = useCheckOwnership(); const { register, @@ -106,12 +104,6 @@ export const SystemVariables = ({ await updateMixnodeCostParams(defaultCostParams); }; - const vestingUpdateMixnodeProfitMargin = async (profitMarginPercent: string) => { - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(profitMarginPercent)); - await vestingUpdateMixnodeCostParams(defaultCostParams); - }; - if (!mixnodeDetails) return null; return ( @@ -175,12 +167,7 @@ export const SystemVariables = ({ - + + + + {nextEpoch instanceof Error ? null : ( diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 19652b6cff..01c04fe813 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -217,24 +217,34 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen const migrateVestedMixnode = async () => { setIsLoading(true); - const tx = await tauriMigrateVestedMixnode(); - setIsLoading(false); - return tx; + try { + const tx = await tauriMigrateVestedMixnode(); + setIsLoading(false); + return tx; + } catch (e) { + Console.error(e); + setError(`an error occurred: ${e}`); + } }; const migrateLegacyNode = async () => { setIsLoading(true); - let tx: TransactionExecuteResult | undefined; + try { + let tx: TransactionExecuteResult | undefined; - if (bondedNode && isMixnode(bondedNode)) { - tx = await migrateLegacyMixnodeReq(); - } - if (bondedNode && isGateway(bondedNode)) { - tx = await migrateLegacyGatewayReq(); + if (bondedNode && isMixnode(bondedNode)) { + tx = await migrateLegacyMixnodeReq(); + } + if (bondedNode && isGateway(bondedNode)) { + tx = await migrateLegacyGatewayReq(); + } + return tx; + } catch (e) { + Console.error(e); + setError(`an error occurred: ${e}`); } setIsLoading(false); - return tx; }; const memoizedValue = useMemo( diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index d618775e05..57dc05bbe4 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -53,7 +53,7 @@ export const Bonding = () => { if (!bondedNode) { return false; } - if (isMixnode(bondedNode) && !bondedNode.isUnbonding) { + if (isMixnode(bondedNode) && !bondedNode.isUnbonding && !bondedNode.proxy) { return true; } if (isGateway(bondedNode)) { @@ -197,7 +197,14 @@ export const Bonding = () => { }; if (error) { - return refresh()} />; + return ( + refresh()} + /> + ); } return ( From 532c25c4f5553f4f201bd5a36775960f2a6377f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 14 Nov 2024 08:48:06 +0000 Subject: [PATCH 187/207] change: dont allow mixnodes bonded with vested tokens into the rewarded set (#5129) --- .../epoch_operations/rewarded_set_assignment.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 0d5c97eacd..d92cd1d7f5 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -66,6 +66,7 @@ struct IgnoredNodes { no_self_described: usize, not_nym_node_binary: usize, no_terms_and_conditions: usize, + use_vested_tokens: usize, } impl IgnoredNodes { @@ -75,6 +76,7 @@ impl IgnoredNodes { no_self_described: 0, not_nym_node_binary: 0, no_terms_and_conditions: 0, + use_vested_tokens: 0, } } @@ -82,6 +84,7 @@ impl IgnoredNodes { self.no_self_described == 0 && self.not_nym_node_binary == 0 && self.no_terms_and_conditions == 0 + && self.use_vested_tokens == 0 } fn maybe_log_summary(&self) { @@ -104,6 +107,12 @@ impl IgnoredNodes { self.no_terms_and_conditions, self.typ ) } + if self.use_vested_tokens != 0 { + warn!( + "{} {} operators bonded using vested tokens", + self.use_vested_tokens, self.typ + ) + } } } @@ -286,6 +295,11 @@ impl EpochAdvancer { continue; } + if mix.bond_information.proxy.is_some() { + legacy_mixnodes_info.use_vested_tokens += 1; + continue; + } + let performance = self .load_mixnode_performance(&interval, mix.mix_id()) .await From 46a33b5ef69c22447d3346784ea7a88e316fd12a Mon Sep 17 00:00:00 2001 From: Andrej Mihajlov Date: Thu, 14 Nov 2024 13:05:03 +0100 Subject: [PATCH 188/207] Add NYM_VPN_API to env files (#5099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add missing NYM_VPN_API uri to environment files * Add trailing slashes --------- Co-authored-by: Jon Häggblad --- envs/canary.env | 5 +++-- envs/mainnet.env | 2 +- envs/qa.env | 5 +++-- envs/sandbox.env | 7 ++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/envs/canary.env b/envs/canary.env index db70a19fd0..84aa65eefa 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -18,6 +18,7 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07 MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364 COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 -EXPLORER_API=https://canary-explorer.performance.nymte.ch/api +EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/ NYXD=https://canary-validator.performance.nymte.ch -NYM_API=https://canary-api.performance.nymte.ch/api +NYM_API=https://canary-api.performance.nymte.ch/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-canary-nyx-network-staging.vercel.app/api/ diff --git a/envs/mainnet.env b/envs/mainnet.env index 419547bf97..8a2b1f61bf 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -25,4 +25,4 @@ NYXD=https://rpc.nymtech.net NYM_API=https://validator.nymtech.net/api/ NYXD_WS="wss://rpc.nymtech.net/websocket" EXPLORER_API=https://explorer.nymtech.net/api/ -NYM_VPN_API="https://nymvpn.com/api" +NYM_VPN_API="https://nymvpn.com/api/" diff --git a/envs/qa.env b/envs/qa.env index 34306a62ea..81adaf299c 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -18,6 +18,7 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq683 VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 -EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api +EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/ NYXD=https://qa-validator.qa.nymte.ch -NYM_API=https://qa-nym-api.qa.nymte.ch/api +NYM_API=https://qa-nym-api.qa.nymte.ch/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/ diff --git a/envs/sandbox.env b/envs/sandbox.env index 6310b8fa89..3033312368 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -19,7 +19,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865 ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -EXPLORER_API=https://sandbox-explorer.nymtech.net/api +EXPLORER_API=https://sandbox-explorer.nymtech.net/api/ NYXD=https://rpc.sandbox.nymtech.net -NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket -NYM_API=https://sandbox-nym-api1.nymtech.net/api +NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket/ +NYM_API=https://sandbox-nym-api1.nymtech.net/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-sandbox-nyx-network-staging.vercel.app/api/ From 6809f7302e2cdf30682d121022db42ec9e8b7d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 14 Nov 2024 15:32:20 +0000 Subject: [PATCH 189/207] Pain/polyfill deprecated endpoints (#5131) * polyfilled contract cache endpoints * polyfilled legacy described endpoints --- nym-api/src/nym_contract_cache/handlers.rs | 158 ++++++++++++++++++--- nym-api/src/nym_nodes/handlers/legacy.rs | 98 +++++++++---- nym-api/src/support/legacy_helpers.rs | 85 +++++++++++ nym-api/src/support/mod.rs | 2 + 4 files changed, 295 insertions(+), 48 deletions(-) create mode 100644 nym-api/src/support/legacy_helpers.rs diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs index d57f1317c4..00f02e1fe2 100644 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -6,10 +6,12 @@ use crate::node_status_api::helpers::{ _get_rewarded_set_legacy_mixnodes_detailed, }; use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; use axum::extract::State; use axum::{Json, Router}; use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::MixNodeBondAnnotated; +use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; use std::collections::HashSet; @@ -58,11 +60,47 @@ pub(crate) fn nym_contract_cache_routes() -> Router { )] #[deprecated] async fn get_mixnodes(State(state): State) -> Json> { - state - .nym_contract_cache() - .legacy_mixnodes_filtered() - .await - .into() + let mut out = state.nym_contract_cache().legacy_mixnodes_filtered().await; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -97,15 +135,54 @@ async fn get_mixnodes_detailed(State(state): State) -> Json) -> Json> { - Json( - state - .nym_contract_cache() - .legacy_gateways_filtered() - .await - .into_iter() - .map(Into::into) - .collect(), - ) + // legacy + let mut out: Vec = state + .nym_contract_cache() + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(); + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy gateway being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_gateway(nym_node, description); + out.push(node); + } + + Json(out) } #[utoipa::path( @@ -166,12 +243,59 @@ async fn get_rewarded_set_detailed( )] #[deprecated] async fn get_active_set(State(state): State) -> Json> { - state + let mut out = state .nym_contract_cache() .legacy_v1_active_set_mixnodes() .await - .clone() - .into() + .clone(); + + let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else { + return Json(out); + }; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + // if the node is not in the active set, ignore it + if !rewarded_set.is_active_mixnode(&nym_node.node_id()) { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs index cae94d5254..3dd6a51f74 100644 --- a/nym-api/src/nym_nodes/handlers/legacy.rs +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; use axum::extract::State; use axum::{Json, Router}; +use nym_api_requests::legacy::LegacyMixNodeBondWithLayer; use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; // we want to mark the routes as deprecated in swagger, but still expose them @@ -34,25 +36,44 @@ async fn get_gateways_described( ) -> Json> { let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); - let gateways = contract_cache.legacy_gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } + + // legacy + let legacy = contract_cache.legacy_gateways_filtered().await; // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); }; - Json( - gateways - .into_iter() - .map(|bond| LegacyDescribedGateway { - self_described: self_descriptions.get_description(&bond.node_id).cloned(), - bond: bond.bond, - }) - .collect(), - ) + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedGateway { + self_described: describe_cache + .get_description(&legacy_bond.node_id) + .cloned(), + bond: legacy_bond.bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + + out.push(LegacyDescribedGateway { + bond: to_legacy_gateway(&nym_node, description), + self_described: Some(description.clone()), + }) + } + + Json(out) } #[utoipa::path( @@ -70,28 +91,43 @@ async fn get_mixnodes_described( let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); - let mixnodes = contract_cache + let legacy: Vec = contract_cache .legacy_mixnodes_filtered() .await .into_iter() .map(|m| m.bond_information) .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy mixnodes + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); }; - Json( - mixnodes - .into_iter() - .map(|bond| LegacyDescribedMixNode { - self_described: self_descriptions.get_description(&bond.mix_id).cloned(), - bond, - }) - .collect(), - ) + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedMixNode { + self_described: describe_cache.get_description(&legacy_bond.mix_id).cloned(), + bond: legacy_bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.mixnode { + continue; + } + + out.push(LegacyDescribedMixNode { + bond: to_legacy_mixnode(&nym_node, description).bond_information, + self_described: Some(description.clone()), + }) + } + + Json(out) } diff --git a/nym-api/src/support/legacy_helpers.rs b/nym-api/src/support/legacy_helpers.rs new file mode 100644 index 0000000000..d5135c7643 --- /dev/null +++ b/nym-api/src/support/legacy_helpers.rs @@ -0,0 +1,85 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::legacy::{LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::NymNodeData; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_crypto::aes::cipher::crypto_common::rand_core::OsRng; +use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; +use nym_mixnet_contract_common::{ + Gateway, GatewayBond, LegacyMixLayer, MixNode, MixNodeBond, NymNodeDetails, +}; +use rand::prelude::SliceRandom; + +pub(crate) fn to_legacy_mixnode( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> LegacyMixNodeDetailsWithLayer { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + let layer = layer_choices.choose(&mut rng).copied().unwrap(); + + LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: MixNodeBond { + mix_id: nym_node.node_id(), + owner: nym_node.bond_information.owner.clone(), + original_pledge: nym_node.bond_information.original_pledge.clone(), + mix_node: MixNode { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + verloc_port: description.verloc_port(), + http_api_port: nym_node + .bond_information + .node + .custom_http_port + .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + bonding_height: nym_node.bond_information.bonding_height, + is_unbonding: nym_node.bond_information.is_unbonding, + }, + layer, + }, + rewarding_details: nym_node.rewarding_details.clone(), + pending_changes: LegacyPendingMixNodeChanges { + pledge_change: nym_node.pending_changes.pledge_change, + }, + } +} + +pub(crate) fn to_legacy_gateway( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> GatewayBond { + GatewayBond { + pledge_amount: nym_node.bond_information.original_pledge.clone(), + owner: nym_node.bond_information.owner.clone(), + block_height: nym_node.bond_information.bonding_height, + gateway: Gateway { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + clients_port: description.mixnet_websockets.ws_port, + location: description + .auxiliary_details + .location + .map(|c| c.to_string()) + .unwrap_or_default(), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + } +} diff --git a/nym-api/src/support/mod.rs b/nym-api/src/support/mod.rs index 28abaf3b94..a1773f9804 100644 --- a/nym-api/src/support/mod.rs +++ b/nym-api/src/support/mod.rs @@ -7,3 +7,5 @@ pub(crate) mod config; pub(crate) mod http; pub(crate) mod nyxd; pub(crate) mod storage; + +pub(crate) mod legacy_helpers; From 94f247563b8edda297a6ffc56cb6c16c78bac166 Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Fri, 15 Nov 2024 17:45:26 +0000 Subject: [PATCH 190/207] nym-node-status-agent bump version --- Cargo.lock | 2 +- nym-node-status-agent/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65a44d2f9a..99d53669da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5981,7 +5981,7 @@ dependencies = [ [[package]] name = "nym-node-status-agent" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "clap 4.5.18", diff --git a/nym-node-status-agent/Cargo.toml b/nym-node-status-agent/Cargo.toml index 45363a8cb6..f7d8543a1f 100644 --- a/nym-node-status-agent/Cargo.toml +++ b/nym-node-status-agent/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-node-status-agent" -version = "0.1.5" +version = "0.1.6" authors.workspace = true repository.workspace = true homepage.workspace = true From db20c2e2faba2b22b9675b0f7b9a82f663b71cbd Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Fri, 15 Nov 2024 17:55:07 +0000 Subject: [PATCH 191/207] node-status-agent: cherry-pick GH Actions pipeline and dockerfile from `9c680fd` --- .github/workflows/push-node-status-agent.yaml | 27 +++++++++++-------- nym-node-status-agent/Dockerfile | 14 +++++++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.github/workflows/push-node-status-agent.yaml b/.github/workflows/push-node-status-agent.yaml index a66d4dee41..fb25f9dac0 100644 --- a/.github/workflows/push-node-status-agent.yaml +++ b/.github/workflows/push-node-status-agent.yaml @@ -2,6 +2,10 @@ name: Build and upload Node Status agent container to harbor.nymte.ch on: workflow_dispatch: + inputs: + gateway_probe_git_ref: + type: string + description: Which gateway probe git ref to build the image with env: WORKING_DIRECTORY: "nym-node-status-agent" @@ -32,25 +36,26 @@ jobs: with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml - - name: Check if tag exists + - name: cleanup-gateway-probe-ref + id: cleanup_gateway_probe_ref run: | - if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then - echo "Tag ${{ steps.get_version.outputs.value }} already exists" - fi + GATEWAY_PROBE_GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} + GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}" + echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT - name: Remove existing tag if exists run: | - if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then - git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} - git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} >/dev/null 2>&1; then + git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} + git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} fi - name: Create tag run: | - git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" - git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} - name: BuildAndPushImageOnHarbor run: | - docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest - docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags + docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags \ No newline at end of file diff --git a/nym-node-status-agent/Dockerfile b/nym-node-status-agent/Dockerfile index 0280fc5687..b50ce8be3d 100644 --- a/nym-node-status-agent/Dockerfile +++ b/nym-node-status-agent/Dockerfile @@ -1,5 +1,7 @@ FROM rust:latest AS builder +ARG GIT_REF=main + RUN apt update && apt install -yy libdbus-1-dev pkg-config libclang-dev # Install go @@ -7,6 +9,7 @@ RUN wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz -O go.tar.gz RUN tar -xzvf go.tar.gz -C /usr/local RUN git clone https://github.com/nymtech/nym-vpn-client /usr/src/nym-vpn-client +RUN cd /usr/src/nym-vpn-client && git checkout $GIT_REF ENV PATH=/go/bin:/usr/local/go/bin:$PATH WORKDIR /usr/src/nym-vpn-client/nym-vpn-core RUN cargo build --release --package nym-gateway-probe @@ -15,6 +18,15 @@ COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nym-node-status-agent RUN cargo build --release +#------------------------------------------------------------------- +# The following environment variables are required at runtime: +# +# NODE_STATUS_AGENT_SERVER_ADDRESS +# NODE_STATUS_AGENT_SERVER_PORT +# +# see https://github.com/nymtech/nym/blob/develop/nym-node-status-agent/src/cli.rs for details +#------------------------------------------------------------------- + FROM ubuntu:24.04 RUN apt-get update && apt-get install -y ca-certificates @@ -25,4 +37,4 @@ COPY --from=builder /usr/src/nym/target/release/nym-node-status-agent ./ COPY --from=builder /usr/src/nym-vpn-client/nym-vpn-core/target/release/nym-gateway-probe ./ ENV NODE_STATUS_AGENT_PROBE_PATH=/nym/nym-gateway-probe -ENTRYPOINT [ "/nym/nym-node-status-agent" ] +ENTRYPOINT [ "/nym/nym-node-status-agent", "run-probe" ] \ No newline at end of file From e44a36e5b5447e6c9ea28f0c5da431743a953c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 18 Nov 2024 11:21:07 +0200 Subject: [PATCH 192/207] Respond to auth messages with same version (#5140) * Introduce traits for response * Ugly responde with same protocol version * Don't pull sdk crate unnecessarily --- common/authenticator-requests/src/error.rs | 3 + common/authenticator-requests/src/lib.rs | 1 + common/authenticator-requests/src/traits.rs | 269 +++++++++++ .../src/lib.rs | 2 +- service-providers/authenticator/src/error.rs | 3 + .../authenticator/src/mixnet_listener.rs | 422 +++++++++++++----- .../authenticator/src/peer_manager.rs | 9 +- 7 files changed, 593 insertions(+), 116 deletions(-) create mode 100644 common/authenticator-requests/src/traits.rs diff --git a/common/authenticator-requests/src/error.rs b/common/authenticator-requests/src/error.rs index fc9f8700d5..1c43da3cad 100644 --- a/common/authenticator-requests/src/error.rs +++ b/common/authenticator-requests/src/error.rs @@ -22,4 +22,7 @@ pub enum Error { #[error("conversion: {0}")] Conversion(String), + + #[error("failed to serialize response packet: {source}")] + FailedToSerializeResponsePacket { source: Box }, } diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index dd98cc9f7f..f2b2fb55ce 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod traits; pub mod v1; pub mod v2; pub mod v3; diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs new file mode 100644 index 0000000000..cb7cf37ee1 --- /dev/null +++ b/common/authenticator-requests/src/traits.rs @@ -0,0 +1,269 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519::PrivateKey; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_sphinx::addressing::clients::Recipient; +use nym_wireguard_types::PeerPublicKey; + +use crate::{v1, v2, v3, Error}; + +#[derive(Copy, Clone, Debug)] +pub enum AuthenticatorVersion { + V1, + V2, + V3, + UNKNOWN, +} + +impl From for AuthenticatorVersion { + fn from(value: Protocol) -> Self { + if value.service_provider_type != ServiceProviderType::Authenticator { + AuthenticatorVersion::UNKNOWN + } else if value.version == v1::VERSION { + AuthenticatorVersion::V1 + } else if value.version == v2::VERSION { + AuthenticatorVersion::V2 + } else if value.version == v3::VERSION { + AuthenticatorVersion::V3 + } else { + AuthenticatorVersion::UNKNOWN + } + } +} + +pub trait InitMessage { + fn pub_key(&self) -> PeerPublicKey; +} + +impl InitMessage for v1::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v2::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v3::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +pub trait FinalMessage { + fn pub_key(&self) -> PeerPublicKey; + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; + fn private_ip(&self) -> IpAddr; + fn credential(&self) -> Option; +} + +impl FinalMessage for v1::GatewayClient { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.verify(private_key, nonce) + } + + fn private_ip(&self) -> IpAddr { + self.private_ip + } + + fn credential(&self) -> Option { + None + } +} + +impl FinalMessage for v2::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ip(&self) -> IpAddr { + self.gateway_client.private_ip + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v3::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ip(&self) -> IpAddr { + self.gateway_client.private_ip + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +pub trait QueryBandwidthMessage { + fn pub_key(&self) -> PeerPublicKey; +} + +impl QueryBandwidthMessage for PeerPublicKey { + fn pub_key(&self) -> PeerPublicKey { + *self + } +} + +pub trait TopUpMessage { + fn pub_key(&self) -> PeerPublicKey; + fn credential(&self) -> CredentialSpendingData; +} + +impl TopUpMessage for v3::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +pub enum AuthenticatorRequest { + Initial { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + Final { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + QueryBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + TopUpBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, +} + +impl From for AuthenticatorRequest { + fn from(value: v1::request::AuthenticatorRequest) -> Self { + match value.data { + v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { + msg: Box::new(gateway_client), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v2::request::AuthenticatorRequest) -> Self { + match value.data { + v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v3::request::AuthenticatorRequest) -> Self { + match value.data { + v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index d13a7156c9..f9f0564e1d 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[repr(u8)] pub enum ServiceProviderType { NetworkRequester = 0, diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index bfc04c3fbc..6645369349 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -88,6 +88,9 @@ pub enum AuthenticatorError { #[error("storage should have the requested bandwidht entry")] MissingClientBandwidthEntry, + + #[error("unknown version number")] + UnknownVersion, } pub type Result = std::result::Result; diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 8fc5c62582..606e38a259 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -11,15 +11,17 @@ use defguard_wireguard_rs::net::IpAddrMask; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::StreamExt; use nym_authenticator_requests::{ + traits::{ + AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage, + QueryBandwidthMessage, TopUpMessage, + }, v1, v2, v3::{ self, registration::{ - FinalMessage, GatewayClient, InitMessage, PendingRegistrations, PrivateIPs, - RegistrationData, RegistredData, RemainingBandwidthData, + GatewayClient, PendingRegistrations, PrivateIPs, RegistrationData, + RemainingBandwidthData, }, - request::{AuthenticatorRequest, AuthenticatorRequestData}, - response::AuthenticatorResponse, }, CURRENT_VERSION, }; @@ -32,7 +34,7 @@ use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::Storage; use nym_sdk::mixnet::{InputMessage, MixnetMessageSender, Recipient, TransmissionLane}; -use nym_service_provider_requests_common::ServiceProviderType; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; use nym_wireguard::WireguardGatewayData; @@ -43,7 +45,7 @@ use tokio_stream::wrappers::IntervalStream; use crate::{config::Config, error::*}; -type AuthenticatorHandleResult = Result; +type AuthenticatorHandleResult = Result<(Vec, Recipient)>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute pub(crate) struct RegistredAndFree { @@ -153,22 +155,72 @@ impl MixnetListener { async fn on_initial_request( &mut self, - init_message: InitMessage, + init_message: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { - let remote_public = init_message.pub_key; + let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); let mut registred_and_free = self.registred_and_free.write().await; if let Some(registration_data) = registred_and_free .registration_in_progres .get(&remote_public) { - return Ok(AuthenticatorResponse::new_pending_registration_success( - registration_data.clone(), - request_id, - reply_to, - )); + let gateway_data = registration_data.gateway_data.clone(); + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => { + v1::response::AuthenticatorResponse::new_pending_registration_success( + v1::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: v1::GatewayClient { + pub_key: gateway_data.pub_key, + private_ip: gateway_data.private_ip, + mac: v1::ClientMac::new(gateway_data.mac.to_vec()), + }, + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::new_pending_registration_success( + v2::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone().into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::new_pending_registration_success( + v3::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + return Ok((bytes, reply_to)); } let peer = self.peer_manager.query_peer(remote_public).await?; @@ -178,15 +230,49 @@ impl MixnetListener { "private ip list should not be empty".to_string(), )); }; - return Ok(AuthenticatorResponse::new_registered( - RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), - private_ip: allowed_ip.ip, - wg_port: self.config.authenticator.announced_port, - }, - reply_to, - request_id, - )); + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( + v1::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ip: allowed_ip.ip, + wg_port: self.config.authenticator.announced_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( + v2::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ip: allowed_ip.ip, + wg_port: self.config.authenticator.announced_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( + v3::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ip: allowed_ip.ip, + wg_port: self.config.authenticator.announced_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + return Ok((bytes, reply_to)); } let private_ip_ref = registred_and_free @@ -205,51 +291,98 @@ impl MixnetListener { ); let registration_data = RegistrationData { nonce, - gateway_data, + gateway_data: gateway_data.clone(), wg_port: self.config.authenticator.announced_port, }; registred_and_free .registration_in_progres .insert(remote_public, registration_data.clone()); + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => { + v1::response::AuthenticatorResponse::new_pending_registration_success( + v1::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: v1::GatewayClient { + pub_key: gateway_data.pub_key, + private_ip: gateway_data.private_ip, + mac: v1::ClientMac::new(gateway_data.mac.to_vec()), + }, + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::new_pending_registration_success( + v2::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::new_pending_registration_success( + v3::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data, + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; - Ok(AuthenticatorResponse::new_pending_registration_success( - registration_data, - request_id, - reply_to, - )) + Ok((bytes, reply_to)) } async fn on_final_request( &mut self, - final_message: FinalMessage, + final_message: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { let mut registred_and_free = self.registred_and_free.write().await; let registration_data = registred_and_free .registration_in_progres - .get(&final_message.gateway_client.pub_key()) + .get(&final_message.pub_key()) .ok_or(AuthenticatorError::RegistrationNotInProgress)? .clone(); if final_message - .gateway_client .verify(self.keypair().private_key(), registration_data.nonce) .is_err() { return Err(AuthenticatorError::MacVerificationFailure); } - let mut peer = Peer::new(Key::new(final_message.gateway_client.pub_key.to_bytes())); + let mut peer = Peer::new(Key::new(final_message.pub_key().to_bytes())); peer.allowed_ips - .push(IpAddrMask::new(final_message.gateway_client.private_ip, 32)); + .push(IpAddrMask::new(final_message.private_ip(), 32)); // If gateway does ecash verification and client sends a credential, we do the additional // credential verification. Later this will become mandatory. - if let (Some(ecash_verifier), Some(credential)) = ( - self.ecash_verifier.clone(), - final_message.credential.clone(), - ) { + if let (Some(ecash_verifier), Some(credential)) = + (self.ecash_verifier.clone(), final_message.credential()) + { let client_id = ecash_verifier .storage() .insert_wireguard_peer(&peer, true) @@ -279,17 +412,45 @@ impl MixnetListener { } registred_and_free .registration_in_progres - .remove(&final_message.gateway_client.pub_key()); + .remove(&final_message.pub_key()); - Ok(AuthenticatorResponse::new_registered( - RegistredData { - pub_key: registration_data.gateway_data.pub_key, - private_ip: registration_data.gateway_data.private_ip, - wg_port: registration_data.wg_port, - }, - reply_to, - request_id, - )) + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( + v1::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ip: registration_data.gateway_data.private_ip, + wg_port: registration_data.wg_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( + v2::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ip: registration_data.gateway_data.private_ip, + wg_port: registration_data.wg_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( + v3::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ip: registration_data.gateway_data.private_ip, + wg_port: registration_data.wg_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + Ok((bytes, reply_to)) } async fn credential_verification( @@ -325,22 +486,60 @@ impl MixnetListener { async fn on_query_bandwidth_request( &mut self, - peer_public_key: PeerPublicKey, + msg: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { - let bandwidth_data = self.peer_manager.query_bandwidth(peer_public_key).await?; - Ok(AuthenticatorResponse::new_remaining_bandwidth( - bandwidth_data, - reply_to, - request_id, - )) + let bandwidth_data = self.peer_manager.query_bandwidth(msg).await?; + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => { + v1::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v1::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth as u64, + suspended: false, + }), + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v2::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth, + }), + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + Ok((bytes, reply_to)) } async fn on_topup_bandwidth_request( &mut self, - peer_public_key: PeerPublicKey, - credential: CredentialSpendingData, + msg: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { @@ -349,7 +548,7 @@ impl MixnetListener { }; let client_id = ecash_verifier .storage() - .get_wireguard_peer(&peer_public_key.to_string()) + .get_wireguard_peer(&msg.pub_key().to_string()) .await? .ok_or(AuthenticatorError::MissingClientBandwidthEntry)? .client_id @@ -364,7 +563,7 @@ impl MixnetListener { let client_bandwidth = ClientBandwidth::new(bandwidth.into()); let mut verifier = CredentialVerifier::new( - CredentialSpendingRequest::new(credential), + CredentialSpendingRequest::new(msg.credential()), ecash_verifier.clone(), BandwidthStorageManager::new( ecash_verifier.storage().clone(), @@ -376,13 +575,22 @@ impl MixnetListener { ); let available_bandwidth = verifier.verify().await?; - Ok(AuthenticatorResponse::new_topup_bandwidth( - RemainingBandwidthData { - available_bandwidth, - }, - reply_to, - request_id, - )) + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_topup_bandwidth( + RemainingBandwidthData { + available_bandwidth, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { + return Err(AuthenticatorError::UnknownVersion) + } + }; + + Ok((bytes, reply_to)) } async fn on_reconstructed_message( @@ -394,62 +602,52 @@ impl MixnetListener { reconstructed.sender_tag ); - let request = match deserialize_request(&reconstructed) { - Err(AuthenticatorError::InvalidPacketVersion(version)) => { - return self.on_version_mismatch(version, &reconstructed); - } - req => req, - }?; + let request = deserialize_request(&reconstructed)?; - match request.data { - AuthenticatorRequestData::Initial(init_msg) => { - self.on_initial_request(init_msg, request.request_id, request.reply_to) + match request { + AuthenticatorRequest::Initial { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_initial_request(msg, protocol, request_id, reply_to) .await } - AuthenticatorRequestData::Final(final_msg) => { - self.on_final_request(*final_msg, request.request_id, request.reply_to) + AuthenticatorRequest::Final { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_final_request(msg, protocol, request_id, reply_to) .await } - AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { - self.on_query_bandwidth_request( - peer_public_key, - request.request_id, - request.reply_to, - ) - .await + AuthenticatorRequest::QueryBandwidth { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_query_bandwidth_request(msg, protocol, request_id, reply_to) + .await } - AuthenticatorRequestData::TopUpBandwidth(topup_message) => { - self.on_topup_bandwidth_request( - topup_message.pub_key, - topup_message.credential, - request.request_id, - request.reply_to, - ) - .await + AuthenticatorRequest::TopUpBandwidth { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_topup_bandwidth_request(msg, protocol, request_id, reply_to) + .await } } } - fn on_version_mismatch( - &self, - version: u8, - _reconstructed: &ReconstructedMessage, - ) -> AuthenticatorHandleResult { - // If it's possible to parse, do so and return back a response, otherwise just drop - Err(AuthenticatorError::InvalidPacketVersion(version)) - } - // When an incoming mixnet message triggers a response that we send back. - async fn handle_response(&self, response: AuthenticatorResponse) -> Result<()> { - let recipient = response.recipient(); - - let response_packet = response.to_bytes().map_err(|err| { - log::error!("Failed to serialize response packet"); - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?; - + async fn handle_response(&self, response: Vec, recipient: Recipient) -> Result<()> { let input_message = - InputMessage::new_regular(recipient, response_packet, TransmissionLane::General, None); + InputMessage::new_regular(recipient, response, TransmissionLane::General, None); self.mixnet_client .send(input_message) .await @@ -473,8 +671,8 @@ impl MixnetListener { msg = self.mixnet_client.next() => { if let Some(msg) = msg { match self.on_reconstructed_message(msg).await { - Ok(response) => { - if let Err(err) = self.handle_response(response).await { + Ok((response, recipient)) => { + if let Err(err) = self.handle_response(response, recipient).await { log::error!("Mixnet listener failed to handle response: {err}"); } } @@ -506,7 +704,6 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) - .map(Into::::into) .map(Into::into), [2, request_type] => { if request_type == ServiceProviderType::Authenticator as u8 { @@ -525,6 +722,7 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result, ) -> Result> { - let key = Key::new(peer_public_key.to_bytes()); + let key = Key::new(msg.pub_key().to_bytes()); let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::QueryBandwidth { key, response_tx }; self.wireguard_gateway_data From 35343b5220f84edfc4cc845d9c0a1228adaa6d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 18 Nov 2024 10:00:40 +0000 Subject: [PATCH 193/207] bugfix: make sure to assign correct node_id and identity during 'gateway_details' table migration (#5142) --- nym-api/src/support/storage/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index 4731f90ac3..8a2959aed7 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -1329,7 +1329,7 @@ pub(crate) mod v3_migration { identity VARCHAR NOT NULL UNIQUE ); - INSERT INTO gateway_details_temp SELECT * FROM gateway_details; + INSERT INTO gateway_details_temp(id, node_id, identity) SELECT id, node_id, identity FROM gateway_details; DROP TABLE gateway_details; ALTER TABLE gateway_details_temp RENAME TO gateway_details; "#, From 6dc9b79aceb2aacd045ee15cbb7d2b5cd92fba84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 18 Nov 2024 10:28:46 +0000 Subject: [PATCH 194/207] bugifx: assign 'node_id' when converting from 'GatewayDetails' to 'TestNode' (#5143) --- nym-api/src/support/storage/models.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index 6a7b120e66..1418cab9f8 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -87,7 +87,7 @@ pub struct GatewayDetails { impl From for TestNode { fn from(value: GatewayDetails) -> Self { TestNode { - node_id: None, + node_id: Some(value.node_id), identity_key: Some(value.identity), } } From 5ad11f2048f24b0c8902495359ac706b803cadc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 18 Nov 2024 13:33:19 +0200 Subject: [PATCH 195/207] Limit race probability (#5145) * Limit race probability * Actually assign value --- common/wireguard/src/peer_controller.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index bc143745c4..8c7d94784a 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -194,6 +194,10 @@ impl PeerController { ); self.bw_storage_managers .insert(peer.public_key.clone(), bandwidth_storage_manager); + // try to immediately update the host information, to eliminate races + if let Ok(host_information) = self.wg_api.inner.read_interface_data() { + *self.host_information.write().await = host_information; + } tokio::spawn(async move { if let Err(e) = handle.run().await { log::error!("Peer handle shut down ungracefully - {e}"); From 086b4f6f54d6bf6baf1fc0bc7fe0b011bbc41531 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 18 Nov 2024 13:01:27 +0100 Subject: [PATCH 196/207] update changelog --- CHANGELOG.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdec67361..5618fe59c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,198 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2024.13-magura] (2024-11-18) + +- Limit race probability ([#5145]) +- bugifx: assign 'node_id' when converting from 'GatewayDetails' to 'TestNode' ([#5143]) +- bugfix: make sure to assign correct node_id and identity during 'gateway_details' table migration ([#5142]) +- Respond to auth messages with same version ([#5140]) +- Pain/polyfill deprecated endpoints ([#5131]) +- change: dont select mixnodes bonded with vested tokens into the rewarded set ([#5129]) +- nym-credential-proxy-requests: reqwest use rustls-tls ([#5116]) +- bugfix: preserve as much as possible of the rewarded set during migration ([#5103]) +- Feature/force refresh node ([#5101]) +- Add NYM_VPN_API to env files ([#5099]) +- bugfix: fixed historical uptimes for nodes ([#5097]) +- Remove old use of 1GB constant ([#5096]) +- Graceful agent 1.1.5 ([#5093]) +- Add more translations from v2 to v3 authenticator ([#5091]) +- Nym node - Fix claim delegator rewards ([#5090]) +- Make 250 GB/30 days for free ride mode ([#5083]) +- Don't increase bandwidth two times ([#5081]) +- Fix expiration date as today + 7 days ([#5076]) +- Fix gateway decreasing bandwidth ([#5075]) +- Allow custom http port to be reset ([#5073]) +- bugfix: additional checks inside credential proxy ([#5072]) +- chore: deprecated old nym-api client methods and replaced them when possible ([#5069]) +- NS API with directory v2 (#5058) ([#5068]) +- bugfix: credential-proxy obtain-async ([#5067]) +- Allow nym node config updates ([#5066]) +- bugfix: use corrext axum extractors for ecash route arguments ([#5065]) +- Merge2/release/2024.13 magura ([#5063]) +- bugfix/feature: added NymApiClient method to get all skimmed nodes ([#5062]) +- Merge1/release/2024.13 magura ([#5061]) +- added hacky routes to return nymnodes alongside legacy nodes ([#5051]) +- bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set ([#5049]) +- bugfix: adjust runtime storage migration ([#5047]) +- bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using… ([#5046]) +- bugfix: restore default http port for nym-api ([#5045]) +- bugfix: fix ecash handlers routes ([#5043]) +- bugfix: don't assign exit gateways to standby set ([#5041]) +- bugfix: make sure nym-nodes are also tested by network monitor ([#5040]) +- bugfix: use bonded nym-nodes for determining initial network monitor … ([#5039]) +- bugfix: make gateways insert themselves into [local] topology ([#5038]) +- Pass poisson flag ([#5037]) +- bugfix: use human readable roles for annotations ([#5036]) +- bugfix: use old name for 'epoch_role' in SkimmedNode ([#5034]) +- bugfix: make sure to use correct highest node id when assigning role ([#5032]) +- feature: use axum_client_ip for attempting to extract source ip ([#5031]) +- bugfix: fixed backwards incompatibility for /gateways/described endpoint ([#5030]) +- bugfix: verifying signed information of legacy nodes ([#5029]) +- bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change' ([#5028]) +- bugfix: missing #[serde(default)] for announce port ([#5024]) +- bugfix: directory v2.1 `get_all_avg_gateway_reliability_in_interval` query ([#5023]) +- added 'get_all_described_nodes' to NymApiClient and adjusted return t… ([#5016]) +- Reapply fixes to new branch ([#5014]) +- Consume only positive bandwidth ([#5013]) +- feature: adjusted ticket sizes to the agreed amounts ([#5009]) +- Push private ip before inserting ([#5008]) +- chore: update itertools in compact ecash ([#4994]) +- feature: make accepting t&c a hard requirement for rewarded set selection ([#4993]) +- Fix rustfmt in nym-credential-proxy ([#4992]) +- bugfix: client memory leak ([#4991]) +- Eliminate 0 bandwidth race check ([#4988]) +- [DOCs;/operators]: Release notes for v2024.12 aero ([#4984]) +- Add topup req constructor ([#4983]) +- Fix critical issues SI86 and SI87 from Cure53 ([#4982]) +- Rename nym-vpn-api to nym-credential-proxy ([#4981]) +- enable global ecash routes even if api is not a signer ([#4980]) +- resolve beta clippy issues in contracts ([#4978]) +- Re-enable vested delegation migration ([#4977]) +- feature: require reporting using nym-node binary for rewarded set selection ([#4976]) +- Top up bandwidth ([#4975]) +- [Product Data] Add session type based on ecash ticket received ([#4974]) +- Bugfix/additional directory fixes ([#4973]) +- feat: add Dockerfile for nym node ([#4972]) +- chore: remove unused rocket code ([#4968]) +- Import nym-vpn-api crates ([#4967]) +- feature: importer-cli to correctly handle mixnet/vesting import ([#4966]) +- bugfix: fix expected return type on /v1/gateways endpoint ([#4965]) +- [Product Data] First step in gateway usage data collection ([#4963]) +- Bump sqlx to 0.7.4 ([#4959]) +- Add env feature to clap and make clap parameters available as env variables ([#4957]) +- Feature/contract state tools ([#4954]) +- expose authenticator address along other address in node-details ([#4953]) +- Extract packet processing from mixnode-common ([#4949]) +- nym-api container ([#4948]) +- Ticket type storage ([#4947]) +- Add "utoipa" feature to nym-node ([#4945]) +- build(deps): bump the patch-updates group across 1 directory with 9 updates ([#4944]) +- V2 performance monitoring feature flag ([#4943]) +- Bugfix/rewarder post pruning adjustments ([#4942]) +- Switch over the last set of jobs to arc runners ([#4938]) +- Fix broken build after merge ([#4937]) +- bugfix: correctly paginate through 'search_tx' endpoint ([#4936]) +- Add more conversions for responses of authenticator messages ([#4929]) +- Directory Sevices v2.1 ([#4903]) +- Migrate Legacy Node (Frontend) ([#4826]) +- Fix critical issues SI84 and SI85 from Cure53 ([#4758]) + +[#5145]: https://github.com/nymtech/nym/pull/5145 +[#5143]: https://github.com/nymtech/nym/pull/5143 +[#5142]: https://github.com/nymtech/nym/pull/5142 +[#5140]: https://github.com/nymtech/nym/pull/5140 +[#5131]: https://github.com/nymtech/nym/pull/5131 +[#5129]: https://github.com/nymtech/nym/pull/5129 +[#5116]: https://github.com/nymtech/nym/pull/5116 +[#5103]: https://github.com/nymtech/nym/pull/5103 +[#5101]: https://github.com/nymtech/nym/pull/5101 +[#5099]: https://github.com/nymtech/nym/pull/5099 +[#5097]: https://github.com/nymtech/nym/pull/5097 +[#5096]: https://github.com/nymtech/nym/pull/5096 +[#5093]: https://github.com/nymtech/nym/pull/5093 +[#5091]: https://github.com/nymtech/nym/pull/5091 +[#5090]: https://github.com/nymtech/nym/pull/5090 +[#5083]: https://github.com/nymtech/nym/pull/5083 +[#5081]: https://github.com/nymtech/nym/pull/5081 +[#5076]: https://github.com/nymtech/nym/pull/5076 +[#5075]: https://github.com/nymtech/nym/pull/5075 +[#5073]: https://github.com/nymtech/nym/pull/5073 +[#5072]: https://github.com/nymtech/nym/pull/5072 +[#5069]: https://github.com/nymtech/nym/pull/5069 +[#5068]: https://github.com/nymtech/nym/pull/5068 +[#5067]: https://github.com/nymtech/nym/pull/5067 +[#5066]: https://github.com/nymtech/nym/pull/5066 +[#5065]: https://github.com/nymtech/nym/pull/5065 +[#5063]: https://github.com/nymtech/nym/pull/5063 +[#5062]: https://github.com/nymtech/nym/pull/5062 +[#5061]: https://github.com/nymtech/nym/pull/5061 +[#5051]: https://github.com/nymtech/nym/pull/5051 +[#5049]: https://github.com/nymtech/nym/pull/5049 +[#5047]: https://github.com/nymtech/nym/pull/5047 +[#5046]: https://github.com/nymtech/nym/pull/5046 +[#5045]: https://github.com/nymtech/nym/pull/5045 +[#5043]: https://github.com/nymtech/nym/pull/5043 +[#5041]: https://github.com/nymtech/nym/pull/5041 +[#5040]: https://github.com/nymtech/nym/pull/5040 +[#5039]: https://github.com/nymtech/nym/pull/5039 +[#5038]: https://github.com/nymtech/nym/pull/5038 +[#5037]: https://github.com/nymtech/nym/pull/5037 +[#5036]: https://github.com/nymtech/nym/pull/5036 +[#5034]: https://github.com/nymtech/nym/pull/5034 +[#5032]: https://github.com/nymtech/nym/pull/5032 +[#5031]: https://github.com/nymtech/nym/pull/5031 +[#5030]: https://github.com/nymtech/nym/pull/5030 +[#5029]: https://github.com/nymtech/nym/pull/5029 +[#5028]: https://github.com/nymtech/nym/pull/5028 +[#5024]: https://github.com/nymtech/nym/pull/5024 +[#5023]: https://github.com/nymtech/nym/pull/5023 +[#5016]: https://github.com/nymtech/nym/pull/5016 +[#5014]: https://github.com/nymtech/nym/pull/5014 +[#5013]: https://github.com/nymtech/nym/pull/5013 +[#5009]: https://github.com/nymtech/nym/pull/5009 +[#5008]: https://github.com/nymtech/nym/pull/5008 +[#4994]: https://github.com/nymtech/nym/pull/4994 +[#4993]: https://github.com/nymtech/nym/pull/4993 +[#4992]: https://github.com/nymtech/nym/pull/4992 +[#4991]: https://github.com/nymtech/nym/pull/4991 +[#4988]: https://github.com/nymtech/nym/pull/4988 +[#4984]: https://github.com/nymtech/nym/pull/4984 +[#4983]: https://github.com/nymtech/nym/pull/4983 +[#4982]: https://github.com/nymtech/nym/pull/4982 +[#4981]: https://github.com/nymtech/nym/pull/4981 +[#4980]: https://github.com/nymtech/nym/pull/4980 +[#4978]: https://github.com/nymtech/nym/pull/4978 +[#4977]: https://github.com/nymtech/nym/pull/4977 +[#4976]: https://github.com/nymtech/nym/pull/4976 +[#4975]: https://github.com/nymtech/nym/pull/4975 +[#4974]: https://github.com/nymtech/nym/pull/4974 +[#4973]: https://github.com/nymtech/nym/pull/4973 +[#4972]: https://github.com/nymtech/nym/pull/4972 +[#4968]: https://github.com/nymtech/nym/pull/4968 +[#4967]: https://github.com/nymtech/nym/pull/4967 +[#4966]: https://github.com/nymtech/nym/pull/4966 +[#4965]: https://github.com/nymtech/nym/pull/4965 +[#4963]: https://github.com/nymtech/nym/pull/4963 +[#4959]: https://github.com/nymtech/nym/pull/4959 +[#4957]: https://github.com/nymtech/nym/pull/4957 +[#4954]: https://github.com/nymtech/nym/pull/4954 +[#4953]: https://github.com/nymtech/nym/pull/4953 +[#4949]: https://github.com/nymtech/nym/pull/4949 +[#4948]: https://github.com/nymtech/nym/pull/4948 +[#4947]: https://github.com/nymtech/nym/pull/4947 +[#4945]: https://github.com/nymtech/nym/pull/4945 +[#4944]: https://github.com/nymtech/nym/pull/4944 +[#4943]: https://github.com/nymtech/nym/pull/4943 +[#4942]: https://github.com/nymtech/nym/pull/4942 +[#4938]: https://github.com/nymtech/nym/pull/4938 +[#4937]: https://github.com/nymtech/nym/pull/4937 +[#4936]: https://github.com/nymtech/nym/pull/4936 +[#4929]: https://github.com/nymtech/nym/pull/4929 +[#4903]: https://github.com/nymtech/nym/pull/4903 +[#4826]: https://github.com/nymtech/nym/pull/4826 +[#4758]: https://github.com/nymtech/nym/pull/4758 + ## [2024.12-aero] (2024-10-17) - nym-node: don't use bloomfilters for double spending checks ([#4960]) From b5f1d674feda0b8407a8b521259995855903f346 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Mon, 18 Nov 2024 14:07:01 +0100 Subject: [PATCH 197/207] update wallet versions and changelog --- nym-wallet/CHANGELOG.md | 10 ++++++++++ nym-wallet/package.json | 2 +- nym-wallet/src-tauri/Cargo.toml | 2 +- nym-wallet/src-tauri/tauri.conf.json | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/nym-wallet/CHANGELOG.md b/nym-wallet/CHANGELOG.md index 146e6174ed..aba29abef0 100644 --- a/nym-wallet/CHANGELOG.md +++ b/nym-wallet/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +## [2024.13-magura] (2024-11-18) + +- bugfix: [wallet] displaying delegations for native nymnodes ([#5087]) +- bugfix: wallet backend fixes ([#5070]) +- Feature/wallet bonding fixes ([#5064]) + +[#5087]: https://github.com/nymtech/nym/pull/5087 +[#5070]: https://github.com/nymtech/nym/pull/5070 +[#5064]: https://github.com/nymtech/nym/pull/5064 + ## [v1.2.13] (2024-05-08) - Bug fix: wallet delegations list is empty when RPC node doesn't hold block ([#4565]) diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 3d97eefd26..341da85fb1 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.2.14", + "version": "1.2.15", "license": "MIT", "main": "index.js", "scripts": { diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 88032e5efb..fcf0f6417e 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym_wallet" -version = "1.2.14" +version = "1.2.15" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index 0067868c37..e493efdfed 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-wallet", - "version": "1.2.14" + "version": "1.2.15" }, "build": { "distDir": "../dist", From 62e0771236cc84da789afa68f8d455ad410d23b5 Mon Sep 17 00:00:00 2001 From: Tommy Verrall <60836166+tommyv1987@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:24:52 +0100 Subject: [PATCH 198/207] Update publish-nym-contracts.yml --- .github/workflows/publish-nym-contracts.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-nym-contracts.yml b/.github/workflows/publish-nym-contracts.yml index 85f1363b9b..ed1141ddf1 100644 --- a/.github/workflows/publish-nym-contracts.yml +++ b/.github/workflows/publish-nym-contracts.yml @@ -14,13 +14,14 @@ jobs: - name: Install Rust stable uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: 1.77 target: wasm32-unknown-unknown override: true - components: rustfmt, clippy - name: Install wasm-opt - run: cargo install --version 0.114.0 wasm-opt + uses: ./.github/actions/install-wasm-opt + with: + version: '114' - name: Build release contracts run: make contracts From b49ef643df86f0c670672429812c632fbbaf6cf1 Mon Sep 17 00:00:00 2001 From: Tommy Verrall <60836166+tommyv1987@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:56:57 +0100 Subject: [PATCH 199/207] Update publish-nym-binaries.yml --- .github/workflows/publish-nym-binaries.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index e64825e425..b28f118ec7 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -55,6 +55,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + override: true - name: Build all binaries uses: actions-rs/cargo@v1 From 6b5b97199b26755114bd4b30743cafd9e5d45e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 19 Nov 2024 17:47:13 +0000 Subject: [PATCH 200/207] fix: tracing logger to output to stderr --- common/bin-common/src/logging/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/common/bin-common/src/logging/mod.rs b/common/bin-common/src/logging/mod.rs index 250fef793f..bd9a76374b 100644 --- a/common/bin-common/src/logging/mod.rs +++ b/common/bin-common/src/logging/mod.rs @@ -47,6 +47,7 @@ pub fn setup_logging() { #[cfg(feature = "basic_tracing")] pub fn setup_tracing_logger() { let log_builder = tracing_subscriber::fmt() + .with_writer(std::io::stderr) // Use a more compact, abbreviated log format .compact() // Display source code file paths From 119c36b0bbb444c93558ff6e5d687908e7c8b56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 21 Nov 2024 18:21:01 +0000 Subject: [PATCH 201/207] added 'deterministic_route_selection' for sphinx packet route selection (#5157) * added 'deterministic_route_selection' for sphinx packet route selection * clippy + wasm * Switch to ChaCha8Rng for deterministic route generation --------- Co-authored-by: durch --- common/client-core/config-types/src/lib.rs | 6 ++- common/client-core/config-types/src/old/v5.rs | 1 + .../real_messages_control/message_handler.rs | 6 +++ .../src/client/real_messages_control/mod.rs | 1 + common/node-tester-utils/src/tester.rs | 18 +++++++-- common/nymsphinx/src/preparer/mod.rs | 38 ++++++++++++++----- common/wasm/client-core/src/config/mod.rs | 5 +++ .../wasm/client-core/src/config/override.rs | 7 ++++ .../src/network_monitor/monitor/preparer.rs | 1 + nym-wallet/Cargo.lock | 2 +- wasm/node-tester/src/tester.rs | 1 + 11 files changed, 70 insertions(+), 16 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 4eaba838df..445c071e77 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -381,13 +381,16 @@ pub struct Traffic { /// poisson distribution. pub disable_main_poisson_packet_distribution: bool, + /// Specify whether route selection should be determined by the packet header. + pub deterministic_route_selection: bool, + /// Specifies the packet size used for sent messages. /// Do not override it unless you understand the consequences of that change. pub primary_packet_size: PacketSize, /// Specifies the optional auxiliary packet size for optimizing message streams. /// Note that its use decreases overall anonymity. - /// Do not set it it unless you understand the consequences of that change. + /// Do not set it unless you understand the consequences of that change. pub secondary_packet_size: Option, pub packet_type: PacketType, @@ -412,6 +415,7 @@ impl Default for Traffic { average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, disable_main_poisson_packet_distribution: false, + deterministic_route_selection: false, primary_packet_size: PacketSize::RegularPacket, secondary_packet_size: None, packet_type: PacketType::Mix, diff --git a/common/client-core/config-types/src/old/v5.rs b/common/client-core/config-types/src/old/v5.rs index 0b4dab44c7..e6d4fa26ea 100644 --- a/common/client-core/config-types/src/old/v5.rs +++ b/common/client-core/config-types/src/old/v5.rs @@ -111,6 +111,7 @@ impl From for Config { primary_packet_size: value.debug.traffic.primary_packet_size, secondary_packet_size: value.debug.traffic.secondary_packet_size, packet_type: value.debug.traffic.packet_type, + ..Default::default() }, cover_traffic: CoverTraffic { loop_cover_traffic_average_delay: value diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 8262e5b996..3d37ccca4d 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -91,6 +91,9 @@ pub(crate) struct Config { /// and surb-based are going to be sent. sender_address: Recipient, + /// Specify whether route selection should be determined by the packet header. + deterministic_route_selection: bool, + /// Average delay a data packet is going to get delay at a single mixnode. average_packet_delay: Duration, @@ -114,10 +117,12 @@ impl Config { sender_address: Recipient, average_packet_delay: Duration, average_ack_delay: Duration, + deterministic_route_selection: bool, ) -> Self { Config { ack_key, sender_address, + deterministic_route_selection, average_packet_delay, average_ack_delay, num_mix_hops: DEFAULT_NUM_MIX_HOPS, @@ -176,6 +181,7 @@ where { let message_preparer = MessagePreparer::new( rng, + config.deterministic_route_selection, config.sender_address, config.average_packet_delay, config.average_ack_delay, diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index d4914f1440..41b3c8df06 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -97,6 +97,7 @@ impl<'a> From<&'a Config> for message_handler::Config { cfg.self_recipient, cfg.traffic.average_packet_delay, cfg.acks.average_ack_delay, + cfg.traffic.deterministic_route_selection, ) .with_custom_primary_packet_size(cfg.traffic.primary_packet_size) .with_custom_secondary_packet_size(cfg.traffic.secondary_packet_size) diff --git a/common/node-tester-utils/src/tester.rs b/common/node-tester-utils/src/tester.rs index 2697c60831..d60ec55e67 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -29,6 +29,9 @@ pub struct NodeTester { packet_size: PacketSize, + /// Specify whether route selection should be determined by the packet header. + deterministic_route_selection: bool, + /// Average delay a data packet is going to get delay at a single mixnode. average_packet_delay: Duration, @@ -48,11 +51,13 @@ impl NodeTester where R: Rng + CryptoRng, { + #[allow(clippy::too_many_arguments)] pub fn new( rng: R, base_topology: NymTopology, self_address: Option, packet_size: PacketSize, + deterministic_route_selection: bool, average_packet_delay: Duration, average_ack_delay: Duration, ack_key: Arc, @@ -62,6 +67,7 @@ where base_topology, self_address, packet_size, + deterministic_route_selection, average_packet_delay, average_ack_delay, num_mix_hops: DEFAULT_NUM_MIX_HOPS, @@ -289,10 +295,18 @@ where impl FragmentPreparer for NodeTester { type Rng = R; + fn deterministic_route_selection(&self) -> bool { + self.deterministic_route_selection + } + fn rng(&mut self) -> &mut Self::Rng { &mut self.rng } + fn nonce(&self) -> i32 { + 1 + } + fn num_mix_hops(&self) -> u8 { self.num_mix_hops } @@ -304,8 +318,4 @@ impl FragmentPreparer for NodeTester { fn average_ack_delay(&self) -> Duration { self.average_ack_delay } - - fn nonce(&self) -> i32 { - 1 - } } diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index bdd9102d3c..8578b655b0 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -18,7 +18,7 @@ use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX use nym_sphinx_types::{Delay, NymPacket}; use nym_topology::{NymTopology, NymTopologyError}; use rand::{CryptoRng, Rng, SeedableRng}; -use rand_chacha::ChaCha20Rng; +use rand_chacha::ChaCha8Rng; use nym_sphinx_chunking::monitoring; use std::time::Duration; @@ -51,6 +51,7 @@ impl From for MixPacket { pub trait FragmentPreparer { type Rng: CryptoRng + Rng; + fn deterministic_route_selection(&self) -> bool; fn rng(&mut self) -> &mut Self::Rng; fn nonce(&self) -> i32; fn num_mix_hops(&self) -> u8; @@ -201,9 +202,7 @@ pub trait FragmentPreparer { // could perform diffie-hellman with its own keys followed by a kdf to re-derive // the packet encryption key - let seed = fragment.seed().wrapping_mul(self.nonce()); - let mut rng = ChaCha20Rng::seed_from_u64(seed as u64); - + let fragment_header = fragment.header(); let destination = packet_recipient.gateway(); let hops = mix_hops.unwrap_or(self.num_mix_hops()); monitoring::fragment_sent(&fragment, self.nonce(), *destination, hops); @@ -241,8 +240,18 @@ pub trait FragmentPreparer { }; // generate pseudorandom route for the packet - log::trace!("Preparing chunk for sending with {} mix hops", hops); - let route = topology.random_route_to_gateway(&mut rng, hops, destination)?; + log::trace!("Preparing chunk for sending with {hops} mix hops"); + let route = if self.deterministic_route_selection() { + log::trace!("using deterministic route selection"); + let seed = fragment_header.seed().wrapping_mul(self.nonce()); + let mut rng = ChaCha8Rng::seed_from_u64(seed as u64); + topology.random_route_to_gateway(&mut rng, hops, destination)? + } else { + log::trace!("using pseudorandom route selection"); + let mut rng = self.rng(); + topology.random_route_to_gateway(&mut rng, hops, destination)? + }; + let destination = packet_recipient.as_sphinx_destination(); // including set of delays @@ -313,6 +322,9 @@ pub struct MessagePreparer { /// Instance of a cryptographically secure random number generator. rng: R, + /// Specify whether route selection should be determined by the packet header. + deterministic_route_selection: bool, + /// Address of this client which also represent an address to which all acknowledgements /// and surb-based are going to be sent. sender_address: Recipient, @@ -336,6 +348,7 @@ where { pub fn new( rng: R, + deterministic_route_selection: bool, sender_address: Recipient, average_packet_delay: Duration, average_ack_delay: Duration, @@ -344,6 +357,7 @@ where let nonce = rng.gen(); MessagePreparer { rng, + deterministic_route_selection, sender_address, average_packet_delay, average_ack_delay, @@ -457,10 +471,18 @@ where impl FragmentPreparer for MessagePreparer { type Rng = R; + fn deterministic_route_selection(&self) -> bool { + self.deterministic_route_selection + } + fn rng(&mut self) -> &mut Self::Rng { &mut self.rng } + fn nonce(&self) -> i32 { + self.nonce + } + fn num_mix_hops(&self) -> u8 { self.num_mix_hops } @@ -472,10 +494,6 @@ impl FragmentPreparer for MessagePreparer { fn average_ack_delay(&self) -> Duration { self.average_ack_delay } - - fn nonce(&self) -> i32 { - self.nonce - } } /* diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index eb69a735bc..81afb1273e 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -156,6 +156,9 @@ pub struct TrafficWasm { /// a loop cover message is sent instead in order to preserve the rate. pub message_sending_average_delay_ms: u32, + /// Specify whether route selection should be determined by the packet header. + pub deterministic_route_selection: bool, + /// Controls whether the main packet stream constantly produces packets according to the predefined /// poisson distribution. pub disable_main_poisson_packet_distribution: bool, @@ -190,6 +193,7 @@ impl From for ConfigTraffic { message_sending_average_delay: Duration::from_millis( traffic.message_sending_average_delay_ms as u64, ), + deterministic_route_selection: traffic.deterministic_route_selection, disable_main_poisson_packet_distribution: traffic .disable_main_poisson_packet_distribution, primary_packet_size: PacketSize::RegularPacket, @@ -205,6 +209,7 @@ impl From for TrafficWasm { average_packet_delay_ms: traffic.average_packet_delay.as_millis() as u32, message_sending_average_delay_ms: traffic.message_sending_average_delay.as_millis() as u32, + deterministic_route_selection: traffic.deterministic_route_selection, disable_main_poisson_packet_distribution: traffic .disable_main_poisson_packet_distribution, use_extended_packet_size: traffic.secondary_packet_size.is_some(), diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index 91fa4ec20e..fe884a7dda 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -83,6 +83,10 @@ pub struct TrafficWasmOverride { #[tsify(optional)] pub message_sending_average_delay_ms: Option, + /// Specify whether route selection should be determined by the packet header. + #[tsify(optional)] + pub deterministic_route_selection: Option, + /// Controls whether the main packet stream constantly produces packets according to the predefined /// poisson distribution. #[tsify(optional)] @@ -108,6 +112,9 @@ impl From for TrafficWasm { message_sending_average_delay_ms: value .message_sending_average_delay_ms .unwrap_or(def.message_sending_average_delay_ms), + deterministic_route_selection: value + .deterministic_route_selection + .unwrap_or(def.deterministic_route_selection), disable_main_poisson_packet_distribution: value .disable_main_poisson_packet_distribution .unwrap_or(def.disable_main_poisson_packet_distribution), diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 8650cf257f..04fdceebef 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -121,6 +121,7 @@ impl PacketPreparer { test_route.topology().clone(), self_address, PacketSize::RegularPacket, + false, DEFAULT_AVERAGE_PACKET_DELAY, DEFAULT_AVERAGE_ACK_DELAY, self.ack_key.clone(), diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 159d861a3f..0d1cedc3b2 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3518,7 +3518,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.2.14" +version = "1.2.15" dependencies = [ "async-trait", "base64 0.13.1", diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index 5d4958fa9f..008e090a5a 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -229,6 +229,7 @@ impl NymNodeTesterBuilder { self.base_topology, Some(address(&managed_keys, gateway_identity)), PacketSize::default(), + false, Duration::from_millis(5), Duration::from_millis(5), managed_keys.ack_key(), From 3d704fbbf171fc74f9268708ad1ef04b4d848f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 21 Nov 2024 18:54:14 +0000 Subject: [PATCH 202/207] change: make nym-api optionally ignore nodes with illegal ip addresses, like loopback (#5159) --- nym-api/src/ecash/tests/mod.rs | 4 +- nym-api/src/node_describe_cache/mod.rs | 49 ++++++++++++++----- nym-api/src/nym_nodes/handlers/mod.rs | 3 +- nym-api/src/support/cli/run.rs | 8 ++- nym-api/src/support/config/mod.rs | 3 ++ nym-api/src/support/http/state.rs | 10 +++- .../src/api/v1/node/models.rs | 11 +++++ 7 files changed, 70 insertions(+), 18 deletions(-) diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 3e81637322..4ffe0b4e5f 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -13,7 +13,7 @@ use crate::node_status_api::handlers::unstable; use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; -use crate::support::http::state::AppState; +use crate::support::http::state::{AppState, ForcedRefresh}; use crate::support::storage::NymApiStorage; use async_trait::async_trait; use axum::Router; @@ -1261,7 +1261,7 @@ struct TestFixture { impl TestFixture { fn build_app_state(storage: NymApiStorage) -> AppState { AppState { - forced_refresh: Default::default(), + forced_refresh: ForcedRefresh::new(true), nym_contract_cache: NymContractCache::new(), node_status_cache: NodeStatusCache::new(), circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index aa80d831ab..adf25c29f1 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -57,6 +57,9 @@ pub enum NodeDescribeCacheError { // TODO: perhaps include more details here like whether key/signature/payload was malformed #[error("could not verify signed host information for node {node_id}")] MissignedHostInformation { node_id: NodeId }, + + #[error("node {node_id} is announcing an illegal ip address")] + IllegalIpAddress { node_id: NodeId }, } // this exists because I've been moving things around quite a lot and now the place that holds the type @@ -199,13 +202,18 @@ impl DescribedNodes { pub struct NodeDescriptionProvider { contract_cache: NymContractCache, + allow_all_ips: bool, batch_size: usize, } impl NodeDescriptionProvider { - pub(crate) fn new(contract_cache: NymContractCache) -> NodeDescriptionProvider { + pub(crate) fn new( + contract_cache: NymContractCache, + allow_all_ips: bool, + ) -> NodeDescriptionProvider { NodeDescriptionProvider { contract_cache, + allow_all_ips, batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, } } @@ -270,6 +278,7 @@ async fn try_get_client( async fn try_get_description( data: RefreshData, + allow_all_ips: bool, ) -> Result { let client = try_get_client(&data.host, data.node_id, data.port).await?; @@ -286,6 +295,12 @@ async fn try_get_description( }); } + if !allow_all_ips && !host_info.data.check_ips() { + return Err(NodeDescribeCacheError::IllegalIpAddress { + node_id: data.node_id, + }); + } + let node_info = query_for_described_data(&client, data.node_id).await?; let description = node_info.into_node_description(host_info.data); @@ -357,8 +372,8 @@ impl RefreshData { self.node_id } - pub(crate) async fn try_refresh(self) -> Option { - match try_get_description(self).await { + pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option { + match try_get_description(self, allow_all_ips).await { Ok(description) => Some(description), Err(err) => { debug!("failed to obtain node self-described data: {err}"); @@ -412,11 +427,15 @@ impl CacheItemProvider for NodeDescriptionProvider { } } - let nodes = stream::iter(nodes_to_query.into_iter().map(|n| n.try_refresh())) - .buffer_unordered(self.batch_size) - .filter_map(|x| async move { x.map(|d| (d.node_id, d)) }) - .collect::>() - .await; + let nodes = stream::iter( + nodes_to_query + .into_iter() + .map(|n| n.try_refresh(self.allow_all_ips)), + ) + .buffer_unordered(self.batch_size) + .filter_map(|x| async move { x.map(|d| (d.node_id, d)) }) + .collect::>() + .await; info!("refreshed self described data for {} nodes", nodes.len()); @@ -432,8 +451,11 @@ pub(crate) fn new_refresher( ) -> CacheRefresher { CacheRefresher::new( Box::new( - NodeDescriptionProvider::new(contract_cache) - .with_batch_size(config.debug.node_describe_batch_size), + NodeDescriptionProvider::new( + contract_cache, + config.debug.node_describe_allow_illegal_ips, + ) + .with_batch_size(config.debug.node_describe_batch_size), ), config.debug.node_describe_caching_interval, ) @@ -446,8 +468,11 @@ pub(crate) fn new_refresher_with_initial_value( ) -> CacheRefresher { CacheRefresher::new_with_initial_value( Box::new( - NodeDescriptionProvider::new(contract_cache) - .with_batch_size(config.debug.node_describe_batch_size), + NodeDescriptionProvider::new( + contract_cache, + config.debug.node_describe_allow_illegal_ips, + ) + .with_batch_size(config.debug.node_describe_batch_size), ), config.debug.node_describe_caching_interval, initial, diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 47530a6c5c..42cfee3c86 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -86,8 +86,9 @@ async fn refresh_described( } // to make sure you can't ddos the endpoint while a request is in progress state.forced_refresh.set_last_refreshed(node_id).await; + let allow_all_ips = state.forced_refresh.allow_all_ip_addresses; - if let Some(updated_data) = refresh_data.try_refresh().await { + if let Some(updated_data) = refresh_data.try_refresh(allow_all_ips).await { let Ok(mut describe_cache) = state.described_nodes_cache.write().await else { return Err(AxumErrorResponse::service_unavailable()); }; diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 493def1c5a..fa08abe376 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -21,7 +21,9 @@ use crate::status::{ApiStatusState, SignerState}; use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; use crate::support::config::Config; -use crate::support::http::state::{AppState, ShutdownHandles, TASK_MANAGER_TIMEOUT_S}; +use crate::support::http::state::{ + AppState, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S, +}; use crate::support::http::RouterBuilder; use crate::support::nyxd; use crate::support::storage::NymApiStorage; @@ -188,7 +190,9 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result>>, } impl ForcedRefresh { + pub(crate) fn new(allow_all_ip_addresses: bool) -> ForcedRefresh { + ForcedRefresh { + allow_all_ip_addresses, + refreshes: Arc::new(Default::default()), + } + } + pub(crate) async fn last_refreshed(&self, node_id: NodeId) -> Option { self.refreshes.read().await.get(&node_id).copied() } diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 6979beb949..d0f1e3df03 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -59,6 +59,17 @@ pub struct HostInformation { pub keys: HostKeys, } +impl HostInformation { + pub fn check_ips(&self) -> bool { + for ip in &self.ip_address { + if ip.is_unspecified() || ip.is_loopback() || ip.is_multicast() { + return false; + } + } + true + } +} + #[derive(Serialize)] pub struct LegacyHostInformationV2 { pub ip_address: Vec, From d6d22396859f9f456134fcf5f9aa462c327580cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 21 Nov 2024 18:57:24 +0000 Subject: [PATCH 203/207] implement optional cap for number of ack retransmissions (#5158) * implement optional cap for number of ack retransmissions * Wasm --- common/client-core/config-types/src/lib.rs | 5 +++++ .../action_controller.rs | 21 +++++++++++-------- .../acknowledgement_control/mod.rs | 17 +++++++++++++-- .../retransmission_request_listener.rs | 19 ++++++++++++++--- .../real_messages_control/message_handler.rs | 2 +- .../src/client/real_messages_control/mod.rs | 1 + common/wasm/client-core/src/config/mod.rs | 6 ++++++ .../wasm/client-core/src/config/override.rs | 5 +++++ 8 files changed, 61 insertions(+), 15 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 445c071e77..508bc2fca0 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -384,6 +384,10 @@ pub struct Traffic { /// Specify whether route selection should be determined by the packet header. pub deterministic_route_selection: bool, + /// Specify how many times particular packet can be retransmitted + /// None - no limit + pub maximum_number_of_retransmissions: Option, + /// Specifies the packet size used for sent messages. /// Do not override it unless you understand the consequences of that change. pub primary_packet_size: PacketSize, @@ -416,6 +420,7 @@ impl Default for Traffic { message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, disable_main_poisson_packet_distribution: false, deterministic_route_selection: false, + maximum_number_of_retransmissions: None, primary_packet_size: PacketSize::RegularPacket, secondary_packet_size: None, packet_type: PacketType::Mix, diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index cb50d4ba09..6f6753c35f 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -30,7 +30,8 @@ pub(crate) enum Action { InsertPending(Vec), /// Removes given `PendingAcknowledgement` from the 'shared' state. Also cancels the retransmission timer. - /// Initiated by `AcknowledgementListener` + /// Initiated by `AcknowledgementListener` upon receiving the acknowledgement. Also by `RetransmissionRequestListener` + /// upon deciding to abandon the data. RemovePending(FragmentIdentifier), /// Starts the retransmission timer on given `PendingAcknowledgement` with the `Duration` based on @@ -41,7 +42,7 @@ pub(crate) enum Action { /// Updates the expected delay of given `PendingAcknowledgement` with the new provided `SphinxDelay`. /// Initiated by `RetransmissionRequestListener` - UpdateDelay(FragmentIdentifier, SphinxDelay), + UpdatePendingAck(FragmentIdentifier, SphinxDelay), } impl Action { @@ -57,8 +58,8 @@ impl Action { Action::StartTimer(frag_id) } - pub(crate) fn new_update_delay(frag_id: FragmentIdentifier, delay: SphinxDelay) -> Self { - Action::UpdateDelay(frag_id, delay) + pub(crate) fn new_update_pending_ack(frag_id: FragmentIdentifier, delay: SphinxDelay) -> Self { + Action::UpdatePendingAck(frag_id, delay) } } @@ -135,7 +136,7 @@ impl ActionController { } fn handle_start_timer(&mut self, frag_id: FragmentIdentifier) { - trace!("{} is starting its timer", frag_id); + trace!("{frag_id} is starting its timer"); if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.get_mut(&frag_id) { // the fact that this branch is now POSSIBLE is a sign of a need to refactor this whole @@ -193,7 +194,7 @@ impl ActionController { // initiated basically as a first step of retransmission. At first data has its delay updated // (as new sphinx packet was created with new expected delivery time) - fn handle_update_delay(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { + fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { trace!("{} is updating its delay", frag_id); // TODO: is it possible to solve this without either locking or temporarily removing the value? if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) { @@ -202,7 +203,7 @@ impl ActionController { // reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference // was dropped hence this unwrap is safe. let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap(); - inner_data.update_delay(delay); + inner_data.update_retransmitted(delay); self.pending_acks_data .insert(frag_id, (Arc::new(inner_data), queue_key)); @@ -225,7 +226,7 @@ impl ActionController { // about it. Perhaps just reschedule it at later point? let frag_id = expired_ack.into_inner(); - trace!("{} has expired", frag_id); + trace!("{frag_id} has expired"); if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.get_mut(&frag_id) { if queue_key.is_none() { @@ -258,7 +259,9 @@ impl ActionController { Action::InsertPending(pending_acks) => self.handle_insert(pending_acks), Action::RemovePending(frag_id) => self.handle_remove(frag_id), Action::StartTimer(frag_id) => self.handle_start_timer(frag_id), - Action::UpdateDelay(frag_id, delay) => self.handle_update_delay(frag_id, delay), + Action::UpdatePendingAck(frag_id, delay) => { + self.handle_update_pending_ack(frag_id, delay) + } } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index ec35429b9f..e3810a91ab 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -71,6 +71,7 @@ pub(crate) struct PendingAcknowledgement { delay: SphinxDelay, destination: PacketDestination, mix_hops: Option, + retransmissions: u32, } impl PendingAcknowledgement { @@ -86,6 +87,7 @@ impl PendingAcknowledgement { delay, destination: PacketDestination::KnownRecipient(recipient.into()), mix_hops, + retransmissions: 0, } } @@ -105,6 +107,7 @@ impl PendingAcknowledgement { // Messages sent using SURBs are using the number of mix hops set by the recipient when // they provided the SURBs, so it doesn't make sense to include it here. mix_hops: None, + retransmissions: 0, } } @@ -116,8 +119,9 @@ impl PendingAcknowledgement { self.message_chunk.clone() } - fn update_delay(&mut self, new_delay: SphinxDelay) { + fn update_retransmitted(&mut self, new_delay: SphinxDelay) { self.delay = new_delay; + self.retransmissions += 1; } } @@ -163,6 +167,9 @@ impl AcknowledgementControllerConnectors { /// Configurable parameters of the `AcknowledgementController` pub(super) struct Config { + /// Specify how many times particular packet can be retransmitted + maximum_retransmissions: Option, + /// Given ack timeout in the form a * BASE_DELAY + b, it specifies the additive part `b` ack_wait_addition: Duration, @@ -174,8 +181,13 @@ pub(super) struct Config { } impl Config { - pub(super) fn new(ack_wait_addition: Duration, ack_wait_multiplier: f64) -> Self { + pub(super) fn new( + maximum_retransmissions: Option, + ack_wait_addition: Duration, + ack_wait_multiplier: f64, + ) -> Self { Config { + maximum_retransmissions, ack_wait_addition, ack_wait_multiplier, packet_size: Default::default(), @@ -238,6 +250,7 @@ where // will listen for any ack timeouts and trigger retransmission let retransmission_request_listener = RetransmissionRequestListener::new( + config.maximum_retransmissions, connectors.ack_action_sender.clone(), message_handler, retransmission_rx, diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 1d305ff5ff..6eda1c8adc 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -20,6 +20,7 @@ use std::sync::{Arc, Weak}; // responsible for packet retransmission upon fired timer pub(super) struct RetransmissionRequestListener { + maximum_retransmissions: Option, action_sender: AckActionSender, message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, @@ -31,12 +32,14 @@ where R: CryptoRng + Rng, { pub(super) fn new( + maximum_retransmissions: Option, action_sender: AckActionSender, message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, reply_controller_sender: ReplyControllerSender, ) -> Self { RetransmissionRequestListener { + maximum_retransmissions, action_sender, message_handler, request_receiver, @@ -77,6 +80,18 @@ where } }; + let frag_id = timed_out_ack.message_chunk.fragment_identifier(); + + if let Some(limit) = self.maximum_retransmissions { + if timed_out_ack.retransmissions >= limit { + warn!("reached maximum number of allowed retransmissions for the packet"); + self.action_sender + .unbounded_send(Action::new_remove(frag_id)) + .unwrap(); + return; + } + } + let maybe_prepared_fragment = match &timed_out_ack.destination { PacketDestination::Anonymous { recipient_tag, @@ -101,8 +116,6 @@ where } }; - let frag_id = timed_out_ack.message_chunk.fragment_identifier(); - let prepared_fragment = match maybe_prepared_fragment { Ok(prepared_fragment) => prepared_fragment, Err(err) => { @@ -136,7 +149,7 @@ where // with the additional poisson delay. // And since Actions are executed in order `UpdateTimer` will HAVE TO be executed before `StartTimer` self.action_sender - .unbounded_send(Action::new_update_delay(frag_id, new_delay)) + .unbounded_send(Action::new_update_pending_ack(frag_id, new_delay)) .unwrap(); // send to `OutQueueControl` to eventually send to the mix network diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 3d37ccca4d..75cca06f76 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -640,7 +640,7 @@ where pub(crate) fn update_ack_delay(&self, id: FragmentIdentifier, new_delay: Delay) { self.action_sender - .unbounded_send(Action::UpdateDelay(id, new_delay)) + .unbounded_send(Action::UpdatePendingAck(id, new_delay)) .expect("action control task has died") } diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index 41b3c8df06..fa7da0f99b 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -65,6 +65,7 @@ pub struct Config { impl<'a> From<&'a Config> for acknowledgement_control::Config { fn from(cfg: &'a Config) -> Self { acknowledgement_control::Config::new( + cfg.traffic.maximum_number_of_retransmissions, cfg.acks.ack_wait_addition, cfg.acks.ack_wait_multiplier, ) diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index 81afb1273e..05fb1cfe9a 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -156,6 +156,10 @@ pub struct TrafficWasm { /// a loop cover message is sent instead in order to preserve the rate. pub message_sending_average_delay_ms: u32, + /// Specify how many times particular packet can be retransmitted + /// None - no limit + pub maximum_number_of_retransmissions: Option, + /// Specify whether route selection should be determined by the packet header. pub deterministic_route_selection: bool, @@ -194,6 +198,7 @@ impl From for ConfigTraffic { traffic.message_sending_average_delay_ms as u64, ), deterministic_route_selection: traffic.deterministic_route_selection, + maximum_number_of_retransmissions: traffic.maximum_number_of_retransmissions, disable_main_poisson_packet_distribution: traffic .disable_main_poisson_packet_distribution, primary_packet_size: PacketSize::RegularPacket, @@ -210,6 +215,7 @@ impl From for TrafficWasm { message_sending_average_delay_ms: traffic.message_sending_average_delay.as_millis() as u32, deterministic_route_selection: traffic.deterministic_route_selection, + maximum_number_of_retransmissions: traffic.maximum_number_of_retransmissions, disable_main_poisson_packet_distribution: traffic .disable_main_poisson_packet_distribution, use_extended_packet_size: traffic.secondary_packet_size.is_some(), diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index fe884a7dda..75d4228e4d 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -83,6 +83,10 @@ pub struct TrafficWasmOverride { #[tsify(optional)] pub message_sending_average_delay_ms: Option, + /// Specify how many times particular packet can be retransmitted + #[tsify(optional)] + pub maximum_number_of_retransmissions: Option, + /// Specify whether route selection should be determined by the packet header. #[tsify(optional)] pub deterministic_route_selection: Option, @@ -112,6 +116,7 @@ impl From for TrafficWasm { message_sending_average_delay_ms: value .message_sending_average_delay_ms .unwrap_or(def.message_sending_average_delay_ms), + maximum_number_of_retransmissions: value.maximum_number_of_retransmissions, deterministic_route_selection: value .deterministic_route_selection .unwrap_or(def.deterministic_route_selection), From d8ab2a8f15d8116a742f71a7e938bae2d505992a Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Fri, 22 Nov 2024 07:25:54 +0100 Subject: [PATCH 204/207] update version for clients and nym-node --- Cargo.lock | 8 ++++---- clients/native/Cargo.toml | 2 +- clients/socks5/Cargo.toml | 2 +- nym-node/Cargo.toml | 2 +- service-providers/network-requester/Cargo.toml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99d53669da..e36482a2a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4773,7 +4773,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.43" +version = "1.1.44" dependencies = [ "bs58", "clap 4.5.18", @@ -5828,7 +5828,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.44" +version = "1.1.45" dependencies = [ "addr", "anyhow", @@ -5879,7 +5879,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.10" +version = "1.1.11" dependencies = [ "anyhow", "bip39", @@ -6225,7 +6225,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.43" +version = "1.1.44" dependencies = [ "bs58", "clap 4.5.18", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 9550c65b3f..c5679d02de 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.43" +version = "1.1.44" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 5243206efa..f5d3cd8967 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.43" +version = "1.1.44" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 3b2bf0cb87..07c8b33476 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.10" +version = "1.1.11" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 9ab6b58afd..a419abd4f6 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.44" +version = "1.1.45" authors.workspace = true edition.workspace = true rust-version = "1.70" From 042a8a58aa920bdaab8d4bd8ed261f963ab7965b Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 22 Nov 2024 09:12:01 +0100 Subject: [PATCH 205/207] start session collection for exit gateways (#5148) (#5161) --- nym-node/src/node/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index bd687ff0c3..e2abef5970 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -618,6 +618,7 @@ impl NymNode { ); exit_gateway.disable_http_server(); exit_gateway.set_task_client(task_client); + exit_gateway.set_session_stats(self.entry_gateway.sessions_stats.clone()); //Weird naming I'll give you that, but Andrew is gonna rework it anyway if self.config.wireguard.enabled { exit_gateway.set_wireguard_data(self.wireguard.into()); } From 01c7b2819ee3d328deccd303b4113ff415d7e276 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Fri, 22 Nov 2024 10:50:59 +0100 Subject: [PATCH 206/207] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5618fe59c8..46cb35531e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2024.13-magura-patched] (2024-11-22) + +- [experimental] allow clients to change between deterministic route selection based on packet headers and a pseudorandom distribution +- Introduced a configurable limit on retransmission frequency of packets if ACKs are not received +- Filtered out invalid IP addresses on nym-api + ## [2024.13-magura] (2024-11-18) - Limit race probability ([#5145]) From 6acd93636829fbc375c32bc90a07135a5347cd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 22 Nov 2024 15:52:26 +0000 Subject: [PATCH 207/207] bugfix: additional improvements to nym-api db performance (#5168) * added statement logging to nym-api db * adding additional pool options * dont blow up upon failing to submit network monitor results * store in-memory cache of node database ids --- Cargo.lock | 1 + nym-api/Cargo.toml | 1 + nym-api/src/network_monitor/monitor/mod.rs | 6 +- .../handlers/without_monitor.rs | 2 - nym-api/src/support/storage/manager.rs | 192 +++++++++--------- nym-api/src/support/storage/mod.rs | 113 ++++++++--- 6 files changed, 184 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e36482a2a3..3be4b4751b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4470,6 +4470,7 @@ dependencies = [ "cw2", "cw3", "cw4", + "dashmap", "dirs", "futures", "getset", diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index fa31615f70..fb8c58c9f1 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -20,6 +20,7 @@ bloomfilter = { workspace = true } cfg-if = { workspace = true } clap = { workspace = true, features = ["cargo", "derive", "env"] } console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" +dashmap = { workspace = true } dirs = { workspace = true } futures = { workspace = true } itertools = { workspace = true } diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 0f04d971c6..4d24cb0504 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -14,7 +14,6 @@ use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; use nym_task::TaskClient; use std::collections::{HashMap, HashSet}; -use std::process; use tokio::time::{sleep, Duration, Instant}; use tracing::{debug, error, info, trace}; @@ -95,10 +94,7 @@ impl Monitor { ) .await { - error!("Failed to submit monitor run information to the database - {err}",); - - // TODO: slightly more graceful shutdown here - process::exit(1); + error!("Failed to submit monitor run information to the database: {err}",); } } diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs index c981075a00..ef87fa025c 100644 --- a/nym-api/src/node_status_api/handlers/without_monitor.rs +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -89,7 +89,6 @@ pub(crate) async fn submit_gateway_monitoring_results( match state .storage - .manager .submit_gateway_statuses_v2(message.results()) .await { @@ -134,7 +133,6 @@ pub(crate) async fn submit_node_monitoring_results( match state .storage - .manager .submit_mixnode_statuses_v2(message.results()) .await { diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index 8a2959aed7..f34ddb4045 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -7,6 +7,7 @@ use crate::support::storage::models::{ ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, NodeStatus, RewardingReport, TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, }; +use crate::support::storage::DbIdCache; use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; use nym_types::monitoring::NodeResult; use sqlx::FromRow; @@ -51,24 +52,7 @@ impl AvgGatewayReliability { // all SQL goes here impl StorageManager { - pub(crate) async fn get_mixnode_mix_ids_by_identity( - &self, - identity: &str, - ) -> Result, sqlx::Error> { - let ids = sqlx::query!( - r#"SELECT mix_id as "mix_id: NodeId" FROM mixnode_details WHERE identity_key = ?"#, - identity - ) - .fetch_all(&self.connection_pool) - .await? - .into_iter() - .map(|row| row.mix_id) - .collect(); - - Ok(ids) - } - - pub(crate) async fn get_all_avg_mix_reliability_in_last_24hr( + pub(super) async fn get_all_avg_mix_reliability_in_last_24hr( &self, end_ts_secs: i64, ) -> Result, sqlx::Error> { @@ -77,7 +61,7 @@ impl StorageManager { .await } - pub(crate) async fn get_all_avg_gateway_reliability_in_last_24hr( + pub(super) async fn get_all_avg_gateway_reliability_in_last_24hr( &self, end_ts_secs: i64, ) -> Result, sqlx::Error> { @@ -86,7 +70,7 @@ impl StorageManager { .await } - pub(crate) async fn get_all_avg_mix_reliability_in_time_interval( + pub(super) async fn get_all_avg_mix_reliability_in_time_interval( &self, start_ts_secs: i64, end_ts_secs: i64, @@ -114,7 +98,7 @@ impl StorageManager { Ok(result) } - pub(crate) async fn get_all_avg_gateway_reliability_in_interval( + pub(super) async fn get_all_avg_gateway_reliability_in_interval( &self, start_ts_secs: i64, end_ts_secs: i64, @@ -147,7 +131,7 @@ impl StorageManager { /// # Arguments /// /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_database_id( + pub(super) async fn get_mixnode_database_id( &self, mix_id: NodeId, ) -> Result, sqlx::Error> { @@ -159,7 +143,7 @@ impl StorageManager { Ok(id) } - pub(crate) async fn get_gateway_database_id( + pub(super) async fn get_gateway_database_id( &self, node_id: NodeId, ) -> Result, sqlx::Error> { @@ -172,7 +156,7 @@ impl StorageManager { } /// Tries to obtain row id of given gateway given its identity - pub(crate) async fn get_gateway_database_id_by_identity( + pub(super) async fn get_gateway_database_id_by_identity( &self, identity: &str, ) -> Result, sqlx::Error> { @@ -187,7 +171,7 @@ impl StorageManager { Ok(id) } - pub(crate) async fn get_gateway_node_id_from_identity_key( + pub(super) async fn get_gateway_node_id_from_identity_key( &self, identity: &str, ) -> Result, sqlx::Error> { @@ -202,7 +186,7 @@ impl StorageManager { Ok(node_id) } - pub(crate) async fn get_gateway_identity_key( + pub(super) async fn get_gateway_identity_key( &self, node_id: NodeId, ) -> Result, sqlx::Error> { @@ -222,7 +206,7 @@ impl StorageManager { /// # Arguments /// /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_identity_key( + pub(super) async fn get_mixnode_identity_key( &self, mix_id: NodeId, ) -> Result, sqlx::Error> { @@ -244,7 +228,7 @@ impl StorageManager { /// /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(crate) async fn get_mixnode_statuses_since( + pub(super) async fn get_mixnode_statuses_since( &self, mix_id: NodeId, timestamp: i64, @@ -272,7 +256,7 @@ impl StorageManager { /// /// * `identity`: identity (base58-encoded public key) of the gateway. /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(crate) async fn get_gateway_statuses_since( + pub(super) async fn get_gateway_statuses_since( &self, node_id: NodeId, timestamp: i64, @@ -298,7 +282,7 @@ impl StorageManager { /// # Arguments /// /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_historical_uptimes( + pub(super) async fn get_mixnode_historical_uptimes( &self, mix_id: NodeId, ) -> Result, sqlx::Error> { @@ -336,7 +320,7 @@ impl StorageManager { /// # Arguments /// /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_historical_uptimes( + pub(super) async fn get_gateway_historical_uptimes( &self, node_id: NodeId, ) -> Result, sqlx::Error> { @@ -369,7 +353,7 @@ impl StorageManager { Ok(uptimes) } - pub(crate) async fn get_historical_mix_uptime_on( + pub(super) async fn get_historical_mix_uptime_on( &self, contract_node_id: i64, date: Date, @@ -393,7 +377,7 @@ impl StorageManager { .await } - pub(crate) async fn get_historical_gateway_uptime_on( + pub(super) async fn get_historical_gateway_uptime_on( &self, contract_node_id: i64, date: Date, @@ -424,7 +408,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_mixnode_statuses_by_database_id( + pub(super) async fn get_mixnode_statuses_by_database_id( &self, id: i64, since: i64, @@ -445,7 +429,7 @@ impl StorageManager { .await } - pub(crate) async fn get_mixnode_average_reliability_in_interval( + pub(super) async fn get_mixnode_average_reliability_in_interval( &self, id: i64, start: i64, @@ -507,7 +491,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_gateway_statuses_by_database_id( + pub(super) async fn get_gateway_statuses_by_database_id( &self, id: i64, since: i64, @@ -534,27 +518,36 @@ impl StorageManager { /// /// * `timestamp`: unix timestamp indicating when the measurements took place. /// * `mixnode_results`: reliability results of each node that got tested. - pub(crate) async fn submit_mixnode_statuses( + pub(super) async fn submit_mixnode_statuses( &self, timestamp: i64, mixnode_results: Vec, + id_cache: &DbIdCache, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) let mut tx = self.connection_pool.begin().await?; for mixnode_result in mixnode_results { - let mixnode_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key) VALUES (?, ?); - SELECT id FROM mixnode_details WHERE mix_id = ?; - "#, - mixnode_result.node_id, - mixnode_result.identity, - mixnode_result.node_id, - ) - .fetch_one(&mut *tx) - .await? - .id; + let mixnode_id = match id_cache.mixnode_db_id(mixnode_result.node_id) { + Some(id) => id, + None => { + let mixnode_id = sqlx::query!( + r#" + INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key) VALUES (?, ?); + SELECT id FROM mixnode_details WHERE mix_id = ?; + "#, + mixnode_result.node_id, + mixnode_result.identity, + mixnode_result.node_id, + ) + .fetch_one(&mut *tx) + .await? + .id; + id_cache.set_mixnode_db_id(mixnode_result.node_id, mixnode_id); + + mixnode_id + } + }; // insert the actual status sqlx::query!( @@ -573,7 +566,7 @@ impl StorageManager { tx.commit().await } - pub(crate) async fn submit_mixnode_statuses_v2( + pub(super) async fn submit_mixnode_statuses_v2( &self, mixnode_results: &[NodeResult], ) -> Result<(), sqlx::Error> { @@ -620,10 +613,11 @@ impl StorageManager { /// /// * `timestamp`: unix timestamp indicating when the measurements took place. /// * `gateway_results`: reliability results of each node that got tested. - pub(crate) async fn submit_gateway_statuses( + pub(super) async fn submit_gateway_statuses( &self, timestamp: i64, gateway_results: Vec, + id_cache: &DbIdCache, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) @@ -631,39 +625,45 @@ impl StorageManager { for gateway_result in gateway_results { // if gateway info doesn't exist, insert it and get its id - // same ID "problem" as described for mixnode insertion - let gateway_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO gateway_details(node_id, identity) VALUES (?, ?); - SELECT id FROM gateway_details WHERE identity = ?; - "#, - gateway_result.node_id, - gateway_result.identity, - gateway_result.identity, - ) - .fetch_one(&mut *tx) - .await? - .id; + let gateway_id = match id_cache.gateway_db_id(gateway_result.node_id) { + Some(id) => id, + None => { + let gateway_id = sqlx::query!( + r#" + INSERT OR IGNORE INTO gateway_details(node_id, identity) VALUES (?, ?); + SELECT id FROM gateway_details WHERE identity = ?; + "#, + gateway_result.node_id, + gateway_result.identity, + gateway_result.identity, + ) + .fetch_one(&mut *tx) + .await? + .id; + id_cache.set_gateway_db_id(gateway_result.node_id, gateway_id); + gateway_id + } + }; // insert the actual status sqlx::query!( - r#" - INSERT INTO gateway_status (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); - "#, - gateway_id, - gateway_result.reliability, - timestamp - ) - .execute(&mut *tx) - .await?; + r#" + INSERT INTO gateway_status (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); + "#, + gateway_id, + gateway_result.reliability, + timestamp + ) + .execute(&mut *tx) + .await?; } // finally commit the transaction tx.commit().await } - pub(crate) async fn submit_gateway_statuses_v2( + pub(super) async fn submit_gateway_statuses_v2( &self, gateway_results: &[NodeResult], ) -> Result<(), sqlx::Error> { @@ -714,7 +714,7 @@ impl StorageManager { /// # Arguments /// /// * `testing_route`: test route used for this particular network monitor run. - pub(crate) async fn submit_testing_route_used( + pub(super) async fn submit_testing_route_used( &self, testing_route: TestingRoute, ) -> Result<(), sqlx::Error> { @@ -742,7 +742,7 @@ impl StorageManager { /// /// * `db_mixnode_id`: id (as saved in the database) of the mixnode. /// * `since`: unix timestamp indicating the lower bound interval of the selection. - pub(crate) async fn get_mixnode_testing_route_presence_count_since( + pub(super) async fn get_mixnode_testing_route_presence_count_since( &self, db_mixnode_id: i64, since: i64, @@ -781,7 +781,7 @@ impl StorageManager { /// /// * `gateway_id`: id (as saved in the database) of the gateway. /// * `since`: unix timestamp indicating the lower bound interval of the selection. - pub(crate) async fn get_gateway_testing_route_presence_count_since( + pub(super) async fn get_gateway_testing_route_presence_count_since( &self, gateway_id: i64, since: i64, @@ -813,7 +813,7 @@ impl StorageManager { } /// Checks whether there are already any historical uptimes with this particular date. - pub(crate) async fn check_for_historical_uptime_existence( + pub(super) async fn check_for_historical_uptime_existence( &self, today_iso_8601: &str, ) -> Result { @@ -833,7 +833,7 @@ impl StorageManager { /// * `node_id`: id of the mixnode (as inserted in `mixnode_details_id` table). /// * `date`: date associated with the uptime represented in ISO 8601, i.e. YYYY-MM-DD. /// * `uptime`: the actual uptime of the node during the specified day. - pub(crate) async fn insert_mixnode_historical_uptime( + pub(super) async fn insert_mixnode_historical_uptime( &self, mix_id: i64, date: &str, @@ -855,7 +855,7 @@ impl StorageManager { /// * `node_id`: id of the gateway (as inserted in `gateway_details_id` table). /// * `date`: date associated with the uptime represented in ISO 8601, i.e. YYYY-MM-DD. /// * `uptime`: the actual uptime of the node during the specified day. - pub(crate) async fn insert_gateway_historical_uptime( + pub(super) async fn insert_gateway_historical_uptime( &self, db_id: i64, date: &str, @@ -876,7 +876,7 @@ impl StorageManager { /// # Arguments /// /// * `timestamp`: unix timestamp at which the monitor test run has occurred - pub(crate) async fn insert_monitor_run(&self, timestamp: i64) -> Result { + pub(super) async fn insert_monitor_run(&self, timestamp: i64) -> Result { let res = sqlx::query!("INSERT INTO monitor_run(timestamp) VALUES (?)", timestamp) .execute(&self.connection_pool) .await?; @@ -889,7 +889,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_monitor_runs_count( + pub(super) async fn get_monitor_runs_count( &self, since: i64, until: i64, @@ -911,7 +911,7 @@ impl StorageManager { /// # Arguments /// /// * `until`: timestamp specifying the purge cutoff. - pub(crate) async fn purge_old_mixnode_statuses( + pub(super) async fn purge_old_mixnode_statuses( &self, timestamp: i64, ) -> Result<(), sqlx::Error> { @@ -927,7 +927,7 @@ impl StorageManager { /// # Arguments /// /// * `until`: timestamp specifying the purge cutoff. - pub(crate) async fn purge_old_gateway_statuses( + pub(super) async fn purge_old_gateway_statuses( &self, timestamp: i64, ) -> Result<(), sqlx::Error> { @@ -944,7 +944,7 @@ impl StorageManager { /// /// * `since`: indicates the lower bound timestamp for deciding whether given mixnode is active /// * `until`: indicates the upper bound timestamp for deciding whether given mixnode is active - pub(crate) async fn get_all_active_mixnodes_in_interval( + pub(super) async fn get_all_active_mixnodes_in_interval( &self, since: i64, until: i64, @@ -978,7 +978,7 @@ impl StorageManager { /// /// * `since`: indicates the lower bound timestamp for deciding whether given gateway is active /// * `until`: indicates the upper bound timestamp for deciding whether given gateway is active - pub(crate) async fn get_all_active_gateways_in_interval( + pub(super) async fn get_all_active_gateways_in_interval( &self, since: i64, until: i64, @@ -1025,7 +1025,7 @@ impl StorageManager { /// /// * `report`: report to insert into the database #[allow(unused)] - pub(crate) async fn insert_rewarding_report( + pub(super) async fn insert_rewarding_report( &self, report: RewardingReport, ) -> Result<(), sqlx::Error> { @@ -1044,7 +1044,7 @@ impl StorageManager { } #[allow(unused)] - pub(crate) async fn get_rewarding_report( + pub(super) async fn get_rewarding_report( &self, absolute_epoch_id: EpochId, ) -> Result, sqlx::Error> { @@ -1069,7 +1069,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_all_active_mixnodes_statuses_in_interval( + pub(super) async fn get_all_active_mixnodes_statuses_in_interval( &self, since: i64, until: i64, @@ -1102,7 +1102,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_all_active_gateways_statuses_in_interval( + pub(super) async fn get_all_active_gateways_statuses_in_interval( &self, since: i64, until: i64, @@ -1129,7 +1129,7 @@ impl StorageManager { Ok(active_day_statuses) } - pub(crate) async fn get_mixnode_details_by_db_id( + pub(super) async fn get_mixnode_details_by_db_id( &self, id: i64, ) -> Result, sqlx::Error> { @@ -1142,7 +1142,7 @@ impl StorageManager { .await } - pub(crate) async fn get_gateway_details_by_db_id( + pub(super) async fn get_gateway_details_by_db_id( &self, id: i64, ) -> Result, sqlx::Error> { @@ -1154,7 +1154,7 @@ impl StorageManager { .await } - pub(crate) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result { + pub(super) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result { sqlx::query!( r#" SELECT COUNT(*) as count @@ -1170,7 +1170,7 @@ impl StorageManager { .map(|record| record.count) } - pub(crate) async fn get_mixnode_statuses( + pub(super) async fn get_mixnode_statuses( &self, mix_id: NodeId, limit: u32, @@ -1206,7 +1206,7 @@ impl StorageManager { .await } - pub(crate) async fn get_gateway_statuses_count(&self, db_id: i64) -> Result { + pub(super) async fn get_gateway_statuses_count(&self, db_id: i64) -> Result { sqlx::query!( r#" SELECT COUNT(*) as count @@ -1222,7 +1222,7 @@ impl StorageManager { .map(|record| record.count) } - pub(crate) async fn get_gateway_statuses( + pub(super) async fn get_gateway_statuses( &self, gateway_identity: &str, limit: u32, diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index 60235f0a51..26accca24e 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime as ApiHistoricalUptime, @@ -12,36 +13,70 @@ use crate::storage::models::{NodeStatus, TestingRoute}; use crate::support::storage::models::{ GatewayDetails, HistoricalUptime, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, }; +use dashmap::DashMap; use nym_mixnet_contract_common::NodeId; use nym_types::monitoring::NodeResult; use sqlx::ConnectOptions; use std::path::Path; +use std::sync::Arc; +use std::time::Duration; use time::{Date, OffsetDateTime}; +use tracing::log::LevelFilter; use tracing::{error, info, warn}; -use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; - pub(crate) mod manager; pub(crate) mod models; +#[derive(Default)] +pub(crate) struct DbIdCache { + pub mixnodes_v1: DashMap, + pub gateways_v1: DashMap, +} + +impl DbIdCache { + pub(crate) fn mixnode_db_id(&self, node_id: NodeId) -> Option { + self.mixnodes_v1.get(&node_id).map(|v| *v) + } + + pub(crate) fn gateway_db_id(&self, node_id: NodeId) -> Option { + self.gateways_v1.get(&node_id).map(|v| *v) + } + + pub(crate) fn set_mixnode_db_id(&self, node_id: NodeId, db_id: i64) { + self.mixnodes_v1.insert(node_id, db_id); + } + + pub(crate) fn set_gateway_db_id(&self, node_id: NodeId, db_id: i64) { + self.gateways_v1.insert(node_id, db_id); + } +} + // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] pub(crate) struct NymApiStorage { pub manager: StorageManager, + + pub db_id_cache: Arc, } impl NymApiStorage { pub async fn init>(database_path: P) -> Result { // TODO: we can inject here more stuff based on our nym-api global config // struct. Maybe different pool size or timeout intervals? - let opts = sqlx::sqlite::SqliteConnectOptions::new() + let connect_opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) .create_if_missing(true) - .disable_statement_logging(); + .log_statements(LevelFilter::Trace) + .log_slow_statements(LevelFilter::Warn, Duration::from_millis(250)); // TODO: do we want auto_vacuum ? - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + let pool_opts = sqlx::sqlite::SqlitePoolOptions::new() + .min_connections(5) + .max_connections(25) + .acquire_timeout(Duration::from_secs(60)); + + let connection_pool = match pool_opts.connect_with(connect_opts).await { Ok(db) => db, Err(err) => { error!("Failed to connect to SQLx database: {err}"); @@ -58,32 +93,38 @@ impl NymApiStorage { let storage = NymApiStorage { manager: StorageManager { connection_pool }, + db_id_cache: Arc::new(Default::default()), }; Ok(storage) } - #[allow(unused)] - pub(crate) async fn mix_identity_to_mix_ids( + pub(crate) async fn get_mixnode_database_id( &self, - identity: &str, - ) -> Result, NymApiStorageError> { - Ok(self - .manager - .get_mixnode_mix_ids_by_identity(identity) - .await?) + node_id: NodeId, + ) -> Result, NymApiStorageError> { + if let Some(cached) = self.db_id_cache.mixnode_db_id(node_id) { + return Ok(Some(cached)); + } + if let Some(retrieved) = self.manager.get_mixnode_database_id(node_id).await? { + self.db_id_cache.set_mixnode_db_id(node_id, retrieved); + return Ok(Some(retrieved)); + } + Ok(None) } - #[allow(unused)] - pub(crate) async fn mix_identity_to_latest_mix_id( + pub(crate) async fn get_gateway_database_id( &self, - identity: &str, - ) -> Result, NymApiStorageError> { - Ok(self - .mix_identity_to_mix_ids(identity) - .await? - .into_iter() - .max()) + node_id: NodeId, + ) -> Result, NymApiStorageError> { + if let Some(cached) = self.db_id_cache.gateway_db_id(node_id) { + return Ok(Some(cached)); + } + if let Some(retrieved) = self.manager.get_gateway_database_id(node_id).await? { + self.db_id_cache.set_gateway_db_id(node_id, retrieved); + return Ok(Some(retrieved)); + } + Ok(None) } pub(crate) async fn get_all_avg_gateway_reliability_in_last_24hr( @@ -536,7 +577,6 @@ impl NymApiStorage { // we MUST have those entries in the database, otherwise the route wouldn't have been chosen // in the first place let layer1_mix_db_id = self - .manager .get_mixnode_database_id(test_route.layer_one_mix().mix_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { @@ -544,7 +584,6 @@ impl NymApiStorage { })?; let layer2_mix_db_id = self - .manager .get_mixnode_database_id(test_route.layer_two_mix().mix_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { @@ -552,7 +591,6 @@ impl NymApiStorage { })?; let layer3_mix_db_id = self - .manager .get_mixnode_database_id(test_route.layer_three_mix().mix_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { @@ -560,7 +598,6 @@ impl NymApiStorage { })?; let gateway_db_id = self - .manager .get_gateway_database_id(test_route.gateway().node_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { @@ -661,11 +698,11 @@ impl NymApiStorage { let monitor_run_id = self.manager.insert_monitor_run(now).await?; self.manager - .submit_mixnode_statuses(now, mixnode_results) + .submit_mixnode_statuses(now, mixnode_results, &self.db_id_cache) .await?; self.manager - .submit_gateway_statuses(now, gateway_results) + .submit_gateway_statuses(now, gateway_results, &self.db_id_cache) .await?; for test_route in test_routes { @@ -675,6 +712,26 @@ impl NymApiStorage { Ok(()) } + pub(crate) async fn submit_mixnode_statuses_v2( + &self, + mixnode_results: &[NodeResult], + ) -> Result<(), NymApiStorageError> { + self.manager + .submit_mixnode_statuses_v2(mixnode_results) + .await?; + Ok(()) + } + + pub(crate) async fn submit_gateway_statuses_v2( + &self, + gateway_results: &[NodeResult], + ) -> Result<(), NymApiStorageError> { + self.manager + .submit_gateway_statuses_v2(gateway_results) + .await?; + Ok(()) + } + /// Obtains number of network monitor test runs that have occurred within the specified interval. /// /// # Arguments