Skip to main content

hiero_sdk/address_book/
node_update_transaction.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::net::Ipv4Addr;
4
5use hiero_sdk_proto::services;
6use hiero_sdk_proto::services::address_book_service_client::AddressBookServiceClient;
7use tonic::transport::Channel;
8
9use crate::ledger_id::RefLedgerId;
10use crate::protobuf::FromProtobuf;
11use crate::service_endpoint::ServiceEndpoint;
12use crate::transaction::{
13    AnyTransactionData,
14    ChunkInfo,
15    ToSchedulableTransactionDataProtobuf,
16    ToTransactionDataProtobuf,
17    TransactionData,
18    TransactionExecute,
19};
20use crate::{
21    AccountId,
22    BoxGrpcFuture,
23    Error,
24    Key,
25    ToProtobuf,
26    Transaction,
27    ValidateChecksums,
28};
29
30/// Transaction body to modify address book node attributes.
31///
32/// - This transaction SHALL enable the node operator, as identified by the
33///    `admin_key`, to modify operational attributes of the node.
34/// - This transaction MUST be signed by the active `admin_key` for the node.
35/// - If this transaction sets a new value for the `admin_key`, then both the
36///    current `admin_key`, and the new `admin_key` MUST sign this transaction.
37/// - This transaction SHALL NOT change any field that is not set (is null) in
38///    this transaction body.
39/// - This SHALL create a pending update to the node, but the change SHALL NOT
40///    be immediately applied to the active configuration.
41/// - All pending node updates SHALL be applied to the active network
42///    configuration during the next `freeze` transaction with the field
43///    `freeze_type` set to `PREPARE_UPGRADE`.
44///
45/// ### Record Stream Effects
46/// Upon completion the `node_id` for the updated entry SHALL be in the
47/// transaction receipt.
48pub type NodeUpdateTransaction = Transaction<NodeUpdateTransactionData>;
49
50/// Transaction body to modify address book node attributes.
51#[derive(Debug, Clone, Default)]
52pub struct NodeUpdateTransactionData {
53    /// A consensus node identifier in the network state.
54    node_id: u64,
55
56    /// A Node account identifier.
57    account_id: Option<AccountId>,
58
59    /// A short description of the node.
60    description: Option<String>,
61
62    /// A list of service endpoints for gossip.
63    gossip_endpoints: Vec<ServiceEndpoint>,
64
65    /// A list of service endpoints for gRPC calls.
66    service_endpoints: Vec<ServiceEndpoint>,
67
68    /// A certificate used to sign gossip events.
69    gossip_ca_certificate: Option<Vec<u8>>,
70
71    /// A hash of the node gRPC TLS certificate.
72    grpc_certificate_hash: Option<Vec<u8>>,
73
74    /// An administrative key controlled by the node operator.
75    admin_key: Option<Key>,
76
77    /// A flag indicating whether the node operator declines rewards.
78    decline_reward: Option<bool>,
79
80    /// A service endpoint for gRPC proxy.
81    grpc_proxy_endpoint: Option<ServiceEndpoint>,
82}
83
84impl NodeUpdateTransaction {
85    /// Returns the account associated with the new node.
86    #[must_use]
87    pub fn get_node_id(&self) -> u64 {
88        self.data().node_id
89    }
90
91    /// Sets the account associated with the new node.
92    pub fn node_id(&mut self, node_id: u64) -> &mut Self {
93        self.data_mut().node_id = node_id;
94        self
95    }
96
97    /// Returns the account associated with the new node.
98    #[must_use]
99    pub fn get_account_id(&self) -> Option<AccountId> {
100        self.data().account_id
101    }
102
103    /// Sets the account associated with the new node.
104    pub fn account_id(&mut self, account_id: AccountId) -> &mut Self {
105        self.data_mut().account_id = Some(account_id);
106        self
107    }
108
109    /// Returns the description of the new node.
110    #[must_use]
111    pub fn get_description(&self) -> Option<&str> {
112        self.data().description.as_deref()
113    }
114
115    /// Sets the description of the new node.
116    pub fn description(&mut self, description: impl Into<String>) -> &mut Self {
117        self.data_mut().description = Some(description.into());
118        self
119    }
120
121    /// Returns the list of service endpoints for gossip.
122    #[must_use]
123    pub fn get_gossip_endpoints(&self) -> Vec<ServiceEndpoint> {
124        self.data().gossip_endpoints.clone()
125    }
126
127    /// Sets the list of service endpoints for gossip.
128    pub fn gossip_endpoints(
129        &mut self,
130        gossip_endpoint: impl IntoIterator<Item = ServiceEndpoint>,
131    ) -> &mut Self {
132        self.data_mut().gossip_endpoints = gossip_endpoint.into_iter().collect();
133        self
134    }
135
136    /// Adds a service endpoint for gossip to the list of service endpoints.
137    pub fn add_gossip_endpoint(&mut self, gossip_endpoint: ServiceEndpoint) -> &mut Self {
138        self.data_mut().gossip_endpoints.push(gossip_endpoint);
139        self
140    }
141
142    /// Returns the updated list of service endpoints for gRPC calls.
143    #[must_use]
144    pub fn get_service_endpoints(&self) -> Vec<ServiceEndpoint> {
145        self.data().service_endpoints.clone()
146    }
147
148    /// Sets the updated list of service endpoints for gRPC calls.
149    pub fn service_endpoints(
150        &mut self,
151        service_endpoint: impl IntoIterator<Item = ServiceEndpoint>,
152    ) -> &mut Self {
153        self.data_mut().service_endpoints = service_endpoint.into_iter().collect();
154        self
155    }
156
157    /// Adds a service endpoint to the list of service endpoints for gRPC calls.
158    pub fn add_service_endpoint(&mut self, service_endpoint: ServiceEndpoint) -> &mut Self {
159        self.data_mut().service_endpoints.push(service_endpoint);
160        self
161    }
162
163    /// Returns the updated certificate used to sign gossip events.
164    #[must_use]
165    pub fn get_gossip_ca_certificate(&self) -> Option<Vec<u8>> {
166        self.data().gossip_ca_certificate.clone()
167    }
168
169    /// Updates the certificate used to sign gossip events.
170    pub fn gossip_ca_certificate(
171        &mut self,
172        gossip_ca_certificate: impl Into<Vec<u8>>,
173    ) -> &mut Self {
174        self.data_mut().gossip_ca_certificate = Some(gossip_ca_certificate.into());
175        self
176    }
177
178    /// Returns the updated hash of the node gRPC TLS certificate.
179    #[must_use]
180    pub fn get_grpc_certificate_hash(&self) -> Option<Vec<u8>> {
181        self.data().grpc_certificate_hash.clone()
182    }
183
184    /// Updates the hash of the node gRPC TLS certificate.
185    pub fn grpc_certificate_hash(
186        &mut self,
187        grpc_certificate_hash: impl Into<Vec<u8>>,
188    ) -> &mut Self {
189        self.data_mut().grpc_certificate_hash = Some(grpc_certificate_hash.into());
190        self
191    }
192
193    /// Returns the updated admin key.
194    #[must_use]
195    pub fn get_admin_key(&self) -> Option<&Key> {
196        self.data().admin_key.as_ref()
197    }
198
199    /// Updated the admin key.
200    pub fn admin_key(&mut self, key: impl Into<Key>) -> &mut Self {
201        self.data_mut().admin_key = Some(key.into());
202        self
203    }
204
205    /// Returns the decline reward.
206    #[must_use]
207    pub fn get_decline_reward(&self) -> Option<bool> {
208        self.data().decline_reward
209    }
210
211    /// Sets the decline reward.
212    pub fn decline_reward(&mut self, decline_reward: bool) -> &mut Self {
213        self.data_mut().decline_reward = Some(decline_reward);
214        self
215    }
216
217    /// Returns the grpc proxy endpoint.
218    #[must_use]
219    pub fn get_grpc_proxy_endpoint(&self) -> Option<&ServiceEndpoint> {
220        self.data().grpc_proxy_endpoint.as_ref()
221    }
222
223    /// Sets the grpc proxy endpoint.
224    pub fn grpc_proxy_endpoint(&mut self, grpc_proxy_endpoint: ServiceEndpoint) -> &mut Self {
225        self.data_mut().grpc_proxy_endpoint = Some(grpc_proxy_endpoint);
226        self
227    }
228
229    /// Deletes the gRPC proxy endpoint and sets it to null.
230    ///
231    /// This clears the gRPC proxy endpoint field, effectively removing it from the node update.
232    pub fn delete_grpc_proxy_endpoint(&mut self) -> &mut Self {
233        self.data_mut().grpc_proxy_endpoint = None;
234        self
235    }
236}
237
238impl TransactionData for NodeUpdateTransactionData {}
239
240impl TransactionExecute for NodeUpdateTransactionData {
241    fn execute(
242        &self,
243        channel: Channel,
244        request: services::Transaction,
245    ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
246        Box::pin(async { AddressBookServiceClient::new(channel).update_node(request).await })
247    }
248}
249
250impl ValidateChecksums for NodeUpdateTransactionData {
251    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
252        self.account_id.validate_checksums(ledger_id)?;
253        Ok(())
254    }
255}
256
257impl ToTransactionDataProtobuf for NodeUpdateTransactionData {
258    fn to_transaction_data_protobuf(
259        &self,
260        chunk_info: &ChunkInfo,
261    ) -> services::transaction_body::Data {
262        let _ = chunk_info.assert_single_transaction();
263
264        services::transaction_body::Data::NodeUpdate(self.to_protobuf())
265    }
266}
267
268impl ToSchedulableTransactionDataProtobuf for NodeUpdateTransactionData {
269    fn to_schedulable_transaction_data_protobuf(
270        &self,
271    ) -> services::schedulable_transaction_body::Data {
272        services::schedulable_transaction_body::Data::NodeUpdate(self.to_protobuf())
273    }
274}
275
276impl From<NodeUpdateTransactionData> for AnyTransactionData {
277    fn from(transaction: NodeUpdateTransactionData) -> Self {
278        Self::NodeUpdate(transaction)
279    }
280}
281
282impl FromProtobuf<services::NodeUpdateTransactionBody> for NodeUpdateTransactionData {
283    fn from_protobuf(pb: services::NodeUpdateTransactionBody) -> crate::Result<Self> {
284        let gossip_endpoints = pb
285            .gossip_endpoint
286            .iter()
287            .map(|it| {
288                let ip_addr_v4 = &it.ip_address_v4[..];
289                let ip = Ipv4Addr::new(ip_addr_v4[0], ip_addr_v4[1], ip_addr_v4[2], ip_addr_v4[3]);
290                ServiceEndpoint {
291                    ip_address_v4: Some(ip),
292                    port: it.port,
293                    domain_name: it.domain_name.clone(),
294                }
295            })
296            .collect();
297        let service_endpoints = pb
298            .service_endpoint
299            .iter()
300            .map(|it| {
301                let ip_addr_v4 = &it.ip_address_v4[..];
302                let ip = Ipv4Addr::new(ip_addr_v4[0], ip_addr_v4[1], ip_addr_v4[2], ip_addr_v4[3]);
303                ServiceEndpoint {
304                    ip_address_v4: Some(ip),
305                    port: it.port,
306                    domain_name: it.domain_name.clone(),
307                }
308            })
309            .collect();
310
311        Ok(Self {
312            node_id: pb.node_id,
313            account_id: FromProtobuf::from_protobuf(pb.account_id)?,
314            description: pb.description,
315            gossip_endpoints: gossip_endpoints,
316            service_endpoints: service_endpoints,
317            gossip_ca_certificate: pb.gossip_ca_certificate,
318            grpc_certificate_hash: pb.grpc_certificate_hash,
319            admin_key: Option::from_protobuf(pb.admin_key)?,
320            decline_reward: pb.decline_reward,
321            grpc_proxy_endpoint: pb.grpc_proxy_endpoint.map(|it| ServiceEndpoint {
322                ip_address_v4: Some(Ipv4Addr::new(
323                    it.ip_address_v4[0],
324                    it.ip_address_v4[1],
325                    it.ip_address_v4[2],
326                    it.ip_address_v4[3],
327                )),
328                port: it.port,
329                domain_name: it.domain_name.clone(),
330            }),
331        })
332    }
333}
334
335impl ToProtobuf for NodeUpdateTransactionData {
336    type Protobuf = services::NodeUpdateTransactionBody;
337
338    fn to_protobuf(&self) -> Self::Protobuf {
339        let gossip_endpoints =
340            self.gossip_endpoints.iter().map(|it| it.to_protobuf()).collect::<Vec<_>>();
341        let service_endpoints =
342            self.service_endpoints.iter().map(|it| it.to_protobuf()).collect::<Vec<_>>();
343
344        services::NodeUpdateTransactionBody {
345            node_id: self.node_id,
346            account_id: self.account_id.to_protobuf(),
347            description: self.description.clone(),
348            gossip_endpoint: gossip_endpoints,
349            service_endpoint: service_endpoints,
350            gossip_ca_certificate: self.gossip_ca_certificate.clone(),
351            grpc_certificate_hash: self.grpc_certificate_hash.clone(),
352            admin_key: self.admin_key.to_protobuf(),
353            decline_reward: self.decline_reward,
354            grpc_proxy_endpoint: self.grpc_proxy_endpoint.as_ref().map(|it| it.to_protobuf()),
355            associated_registered_node_list: None,
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use std::net::Ipv4Addr;
363
364    use expect_test::expect_file;
365    use hiero_sdk_proto::services;
366
367    use super::NodeUpdateTransaction;
368    use crate::address_book::NodeUpdateTransactionData;
369    use crate::protobuf::{
370        FromProtobuf,
371        ToProtobuf,
372    };
373    use crate::service_endpoint::ServiceEndpoint;
374    use crate::transaction::test_helpers::{
375        check_body,
376        transaction_body,
377        unused_private_key,
378        TEST_ACCOUNT_ID,
379    };
380    use crate::{
381        AnyTransaction,
382        Key,
383    };
384
385    const TEST_DESCRIPTION: &str = "test description";
386    const TEST_GOSSIP_CA_CERTIFICATE: &[u8] = &[1, 2, 3, 4];
387    const TEST_GRPC_CERTIFICATE_HASH: &[u8] = &[5, 6, 7, 8];
388
389    fn make_ip_address_list() -> Vec<ServiceEndpoint> {
390        vec![
391            ServiceEndpoint {
392                ip_address_v4: Some(Ipv4Addr::new(127, 0, 0, 1)),
393                port: 1234,
394                domain_name: "".to_owned(),
395            },
396            ServiceEndpoint {
397                ip_address_v4: Some(Ipv4Addr::new(127, 0, 0, 1)),
398                port: 8008,
399                domain_name: "".to_owned(),
400            },
401        ]
402    }
403
404    fn make_transaction() -> NodeUpdateTransaction {
405        let mut tx = NodeUpdateTransaction::new_for_tests();
406
407        tx.account_id(TEST_ACCOUNT_ID)
408            .description(TEST_DESCRIPTION)
409            .gossip_endpoints(make_ip_address_list())
410            .service_endpoints(make_ip_address_list())
411            .gossip_ca_certificate(TEST_GOSSIP_CA_CERTIFICATE)
412            .grpc_certificate_hash(TEST_GRPC_CERTIFICATE_HASH)
413            .admin_key(unused_private_key().public_key())
414            .freeze()
415            .unwrap();
416
417        tx
418    }
419
420    #[test]
421    fn serialize() {
422        let tx = make_transaction();
423
424        let tx = transaction_body(tx);
425
426        let tx = check_body(tx);
427
428        expect_file!["./snapshots/node_update_transaction/serialize.txt"].assert_debug_eq(&tx);
429    }
430
431    #[test]
432    fn to_from_bytes() {
433        let tx = make_transaction();
434
435        let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
436
437        let tx = transaction_body(tx);
438        let tx2 = transaction_body(tx2);
439
440        assert_eq!(tx, tx2)
441    }
442
443    #[test]
444    fn from_proto_body() {
445        let grpc_proxy_endpoint = make_ip_address_list().into_iter().next().unwrap();
446        let tx = services::NodeUpdateTransactionBody {
447            node_id: 1,
448            account_id: Some(TEST_ACCOUNT_ID.to_protobuf()),
449            description: Some(TEST_DESCRIPTION.to_owned()),
450            gossip_endpoint: make_ip_address_list()
451                .into_iter()
452                .map(|it| it.to_protobuf())
453                .collect(),
454            service_endpoint: make_ip_address_list()
455                .into_iter()
456                .map(|it| it.to_protobuf())
457                .collect(),
458            gossip_ca_certificate: Some(TEST_GOSSIP_CA_CERTIFICATE.to_vec()),
459            grpc_certificate_hash: Some(TEST_GRPC_CERTIFICATE_HASH.to_vec()),
460            admin_key: Some(unused_private_key().public_key().to_protobuf()),
461            decline_reward: Some(false),
462            grpc_proxy_endpoint: Some(grpc_proxy_endpoint.to_protobuf()),
463            associated_registered_node_list: None,
464        };
465
466        let data = NodeUpdateTransactionData::from_protobuf(tx).unwrap();
467
468        assert_eq!(data.account_id, Some(TEST_ACCOUNT_ID));
469        assert_eq!(data.description, Some(TEST_DESCRIPTION.to_string()));
470        assert_eq!(data.gossip_endpoints, make_ip_address_list());
471        assert_eq!(data.service_endpoints, make_ip_address_list());
472        assert_eq!(data.gossip_ca_certificate, Some(TEST_GOSSIP_CA_CERTIFICATE.to_vec()));
473        assert_eq!(data.grpc_certificate_hash, Some(TEST_GRPC_CERTIFICATE_HASH.to_vec()));
474        assert_eq!(data.admin_key, Some(Key::from(unused_private_key().public_key())));
475        assert_eq!(data.decline_reward, Some(false));
476        assert_eq!(data.grpc_proxy_endpoint, Some(grpc_proxy_endpoint));
477    }
478
479    #[test]
480    fn get_set_node_id() {
481        let mut tx = NodeUpdateTransaction::new();
482        tx.node_id(1);
483
484        assert_eq!(tx.get_node_id(), 1);
485    }
486
487    #[test]
488    #[should_panic]
489    fn get_set_node_id_frozen_panic() {
490        make_transaction().node_id(1);
491    }
492
493    #[test]
494    fn get_set_account_id() {
495        let account_id = TEST_ACCOUNT_ID;
496        let mut tx = NodeUpdateTransaction::new();
497        tx.account_id(account_id.to_owned());
498
499        assert_eq!(tx.get_account_id(), Some(account_id));
500    }
501
502    #[test]
503    #[should_panic]
504    fn get_set_account_id_frozen_panic() {
505        make_transaction().account_id(TEST_ACCOUNT_ID);
506    }
507
508    #[test]
509    fn get_set_description() {
510        let description = TEST_DESCRIPTION.to_owned();
511        let mut tx = NodeUpdateTransaction::new();
512        tx.description(description.to_owned());
513
514        assert_eq!(tx.get_description(), Some(TEST_DESCRIPTION));
515    }
516
517    #[test]
518    #[should_panic]
519    fn get_set_description_frozen_panic() {
520        make_transaction().description(TEST_DESCRIPTION);
521    }
522
523    #[test]
524    fn get_set_gossip_endpoints() {
525        let gossip_endpoints = make_ip_address_list();
526        let mut tx = NodeUpdateTransaction::new();
527        tx.gossip_endpoints(gossip_endpoints.to_owned());
528
529        assert_eq!(tx.get_gossip_endpoints(), gossip_endpoints);
530    }
531
532    #[test]
533    #[should_panic]
534    fn get_set_gossip_endpoint_frozen_panic() {
535        make_transaction().gossip_endpoints(make_ip_address_list());
536    }
537
538    #[test]
539    fn get_set_decline_reward() {
540        let mut tx = NodeUpdateTransaction::new();
541        tx.decline_reward(true);
542
543        assert_eq!(tx.get_decline_reward(), Some(true));
544    }
545
546    #[test]
547    fn get_set_grpc_proxy_endpoint() {
548        let grpc_proxy_endpoint = make_ip_address_list().into_iter().next().unwrap();
549        let mut tx = NodeUpdateTransaction::new();
550        tx.grpc_proxy_endpoint(grpc_proxy_endpoint.clone());
551
552        assert_eq!(tx.get_grpc_proxy_endpoint(), Some(&grpc_proxy_endpoint));
553    }
554
555    #[test]
556    fn delete_grpc_proxy_endpoint() {
557        let grpc_proxy_endpoint = make_ip_address_list().into_iter().next().unwrap();
558        let mut tx = NodeUpdateTransaction::new();
559
560        // First set the grpc proxy endpoint
561        tx.grpc_proxy_endpoint(grpc_proxy_endpoint.clone());
562        assert_eq!(tx.get_grpc_proxy_endpoint(), Some(&grpc_proxy_endpoint));
563
564        // Then delete it
565        tx.delete_grpc_proxy_endpoint();
566        assert_eq!(tx.get_grpc_proxy_endpoint(), None);
567    }
568
569    #[test]
570    #[should_panic]
571    fn delete_grpc_proxy_endpoint_frozen_panic() {
572        make_transaction().delete_grpc_proxy_endpoint();
573    }
574
575    #[test]
576    fn get_set_service_endpoints() {
577        let service_endpoints = make_ip_address_list();
578        let mut tx = NodeUpdateTransaction::new();
579        tx.service_endpoints(service_endpoints.to_owned());
580
581        assert_eq!(tx.get_service_endpoints(), service_endpoints);
582    }
583
584    #[test]
585    #[should_panic]
586    fn get_set_service_endpoints_frozen_panic() {
587        make_transaction().service_endpoints(make_ip_address_list());
588    }
589
590    #[test]
591    fn get_set_grpc_certificate_hash() {
592        let mut tx = NodeUpdateTransaction::new();
593        tx.grpc_certificate_hash(TEST_GOSSIP_CA_CERTIFICATE);
594
595        assert_eq!(tx.get_grpc_certificate_hash(), Some(TEST_GOSSIP_CA_CERTIFICATE.to_vec()));
596    }
597
598    #[test]
599    #[should_panic]
600    fn get_set_grpc_certificate_hash_frozen_panic() {
601        make_transaction().grpc_certificate_hash(TEST_GOSSIP_CA_CERTIFICATE);
602    }
603
604    #[test]
605    fn get_set_admin_key() {
606        let mut tx = NodeUpdateTransaction::new();
607        tx.admin_key(unused_private_key().public_key());
608
609        assert_eq!(tx.get_admin_key(), Some(&Key::from(unused_private_key().public_key())));
610    }
611
612    #[test]
613    #[should_panic]
614    fn get_set_admin_key_frozen_panic() {
615        make_transaction().admin_key(Key::from(unused_private_key().public_key()));
616    }
617}