librats 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/binding.gyp +1 -0
  3. package/lib/index.d.ts +2 -1
  4. package/native-src/3rdparty/android/ifaddrs-android.c +600 -0
  5. package/native-src/3rdparty/android/ifaddrs-android.h +54 -0
  6. package/native-src/CMakeLists.txt +360 -0
  7. package/native-src/LICENSE +21 -0
  8. package/native-src/src/bencode.cpp +485 -0
  9. package/native-src/src/bencode.h +145 -0
  10. package/native-src/src/bittorrent.cpp +3682 -0
  11. package/native-src/src/bittorrent.h +731 -0
  12. package/native-src/src/dht.cpp +2460 -0
  13. package/native-src/src/dht.h +508 -0
  14. package/native-src/src/encrypted_socket.cpp +817 -0
  15. package/native-src/src/encrypted_socket.h +239 -0
  16. package/native-src/src/file_transfer.cpp +1808 -0
  17. package/native-src/src/file_transfer.h +567 -0
  18. package/native-src/src/fs.cpp +639 -0
  19. package/native-src/src/fs.h +108 -0
  20. package/native-src/src/gossipsub.cpp +1137 -0
  21. package/native-src/src/gossipsub.h +403 -0
  22. package/native-src/src/ice.cpp +1386 -0
  23. package/native-src/src/ice.h +328 -0
  24. package/native-src/src/json.hpp +25526 -0
  25. package/native-src/src/krpc.cpp +558 -0
  26. package/native-src/src/krpc.h +145 -0
  27. package/native-src/src/librats.cpp +2735 -0
  28. package/native-src/src/librats.h +1732 -0
  29. package/native-src/src/librats_bittorrent.cpp +167 -0
  30. package/native-src/src/librats_c.cpp +1333 -0
  31. package/native-src/src/librats_c.h +239 -0
  32. package/native-src/src/librats_encryption.cpp +123 -0
  33. package/native-src/src/librats_file_transfer.cpp +226 -0
  34. package/native-src/src/librats_gossipsub.cpp +293 -0
  35. package/native-src/src/librats_ice.cpp +515 -0
  36. package/native-src/src/librats_logging.cpp +158 -0
  37. package/native-src/src/librats_mdns.cpp +171 -0
  38. package/native-src/src/librats_nat.cpp +571 -0
  39. package/native-src/src/librats_persistence.cpp +815 -0
  40. package/native-src/src/logger.h +412 -0
  41. package/native-src/src/mdns.cpp +1178 -0
  42. package/native-src/src/mdns.h +253 -0
  43. package/native-src/src/network_utils.cpp +598 -0
  44. package/native-src/src/network_utils.h +162 -0
  45. package/native-src/src/noise.cpp +981 -0
  46. package/native-src/src/noise.h +227 -0
  47. package/native-src/src/os.cpp +371 -0
  48. package/native-src/src/os.h +40 -0
  49. package/native-src/src/rats_export.h +17 -0
  50. package/native-src/src/sha1.cpp +163 -0
  51. package/native-src/src/sha1.h +42 -0
  52. package/native-src/src/socket.cpp +1376 -0
  53. package/native-src/src/socket.h +309 -0
  54. package/native-src/src/stun.cpp +484 -0
  55. package/native-src/src/stun.h +349 -0
  56. package/native-src/src/threadmanager.cpp +105 -0
  57. package/native-src/src/threadmanager.h +53 -0
  58. package/native-src/src/tracker.cpp +1110 -0
  59. package/native-src/src/tracker.h +268 -0
  60. package/native-src/src/version.cpp +24 -0
  61. package/native-src/src/version.h.in +45 -0
  62. package/native-src/version.rc.in +31 -0
  63. package/package.json +2 -8
  64. package/scripts/build-librats.js +59 -12
  65. package/scripts/prepare-package.js +133 -37
  66. package/src/librats_node.cpp +46 -1
