Skip to main content

nym_mixnet_contract_common/
nym_node.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::error::MixnetContractError;
5use crate::{EpochEventId, EpochId, Gateway, IntervalEventId, MixNode, NodeId, NodeRewarding};
6use cosmwasm_schema::cw_serde;
7use cosmwasm_std::{Addr, Coin, Decimal, StdError, StdResult};
8use cw_storage_plus::{IntKey, Key, KeyDeserialize, PrimaryKey};
9use nym_contracts_common::IdentityKey;
10use std::fmt::{Display, Formatter};
11
12#[cw_serde]
13#[derive(PartialOrd, Copy, Hash, Eq)]
14#[repr(u8)]
15#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
16#[cfg_attr(
17    feature = "generate-ts",
18    ts(export, export_to = "ts-packages/types/src/types/rust/Role.ts")
19)]
20pub enum Role {
21    #[serde(rename = "eg", alias = "entry", alias = "entry_gateway")]
22    EntryGateway = 0,
23
24    #[serde(rename = "l1", alias = "layer1")]
25    Layer1 = 1,
26
27    #[serde(rename = "l2", alias = "layer2")]
28    Layer2 = 2,
29
30    #[serde(rename = "l3", alias = "layer3")]
31    Layer3 = 3,
32
33    #[serde(rename = "xg", alias = "exit", alias = "exit_gateway")]
34    ExitGateway = 4,
35
36    #[serde(rename = "stb", alias = "standby")]
37    Standby = 128,
38}
39
40impl TryFrom<u8> for Role {
41    type Error = MixnetContractError;
42    fn try_from(value: u8) -> Result<Self, Self::Error> {
43        match value {
44            n if n == Role::EntryGateway as u8 => Ok(Role::EntryGateway),
45            n if n == Role::Layer1 as u8 => Ok(Role::Layer1),
46            n if n == Role::Layer2 as u8 => Ok(Role::Layer2),
47            n if n == Role::Layer3 as u8 => Ok(Role::Layer3),
48            n if n == Role::ExitGateway as u8 => Ok(Role::ExitGateway),
49            n if n == Role::Standby as u8 => Ok(Role::Standby),
50            n => Err(MixnetContractError::UnknownRoleRepresentation { got: n }),
51        }
52    }
53}
54
55impl<'a> PrimaryKey<'a> for Role {
56    type Prefix = <u8 as PrimaryKey<'a>>::Prefix;
57    type SubPrefix = <u8 as PrimaryKey<'a>>::SubPrefix;
58    type Suffix = <u8 as PrimaryKey<'a>>::Suffix;
59    type SuperSuffix = <u8 as PrimaryKey<'a>>::SuperSuffix;
60
61    fn key(&self) -> Vec<Key<'_>> {
62        // I'm not sure why it wasn't possible to delegate the call to
63        // `(*self as u8).key()` directly...
64        // I guess because of the `Key::Ref(&'a [u8])` variant?
65        vec![Key::Val8((*self as u8).to_cw_bytes())]
66    }
67
68    fn joined_key(&self) -> Vec<u8> {
69        (*self as u8).joined_key()
70    }
71
72    fn joined_extra_key(&self, key: &[u8]) -> Vec<u8> {
73        (*self as u8).joined_extra_key(key)
74    }
75}
76
77impl KeyDeserialize for Role {
78    type Output = Role;
79
80    const KEY_ELEMS: u16 = 1;
81
82    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
83        let u8_key: <u8 as KeyDeserialize>::Output = <u8 as KeyDeserialize>::from_vec(value)?;
84        Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string()))
85    }
86
87    fn from_slice(value: &[u8]) -> StdResult<Self::Output> {
88        let u8_key: <u8 as KeyDeserialize>::Output = <u8 as KeyDeserialize>::from_slice(value)?;
89        Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string()))
90    }
91}
92
93impl Role {
94    pub fn first() -> Role {
95        Role::ExitGateway
96    }
97
98    pub fn next(&self) -> Option<Self> {
99        // roles have to be assigned in the following order:
100        // exit -> entry -> l1 -> l2 -> l3 -> standby
101        match self {
102            Role::ExitGateway => Some(Role::EntryGateway),
103            Role::EntryGateway => Some(Role::Layer1),
104            Role::Layer1 => Some(Role::Layer2),
105            Role::Layer2 => Some(Role::Layer3),
106            Role::Layer3 => Some(Role::Standby),
107            Role::Standby => None,
108        }
109    }
110
111    pub fn is_first(&self) -> bool {
112        self == &Role::first()
113    }
114
115    pub fn is_standby(&self) -> bool {
116        matches!(self, Role::Standby)
117    }
118
119    pub fn is_mixnode(&self) -> bool {
120        matches!(self, Role::Layer1 | Role::Layer2 | Role::Layer3)
121    }
122}
123
124impl Display for Role {
125    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
126        match self {
127            Role::Layer1 => write!(f, "mix layer 1"),
128            Role::Layer2 => write!(f, "mix layer 2"),
129            Role::Layer3 => write!(f, "mix layer 3"),
130            Role::EntryGateway => write!(f, "entry gateway"),
131            Role::ExitGateway => write!(f, "exit gateway"),
132            Role::Standby => write!(f, "standby"),
133        }
134    }
135}
136
137/// Metadata associated with the rewarded set.
138#[cw_serde]
139#[derive(Default, Copy)]
140pub struct RewardedSetMetadata {
141    /// Epoch that this data corresponds to.
142    pub epoch_id: EpochId,
143
144    /// Indicates whether all roles got assigned to the set for this epoch.
145    pub fully_assigned: bool,
146
147    /// Metadata for the 'EntryGateway' role
148    pub entry_gateway_metadata: RoleMetadata,
149
150    /// Metadata for the 'ExitGateway' role
151    pub exit_gateway_metadata: RoleMetadata,
152
153    /// Metadata for the 'Layer1' role
154    pub layer1_metadata: RoleMetadata,
155
156    /// Metadata for the 'Layer2' role
157    pub layer2_metadata: RoleMetadata,
158
159    /// Metadata for the 'Layer3' role
160    pub layer3_metadata: RoleMetadata,
161
162    /// Metadata for the 'Standby' role
163    pub standby_metadata: RoleMetadata,
164}
165
166impl RewardedSetMetadata {
167    pub fn new(epoch_id: EpochId) -> Self {
168        RewardedSetMetadata {
169            epoch_id,
170            fully_assigned: false,
171            entry_gateway_metadata: Default::default(),
172            exit_gateway_metadata: Default::default(),
173            layer1_metadata: Default::default(),
174            layer2_metadata: Default::default(),
175            layer3_metadata: Default::default(),
176            standby_metadata: Default::default(),
177        }
178    }
179
180    pub fn set_role_count(&mut self, role: Role, num_nodes: u32) {
181        match role {
182            Role::EntryGateway => self.entry_gateway_metadata.num_nodes = num_nodes,
183            Role::Layer1 => self.layer1_metadata.num_nodes = num_nodes,
184            Role::Layer2 => self.layer2_metadata.num_nodes = num_nodes,
185            Role::Layer3 => self.layer3_metadata.num_nodes = num_nodes,
186            Role::ExitGateway => self.exit_gateway_metadata.num_nodes = num_nodes,
187            Role::Standby => self.standby_metadata.num_nodes = num_nodes,
188        }
189    }
190
191    pub fn set_highest_id(&mut self, highest_id: NodeId, role: Role) {
192        match role {
193            Role::EntryGateway => self.entry_gateway_metadata.highest_id = highest_id,
194            Role::Layer1 => self.layer1_metadata.highest_id = highest_id,
195            Role::Layer2 => self.layer2_metadata.highest_id = highest_id,
196            Role::Layer3 => self.layer3_metadata.highest_id = highest_id,
197            Role::ExitGateway => self.exit_gateway_metadata.highest_id = highest_id,
198            Role::Standby => self.standby_metadata.highest_id = highest_id,
199        }
200    }
201
202    pub fn highest_rewarded_id(&self) -> NodeId {
203        let mut highest = 0;
204        if self.layer1_metadata.highest_id > highest {
205            highest = self.layer1_metadata.highest_id;
206        }
207        if self.layer2_metadata.highest_id > highest {
208            highest = self.layer2_metadata.highest_id;
209        }
210        if self.layer3_metadata.highest_id > highest {
211            highest = self.layer3_metadata.highest_id;
212        }
213        if self.entry_gateway_metadata.highest_id > highest {
214            highest = self.entry_gateway_metadata.highest_id;
215        }
216        if self.exit_gateway_metadata.highest_id > highest {
217            highest = self.exit_gateway_metadata.highest_id;
218        }
219        if self.standby_metadata.highest_id > highest {
220            highest = self.standby_metadata.highest_id;
221        }
222
223        highest
224    }
225}
226
227/// Metadata associated with particular node role.
228#[cw_serde]
229#[derive(Default, Copy)]
230pub struct RoleMetadata {
231    /// Highest, also latest, node-id of a node assigned this role.
232    pub highest_id: NodeId,
233
234    /// Number of nodes assigned this particular role.
235    pub num_nodes: u32,
236}
237
238/// Full details associated with given node.
239#[cw_serde]
240#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
241pub struct NymNodeDetails {
242    /// Basic bond information of this node, such as owner address, original pledge, etc.
243    pub bond_information: NymNodeBond,
244
245    /// Details used for computation of rewarding related data.
246    pub rewarding_details: NodeRewarding,
247
248    /// Adjustments to the node that are scheduled to happen during future epoch/interval transitions.
249    pub pending_changes: PendingNodeChanges,
250}
251
252impl NymNodeDetails {
253    pub fn new(
254        bond_information: NymNodeBond,
255        rewarding_details: NodeRewarding,
256        pending_changes: PendingNodeChanges,
257    ) -> Self {
258        NymNodeDetails {
259            bond_information,
260            rewarding_details,
261            pending_changes,
262        }
263    }
264
265    pub fn node_id(&self) -> NodeId {
266        self.bond_information.node_id
267    }
268
269    pub fn is_unbonding(&self) -> bool {
270        self.bond_information.is_unbonding
271    }
272
273    pub fn original_pledge(&self) -> &Coin {
274        &self.bond_information.original_pledge
275    }
276
277    pub fn pending_operator_reward(&self) -> Coin {
278        let pledge = self.original_pledge();
279        self.rewarding_details.pending_operator_reward(pledge)
280    }
281
282    pub fn pending_detailed_operator_reward(&self) -> StdResult<Decimal> {
283        let pledge = self.original_pledge();
284        self.rewarding_details
285            .pending_detailed_operator_reward(pledge)
286    }
287
288    pub fn total_stake(&self) -> Decimal {
289        self.rewarding_details.node_bond()
290    }
291
292    pub fn pending_pledge_change(&self) -> Option<EpochEventId> {
293        self.pending_changes.pledge_change
294    }
295}
296
297#[cw_serde]
298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
299pub struct NymNodeBond {
300    /// Unique id assigned to the bonded node.
301    #[cfg_attr(feature = "utoipa", schema(value_type = u32))]
302    pub node_id: NodeId,
303
304    /// Address of the owner of this nym-node.
305    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
306    pub owner: Addr,
307
308    /// Original amount pledged by the operator of this node.
309
310    #[cfg_attr(feature = "utoipa", schema(value_type = crate::CoinSchema))]
311    pub original_pledge: Coin,
312
313    /// Block height at which this nym-node has been bonded.
314    pub bonding_height: u64,
315
316    /// Flag to indicate whether this node is in the process of unbonding,
317    /// that will conclude upon the epoch finishing.
318    pub is_unbonding: bool,
319
320    /// Information provided by the operator for the purposes of bonding.
321    pub node: NymNode,
322}
323
324impl NymNodeBond {
325    pub fn new(
326        node_id: NodeId,
327        owner: Addr,
328        original_pledge: Coin,
329        node: impl Into<NymNode>,
330        bonding_height: u64,
331    ) -> NymNodeBond {
332        Self {
333            node_id,
334            owner,
335            original_pledge,
336            bonding_height,
337            is_unbonding: false,
338            node: node.into(),
339        }
340    }
341
342    pub fn identity(&self) -> &str {
343        &self.node.identity_key
344    }
345
346    pub fn ensure_bonded(&self) -> Result<(), MixnetContractError> {
347        if self.is_unbonding {
348            return Err(MixnetContractError::NodeIsUnbonding {
349                node_id: self.node_id,
350            });
351        }
352        Ok(())
353    }
354}
355
356/// Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.
357#[cw_serde]
358#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
359#[cfg_attr(
360    feature = "generate-ts",
361    ts(export, export_to = "ts-packages/types/src/types/rust/NymNode.ts")
362)]
363#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
364pub struct NymNode {
365    /// Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com
366    /// that is used to discover other capabilities of this node.
367    pub host: String,
368
369    /// Allow specifying custom port for accessing the http, and thus self-described, api
370    /// of this node for the capabilities discovery.
371    pub custom_http_port: Option<u16>,
372
373    /// Base58-encoded ed25519 EdDSA public key.
374    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
375    pub identity_key: IdentityKey,
376    // TODO: I don't think we want to include sphinx keys here,
377    // given we want to rotate them and keeping that in sync with contract will be a PITA
378}
379
380impl NymNode {
381    /// Perform naive validation of the attached identity key - makes sure it's correctly encoded
382    /// and has 32 bytes (as expected from ed25519). we're not, however, checking if it's a valid curve point
383    pub fn naive_ensure_valid_pubkey(&self) -> Result<(), MixnetContractError> {
384        let decoded = bs58::decode(&self.identity_key)
385            .into_vec()
386            .map_err(|_| MixnetContractError::InvalidPubKey)?;
387        if decoded.len() != 32 {
388            return Err(MixnetContractError::InvalidPubKey);
389        }
390        Ok(())
391    }
392
393    /// Makes sure the provided host's length is at most 255 characters to prevent abuse.
394    pub fn ensure_host_in_range(&self) -> Result<(), MixnetContractError> {
395        if self.host.len() > 255 {
396            return Err(MixnetContractError::HostTooLong);
397        }
398        Ok(())
399    }
400}
401
402impl From<MixNode> for NymNode {
403    fn from(value: MixNode) -> Self {
404        NymNode {
405            host: value.host,
406            custom_http_port: Some(value.http_api_port),
407            identity_key: value.identity_key,
408        }
409    }
410}
411
412impl From<Gateway> for NymNode {
413    fn from(value: Gateway) -> Self {
414        NymNode {
415            host: value.host,
416            custom_http_port: None,
417            identity_key: value.identity_key,
418        }
419    }
420}
421
422#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
423#[cfg_attr(
424    feature = "generate-ts",
425    ts(
426        export,
427        export_to = "ts-packages/types/src/types/rust/NodeConfigUpdate.ts"
428    )
429)]
430#[cw_serde]
431#[derive(Default)]
432pub struct NodeConfigUpdate {
433    pub host: Option<String>,
434    // ideally this would have been `Option<Option<u16>>`, but not sure if json would have recognised it
435    pub custom_http_port: Option<u16>,
436
437    // equivalent to setting `custom_http_port` to `None`
438    #[serde(default)]
439    pub restore_default_http_port: bool,
440}
441
442#[cw_serde]
443#[derive(Default, Copy)]
444#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
445#[cfg_attr(
446    feature = "generate-ts",
447    ts(
448        export,
449        export_to = "ts-packages/types/src/types/rust/PendingNodeChanges.ts"
450    )
451)]
452#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
453pub struct PendingNodeChanges {
454    #[cfg_attr(feature = "utoipa", schema(value_type = Option<u32>))]
455    pub pledge_change: Option<EpochEventId>,
456    #[cfg_attr(feature = "utoipa", schema(value_type = Option<u32>))]
457    pub cost_params_change: Option<IntervalEventId>,
458}
459
460impl PendingNodeChanges {
461    pub fn new_empty() -> PendingNodeChanges {
462        PendingNodeChanges {
463            pledge_change: None,
464            cost_params_change: None,
465        }
466    }
467
468    pub fn ensure_no_pending_pledge_changes(&self) -> Result<(), MixnetContractError> {
469        if let Some(pending_event_id) = self.pledge_change {
470            return Err(MixnetContractError::PendingPledgeChange { pending_event_id });
471        }
472        Ok(())
473    }
474
475    pub fn ensure_no_pending_params_changes(&self) -> Result<(), MixnetContractError> {
476        if let Some(pending_event_id) = self.cost_params_change {
477            return Err(MixnetContractError::PendingParamsChange { pending_event_id });
478        }
479        Ok(())
480    }
481}
482
483/// Basic information of a node that used to be part of the nym network but has already unbonded.
484#[cw_serde]
485pub struct UnbondedNymNode {
486    /// Base58-encoded ed25519 EdDSA public key.
487    pub identity_key: IdentityKey,
488
489    /// NodeId assigned to this node.
490    pub node_id: NodeId,
491
492    /// Address of the owner of this nym node.
493    pub owner: Addr,
494
495    /// Block height at which this nym node has unbonded.
496    pub unbonding_height: u64,
497}
498
499/// Response containing rewarding information of a node with the provided id.
500#[cw_serde]
501pub struct NodeRewardingDetailsResponse {
502    /// Id of the requested node.
503    pub node_id: NodeId,
504
505    /// If there exists a node with the provided id, this field contains its rewarding information.
506    pub rewarding_details: Option<NodeRewarding>,
507}
508
509/// Response containing details of a node belonging to the particular owner.
510#[cw_serde]
511pub struct NodeOwnershipResponse {
512    /// Validated address of the node owner.
513    pub address: Addr,
514
515    /// If the provided address owns a nym-node, this field contains its detailed information.
516    pub details: Option<NymNodeDetails>,
517}
518
519/// Response containing details of a node with the provided id.
520#[cw_serde]
521pub struct NodeDetailsResponse {
522    /// Id of the requested node.
523    pub node_id: NodeId,
524
525    /// If there exists a node with the provided id, this field contains its detailed information.
526    pub details: Option<NymNodeDetails>,
527}
528
529/// Response containing details of a bonded node with the provided identity key.
530#[cw_serde]
531pub struct NodeDetailsByIdentityResponse {
532    /// The identity key (base58-encoded ed25519 public key) of the node.
533    pub identity_key: IdentityKey,
534
535    /// If there exists a bonded node with the provided identity key, this field contains its detailed information.
536    pub details: Option<NymNodeDetails>,
537}
538
539/// Response containing the current state of the stake saturation of a node with the provided id.
540#[cw_serde]
541pub struct StakeSaturationResponse {
542    /// Id of the requested node.
543    pub node_id: NodeId,
544
545    /// The current stake saturation of this node that is indirectly used in reward calculation formulas.
546    /// Note that it can't be larger than 1.
547    pub current_saturation: Option<Decimal>,
548
549    /// The current, absolute, stake saturation of this node.
550    /// Note that as the name suggests it can be larger than 1.
551    /// However, anything beyond that value has no effect on the total node reward.
552    pub uncapped_saturation: Option<Decimal>,
553}
554
555/// Response containing paged list of all nym-nodes that have ever unbonded.
556#[cw_serde]
557pub struct PagedUnbondedNymNodesResponse {
558    /// Basic information of the node such as the owner or the identity key.
559    pub nodes: Vec<UnbondedNymNode>,
560
561    /// Field indicating paging information for the following queries if the caller wishes to get further entries.
562    pub start_next_after: Option<NodeId>,
563}
564
565/// Response containing basic information of an unbonded nym-node with the provided id.
566#[cw_serde]
567pub struct UnbondedNodeResponse {
568    /// Id of the requested nym-node.
569    pub node_id: NodeId,
570
571    /// If there existed a nym-node with the provided id, this field contains its basic information.
572    pub details: Option<UnbondedNymNode>,
573}
574
575#[cw_serde]
576pub struct PagedNymNodeBondsResponse {
577    /// The nym node bond information present in the contract.
578    pub nodes: Vec<NymNodeBond>,
579
580    /// Field indicating paging information for the following queries if the caller wishes to get further entries.
581    pub start_next_after: Option<NodeId>,
582}
583
584#[cw_serde]
585pub struct PagedNymNodeDetailsResponse {
586    /// All nym-node details stored in the contract.
587    /// Apart from the basic bond information it also contains details required for all future reward calculation
588    /// as well as any pending changes requested by the operator.
589    pub nodes: Vec<NymNodeDetails>,
590
591    /// Field indicating paging information for the following queries if the caller wishes to get further entries.
592    pub start_next_after: Option<NodeId>,
593}
594
595#[cw_serde]
596pub struct EpochAssignmentResponse {
597    /// Epoch that this data corresponds to.
598    pub epoch_id: EpochId,
599
600    pub nodes: Vec<NodeId>,
601}
602
603#[cw_serde]
604pub struct RolesMetadataResponse {
605    pub metadata: RewardedSetMetadata,
606}