nucypher_core/
node_metadata.rs

1use alloc::boxed::Box;
2use alloc::string::String;
3use alloc::string::ToString;
4use core::fmt;
5
6use ferveo::api::ValidatorPublicKey as FerveoPublicKey;
7use serde::{Deserialize, Serialize};
8use serde_with::serde_as;
9use sha3::{digest::Update, Digest, Keccak256};
10use umbral_pre::{serde_bytes, PublicKey, RecoverableSignature, Signature, Signer};
11
12use crate::address::Address;
13use crate::fleet_state::FleetStateChecksum;
14use crate::versioning::{
15    messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner,
16};
17use crate::VerificationError;
18
19/// Indicates an error during canonical address derivation from a signature.
20pub enum AddressDerivationError {
21    /// Signature is missing from the payload.
22    NoSignatureInPayload,
23    /// Failed to recover the public key from the signature.
24    RecoveryFailed(String),
25}
26
27impl fmt::Display for AddressDerivationError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::NoSignatureInPayload => write!(f, "Signature is missing from the payload"),
31            Self::RecoveryFailed(err) => write!(
32                f,
33                "Failed to recover the public key from the signature: {err}"
34            ),
35        }
36    }
37}
38
39/// Mimics the format of `eth_account.messages.encode_defunct()` which NuCypher codebase uses.
40fn encode_defunct(message: &[u8]) -> Keccak256 {
41    Keccak256::new()
42        .chain(b"\x19")
43        .chain(b"E") // version
44        .chain(b"thereum Signed Message:\n") // header
45        .chain(message.len().to_string().as_bytes())
46        .chain(message)
47}
48
49/// Node metadata.
50#[serde_as]
51#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
52pub struct NodeMetadataPayload {
53    /// The staking provider's Ethereum address.
54    pub staking_provider_address: Address,
55    /// The network identifier.
56    pub domain: String,
57    /// The timestamp of the metadata creation.
58    pub timestamp_epoch: u32,
59    /// The node's verifying key.
60    pub verifying_key: PublicKey,
61    /// The node's encrypting key.
62    pub encrypting_key: PublicKey,
63    /// Ferveo public key to use for DKG participation.
64    pub ferveo_public_key: FerveoPublicKey,
65    /// The node's SSL certificate (serialized in DER format).
66    #[serde(with = "serde_bytes::as_base64")]
67    pub certificate_der: Box<[u8]>,
68    /// The hostname of the node's REST service.
69    pub host: String,
70    /// The port of the node's REST service.
71    pub port: u16,
72    /// The node's verifying key signed by the private key corresponding to the operator address.
73    pub operator_signature: RecoverableSignature,
74}
75
76impl NodeMetadataPayload {
77    // Standard payload serialization for signing purposes.
78    fn to_bytes(&self) -> Box<[u8]> {
79        messagepack_serialize(self)
80    }
81
82    /// Derives the address corresponding to the public key that was used
83    /// to create `operator_signature`.
84    pub fn derive_operator_address(&self) -> Result<Address, AddressDerivationError> {
85        let digest = encode_defunct(&self.verifying_key.to_compressed_bytes());
86        let key = PublicKey::recover_from_prehash(&digest.finalize(), &self.operator_signature)
87            .map_err(AddressDerivationError::RecoveryFailed)?;
88        Ok(Address::from_public_key(&key))
89    }
90}
91
92/// Signed node metadata.
93#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
94pub struct NodeMetadata {
95    signature: Signature,
96    /// Authorized metadata payload.
97    pub payload: NodeMetadataPayload,
98}
99
100impl NodeMetadata {
101    /// Creates and signs a new metadata object.
102    pub fn new(signer: &Signer, payload: &NodeMetadataPayload) -> Self {
103        // TODO: how can we ensure that `verifying_key` in `payload` is the same as in `signer`?
104        Self {
105            signature: signer.sign(&payload.to_bytes()),
106            payload: payload.clone(),
107        }
108    }
109
110    /// Verifies the consistency of signed node metadata.
111    pub fn verify(&self) -> bool {
112        // This method returns bool and not NodeMetadataPayload,
113        // because NodeMetadata can be used before verification,
114        // so we need access to its fields right away.
115        // This may change depending on the decision in
116        // https://github.com/nucypher/nucypher/issues/2876
117
118        // We could do this on deserialization, but it is a relatively expensive operation.
119        self.signature
120            .verify(&self.payload.verifying_key, &self.payload.to_bytes())
121    }
122}
123
124impl<'a> ProtocolObjectInner<'a> for NodeMetadata {
125    fn brand() -> [u8; 4] {
126        *b"NdMd"
127    }
128
129    fn version() -> (u16, u16) {
130        // Note: if `NodeMetadataPayload` has a field added, it will have be a major version change,
131        // since the whole payload is signed (so we can't just substitute the default).
132        // Alternatively, one can add new fields to `NodeMetadata` itself
133        // (but then they won't be signed).
134        (4, 0)
135    }
136
137    fn unversioned_to_bytes(&self) -> Box<[u8]> {
138        messagepack_serialize(&self)
139    }
140
141    fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option<Result<Self, String>> {
142        if minor_version == 0 {
143            Some(messagepack_deserialize(bytes))
144        } else {
145            None
146        }
147    }
148}
149
150impl<'a> ProtocolObject<'a> for NodeMetadata {}
151
152/// A request for metadata exchange.
153#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
154pub struct MetadataRequest {
155    /// The checksum of the requester's fleet state.
156    pub fleet_state_checksum: FleetStateChecksum,
157    /// A list of node metadata to announce.
158    pub announce_nodes: Box<[NodeMetadata]>,
159}
160
161impl MetadataRequest {
162    /// Creates a new request.
163    pub fn new(fleet_state_checksum: &FleetStateChecksum, announce_nodes: &[NodeMetadata]) -> Self {
164        Self {
165            fleet_state_checksum: *fleet_state_checksum,
166            announce_nodes: announce_nodes.to_vec().into_boxed_slice(),
167        }
168    }
169}
170
171impl<'a> ProtocolObjectInner<'a> for MetadataRequest {
172    fn brand() -> [u8; 4] {
173        *b"MdRq"
174    }
175
176    fn version() -> (u16, u16) {
177        (3, 0)
178    }
179
180    fn unversioned_to_bytes(&self) -> Box<[u8]> {
181        messagepack_serialize(&self)
182    }
183
184    fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option<Result<Self, String>> {
185        if minor_version == 0 {
186            Some(messagepack_deserialize(bytes))
187        } else {
188            None
189        }
190    }
191}
192
193impl<'a> ProtocolObject<'a> for MetadataRequest {}
194
195/// Payload of the metadata response.
196#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
197pub struct MetadataResponsePayload {
198    /// The timestamp of the most recent fleet state
199    /// (the one consisting of the nodes that are being sent).
200    pub timestamp_epoch: u32,
201    /// A list of node metadata to announce.
202    pub announce_nodes: Box<[NodeMetadata]>,
203}
204
205impl MetadataResponsePayload {
206    /// Creates the new metadata response payload.
207    pub fn new(timestamp_epoch: u32, announce_nodes: &[NodeMetadata]) -> Self {
208        Self {
209            timestamp_epoch,
210            announce_nodes: announce_nodes.to_vec().into_boxed_slice(),
211        }
212    }
213
214    // Standard payload serialization for signing purposes.
215    fn to_bytes(&self) -> Box<[u8]> {
216        messagepack_serialize(self)
217    }
218}
219
220/// A response returned by an Ursula containing known node metadata.
221#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
222pub struct MetadataResponse {
223    signature: Signature,
224    payload: MetadataResponsePayload,
225}
226
227impl MetadataResponse {
228    /// Creates and signs a new metadata response.
229    pub fn new(signer: &Signer, payload: &MetadataResponsePayload) -> Self {
230        Self {
231            signature: signer.sign(&payload.to_bytes()),
232            payload: payload.clone(),
233        }
234    }
235
236    /// Verifies the metadata response and returns the contained payload.
237    pub fn verify(
238        self,
239        verifying_pk: &PublicKey,
240    ) -> Result<MetadataResponsePayload, VerificationError> {
241        if self
242            .signature
243            .verify(verifying_pk, &self.payload.to_bytes())
244        {
245            Ok(self.payload)
246        } else {
247            Err(VerificationError)
248        }
249    }
250}
251
252impl<'a> ProtocolObjectInner<'a> for MetadataResponse {
253    fn brand() -> [u8; 4] {
254        *b"MdRs"
255    }
256
257    fn version() -> (u16, u16) {
258        // Note: if `MetadataResponsePayload` has a field added,
259        // it will have be a major version change,
260        // since the whole payload is signed (so we can't just substitute the default).
261        // Alternatively, one can add new fields to `NodeMetadata` itself
262        // (but then they won't be signed).
263        (3, 0)
264    }
265
266    fn unversioned_to_bytes(&self) -> Box<[u8]> {
267        messagepack_serialize(&self)
268    }
269
270    fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option<Result<Self, String>> {
271        if minor_version == 0 {
272            Some(messagepack_deserialize(bytes))
273        } else {
274            None
275        }
276    }
277}
278
279impl<'a> ProtocolObject<'a> for MetadataResponse {}