@@ -0,0 +1,508 @@
1
+ #pragma once
2
+
3
+ #include "socket.h"
4
+ #include "krpc.h"
5
+ #include <string>
6
+ #include <vector>
7
+ #include <array>
8
+ #include <unordered_map>
9
+ #include <unordered_set>
10
+ #include <functional>
11
+ #include <thread>
12
+ #include <mutex>
13
+ #include <atomic>
14
+ #include <chrono>
15
+ #include <memory>
16
+ #include <condition_variable>
17
+
18
+ // Hash specialization for Peer and NodeId (must be defined before use in unordered_map/set)
19
+ namespace std {
20
+ template<>
21
+ struct hash<librats::Peer> {
22
+ std::size_t operator()(const librats::Peer& peer) const noexcept {
23
+ std::hash<std::string> hasher;
24
+ return hasher(peer.ip + ":" + std::to_string(peer.port));
25
+ }
26
+ };
27
+
28
+ template<>
29
+ struct hash<array<uint8_t, 20>> {
30
+ std::size_t operator()(const array<uint8_t, 20>& id) const noexcept {
31
+ std::size_t seed = 0;
32
+ std::hash<uint8_t> hasher;
33
+ for (const auto& byte : id) {
34
+ seed ^= hasher(byte) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
35
+ }
36
+ return seed;
37
+ }
38
+ };
39
+ }
40
+
41
+ namespace librats {
42
+
43
+ // Constants for Kademlia DHT
44
+ constexpr size_t NODE_ID_SIZE = 20; // 160 bits = 20 bytes
45
+ constexpr size_t K_BUCKET_SIZE = 8; // Maximum nodes per k-bucket
46
+ constexpr size_t ALPHA = 3; // Concurrency parameter
47
+ constexpr int DHT_PORT = 6881; // Standard BitTorrent DHT port
48
+
49
+ using NodeId = std::array<uint8_t, NODE_ID_SIZE>;
50
+ using InfoHash = std::array<uint8_t, NODE_ID_SIZE>;
51
+
52
+ /**
53
+ * Search node state flags (bitfield)
54
+ * Flags can be combined to track the full history of a node in a search.
55
+ */
56
+ namespace SearchNodeFlags {
57
+ constexpr uint8_t QUERIED = 1 << 0; // Query has been sent to this node
58
+ constexpr uint8_t SHORT_TIMEOUT = 1 << 1; // Node exceeded short timeout (slot freed, still waiting)
59
+ constexpr uint8_t RESPONDED = 1 << 2; // Node successfully responded
60
+ constexpr uint8_t TIMED_OUT = 1 << 3; // Node fully timed out (failed)
61
+ constexpr uint8_t ABANDONED = 1 << 4; // Node was discarded during search truncation
62
+ }
63
+
64
+ /**
65
+ * DHT Node information
66
+ * Based on libtorrent's node_entry with fail_count and RTT tracking
67
+ */
68
+ struct DhtNode {
69
+ NodeId id;
70
+ Peer peer;
71
+ std::chrono::steady_clock::time_point last_seen;
72
+
73
+ // Round-trip time in milliseconds (0xffff = unknown, lower is better)
74
+ uint16_t rtt = 0xffff;
75
+
76
+ // Number of consecutive failures (0xff = never pinged, 0 = confirmed good)
77
+ uint8_t fail_count = 0xff;
78
+
79
+ DhtNode() : last_seen(std::chrono::steady_clock::now()) {}
80
+ DhtNode(const NodeId& id, const Peer& peer)
81
+ : id(id), peer(peer), last_seen(std::chrono::steady_clock::now()) {}
82
+
83
+ // Has this node ever responded to us?
84
+ bool pinged() const { return fail_count != 0xff; }
85
+
86
+ // Is this node confirmed good? (responded with no recent failures)
87
+ bool confirmed() const { return fail_count == 0; }
88
+
89
+ // Mark node as failed (timed out)
90
+ void mark_failed() {
91
+ if (pinged() && fail_count < 0xfe) ++fail_count;
92
+ }
93
+
94
+ // Mark node as successful (responded)
95
+ void mark_success() {
96
+ fail_count = 0;
97
+ last_seen = std::chrono::steady_clock::now();
98
+ }
99
+
100
+ // Update RTT with exponential moving average
101
+ void update_rtt(uint16_t new_rtt) {
102
+ if (new_rtt == 0xffff) return;
103
+ if (rtt == 0xffff) {
104
+ rtt = new_rtt;
105
+ } else {
106
+ // Weighted average: 2/3 old + 1/3 new
107
+ rtt = static_cast<uint16_t>(rtt * 2 / 3 + new_rtt / 3);
108
+ }
109
+ }
110
+
111
+ // Compare nodes: "less" means "better" node
112
+ // Priority: confirmed > not confirmed, then lower fail_count, then lower RTT
113
+ bool is_worse_than(const DhtNode& other) const {
114
+ // Nodes with failures are worse
115
+ if (fail_count != other.fail_count) {
116
+ return fail_count > other.fail_count;
117
+ }
118
+ // Higher RTT is worse
119
+ return rtt > other.rtt;
120
+ }
121
+ };
122
+
123
+
124
+
125
+ /**
126
+ * Peer discovery callback
127
+ */
128
+ using PeerDiscoveryCallback = std::function<void(const std::vector<Peer>& peers, const InfoHash& info_hash)>;
129
+
130
+ /**
131
+ * Deferred callbacks structure for avoiding deadlock
132
+ * Callbacks are collected while holding the mutex, then invoked after releasing it
133
+ */
134
+ struct DeferredCallbacks {
135
+ std::vector<PeerDiscoveryCallback> callbacks;
136
+ std::vector<Peer> peers;
137
+ InfoHash info_hash;
138
+ bool should_invoke = false;
139
+
140
+ void invoke() {
141
+ if (should_invoke) {
142
+ for (const auto& cb : callbacks) {
143
+ if (cb) cb(peers, info_hash);
144
+ }
145
+ }
146
+ }
147
+ };
148
+
149
+ /**
150
+ * DHT Kademlia implementation
151
+ */
152
+ class DhtClient {
153
+ public:
154
+ /**
155
+ * Constructor
156
+ * @param port The UDP port to bind to (default: 6881)
157
+ * @param bind_address The interface IP address to bind to (empty for all interfaces)
158
+ */
159
+ DhtClient(int port = DHT_PORT, const std::string& bind_address = "", const std::string& data_directory = "");
160
+
161
+ /**
162
+ * Destructor
163
+ */
164
+ ~DhtClient();
165
+
166
+ /**
167
+ * Start the DHT client
168
+ * @return true if successful, false otherwise
169
+ */
170
+ bool start();
171
+
172
+ /**
173
+ * Stop the DHT client
174
+ */
175
+ void stop();
176
+
177
+ /**
178
+ * Trigger immediate shutdown of all background threads
179
+ */
180
+ void shutdown_immediate();
181
+
182
+ /**
183
+ * Bootstrap the DHT with known nodes
184
+ * @param bootstrap_nodes Vector of bootstrap nodes
185
+ * @return true if successful, false otherwise
186
+ */
187
+ bool bootstrap(const std::vector<Peer>& bootstrap_nodes);
188
+
189
+ /**
190
+ * Find peers for a specific info hash
191
+ * @param info_hash The info hash to search for
192
+ * @param callback Callback to receive discovered peers
193
+ * @return true if search started successfully, false otherwise
194
+ */
195
+ bool find_peers(const InfoHash& info_hash, PeerDiscoveryCallback callback);
196
+
197
+ /**
198
+ * Announce that this node is a peer for a specific info hash
199
+ * @param info_hash The info hash to announce
200
+ * @param port The port to announce (0 for DHT port)
201
+ * @param callback Optional callback to receive discovered peers during traversal
202
+ * @return true if announcement started successfully, false otherwise
203
+ */
204
+ bool announce_peer(const InfoHash& info_hash, uint16_t port = 0, PeerDiscoveryCallback callback = nullptr);
205
+
206
+ /**
207
+ * Get our node ID
208
+ * @return The node ID
209
+ */
210
+ const NodeId& get_node_id() const { return node_id_; }
211
+
212
+ /**
213
+ * Get number of nodes in routing table
214
+ * @return Number of nodes
215
+ */
216
+ size_t get_routing_table_size() const;
217
+
218
+ /**
219
+ * Get number of pending ping verifications
220
+ * @return Number of pending ping verifications
221
+ */
222
+ size_t get_pending_ping_verifications_count() const;
223
+
224
+ /**
225
+ * Check if a search is currently active for an info hash
226
+ * @param info_hash The info hash to check
227
+ * @return true if search is active, false otherwise
228
+ */
229
+ bool is_search_active(const InfoHash& info_hash) const;
230
+
231
+ /**
232
+ * Check if an announce is currently active for an info hash
233
+ * @param info_hash The info hash to check
234
+ * @return true if announce is active, false otherwise
235
+ */
236
+ bool is_announce_active(const InfoHash& info_hash) const;
237
+
238
+ /**
239
+ * Get number of active searches
240
+ * @return Number of active searches
241
+ */
242
+ size_t get_active_searches_count() const;
243
+
244
+ /**
245
+ * Get number of active announces
246
+ * @return Number of active announces
247
+ */
248
+ size_t get_active_announces_count() const;
249
+
250
+ /**
251
+ * Check if DHT is running
252
+ * @return true if running, false otherwise
253
+ */
254
+ bool is_running() const { return running_; }
255
+
256
+ /**
257
+ * Get default BitTorrent DHT bootstrap nodes
258
+ * @return Vector of bootstrap nodes
259
+ */
260
+ static std::vector<Peer> get_default_bootstrap_nodes();
261
+
262
+ /**
263
+ * Save routing table to disk
264
+ * @return true if successful, false otherwise
265
+ */
266
+ bool save_routing_table();
267
+
268
+ /**
269
+ * Load routing table from disk
270
+ * @return true if successful, false otherwise
271
+ */
272
+ bool load_routing_table();
273
+
274
+ /**
275
+ * Set data directory for persistence
276
+ * @param directory Directory path
277
+ */
278
+ void set_data_directory(const std::string& directory);
279
+
280
+ private:
281
+ int port_;
282
+ std::string bind_address_;
283
+ std::string data_directory_;
284
+ NodeId node_id_;
285
+ socket_t socket_;
286
+ std::atomic<bool> running_;
287
+
288
+ // ============================================================================
289
+ // MUTEX LOCK ORDER - CRITICAL: Always acquire mutexes in this order to avoid deadlocks
290
+ // ============================================================================
291
+ // When acquiring multiple mutexes, ALWAYS follow this order:
292
+ //
293
+ // 1. pending_pings_mutex_ (Ping verification state, nodes_being_replaced_)
294
+ // 2. pending_searches_mutex_ (Search state and transaction mappings)
295
+ // 3. routing_table_mutex_ (core routing data)
296
+ // 4. announced_peers_mutex_ (Stored peer data)
297
+ // 5. peer_tokens_mutex_ (Token validation data)
298
+ // 6. shutdown_mutex_ (Lowest priority - can be locked independently)
299
+ //
300
+ // Routing table (k-buckets)
301
+ std::vector<std::vector<DhtNode>> routing_table_;
302
+ mutable std::mutex routing_table_mutex_; // Lock order: 3
303
+
304
+ // Tokens for peers (use Peer directly as key for efficiency)
305
+ struct PeerToken {
306
+ std::string token;
307
+ std::chrono::steady_clock::time_point created_at;
308
+
309
+ PeerToken() : created_at(std::chrono::steady_clock::now()) {}
310
+ PeerToken(const std::string& t)
311
+ : token(t), created_at(std::chrono::steady_clock::now()) {}
312
+ };
313
+ std::unordered_map<Peer, PeerToken> peer_tokens_;
314
+ std::mutex peer_tokens_mutex_; // Lock order: 6
315
+
316
+
317
+
318
+ // Pending find_peers tracking (to map transaction IDs to info_hash)
319
+ struct PendingSearch {
320
+ InfoHash info_hash;
321
+ std::chrono::steady_clock::time_point created_at;
322
+
323
+ // Iterative search state - search_nodes is sorted by distance to info_hash (closest first)
324
+ std::vector<DhtNode> search_nodes;
325
+ std::vector<Peer> found_peers; // found peers for this search
326
+ // Single map tracking node states using SearchNodeFlags bitfield
327
+ // A node is "known" if it exists in this map (any flags set or value 0)
328
+ std::unordered_map<NodeId, uint8_t> node_states;
329
+
330
+ int invoke_count; // number of outstanding requests
331
+ int branch_factor; // adaptive concurrency limit (starts at ALPHA)
332
+ bool is_finished; // whether the search is finished
333
+
334
+ // Callbacks to invoke when peers are found (supports multiple concurrent searches for same info_hash)
335
+ std::vector<PeerDiscoveryCallback> callbacks;
336
+
337
+ // Announce support: tokens collected during traversal (BEP 5 compliant)
338
+ // Maps node_id -> write_token received from that node
339
+ std::unordered_map<NodeId, std::string> write_tokens;
340
+ bool is_announce; // true if this search is for announce_peer
341
+ uint16_t announce_port; // port to announce (only valid if is_announce)
342
+
343
+ PendingSearch(const InfoHash& hash)
344
+ : info_hash(hash), created_at(std::chrono::steady_clock::now()),
345
+ invoke_count(0), branch_factor(ALPHA), is_finished(false),
346
+ is_announce(false), announce_port(0) {}
347
+ };
348
+ std::unordered_map<std::string, PendingSearch> pending_searches_; // info_hash (hex) -> PendingSearch
349
+ mutable std::mutex pending_searches_mutex_; // Lock order: 2
350
+
351
+ // Transaction tracking with queried node info for proper responded_nodes tracking
352
+ struct SearchTransaction {
353
+ std::string info_hash_hex;
354
+ NodeId queried_node_id;
355
+ std::chrono::steady_clock::time_point sent_at;
356
+
357
+ SearchTransaction() = default;
358
+ SearchTransaction(const std::string& hash, const NodeId& id)
359
+ : info_hash_hex(hash), queried_node_id(id),
360
+ sent_at(std::chrono::steady_clock::now()) {}
361
+ };
362
+ std::unordered_map<std::string, SearchTransaction> transaction_to_search_; // transaction_id -> SearchTransaction
363
+
364
+ // Peer announcement storage (BEP 5 compliant)
365
+ struct AnnouncedPeer {
366
+ Peer peer;
367
+ std::chrono::steady_clock::time_point announced_at;
368
+
369
+ AnnouncedPeer(const Peer& p)
370
+ : peer(p), announced_at(std::chrono::steady_clock::now()) {}
371
+ };
372
+ // Map from info_hash (as hex string) to list of announced peers
373
+ std::unordered_map<std::string, std::vector<AnnouncedPeer>> announced_peers_;
374
+ std::mutex announced_peers_mutex_; // Lock order: 5
375
+
376
+ // Ping-before-replace eviction tracking (BEP 5 compliant)
377
+ // When a bucket is full and a new node wants to join:
378
+ // 1. We ping the WORST node in the bucket (highest RTT) to check if it's still alive
379
+ // 2. If old node responds -> keep it, discard candidate
380
+ // 3. If old node times out -> replace it with candidate
381
+ struct PingVerification {
382
+ DhtNode candidate_node; // The new node waiting to be added
383
+ DhtNode old_node; // The existing node we're pinging to verify it's alive
384
+ int bucket_index; // Which bucket this affects
385
+ std::chrono::steady_clock::time_point ping_sent_at;
386
+
387
+ PingVerification(const DhtNode& candidate, const DhtNode& old, int bucket_idx)
388
+ : candidate_node(candidate), old_node(old), bucket_index(bucket_idx),
389
+ ping_sent_at(std::chrono::steady_clock::now()) {}
390
+ };
391
+ std::unordered_map<std::string, PingVerification> pending_pings_; // transaction_id -> PingVerification
392
+ std::unordered_set<NodeId> nodes_being_replaced_; // Track old nodes that have pending ping verifications
393
+ mutable std::mutex pending_pings_mutex_; // Lock order: 1 (protects pending_pings_, nodes_being_replaced_)
394
+
395
+ // Network thread
396
+ std::thread network_thread_;
397
+ std::thread maintenance_thread_;
398
+
399
+ // Conditional variables for immediate shutdown
400
+ std::condition_variable shutdown_cv_;
401
+ std::mutex shutdown_mutex_; // Lock order: 7 (can be locked independently)
402
+
403
+ // Helper functions
404
+ void network_loop();
405
+ void maintenance_loop();
406
+ void handle_message(const std::vector<uint8_t>& data, const Peer& sender);
407
+
408
+
409
+
410
+ // KRPC protocol handlers
411
+ void handle_krpc_message(const KrpcMessage& message, const Peer& sender);
412
+ void handle_krpc_ping(const KrpcMessage& message, const Peer& sender);
413
+ void handle_krpc_find_node(const KrpcMessage& message, const Peer& sender);
414
+ void handle_krpc_get_peers(const KrpcMessage& message, const Peer& sender);
415
+ void handle_krpc_announce_peer(const KrpcMessage& message, const Peer& sender);
416
+ void handle_krpc_response(const KrpcMessage& message, const Peer& sender);
417
+ void handle_krpc_error(const KrpcMessage& message, const Peer& sender);
418
+
419
+ // KRPC protocol sending
420
+ bool send_krpc_message(const KrpcMessage& message, const Peer& peer);
421
+ void send_krpc_ping(const Peer& peer);
422
+ void send_krpc_find_node(const Peer& peer, const NodeId& target);
423
+ void send_krpc_get_peers(const Peer& peer, const InfoHash& info_hash);
424
+ void send_krpc_announce_peer(const Peer& peer, const InfoHash& info_hash, uint16_t port, const std::string& token);
425
+
426
+ void add_node(const DhtNode& node, bool confirmed = true);
427
+ std::vector<DhtNode> find_closest_nodes(const NodeId& target, size_t count = K_BUCKET_SIZE);
428
+ std::vector<DhtNode> find_closest_nodes_unlocked(const NodeId& target, size_t count = K_BUCKET_SIZE);
429
+ int get_bucket_index(const NodeId& id);
430
+
431
+ NodeId generate_node_id();
432
+ NodeId xor_distance(const NodeId& a, const NodeId& b);
433
+ bool is_closer(const NodeId& a, const NodeId& b, const NodeId& target);
434
+
435
+
436
+ std::string generate_token(const Peer& peer);
437
+ bool verify_token(const Peer& peer, const std::string& token);
438
+
439
+
440
+
441
+ void cleanup_stale_nodes();
442
+ void cleanup_stale_peer_tokens();
443
+ void refresh_buckets();
444
+ void print_statistics();
445
+
446
+ // Pending search management
447
+ void cleanup_stale_searches();
448
+ void cleanup_timed_out_search_requests();
449
+ void cleanup_search_node_states();
450
+ void handle_get_peers_response_for_search(const std::string& transaction_id, const Peer& responder, const std::vector<Peer>& peers);
451
+ void handle_get_peers_response_with_nodes(const std::string& transaction_id, const Peer& responder, const std::vector<KrpcNode>& nodes);
452
+ void handle_get_peers_empty_response(const std::string& transaction_id, const Peer& responder);
453
+ void save_write_token(PendingSearch& search, const NodeId& node_id, const std::string& token);
454
+ bool add_search_requests(PendingSearch& search, DeferredCallbacks& deferred);
455
+ void add_node_to_search(PendingSearch& search, const DhtNode& node);
456
+ void send_announce_to_closest_nodes(PendingSearch& search);
457
+
458
+ // Peer announcement storage management
459
+ void store_announced_peer(const InfoHash& info_hash, const Peer& peer);
460
+ std::vector<Peer> get_announced_peers(const InfoHash& info_hash);
461
+ void cleanup_stale_announced_peers();
462
+
463
+ // Ping-before-replace eviction management
464
+ void initiate_ping_verification(const DhtNode& candidate_node, const DhtNode& old_node, int bucket_index);
465
+ void handle_ping_verification_response(const std::string& transaction_id, const NodeId& responder_id, const Peer& responder);
466
+ void cleanup_stale_ping_verifications();
467
+ bool perform_replacement(const DhtNode& candidate_node, const DhtNode& node_to_replace, int bucket_index);
468
+
469
+ // Conversion utilities
470
+ static KrpcNode dht_node_to_krpc_node(const DhtNode& node);
471
+ static DhtNode krpc_node_to_dht_node(const KrpcNode& node);
472
+ static std::vector<KrpcNode> dht_nodes_to_krpc_nodes(const std::vector<DhtNode>& nodes);
473
+ static std::vector<DhtNode> krpc_nodes_to_dht_nodes(const std::vector<KrpcNode>& nodes);
474
+ };
475
+
476
+ /**
477
+ * Utility functions
478
+ */
479
+
480
+ /**
481
+ * Convert string to NodeId
482
+ * @param str The string to convert (must be 20 bytes)
483
+ * @return NodeId
484
+ */
485
+ NodeId string_to_node_id(const std::string& str);
486
+
487
+ /**
488
+ * Convert NodeId to string
489
+ * @param id The NodeId to convert
490
+ * @return String representation
491
+ */
492
+ std::string node_id_to_string(const NodeId& id);
493
+
494
+ /**
495
+ * Convert hex string to NodeId
496
+ * @param hex The hex string to convert (must be 40 characters)
497
+ * @return NodeId
498
+ */
499
+ NodeId hex_to_node_id(const std::string& hex);
500
+
501
+ /**
502
+ * Convert NodeId to hex string
503
+ * @param id The NodeId to convert
504
+ * @return Hex string representation
505
+ */
506
+ std::string node_id_to_hex(const NodeId& id);
507
+
508
+ } // namespace librats