Compare commits

...

306 Commits

Author SHA1 Message Date
Mark Qvist 795fdaa2b0
Updated versions 2026-07-20 18:49:54 +02:00
Mark Qvist 7bb4bcfbc8
Updated logging 2026-07-20 18:48:52 +02:00
Mark Qvist d909619eb1
Added inbound message resource transfer tracking and ability to cancel incoming messages 2026-07-16 15:16:09 +02:00
Mark Qvist 5769d46389
Improved logging 2026-07-16 10:22:12 +02:00
Mark Qvist 3e2cd361af
Updated version 2026-07-16 02:52:56 +02:00
Mark Qvist 241b29cc92
Yes, it will run on potato, then 2026-07-16 02:50:10 +02:00
Mark Qvist ca02fa5221
Added lxmd config options stamp_cost, sequential_pn_stamp_validation, static_peers_bypass_sequential, max_inbound_syncs. Added name sanitization to peers output. 2026-07-16 02:10:50 +02:00
Mark Qvist 982c9fc743
Early sync complete if no messages exist after requirements filtration 2026-07-16 01:11:36 +02:00
Mark Qvist 4a93697511
Fixed PN peer offer preparation not adhering to minimum accepted stamp cost for message offer filtration 2026-07-15 20:41:58 +02:00
Mark Qvist 548be107c8
Added transient ID processing locks 2026-07-15 20:08:14 +02:00
Mark Qvist 8395793bad
Ensure atomic writes 2026-07-12 01:27:38 +02:00
Mark Qvist fb0fd24c98
Updated versions 2026-07-10 15:39:15 +02:00
Mark Qvist fab12ad9bf
Updated build scripts 2026-06-01 14:26:47 +02:00
Mark Qvist a29c4a0e17
Updated version 2026-06-01 00:03:39 +02:00
Mark Qvist 20864133a3
Fixed typo 2026-05-31 22:10:48 +02:00
Mark Qvist c877efaec1
Updated version 2026-05-29 09:58:06 +02:00
Mark Qvist 11b2480223
Updated versions 2026-05-29 09:57:51 +02:00
Jeremy O'Brien 5be161cb1e
Prevent message write race among different processes touching the same message 2026-05-28 08:44:22 -04:00
Mark Qvist bf924c739c Cleanup 2026-05-27 12:28:58 +02:00
Mark Qvist 044f3d2879 Updated versions 2026-05-26 23:32:02 +02:00
Mark Qvist 312e0a8ded Updated version 2026-05-26 13:18:33 +02:00
Mark Qvist 575fb7d77d Prevent LXM persist race in write_to_directory when messages change state within very short time spans 2026-05-26 13:17:46 +02:00
Mark Qvist 2ac2b100ae Automatically drop LXMs from blackholed identities 2026-05-26 13:14:05 +02:00
Mark Qvist 599406ed0f Activated compression support signalling 2026-05-25 16:59:24 +02:00
Parnikkapore 55620bf45c Fix lxmd crash when defaulting --config to ~/.config/lxmd/ 2026-05-25 00:46:59 +02:00
Mark Qvist b53a3ce37d Clarified FIELD_THREAD 2026-05-24 23:16:15 +02:00
Mark Qvist 764758d185 Added reply, reaction and comment field standards to LXMF.py 2026-05-24 22:59:57 +02:00
Mark Qvist b415a136f3 Updated makefile 2026-05-21 17:58:59 +02:00
Mark Qvist dffbf4dfe1 Updated versions 2026-05-21 16:17:35 +02:00
Mark Qvist 84613f8b44 Updated versions 2026-05-20 19:56:35 +02:00
Mark Qvist 1bef747306 Cleanup 2026-05-20 19:55:03 +02:00
Mark Qvist d6ec05193b Ensure different tmp-file names if os.getpid() is not available 2026-05-20 19:52:55 +02:00
Jeremy O'Brien 7f0e2627d1 atomically write lxmf message files to prevent reading of partial files from other processes during the write 2026-05-20 09:47:33 -04:00
Mark Qvist 29c79177a6 Updated makefile 2026-05-20 00:33:46 +02:00
Mark Qvist 7b0e702832 Updated versions 2026-05-10 17:20:26 +02:00
Mark Qvist 0cb62ddc36 Use stamp generation worker context manager on Python 3.14+ 2026-05-10 17:16:38 +02:00
Mark Qvist a8505eade9 Added outbound processing trigger for pn node announces 2026-05-10 14:12:51 +02:00
Mark Qvist 6a00d82b28 Updated versions 2026-05-07 20:23:56 +02:00
Mark Qvist 2c4dfdd50e Updated versions 2026-05-06 18:45:57 +02:00
Mark Qvist 83fbe80497 Strip null bytes from display names by default 2026-05-06 18:45:41 +02:00
Mark Qvist 25b7fcf7de Updated versions 2026-05-06 17:20:03 +02:00
Mark Qvist 189f5239a0 Raise explicit IOError for the sloppy vibe-coders who apparently can't figure out that you need to set an outbound propagation node before trying to send a propagated message 2026-05-06 17:19:16 +02:00
Mark Qvist f9967db3ee Updated readme 2026-05-02 18:54:07 +02:00
Mark Qvist ad616fc18e Updated readme 2026-05-02 18:53:10 +02:00
Mark Qvist 9d413c0759 Updated readme 2026-05-02 18:51:56 +02:00
Mark Qvist 4ecbdb334e Updated readme 2026-05-02 18:46:24 +02:00
Mark Qvist 727830cefd Update versions 2026-04-28 18:53:48 +02:00
Mark Qvist 8499729024 Updated versions 2026-04-22 13:40:02 +02:00
Mark Qvist 3f0681fa99 Versions 2026-04-21 19:29:01 +02:00
Mark Qvist eb67b19a5b Versions 2026-04-21 17:24:16 +02:00
Mark Qvist 8bdb434e05 Retain announce data for used destinations 2026-04-21 13:19:07 +02:00
Mark Qvist 41b75737e9 Updated version 2026-04-21 10:50:34 +02:00
Mark Qvist 72932eea28 Added compression support signalling 2026-04-19 13:27:32 +02:00
Mark Qvist 0886795481 Updated versions 2026-04-18 16:36:29 +02:00
Mark Qvist 269ce43afc Added Zen of Reticulum 2026-01-10 21:50:42 +01:00
Mark Qvist 2ad82b68bd Announce control destination 2026-01-06 21:01:53 +01:00
Mark Qvist ba2c6b833e Versions 2026-01-06 17:55:31 +01:00
Mark Qvist 2b7ba9558b Cleanup 2026-01-06 17:03:09 +01:00
Mark Qvist ef2e1234a5 Fix 2026-01-06 13:57:12 +01:00
Mark Qvist 7c9bdd7fa2 Fix 2026-01-04 01:32:40 +01:00
Mark Qvist 72853fcf77 Updated versions 2026-01-04 01:05:01 +01:00
Mark Qvist 97f8c105af Updated readme 2025-12-28 00:55:10 +01:00
Mark Qvist 6ecd271e48 Updated readme 2025-12-22 22:22:17 +01:00
Mark Qvist 694f2413ea Added more descriptive error if propagation node peers file is corrupt 2025-12-02 20:43:44 +01:00
Mark Qvist a6f5a56a38 Improved outbound message processing speed 2025-12-02 20:17:46 +01:00
Mark Qvist f4c805ea35 Updated makefile 2025-11-27 18:38:52 +01:00
Mark Qvist 7c71eb1df4 Cleanup 2025-11-24 22:02:30 +01:00
Mark Qvist bc7522b63d Updated version 2025-11-13 19:42:24 +01:00
Mark Qvist 39e398be65 Fixed missing PN config unpack on incoming sync auto-peering 2025-11-13 17:48:10 +01:00
Mark Qvist ee15e9f0b6 Updated version 2025-11-08 14:30:47 +01:00
Mark Qvist 00ffbc09fe Using multiprocessing start method fork on Linux to avoid issues with Python 3.14. Fixes #35. 2025-11-08 01:20:31 +01:00
Mark Qvist dca6cc2adc Ensure LXMF and RNS exit handlers are called on SIGINT and SIGTERM, since for some ungodly reason atexit events are not always called on some combinations of Python version and platforms, even though they have been registered. 2025-11-07 23:10:30 +01:00
Mark Qvist 62038573f1 Updated version 2025-11-03 22:21:13 +01:00
Mark Qvist fa2d78c351 Fixed message stamps getting overwritten if propagation stamp was also present 2025-11-03 22:19:20 +01:00
Mark Qvist f18ce9ea99 Cleanup 2025-11-03 00:08:50 +01:00
Mark Qvist d7a2979dd0 Cleanup 2025-11-02 23:07:43 +01:00
Mark Qvist 0b067914ea Adjusted default max peering cost 2025-11-02 20:41:03 +01:00
Mark Qvist bc3f4ecff5 Handle client propagation transfer limits separately from PN peers 2025-11-02 18:35:15 +01:00
Mark Qvist 99830b6e8b Only index messages with stamp value set 2025-11-02 12:02:21 +01:00
Mark Qvist 1b5dc419b5 Merge messagestore check 2025-11-02 11:38:15 +01:00
Mark Qvist 9c5fa4a066 Take stamp value into account when cleaning message store 2025-11-02 11:35:12 +01:00
Mark Qvist 383d953e06 Don't hog all cores on high core-count machines 2025-11-02 11:34:22 +01:00
markqvist 557887d13f
Merge pull request #34 from busimus/master
Fix stored messages always getting deleted if config path has underscores
2025-11-02 11:33:00 +01:00
bus e1905b85d7
Fix stored messages always getting deleted if config path has underscores 2025-11-02 14:12:33 +06:00
Mark Qvist 8e3ffb0d2a Stat query default identity as local 2025-11-02 02:48:19 +01:00
Mark Qvist f383450b37 Implemented duplicate signalling on PN message download 2025-11-02 02:29:32 +01:00
Mark Qvist 747ddbddd5 Implemented duplicate signalling on PN message download 2025-11-02 02:27:57 +01:00
Mark Qvist d0f3385f75 Added acceptance rate to lxmd stats output 2025-11-01 18:48:31 +01:00
Mark Qvist 401328fa16 Allow configuring max remote peering cost 2025-11-01 18:27:11 +01:00
Mark Qvist 4350a239e4 Cleanup 2025-11-01 17:35:36 +01:00
Mark Qvist 9dc998f149 Added peering break option to lxmd command line interface 2025-11-01 17:32:45 +01:00
Mark Qvist fa9fd2ae01 Added remote status and control by allow-list for lxmd 2025-11-01 13:10:28 +01:00
Mark Qvist 0cebd5886d Allow specifying remote lxmd destination for status and control 2025-11-01 12:49:32 +01:00
Mark Qvist b35b9213a6 Implemented throttling for naughty propagation node peers 2025-11-01 01:35:15 +01:00
Mark Qvist df6271a026 Handle client message download for stamped propagation messages 2025-11-01 00:10:30 +01:00
Mark Qvist 4afb92bf3e Added peer sync option to lxmd command line interface 2025-11-01 00:09:23 +01:00
Mark Qvist 0a5edb2895 Implemented propagation node name configuration and inclusion in announce metadata 2025-10-31 22:24:55 +01:00
Mark Qvist 704b37dc16 Implemented client-side propagation stamp generation and inclusion in outbound propagation messages 2025-10-31 21:45:40 +01:00
Mark Qvist 606a723e31 Implemented getting static peer configuration from network if unknown at launch 2025-10-31 17:11:17 +01:00
Mark Qvist a44c1f368a Validate peering key on incoming sync offer 2025-10-31 17:02:34 +01:00
Mark Qvist 434267784d Implemented propagation node peering key generation and peering cost signalling 2025-10-31 13:53:59 +01:00
Mark Qvist 9c646aead7 Stamp validation on incoming propagation node transfers 2025-10-31 02:19:24 +01:00
Mark Qvist ebc8bb33c2 Cleanup 2025-10-31 00:28:06 +01:00
Mark Qvist 60bf99d151 Cleanup 2025-10-31 00:25:46 +01:00
Mark Qvist c84aea745a Improved auto-peering on inbound PN sync. Added support for persisting and loading transient message stamp status. Implemented getting transient message stamp value. 2025-10-30 21:19:38 +01:00
Mark Qvist a62ffa12b1 Cleanup 2025-10-30 19:45:40 +01:00
Mark Qvist 6446db4f11 Break peering when peer moves outside auto-peering range 2025-10-30 17:03:05 +01:00
Mark Qvist 81a6d503a3 Automatic legacy PN de-peering 2025-10-30 16:55:44 +01:00
Mark Qvist c28d3b1432 Added metadate structure to propagation node announces 2025-10-30 16:44:15 +01:00
Mark Qvist d8b25e092f Added metadate structure to propagation node announces 2025-10-30 16:43:26 +01:00
Mark Qvist aca5bf9c14 Cleanup 2025-10-30 16:06:53 +01:00
Mark Qvist bd6fe9f9d1 Handle propagation node stamp cost peering signalling 2025-10-30 15:39:00 +01:00
Mark Qvist 0f2d3b06c2 Also receive PN path response announces, but only update peering data on live announces 2025-10-30 15:18:09 +01:00
Mark Qvist 3f91e44a6d Updated RNS version 2025-10-30 14:10:14 +01:00
Mark Qvist 8f54d40abf Updated version 2025-10-30 14:09:54 +01:00
Mark Qvist 9beeafb0c8 Propagation node stamp cost handling 2025-10-30 14:08:39 +01:00
Mark Qvist b572723a5e Cleanup 2025-10-30 13:49:26 +01:00
Mark Qvist 6cf7852271 Cleanup 2025-10-30 13:14:59 +01:00
Mark Qvist e17263d25a Cleanup 2025-10-30 12:41:18 +01:00
Mark Qvist 16dfbc22cd Propagation stamp validation via pool dispatch 2025-10-30 12:38:49 +01:00
Mark Qvist 98347d3ad9 Increased PN peer sync frequency 2025-10-29 23:25:15 +01:00
Mark Qvist 61b75526d2 Added separate propagation node per-message and sync transfer limits. Added persistent PN sync strategy. Added concurrent PN peer sync. 2025-10-29 23:24:45 +01:00
Mark Qvist 85d8f4f583 Updated requirements 2025-07-13 13:42:01 +02:00
Mark Qvist 5b9f121593 Updated version and RNS dependency version 2025-07-13 13:24:52 +02:00
Mark Qvist 416ccf294f Retry path request on unexpectedly closed link 2025-07-13 13:24:46 +02:00
Mark Qvist 787cd069dc Fixed division by zero. Closes #30. 2025-05-26 20:57:46 +02:00
Mark Qvist c2207d1eb7 Added funding 2025-05-17 10:27:21 +02:00
Mark Qvist a9622e3a33 Updated version 2025-05-15 20:30:12 +02:00
Mark Qvist 499fe4cc53 Use no_data_for instead of inactive_for for cleaning links 2025-05-15 20:27:19 +02:00
Mark Qvist 37e99910ec Updated version and RNS dependency version 2025-05-12 11:58:24 +02:00
Mark Qvist 005d71707c Cleanup 2025-04-17 13:31:00 +02:00
Mark Qvist 1bdcf6ad53 Updated license 2025-04-15 20:21:54 +02:00
Mark Qvist e6021b8fed Updated license 2025-04-15 20:21:16 +02:00
Mark Qvist 326c0eed8f Updated version 2025-03-13 19:46:11 +01:00
Mark Qvist 336792c07a Updated dependencies 2025-03-13 19:45:15 +01:00
Mark Qvist 570d2c6846 Added configuration options to default config file 2025-03-07 11:05:50 +01:00
Mark Qvist 1ef4665073 Cleanup 2025-02-18 20:05:19 +01:00
Mark Qvist d5540b927f Added allow_duplicate option to message ingest API 2025-01-31 13:38:56 +01:00
Mark Qvist a6cf585109 Cleanup 2025-01-30 15:11:26 +01:00
Mark Qvist c0a8f3be49 Cleanup 2025-01-30 15:04:21 +01:00
Mark Qvist 7b4780cfb7 Automatically clean messages exceeding propagation transfer limit for peer from unhandled message queues 2025-01-30 11:36:11 +01:00
Mark Qvist b94a712bb6 Automatically clean messages exceeding propagation transfer limit for peer from unhandled message queues 2025-01-30 11:30:45 +01:00
Mark Qvist f42ccfc4e9 Automatically clean messages exceeding propagation transfer limit for peer from unhandled message queues 2025-01-30 11:23:18 +01:00
Mark Qvist 9eca747757 Updated peer rotation timing to align with distribution queue mapping 2025-01-30 10:46:31 +01:00
Mark Qvist b7b6753640 Fixed potential division by zero. Fixes #25. 2025-01-30 00:37:50 +01:00
Mark Qvist 40d0b9a5de Added acceptance rate threshold to peer rotation 2025-01-29 21:21:51 +01:00
Mark Qvist 40fc75f559 Refined peer rotation algorithm 2025-01-29 14:24:09 +01:00
Mark Qvist f1d060a92e Added peer rotation 2025-01-29 01:26:36 +01:00
Mark Qvist e0e901291e Updated logging 2025-01-27 12:04:16 +01:00
Mark Qvist 886ac69a82 Tear down control link after use 2025-01-27 12:04:05 +01:00
Mark Qvist e0163e100a Updated issue template 2025-01-27 10:26:11 +01:00
Mark Qvist 26a10cce8f Status query return code 2025-01-26 01:13:11 +01:00
Mark Qvist cec903a4dc Added status query API function 2025-01-24 14:05:12 +01:00
Mark Qvist 962d9c90d1 Added wanted inbound peers to PN announce data 2025-01-24 13:50:56 +01:00
Mark Qvist 6d2eb4f973 Updated default config 2025-01-24 00:26:47 +01:00
Mark Qvist a8cc5f41cf Fixed typo 2025-01-24 00:21:37 +01:00
Mark Qvist aa57b16cf5 Fixed #23 2025-01-24 00:09:36 +01:00
Mark Qvist cdea838a6c Updated status output 2025-01-23 17:43:24 +01:00
Mark Qvist fb4bf9b0b9 Cleanup 2025-01-23 17:36:30 +01:00
Mark Qvist a3e3868f92 Changed formatting 2025-01-23 17:09:40 +01:00
Mark Qvist 70186cf8d9 Fixed typo 2025-01-23 17:07:20 +01:00
Mark Qvist fe59b265c5 Fixed fstrings not working on Python < 3.12 2025-01-23 16:54:12 +01:00
Mark Qvist a87458d25f Updated version 2025-01-23 16:28:11 +01:00
Mark Qvist 35dd70c59e Format status and peers output 2025-01-23 16:27:48 +01:00
Mark Qvist a198e96064 Include unhandled message count in stats 2025-01-23 16:27:23 +01:00
Mark Qvist e3be7e0cfd Persist last sync attempt 2025-01-23 16:27:01 +01:00
Mark Qvist 460645cea2 Added lxmd status getter 2025-01-23 14:15:31 +01:00
Mark Qvist f683e03891 Added lxmd status getter 2025-01-23 14:15:12 +01:00
Mark Qvist 2c71cea7a0 Added local node stats request handler 2025-01-23 14:13:08 +01:00
Mark Qvist 61b1ecce27 Updated readme 2025-01-22 10:10:57 +01:00
Mark Qvist 68257a441f Set transfer limit on reverse auto-peer 2025-01-22 09:44:03 +01:00
Mark Qvist e69da2ed2a Added static peers and peering limit 2025-01-22 01:37:09 +01:00
Mark Qvist c2a08ef355 Enqueue and batch process distribution queue mappings 2025-01-21 20:44:11 +01:00
Mark Qvist 1430b1ce90 Enqueue and batch process distribution queue mappings 2025-01-21 20:20:39 +01:00
Mark Qvist 1c9c744107 Memory optimisations 2025-01-21 16:51:25 +01:00
Mark Qvist bfed126a7c Memory optimisations 2025-01-21 16:44:24 +01:00
Mark Qvist 44d1d992f8 Updated version 2025-01-21 16:34:00 +01:00
Mark Qvist 7701f326d9 Memory optimisations 2025-01-21 16:33:39 +01:00
Mark Qvist 356cb6412f Optimise structure overhead 2025-01-21 10:46:59 +01:00
Mark Qvist 7bd3cf986d Updated versions 2025-01-18 21:39:39 +01:00
Mark Qvist 3948c9a187 Added message reject on too large transfer size 2025-01-18 21:36:08 +01:00
Mark Qvist d6b1b9c94d Added ability to cancel stamp generation 2025-01-18 20:11:31 +01:00
Mark Qvist a676954116 Added ability to cancel outbound messages 2025-01-18 19:13:43 +01:00
Mark Qvist d97c4f292e Fixed missing checks for file corruption 2025-01-14 21:32:10 +01:00
Mark Qvist 2d175a331f Updated dependencies 2025-01-13 15:26:27 +01:00
Mark Qvist 976305b791 Sort waiting peers by sync transfer rate 2025-01-13 14:37:51 +01:00
Mark Qvist a6a42eff80 Add sync transfer rate to peer stats 2025-01-13 14:35:14 +01:00
Mark Qvist 96dddf1b3a Added handling of corrupted transient ID cache files 2024-12-23 12:36:53 +01:00
Mark Qvist c426c93cc5 Updated versions 2024-12-09 22:10:17 +01:00
Mark Qvist 1a43d93da2 Added message renderer field 2024-12-09 18:16:12 +01:00
Mark Qvist 575fbc9ffe Updated dependencies 2024-11-23 13:20:43 +01:00
Mark Qvist c21da895b6 Improved duplicate message detection when syncing from multiple different PNs 2024-11-23 13:20:24 +01:00
Mark Qvist b172c7fcd4 Added PN announce data validation to announce handler 2024-11-23 12:49:01 +01:00
Mark Qvist 61331b58d7 Updated version 2024-11-23 12:47:31 +01:00
Mark Qvist 9ff76c0473 Updated version 2024-10-13 14:01:10 +02:00
Mark Qvist c9272c9218 Fixed missing byteorder argument in stamp value calculation. Fixes #21. 2024-10-13 13:08:10 +02:00
Mark Qvist 36f0c17c8b Added RNR_REFS field 2024-10-13 13:05:52 +02:00
Mark Qvist aa406d1552 Updated version 2024-10-11 23:45:24 +02:00
Mark Qvist 0cb771439f Fixed incorrect progress values on path waiting 2024-10-11 23:40:27 +02:00
Mark Qvist 672d754238 Updated dependency 2024-10-06 11:13:38 +02:00
Mark Qvist 0178fb0d4f Don't reset propagation node address and links if already set to the same destination 2024-10-05 22:40:14 +02:00
Mark Qvist 19d8909b10 Fixed inbound ticket cleaning bug 2024-10-05 15:26:18 +02:00
Mark Qvist 19f0fa7724 Don't try creating LXMF peer destination until identity is resolved. Fixes #19. 2024-10-04 11:22:39 +02:00
Mark Qvist eefb1c8349 Updated versions 2024-10-02 02:06:00 +02:00
Mark Qvist 0e0d01a0b2 Updated versions 2024-09-30 19:28:13 +02:00
Mark Qvist ac2c9c3a9b Improved opportunistic delivery performance 2024-09-30 19:27:40 +02:00
Mark Qvist 05f144ae50 Updated version 2024-09-19 23:07:58 +02:00
Mark Qvist a461fd415a Rediscover path during opportunistic delivery if path fails 2024-09-19 23:00:24 +02:00
Mark Qvist ef1c3331ad Improved outbound handling 2024-09-17 13:29:36 +02:00
Mark Qvist e1a56be5c1 Pre-emptively request unknown paths for opportunistic messages 2024-09-17 13:18:16 +02:00
Mark Qvist cf6fc4a510 Update max pathless tries 2024-09-17 12:56:56 +02:00
Mark Qvist 67d21deff9 Updated dependencies 2024-09-16 20:11:53 +02:00
Mark Qvist efc15bde26 Updated version 2024-09-16 20:09:57 +02:00
Mark Qvist b5d6ed3d9b Automatically reconfigure to direct delivery if opportunistic message content is too large 2024-09-16 20:08:26 +02:00
Mark Qvist 7789e0bc26 Improved opportunistic delivery logic and performance 2024-09-16 17:49:54 +02:00
Mark Qvist 0e2f0fb090 Updated example 2024-09-16 17:49:22 +02:00
Mark Qvist 19696d206d Actually set property 2024-09-12 21:28:50 +02:00
Mark Qvist 9193aa5e02 Improved logging 2024-09-12 18:24:14 +02:00
Mark Qvist bbf1eda3b0 Added backchannel delivery 2024-09-12 17:57:26 +02:00
Mark Qvist d8e2e2a45f Fix stamp cost extractor function 2024-09-12 10:11:49 +02:00
Mark Qvist 0fc15e6054 Updated version 2024-09-11 11:48:36 +02:00
Mark Qvist 717240c8d4 Cleanup 2024-09-11 02:09:43 +02:00
Mark Qvist 453772859d Removed deprecated query destination 2024-09-10 20:17:09 +02:00
Mark Qvist 98eea1171e Added event field 2024-09-09 15:24:36 +02:00
Mark Qvist ea49d22bce Add test entrypoint to LXStamper 2024-09-09 13:20:54 +02:00
Mark Qvist 058186dfdc Cleanup 2024-09-09 13:15:37 +02:00
Mark Qvist 8134672436 Cleanup 2024-09-09 02:12:27 +02:00
Mark Qvist ae0952455b Ignore stamp requirements for paper messages 2024-09-08 17:54:11 +02:00
Mark Qvist a20f380284 Get message ratchet IDs 2024-09-08 14:52:54 +02:00
Mark Qvist 6d83b019e1 Added stamp value property on inbound 2024-09-08 01:22:00 +02:00
Mark Qvist 40eb014c91 Implemented deferred multiprocessor stamp generation in the background 2024-09-07 22:40:13 +02:00
Mark Qvist 62f5a9eead Implemented ticket exchanges. Implemented stamp generation and validation by tickets. 2024-09-07 15:17:48 +02:00
Mark Qvist 74cbd11473 Cleanup 2024-09-07 11:37:39 +02:00
Mark Qvist 0d76eee6cd Stamp cost API functions and multi-process stamp generation on Android 2024-09-07 11:35:17 +02:00
Mark Qvist 4b5e27a5e2 Updated examples 2024-09-06 20:34:45 +02:00
Mark Qvist dcb0a18cd7 Added automatic signalling of stamp costs, stamp generation and validation 2024-09-06 20:34:09 +02:00
Mark Qvist 015dcc5631 Extract announced stamp cost in handlers 2024-09-06 19:55:18 +02:00
Mark Qvist 537f1823b6 Added utility functions 2024-09-06 19:54:28 +02:00
Mark Qvist fc99010a3d Updated example 2024-09-06 17:37:24 +02:00
Mark Qvist fe14f8744d Added base code for stamp generation and validation 2024-09-06 16:49:01 +02:00
Mark Qvist 4dca031441 Updated fields 2024-09-06 00:57:06 +02:00
Mark Qvist 5366f895b2 Added stamp generation to LXMessage 2024-09-06 00:51:36 +02:00
Mark Qvist 95ba8cba60 Updated dependencies 2024-09-05 14:57:20 +02:00
Mark Qvist 775ac7ff68 Allow ratchet enforcement and fixed opportunistic delivery queue handling 2024-09-05 14:56:49 +02:00
Mark Qvist a9fe086bcf Updated version 2024-09-04 18:58:25 +02:00
Mark Qvist e0f763caee Enabled Reticulum Ratchets for LXMF 2024-09-04 18:57:51 +02:00
Mark Qvist c24e99a92b Updated readme 2024-08-30 00:28:18 +02:00
Mark Qvist c1a327c5b2 Updated readme 2024-08-30 00:26:58 +02:00
Mark Qvist 68892091ec Updated readme 2024-08-30 00:23:09 +02:00
Mark Qvist e87c6c6d28 Merge branch 'master' of github.com:markqvist/LXMF 2024-08-30 00:12:34 +02:00
Mark Qvist 0be569ccd6 Updated field specifiers and descriptions 2024-08-30 00:11:32 +02:00
Mark Qvist 8e686b10e8 Updated version and dependencies 2024-08-30 00:11:11 +02:00
markqvist ae203291b9
Merge pull request #17 from markqvist/dependabot/pip/setuptools-70.0.0
Bump setuptools from 68.0.0 to 70.0.0
2024-08-17 16:58:48 +02:00
dependabot[bot] 8b8008478d
Bump setuptools from 68.0.0 to 70.0.0
Bumps [setuptools](https://github.com/pypa/setuptools) from 68.0.0 to 70.0.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v68.0.0...v70.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 18:35:25 +00:00
Mark Qvist b4ba9d628b Updated readme 2024-06-05 00:32:59 +02:00
Mark Qvist 4520507869 Updated readme 2024-06-04 18:43:46 +02:00
Mark Qvist e5a960b2df Updated OPUS audio modes 2024-06-04 18:31:18 +02:00
Mark Qvist a35e522e75 Added custom audio mode to spec 2024-06-02 09:25:33 +02:00
Mark Qvist a810be316f Added audio modes 2024-06-02 09:19:47 +02:00
Mark Qvist 741b66aab2 Updated version 2024-05-05 20:11:30 +02:00
Mark Qvist d767d40c5f Updated dependency version 2024-05-05 20:09:05 +02:00
Mark Qvist 2b3fa796a1 Updated packet proof logic 2024-03-26 14:55:59 +01:00
Mark Qvist f6cc9fd1cf Updated version 2024-03-23 19:53:05 +01:00
Mark Qvist 4b13d7188c Send packet proof before delivery processing 2024-03-22 21:11:32 +01:00
Mark Qvist 45e39917b5 Updated dependencies 2024-03-19 13:54:09 +01:00
Mark Qvist 27ffea3ea9 Improved message transfer feedback 2024-03-19 13:45:55 +01:00
Mark Qvist d2b2ef54e8 Added outbound transfer progress monitoring 2024-03-19 11:03:12 +01:00
Mark Qvist 3fbe2e94da Append incoming to locally delivered transient IDs and check on arrival 2024-03-19 09:47:37 +01:00
Mark Qvist 21af6a4e5c Updated version 2024-03-17 00:40:15 +01:00
Mark Qvist 739349c2a1 Updated field codes 2024-03-17 00:35:54 +01:00
Mark Qvist 994bb07efd Updated timing 2024-03-17 00:35:45 +01:00
Mark Qvist 1e9fe969fd Updated version 2024-03-02 09:17:34 +01:00
Mark Qvist 1d651a9b53 Added transfer limit awareness to message sync. Added ability to retain messages on node. 2024-03-02 09:09:51 +01:00
Mark Qvist 22493005dc Overhead calculation for propagation transfer limits 2024-03-01 23:48:12 +01:00
Mark Qvist 7ecd3c0d5e Added incoming resource transfer size limits 2024-03-01 23:26:27 +01:00
Mark Qvist 64050d39bf Added propagation transfer limit options 2024-03-01 22:37:54 +01:00
Mark Qvist c7489dc0fa Added transfer limit handling to announce handler 2024-03-01 22:33:53 +01:00
Mark Qvist bb1b9e4163 Updated dependency version 2024-03-01 00:33:42 +01:00
Mark Qvist b3bc8e684e Updated dependency 2024-02-29 23:03:02 +01:00
Mark Qvist 696c78ecea Improved propagation node sync and memory consumption 2024-02-29 23:02:16 +01:00
Mark Qvist 7aea4ea209 Updated version 2024-02-29 20:15:03 +01:00
Mark Qvist 35dc771528 Don't keep links alive after sync 2024-02-29 20:12:54 +01:00
Mark Qvist c369c6fc26 Merge branch 'master' of github.com:markqvist/LXMF 2024-02-29 18:27:48 +01:00
Mark Qvist af2dd56707 Added issue templates 2024-02-29 18:27:19 +01:00
markqvist 1287cb9f11
Merge pull request #15 from qwertyuiopzxcvbnmlkjhgfdsa1/feature/dockerize
dockerize LXMF
2024-02-29 18:26:36 +01:00
Mark Qvist f0e9fa6659 Corrected inconsistent property, getter and setter use. Fixed #16. 2024-01-16 13:26:33 +01:00
Mark Qvist 7a1bcc134b Updated dependencies 2024-01-15 20:29:39 +01:00
Mark Qvist 87413b9355 Updated readme 2024-01-15 20:29:29 +01:00
Mark Qvist 6ed16916d6 Fixed get fields method 2024-01-07 16:51:30 +01:00
Mark Qvist 754ae969e1 Added code examples. Closes #8. 2024-01-03 12:45:18 +01:00
Mark Qvist 1ee0c83168 Updated version and dependencies 2023-11-08 20:41:51 +01:00
Mark Qvist 2812a07165 Cleanup 2023-11-02 18:29:46 +01:00
Mark Qvist 10be1383e5 Updated version and dependencies 2023-11-02 12:48:24 +01:00
Mark Qvist 4d356bcda8 Tweaked timing 2023-11-02 04:36:51 +01:00
Mark Qvist da6fb3d0f3 Added field specifiers 2023-10-30 02:26:24 +01:00
Mark Qvist 56cb637bfa Improved propagation node sync feedback. Added physical layer reception statistics. 2023-10-28 00:06:56 +02:00
Mark Qvist 2872c94394 Merge branch 'master' of github.com:markqvist/LXMF 2023-10-24 01:48:13 +02:00
Mark Qvist f307f33bf8 Updated version and dependencies 2023-10-24 01:47:45 +02:00
Mark Qvist a095ba7e40 Added physical layer link statistics to reception 2023-10-23 01:34:20 +02:00
markqvist 0dc1e13f9d
Update README.md 2023-10-17 02:14:07 +02:00
Mark Qvist f83600430a Updated version 2023-10-16 01:50:37 +02:00
Mark Qvist 537123896b Increased max unreachable time to 14 days 2023-10-16 01:50:01 +02:00
Mark Qvist bc8863c3b1 Updated version 2023-10-15 20:28:55 +02:00
Mark Qvist b894c5a2a6 Fixed paper message generation sequence 2023-10-15 20:27:35 +02:00
Petr Blaha 820c92d38b dockerize LXMF 2023-09-07 08:51:49 +02:00
Petr Blaha 9f051aea17 dockerize LXMF 2023-09-07 08:39:57 +02:00
23 changed files with 5181 additions and 675 deletions

View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: ✨ Feature Request or Idea
url: https://github.com/markqvist/Reticulum/discussions/new?category=ideas
about: Propose and discuss features and ideas
- name: 💬 Questions, Help & Discussion
about: Ask anything, or get help
url: https://github.com/markqvist/Reticulum/discussions/new/choose
- name: 📖 Read the Reticulum Manual
url: https://markqvist.github.io/Reticulum/manual/
about: The complete documentation for Reticulum

View File

@ -0,0 +1,35 @@
---
name: "\U0001F41B Bug Report"
about: Report a reproducible bug
title: ''
labels: ''
assignees: ''
---
**Read the Contribution Guidelines**
Before creating a bug report on this issue tracker, you **must** read the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md). Issues that do not follow the contribution guidelines **will be deleted without comment**.
- The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**.
- Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**.
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**.
**Describe the Bug**
A clear and concise description of what the bug is.
**To Reproduce**
Describe in detail how to reproduce the bug.
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Logs & Screenshots**
Please include any relevant log output. If applicable, also add screenshots to help explain your problem.
**System Information**
- OS and version
- Python version
- Program version
**Additional context**
Add any other context about the problem here.

31
Dockerfile 100644
View File

@ -0,0 +1,31 @@
FROM python:alpine
LABEL authors="Petr Blaha petr.blaha@cleverdata.cz"
USER root
RUN apk update
RUN apk add sdl2_ttf sdl2 build-base libc-dev pkgconfig gstreamer sdl2_mixer sdl2_image sdl2_pango linux-headers mesa-dev py3-virtualenv
RUN addgroup -S myuser && adduser -S -G myuser myuser
USER myuser
WORKDIR /home/myuser
RUN pip install --upgrade pip
ENV PATH="/home/myuser/.local/bin:${PATH}"
################### BEGIN LXMF ###########################################
COPY --chown=myuser:myuser requirements.txt requirements.txt
RUN pip install --user -r requirements.txt
COPY --chown=myuser:myuser . .
#Python create virtual environment
RUN virtualenv /home/myuser/LXMF/venv
RUN source /home/myuser/LXMF/venv/bin/activate
RUN make all
################### END LXMF ###########################################

6
Dockerfile.howto 100644
View File

@ -0,0 +1,6 @@
# Run docker command one by one(all four), it will build LXMF artifact and copy to dist directory.
# No need to build locally and install dependencies
docker build -t lxmfdockerimage .
docker run -d -it --name lxmfdockercontainer lxmfdockerimage /bin/sh
docker cp lxmfdockercontainer:/home/myuser/dist .
docker rm -f lxmfdockercontainer

3
FUNDING.yml 100644
View File

@ -0,0 +1,3 @@
liberapay: Reticulum
ko_fi: markqvist
custom: "https://unsigned.io/donate"

16
LICENSE
View File

@ -1,6 +1,6 @@
MIT License
Reticulum License
Copyright (c) 2020 Mark Qvist / unsigned.io
Copyright (c) 2020-2025 Mark Qvist
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -9,8 +9,16 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
- The Software shall not be used in any kind of system which includes amongst
its functions the ability to purposefully do harm to human beings.
- The Software shall not be used, directly or indirectly, in the creation of
an artificial intelligence, machine learning or language model training
dataset, including but not limited to any use that contributes to the
training or development of such a model or algorithm.
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

View File

@ -1,45 +1,103 @@
import time
import threading
import RNS
import RNS.vendor.umsgpack as msgpack
from .LXMF import APP_NAME
from .LXMF import APP_NAME, stamp_cost_from_app_data, pn_announce_data_is_valid
from .LXMessage import LXMessage
class LXMFDeliveryAnnounceHandler:
def __init__(self, lxmrouter):
self.aspect_filter = APP_NAME+".delivery"
self.lxmrouter = lxmrouter
self.aspect_filter = APP_NAME+".delivery"
self.receive_path_responses = True
self.lxmrouter = lxmrouter
def received_announce(self, destination_hash, announced_identity, app_data):
try:
stamp_cost = stamp_cost_from_app_data(app_data)
self.lxmrouter.update_stamp_cost(destination_hash, stamp_cost)
except Exception as e:
RNS.log(f"Could not decode stamp cost from announce data. The contained exception was: {e}", RNS.LOG_DEBUG)
for lxmessage in self.lxmrouter.pending_outbound:
if destination_hash == lxmessage.destination_hash:
if lxmessage.method == LXMessage.DIRECT:
if destination_hash == lxmessage.destination_hash:
if lxmessage.method == LXMessage.DIRECT or lxmessage.method == LXMessage.OPPORTUNISTIC:
lxmessage.next_delivery_attempt = time.time()
while self.lxmrouter.processing_outbound:
time.sleep(0.1)
def outbound_trigger():
while self.lxmrouter.outbound_processing_lock.locked(): time.sleep(0.1)
self.lxmrouter.process_outbound()
self.lxmrouter.process_outbound()
threading.Thread(target=outbound_trigger, daemon=True).start()
class LXMFPropagationAnnounceHandler:
def __init__(self, lxmrouter):
self.aspect_filter = APP_NAME+".propagation"
self.lxmrouter = lxmrouter
self.aspect_filter = APP_NAME+".propagation"
self.receive_path_responses = True
self.lxmrouter = lxmrouter
def received_announce(self, destination_hash, announced_identity, app_data):
def received_announce(self, destination_hash, announced_identity, app_data, announce_packet_hash, is_path_response):
try:
if type(app_data) == bytes:
data = msgpack.unpackb(app_data)
if destination_hash == self.lxmrouter.get_outbound_propagation_node():
if pn_announce_data_is_valid(app_data):
for lxmessage in self.lxmrouter.pending_outbound:
if lxmessage.method == LXMessage.PROPAGATED:
lxmessage.next_delivery_attempt = time.time()
if self.lxmrouter.propagation_node and self.lxmrouter.autopeer:
if data[0] == True:
if RNS.Transport.hops_to(destination_hash) <= self.lxmrouter.autopeer_maxdepth:
self.lxmrouter.peer(destination_hash, data[1])
elif data[0] == False:
self.lxmrouter.unpeer(destination_hash, data[1])
def outbound_trigger():
while self.lxmrouter.outbound_processing_lock.locked(): time.sleep(0.1)
self.lxmrouter.process_outbound()
threading.Thread(target=outbound_trigger, daemon=True).start()
if self.lxmrouter.propagation_node:
if pn_announce_data_is_valid(app_data):
data = msgpack.unpackb(app_data)
node_timebase = int(data[1])
propagation_enabled = data[2]
propagation_transfer_limit = int(data[3])
propagation_sync_limit = int(data[4])
propagation_stamp_cost = int(data[5][0])
propagation_stamp_cost_flexibility = int(data[5][1])
peering_cost = int(data[5][2])
metadata = data[6]
if destination_hash in self.lxmrouter.static_peers:
static_peer = self.lxmrouter.peers[destination_hash]
if not is_path_response or static_peer.last_heard == 0:
self.lxmrouter.peer(destination_hash=destination_hash,
timestamp=node_timebase,
propagation_transfer_limit=propagation_transfer_limit,
propagation_sync_limit=propagation_sync_limit,
propagation_stamp_cost=propagation_stamp_cost,
propagation_stamp_cost_flexibility=propagation_stamp_cost_flexibility,
peering_cost=peering_cost,
metadata=metadata)
else:
if self.lxmrouter.autopeer and not is_path_response:
if propagation_enabled == True:
if RNS.Transport.hops_to(destination_hash) <= self.lxmrouter.autopeer_maxdepth:
self.lxmrouter.peer(destination_hash=destination_hash,
timestamp=node_timebase,
propagation_transfer_limit=propagation_transfer_limit,
propagation_sync_limit=propagation_sync_limit,
propagation_stamp_cost=propagation_stamp_cost,
propagation_stamp_cost_flexibility=propagation_stamp_cost_flexibility,
peering_cost=peering_cost,
metadata=metadata)
else:
if destination_hash in self.lxmrouter.peers:
RNS.log(f"Peer {self.lxmrouter.peers[destination_hash]} moved outside auto-peering range, breaking peering...")
self.lxmrouter.unpeer(destination_hash, node_timebase)
elif propagation_enabled == False:
self.lxmrouter.unpeer(destination_hash, node_timebase)
except Exception as e:
RNS.log("Error while evaluating propagation node announce, ignoring announce.", RNS.LOG_DEBUG)
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
RNS.log(f"The contained exception was: {str(e)}", RNS.LOG_DEBUG)

View File

@ -1 +1,250 @@
APP_NAME = "lxmf"
APP_NAME = "lxmf"
##########################################################
# The following core fields are provided to facilitate #
# interoperability in data exchange between various LXMF #
# clients and systems. #
##########################################################
FIELD_EMBEDDED_LXMS = 0x01
FIELD_TELEMETRY = 0x02
FIELD_TELEMETRY_STREAM = 0x03
FIELD_ICON_APPEARANCE = 0x04
FIELD_FILE_ATTACHMENTS = 0x05
FIELD_IMAGE = 0x06
FIELD_AUDIO = 0x07
FIELD_THREAD = 0x08 # Bytes, full thread ID hash
FIELD_COMMANDS = 0x09
FIELD_RESULTS = 0x0A
FIELD_GROUP = 0x0B
FIELD_TICKET = 0x0C
FIELD_EVENT = 0x0D
FIELD_RNR_REFS = 0x0E
FIELD_RENDERER = 0x0F
FIELD_REPLY_TO = 0x30 # Bytes, full LXMessage.hash
FIELD_REPLY_QUOTE = 0x31 # Bytes, quoted content in UTF-8 encoding
FIELD_REACTION = 0x40 # Dict, see "Reaction dict indices" below
FIELD_COMMENT = 0x41 # Dict, see "Comment dict indices" below
FIELD_CONTINUATION = 0x42 # Dict, see "Continuation dict indices" below
# Unallocated fields between 0x00 and 0x80, both included,
# should be considered reserved for future extensibility
# For experimental and unstable features, it is recommended
# to use fields above 0xFF.
# For usecases such as including custom data structures,
# embedding or encapsulating other data types or protocols
# that are not native to LXMF, or bridging/tunneling
# external protocols or services over LXMF, the following
# fields are available. A format/type/protocol (or other)
# identifier can be included in the CUSTOM_TYPE field, and
# the embedded payload can be included in the CUSTOM_DATA
# field. It is up to the client application to correctly
# discern and potentially utilise any data embedded using
# this mechanism.
FIELD_CUSTOM_TYPE = 0xFB
FIELD_CUSTOM_DATA = 0xFC
FIELD_CUSTOM_META = 0xFD
# The non-specific and debug fields are intended for
# development, testing and debugging use.
FIELD_NON_SPECIFIC = 0xFE
FIELD_DEBUG = 0xFF
##########################################################
# The following section lists field-specific specifiers, #
# modes and identifiers that are native to LXMF. It is #
# optional for any client or system to support any of #
# these, and they are provided as template for easing #
# interoperability without sacrificing expandability #
# and flexibility of the format. #
##########################################################
# Audio modes for the data structure in FIELD_AUDIO
# Codec2 Audio Modes
AM_CODEC2_450PWB = 0x01
AM_CODEC2_450 = 0x02
AM_CODEC2_700C = 0x03
AM_CODEC2_1200 = 0x04
AM_CODEC2_1300 = 0x05
AM_CODEC2_1400 = 0x06
AM_CODEC2_1600 = 0x07
AM_CODEC2_2400 = 0x08
AM_CODEC2_3200 = 0x09
# Opus Audio Modes
AM_OPUS_OGG = 0x10
AM_OPUS_LBW = 0x11
AM_OPUS_MBW = 0x12
AM_OPUS_PTT = 0x13
AM_OPUS_RT_HDX = 0x14
AM_OPUS_RT_FDX = 0x15
AM_OPUS_STANDARD = 0x16
AM_OPUS_HQ = 0x17
AM_OPUS_BROADCAST = 0x18
AM_OPUS_LOSSLESS = 0x19
# Custom, unspecified audio mode, the client must
# determine it itself based on the included data.
AM_CUSTOM = 0xFF
# Message renderer specifications for FIELD_RENDERER.
# The renderer specification is completely optional,
# and only serves as an indication to the receiving
# client on how to render the message contents. It is
# not mandatory to implement, either on sending or
# receiving sides, but is the recommended way to
# signal how to render a message, if non-plaintext
# formatting is used.
RENDERER_PLAIN = 0x00
RENDERER_MICRON = 0x01
RENDERER_MARKDOWN = 0x02
RENDERER_BBCODE = 0x03
# Clients choose how to handle reaction content, if at all.
# While reactions are typically a single unicode emoji or
# similar, the exact implementation and sanitization is
# left up to the client. When using the FIELD_REACTION
# field, the contents is a dict with the following keys:
REACTION_TO = 0x00 # Bytes, full LXMessage.hash
REACTION_CONTENT = 0x01 # Bytes, the reaction content in UTF-8 encoding
# Clients choose how to handle messages intended as comments
# for other message, if at all. The actual comment content
# is carried as the normal LXM content, meaning clients that
# do not support comments will display them as normal messages.
# When using the FIELD_COMMENT field, the contents is a dict
# with the following keys:
COMMENT_FOR = 0x00 # Bytes, full LXMessage.hash
# Clients choose how to handle messages that continue earlier
# messages, if at all. The actual continuation content is
# carried as the normal LXM content, meaning clients that
# do not support continuations will display them as normal.
# When using the FIELD_CONTINUATION field, the contents is a
# dict with the following keys:
CONTINUATION_OF = 0x00 # Bytes, full LXMessage.hash
# Optional propagation node metadata fields. These
# fields may be highly unstable in allocation and
# availability until the version 1.0.0 release, so use
# at your own risk until then, and expect changes!
PN_META_VERSION = 0x00
PN_META_NAME = 0x01
PN_META_SYNC_STRATUM = 0x02
PN_META_SYNC_THROTTLE = 0x03
PN_META_AUTH_BAND = 0x04
PN_META_UTIL_PRESSURE = 0x05
PN_META_CUSTOM = 0xFF
# Supported functionality codes for signalling
# feature and capability support.
SF_COMPRESSION = 0x00
##########################################################
# The following helper functions makes it easier to #
# handle and operate on LXMF data in client programs #
##########################################################
import RNS
import RNS.vendor.umsgpack as msgpack
def display_name_from_app_data(app_data=None):
if app_data == None: return None
elif len(app_data) == 0: return None
else:
# Version 0.5.0+ announce format
if (app_data[0] >= 0x90 and app_data[0] <= 0x9f) or app_data[0] == 0xdc:
peer_data = msgpack.unpackb(app_data)
if type(peer_data) == list:
if len(peer_data) < 1: return None
else:
dn = peer_data[0]
if dn == None: return None
else:
try:
decoded = dn.decode("utf-8").replace("\x00", "").strip()
return decoded
except Exception as e:
RNS.log(f"Could not decode display name in included announce data. The contained exception was: {e}", RNS.LOG_ERROR)
return None
# Original announce format
else: return app_data.decode("utf-8")
def stamp_cost_from_app_data(app_data=None):
if app_data == None or app_data == b"": return None
else:
# Version 0.5.0+ announce format
if (app_data[0] >= 0x90 and app_data[0] <= 0x9f) or app_data[0] == 0xdc:
peer_data = msgpack.unpackb(app_data)
if type(peer_data) == list:
if len(peer_data) < 2: return None
else: return peer_data[1]
# Original announce format
else: return None
def compression_support_from_app_data(app_data=None):
if app_data == None or app_data == b"": return None
else:
# Version 0.5.0+ announce format
if (app_data[0] >= 0x90 and app_data[0] <= 0x9f) or app_data[0] == 0xdc:
peer_data = msgpack.unpackb(app_data)
if type(peer_data) == list:
if len(peer_data) < 3: return True
else:
if not type(peer_data[2]) == list: return True
else: return SF_COMPRESSION in peer_data[2]
# Original announce format
else: return True
def pn_name_from_app_data(app_data=None):
if app_data == None: return None
else:
if pn_announce_data_is_valid(app_data):
data = msgpack.unpackb(app_data)
metadata = data[6]
if not PN_META_NAME in metadata: return None
else:
try: return metadata[PN_META_NAME].decode("utf-8")
except: return None
return None
def pn_stamp_cost_from_app_data(app_data=None):
if app_data == None: return None
else:
if pn_announce_data_is_valid(app_data):
data = msgpack.unpackb(app_data)
return data[5][0]
else: return None
def pn_announce_data_is_valid(data):
try:
if type(data) != bytes: return False
else: data = msgpack.unpackb(data)
if len(data) < 7: raise ValueError("Invalid announce data: Insufficient peer data, likely from deprecated LXMF version")
else:
try: int(data[1])
except: raise ValueError("Invalid announce data: Could not decode timebase")
if data[2] != True and data[2] != False: raise ValueError("Invalid announce data: Indeterminate propagation node status")
try: int(data[3])
except: raise ValueError("Invalid announce data: Could not decode propagation transfer limit")
try: int(data[4])
except: raise ValueError("Invalid announce data: Could not decode propagation sync limit")
if type(data[5]) != list: raise ValueError("Invalid announce data: Could not decode stamp costs")
try: int(data[5][0])
except: raise ValueError("Invalid announce data: Could not decode target stamp cost")
try: int(data[5][1])
except: raise ValueError("Invalid announce data: Could not decode stamp cost flexibility")
try: int(data[5][2])
except: raise ValueError("Invalid announce data: Could not decode peering cost")
if type(data[6]) != dict: raise ValueError("Invalid announce data: Could not decode metadata")
except Exception as e:
RNS.log(f"Could not validate propagation node announce data: {e}", RNS.LOG_DEBUG)
return False
return True

View File

@ -1,28 +1,42 @@
import os
import time
import threading
import RNS
import RNS.vendor.umsgpack as msgpack
import LXMF.LXStamper as LXStamper
from collections import deque
from .LXMF import APP_NAME
from .LXMF import PN_META_NAME
class LXMPeer:
OFFER_REQUEST_PATH = "/offer"
MESSAGE_GET_PATH = "/get"
IDLE = 0x00
LINK_ESTABLISHING = 0x01
LINK_READY = 0x02
REQUEST_SENT = 0x03
RESPONSE_RECEIVED = 0x04
IDLE = 0x00
LINK_ESTABLISHING = 0x01
LINK_READY = 0x02
REQUEST_SENT = 0x03
RESPONSE_RECEIVED = 0x04
RESOURCE_TRANSFERRING = 0x05
ERROR_NO_IDENTITY = 0xf0
ERROR_NO_ACCESS = 0xf1
ERROR_NO_IDENTITY = 0xf0
ERROR_NO_ACCESS = 0xf1
ERROR_INVALID_KEY = 0xf3
ERROR_INVALID_DATA = 0xf4
ERROR_INVALID_STAMP = 0xf5
ERROR_THROTTLED = 0xf6
ERROR_NOT_FOUND = 0xfd
ERROR_TIMEOUT = 0xfe
STRATEGY_LAZY = 0x01
STRATEGY_PERSISTENT = 0x02
DEFAULT_SYNC_STRATEGY = STRATEGY_PERSISTENT
# Maximum amount of time a peer can
# be unreachable before it is removed
MAX_UNREACHABLE = 4*24*60*60
MAX_UNREACHABLE = 14*24*60*60
# Everytime consecutive time a sync
# link fails to establish, add this
@ -38,33 +52,111 @@ class LXMPeer:
@staticmethod
def from_bytes(peer_bytes, router):
dictionary = msgpack.unpackb(peer_bytes)
peer_destination_hash = dictionary["destination_hash"]
peer_peering_timebase = dictionary["peering_timebase"]
peer_alive = dictionary["alive"]
peer_last_heard = dictionary["last_heard"]
peer = LXMPeer(router, dictionary["destination_hash"])
peer.peering_timebase = dictionary["peering_timebase"]
peer.alive = dictionary["alive"]
peer.last_heard = dictionary["last_heard"]
if "link_establishment_rate" in dictionary:
peer.link_establishment_rate = dictionary["link_establishment_rate"]
else:
peer.link_establishment_rate = 0
peer = LXMPeer(router, peer_destination_hash)
peer.peering_timebase = peer_peering_timebase
peer.alive = peer_alive
peer.last_heard = peer_last_heard
if "link_establishment_rate" in dictionary: peer.link_establishment_rate = dictionary["link_establishment_rate"]
else: peer.link_establishment_rate = 0
if "sync_transfer_rate" in dictionary: peer.sync_transfer_rate = dictionary["sync_transfer_rate"]
else: peer.sync_transfer_rate = 0
if "propagation_transfer_limit" in dictionary:
try: peer.propagation_transfer_limit = float(dictionary["propagation_transfer_limit"])
except Exception as e: peer.propagation_transfer_limit = None
else: peer.propagation_transfer_limit = None
if "propagation_sync_limit" in dictionary:
try: peer.propagation_sync_limit = int(dictionary["propagation_sync_limit"])
except: peer.propagation_sync_limit = peer.propagation_transfer_limit
else: peer.propagation_sync_limit = peer.propagation_transfer_limit
if "propagation_stamp_cost" in dictionary:
try: peer.propagation_stamp_cost = int(dictionary["propagation_stamp_cost"])
except: peer.propagation_stamp_cost = None
else: peer.propagation_stamp_cost = None
if "propagation_stamp_cost_flexibility" in dictionary:
try: peer.propagation_stamp_cost_flexibility = int(dictionary["propagation_stamp_cost_flexibility"])
except: peer.propagation_stamp_cost_flexibility = None
else: peer.propagation_stamp_cost_flexibility = None
if "peering_cost" in dictionary:
try: peer.peering_cost = int(dictionary["peering_cost"])
except: peer.peering_cost = None
else: peer.peering_cost = None
if "sync_strategy" in dictionary:
try: peer.sync_strategy = int(dictionary["sync_strategy"])
except: peer.sync_strategy = LXMPeer.DEFAULT_SYNC_STRATEGY
else: peer.sync_strategy = LXMPeer.DEFAULT_SYNC_STRATEGY
if "offered" in dictionary: peer.offered = dictionary["offered"]
else: peer.offered = 0
if "outgoing" in dictionary: peer.outgoing = dictionary["outgoing"]
else: peer.outgoing = 0
if "incoming" in dictionary: peer.incoming = dictionary["incoming"]
else: peer.incoming = 0
if "rx_bytes" in dictionary: peer.rx_bytes = dictionary["rx_bytes"]
else: peer.rx_bytes = 0
if "tx_bytes" in dictionary: peer.tx_bytes = dictionary["tx_bytes"]
else: peer.tx_bytes = 0
if "last_sync_attempt" in dictionary: peer.last_sync_attempt = dictionary["last_sync_attempt"]
else: peer.last_sync_attempt = 0
if "peering_key" in dictionary: peer.peering_key = dictionary["peering_key"]
else: peer.peering_key = None
if "metadata" in dictionary: peer.metadata = dictionary["metadata"]
else: peer.metadata = None
hm_count = 0
for transient_id in dictionary["handled_ids"]:
if transient_id in router.propagation_entries:
peer.handled_messages[transient_id] = router.propagation_entries[transient_id]
peer.add_handled_message(transient_id)
hm_count += 1
um_count = 0
for transient_id in dictionary["unhandled_ids"]:
if transient_id in router.propagation_entries:
peer.unhandled_messages[transient_id] = router.propagation_entries[transient_id]
peer.add_unhandled_message(transient_id)
um_count += 1
peer._hm_count = hm_count
peer._um_count = um_count
peer._hm_counts_synced = True
peer._um_counts_synced = True
del dictionary
return peer
def to_bytes(self):
dictionary = {}
dictionary["peering_timebase"] = self.peering_timebase
dictionary["alive"] = self.alive
dictionary["metadata"] = self.metadata
dictionary["last_heard"] = self.last_heard
dictionary["sync_strategy"] = self.sync_strategy
dictionary["peering_key"] = self.peering_key
dictionary["destination_hash"] = self.destination_hash
dictionary["link_establishment_rate"] = self.link_establishment_rate
dictionary["sync_transfer_rate"] = self.sync_transfer_rate
dictionary["propagation_transfer_limit"] = self.propagation_transfer_limit
dictionary["propagation_sync_limit"] = self.propagation_sync_limit
dictionary["propagation_stamp_cost"] = self.propagation_stamp_cost
dictionary["propagation_stamp_cost_flexibility"] = self.propagation_stamp_cost_flexibility
dictionary["peering_cost"] = self.peering_cost
dictionary["last_sync_attempt"] = self.last_sync_attempt
dictionary["offered"] = self.offered
dictionary["outgoing"] = self.outgoing
dictionary["incoming"] = self.incoming
dictionary["rx_bytes"] = self.rx_bytes
dictionary["tx_bytes"] = self.tx_bytes
handled_ids = []
for transient_id in self.handled_messages:
@ -77,33 +169,129 @@ class LXMPeer:
dictionary["handled_ids"] = handled_ids
dictionary["unhandled_ids"] = unhandled_ids
return msgpack.packb(dictionary)
peer_bytes = msgpack.packb(dictionary)
del dictionary
def __init__(self, router, destination_hash):
self.alive = False
self.last_heard = 0
self.next_sync_attempt = 0
self.last_sync_attempt = 0
self.sync_backoff = 0
self.peering_timebase = 0
return peer_bytes
def __init__(self, router, destination_hash, sync_strategy=DEFAULT_SYNC_STRATEGY):
self.alive = False
self.last_heard = 0
self.sync_strategy = sync_strategy
self.peering_key = None
self.peering_cost = None
self.metadata = None
self.next_sync_attempt = 0
self.last_sync_attempt = 0
self.sync_backoff = 0
self.peering_timebase = 0
self.link_establishment_rate = 0
self.sync_transfer_rate = 0
self.propagation_transfer_limit = None
self.propagation_sync_limit = None
self.propagation_stamp_cost = None
self.propagation_stamp_cost_flexibility = None
self.currently_transferring_messages = None
self.handled_messages_queue = deque()
self.unhandled_messages_queue = deque()
self.offered = 0 # Messages offered to this peer
self.outgoing = 0 # Messages transferred to this peer
self.incoming = 0 # Messages received from this peer
self.rx_bytes = 0 # Bytes received from this peer
self.tx_bytes = 0 # Bytes sent to this peer
self._hm_count = 0
self._um_count = 0
self._hm_counts_synced = False
self._um_counts_synced = False
self._peering_key_lock = threading.Lock()
self.link = None
self.state = LXMPeer.IDLE
self.unhandled_messages = {}
self.handled_messages = {}
self.last_offer = []
self.router = router
self.destination_hash = destination_hash
self.identity = RNS.Identity.recall(destination_hash)
self.destination = RNS.Destination(self.identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "propagation")
if self.identity != None:
self.destination = RNS.Destination(self.identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "propagation")
else:
self.destination = None
RNS.log(f"Could not recall identity for LXMF propagation peer {RNS.prettyhexrep(self.destination_hash)}, will retry identity resolution on next sync", RNS.LOG_WARNING)
def peering_key_ready(self):
if not self.peering_cost: return False
if type(self.peering_key) == list and len(self.peering_key) == 2:
value = self.peering_key[1]
if value >= self.peering_cost: return True
else:
RNS.log(f"Peering key value mismatch for {self}. Current value is {value}, but peer requires {self.peering_cost}. Scheduling regeneration...", RNS.LOG_WARNING)
self.peering_key = None
return False
def peering_key_value(self):
if type(self.peering_key) == list and len(self.peering_key) == 2: return self.peering_key[1]
else: return None
def generate_peering_key(self):
if self.peering_cost == None: return False
with self._peering_key_lock:
if self.peering_key != None: return True
else:
RNS.log(f"Generating peering key for {self}", RNS.LOG_NOTICE)
if self.router.identity == None:
RNS.log(f"Could not update peering key for {self} since the local LXMF router identity is not configured", RNS.LOG_ERROR)
return False
if self.identity == None:
self.identity = RNS.Identity.recall(destination_hash)
if self.identity == None:
RNS.log(f"Could not update peering key for {self} since its identity could not be recalled", RNS.LOG_ERROR)
return False
key_material = self.identity.hash+self.router.identity.hash
peering_key, value = LXStamper.generate_stamp(key_material, self.peering_cost, expand_rounds=LXStamper.WORKBLOCK_EXPAND_ROUNDS_PEERING)
if value >= self.peering_cost:
self.peering_key = [peering_key, value]
RNS.log(f"Peering key successfully generated for {self}", RNS.LOG_NOTICE)
return True
return False
def sync(self):
RNS.log("Initiating LXMF Propagation Node sync with peer "+RNS.prettyhexrep(self.destination_hash), RNS.LOG_DEBUG)
self.last_sync_attempt = time.time()
if time.time() > self.next_sync_attempt:
sync_time_reached = time.time() > self.next_sync_attempt
stamp_costs_known = self.propagation_stamp_cost != None and self.propagation_stamp_cost_flexibility != None and self.peering_cost != None
peering_key_ready = self.peering_key_ready()
sync_checks = sync_time_reached and stamp_costs_known and peering_key_ready
if not sync_checks:
try:
if not sync_time_reached:
postpone_reason = " due to previous failures"
if self.last_sync_attempt > self.last_heard: self.alive = False
elif not stamp_costs_known:
postpone_reason = " since its required stamp costs are not yet known"
elif not peering_key_ready:
postpone_reason = " since a peering key has not been generated yet"
def job(): self.generate_peering_key()
threading.Thread(target=job, daemon=True).start()
delay = self.next_sync_attempt-time.time()
postpone_delay = f" for {RNS.prettytime(delay)}" if delay > 0 else ""
RNS.log(f"Postponing sync with peer {RNS.prettyhexrep(self.destination_hash)}{postpone_delay}{postpone_reason}", RNS.LOG_DEBUG)
except Exception as e:
RNS.trace_exception(e)
else:
if not RNS.Transport.has_path(self.destination_hash):
RNS.log("No path to peer "+RNS.prettyhexrep(self.destination_hash)+" exists, requesting...", RNS.LOG_DEBUG)
RNS.Transport.request_path(self.destination_hash)
@ -115,10 +303,19 @@ class LXMPeer:
else:
if self.identity == None:
self.identity = RNS.Identity.recall(destination_hash)
self.destination = RNS.Destination(self.identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "propagation")
if self.identity != None:
self.destination = RNS.Destination(self.identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "propagation")
if self.destination != None:
if len(self.unhandled_messages) == 0:
RNS.log(f"Sync requested for {self}, but no unhandled messages exist for peer. Sync complete.", RNS.LOG_DEBUG)
return
if self.identity != None:
if len(self.unhandled_messages) > 0:
if self.currently_transferring_messages != None:
RNS.log(f"Sync requested for {self}, but current message transfer index was not clear. Aborting.", RNS.LOG_ERROR)
return
if self.state == LXMPeer.IDLE:
RNS.log("Establishing link for sync to peer "+RNS.prettyhexrep(self.destination_hash)+"...", RNS.LOG_DEBUG)
self.sync_backoff += LXMPeer.SYNC_BACKOFF_STEP
@ -131,36 +328,74 @@ class LXMPeer:
self.alive = True
self.last_heard = time.time()
self.sync_backoff = 0
min_accepted_cost = max(0, self.propagation_stamp_cost-self.propagation_stamp_cost_flexibility)
RNS.log("Synchronisation link to peer "+RNS.prettyhexrep(self.destination_hash)+" established, preparing request...", RNS.LOG_DEBUG)
unhandled_ids = []
purged_ids = []
RNS.log("Synchronisation link to peer "+RNS.prettyhexrep(self.destination_hash)+" established, preparing sync offer...", RNS.LOG_DEBUG)
unhandled_entries = []
unhandled_ids = []
purged_ids = []
low_value_ids = []
for transient_id in self.unhandled_messages:
if transient_id in self.router.propagation_entries:
unhandled_ids.append(transient_id)
else:
purged_ids.append(transient_id)
if self.router.get_stamp_value(transient_id) < min_accepted_cost: low_value_ids.append(transient_id)
else:
unhandled_entry = [ transient_id,
self.router.get_weight(transient_id),
self.router.get_size(transient_id) ]
unhandled_entries.append(unhandled_entry)
else: purged_ids.append(transient_id)
for transient_id in purged_ids:
RNS.log("Dropping unhandled message "+RNS.prettyhexrep(transient_id)+" for peer "+RNS.prettyhexrep(self.destination_hash)+" since it no longer exists in the message store.", RNS.LOG_DEBUG)
self.unhandled_messages.pop(transient_id)
RNS.log(f"Dropping unhandled message {RNS.prettyhexrep(transient_id)} for peer {RNS.prettyhexrep(self.destination_hash)} since it no longer exists in the message store.", RNS.LOG_DEBUG)
self.remove_unhandled_message(transient_id)
RNS.log("Sending sync request to peer "+str(self.destination), RNS.LOG_DEBUG)
self.link.request(LXMPeer.OFFER_REQUEST_PATH, unhandled_ids, response_callback=self.offer_response, failed_callback=self.request_failed)
for transient_id in low_value_ids:
RNS.log(f"Dropping unhandled message {RNS.prettyhexrep(transient_id)} for peer {RNS.prettyhexrep(self.destination_hash)} since its stamp value is lower than peer requirement of {min_accepted_cost}.", RNS.LOG_DEBUG)
self.remove_unhandled_message(transient_id)
unhandled_entries.sort(key=lambda e: e[1], reverse=False)
per_message_overhead = 16 # Really only 2 bytes, but set a bit higher for now
cumulative_size = 24 # Initialised to highest reasonable binary structure overhead
RNS.log(f"Syncing to peer with per-message limit {RNS.prettysize(self.propagation_transfer_limit*1000)} and sync limit {RNS.prettysize(self.propagation_sync_limit*1000)}") # TODO: Remove debug
for unhandled_entry in unhandled_entries:
transient_id = unhandled_entry[0]
weight = unhandled_entry[1]
lxm_size = unhandled_entry[2]
lxm_transfer_size = lxm_size+per_message_overhead
next_size = cumulative_size + lxm_transfer_size
if self.propagation_transfer_limit != None and lxm_transfer_size > (self.propagation_transfer_limit*1000):
self.remove_unhandled_message(transient_id)
self.add_handled_message(transient_id)
continue
if self.propagation_sync_limit != None and next_size >= (self.propagation_sync_limit*1000):
continue
cumulative_size += lxm_transfer_size
unhandled_ids.append(transient_id)
if len(unhandled_ids) == 0:
RNS.log(f"Sync requested for {self}, but no unhandled messages exist after offer preparation. Sync complete.", RNS.LOG_DEBUG)
return
offer = [self.peering_key[0], unhandled_ids]
RNS.log(f"Offering {len(unhandled_ids)} messages to peer {RNS.prettyhexrep(self.destination.hash)}", RNS.LOG_VERBOSE)
self.last_offer = unhandled_ids
self.link.request(LXMPeer.OFFER_REQUEST_PATH, offer, response_callback=self.offer_response, failed_callback=self.request_failed)
self.state = LXMPeer.REQUEST_SENT
else:
RNS.log("Could not request sync to peer "+RNS.prettyhexrep(self.destination_hash)+" since its identity could not be recalled.", RNS.LOG_ERROR)
else:
RNS.log("Postponing sync with peer "+RNS.prettyhexrep(self.destination_hash)+" for "+RNS.prettytime(self.next_sync_attempt-time.time())+" due to previous failures", RNS.LOG_DEBUG)
if self.last_sync_attempt > self.last_heard:
self.alive = False
RNS.log(f"Could not request sync to peer {RNS.prettyhexrep(self.destination_hash)} since its identity could not be recalled.", RNS.LOG_ERROR)
def request_failed(self, request_receipt):
RNS.log("Sync request to peer "+str(self.destination)+" failed", RNS.LOG_DEBUG)
if self.link != None:
self.link.teardown()
else:
self.state = LXMPeer.IDLE
RNS.log(f"Sync request to peer {self.destination} failed", RNS.LOG_DEBUG)
if self.link != None: self.link.teardown()
self.state = LXMPeer.IDLE
def offer_response(self, request_receipt):
try:
@ -172,44 +407,54 @@ class LXMPeer:
if response == LXMPeer.ERROR_NO_IDENTITY:
if self.link != None:
RNS.log("Remote peer indicated that no identification was received, retrying...", RNS.LOG_DEBUG)
self.link.indentify()
RNS.log("Remote peer indicated that no identification was received, retrying...", RNS.LOG_VERBOSE)
self.link.identify(self.router.identity)
self.state = LXMPeer.LINK_READY
self.sync()
return
elif response == LXMPeer.ERROR_NO_ACCESS:
RNS.log("Remote indicated that access was denied, breaking peering", RNS.LOG_VERBOSE)
self.router.unpeer(self.destination_hash)
return
elif response == LXMPeer.ERROR_THROTTLED:
throttle_time = self.router.PN_STAMP_THROTTLE
RNS.log(f"Remote indicated that we're throttled, postponing sync for {RNS.prettytime(throttle_time)}", RNS.LOG_VERBOSE)
self.next_sync_attempt = time.time()+throttle_time
return
elif response == False:
# Peer already has all advertised messages
for transient_id in self.unhandled_messages:
message_entry = self.unhandled_messages[transient_id]
self.handled_messages[transient_id] = message_entry
self.unhandled_messages = {}
for transient_id in self.last_offer:
if transient_id in self.unhandled_messages:
self.add_handled_message(transient_id)
self.remove_unhandled_message(transient_id)
elif response == True:
# Peer wants all advertised messages
for transient_id in self.unhandled_messages:
wanted_messages.append(self.unhandled_messages[transient_id])
for transient_id in self.last_offer:
wanted_messages.append(self.router.propagation_entries[transient_id])
wanted_message_ids.append(transient_id)
else:
# Peer wants some advertised messages
peer_had_messages = []
for transient_id in self.unhandled_messages.copy():
for transient_id in self.last_offer.copy():
# If the peer did not want the message, it has
# already received it from another peer.
if not transient_id in response:
message_entry = self.unhandled_messages.pop(transient_id)
self.handled_messages[transient_id] = message_entry
self.add_handled_message(transient_id)
self.remove_unhandled_message(transient_id)
for transient_id in response:
wanted_messages.append(self.unhandled_messages[transient_id])
wanted_messages.append(self.router.propagation_entries[transient_id])
wanted_message_ids.append(transient_id)
if len(wanted_messages) > 0:
RNS.log("Peer wanted "+str(len(wanted_messages))+" of the available messages", RNS.LOG_DEBUG)
RNS.log(f"Peer {RNS.prettyhexrep(self.destination_hash)} wanted {str(len(wanted_messages))} of the available messages", RNS.LOG_VERBOSE)
lxm_list = []
for message_entry in wanted_messages:
file_path = message_entry[1]
if os.path.isfile(file_path):
@ -219,11 +464,19 @@ class LXMPeer:
lxm_list.append(lxmf_data)
data = msgpack.packb([time.time(), lxm_list])
RNS.log(f"Total transfer size for this sync is {RNS.prettysize(len(data))}", RNS.LOG_VERBOSE)
resource = RNS.Resource(data, self.link, callback = self.resource_concluded)
resource.transferred_messages = wanted_message_ids
self.currently_transferring_messages = wanted_message_ids
self.current_sync_transfer_started = time.time()
self.state = LXMPeer.RESOURCE_TRANSFERRING
else:
RNS.log("Peer "+RNS.prettyhexrep(self.destination_hash)+" did not request any of the available messages, sync completed", RNS.LOG_DEBUG)
RNS.log(f"Peer {RNS.prettyhexrep(self.destination_hash)} did not request any of the available messages, sync completed", RNS.LOG_VERBOSE)
self.offered += len(self.last_offer)
if self.link != None:
self.link.teardown()
self.link = None
self.state = LXMPeer.IDLE
except Exception as e:
@ -236,25 +489,47 @@ class LXMPeer:
self.link = None
self.state = LXMPeer.IDLE
def resource_concluded(self, resource):
if resource.status == RNS.Resource.COMPLETE:
for transient_id in resource.transferred_messages:
message = self.unhandled_messages.pop(transient_id)
self.handled_messages[transient_id] = message
self.state = LXMPeer.IDLE
self.link.teardown()
RNS.log("Sync to peer "+RNS.prettyhexrep(self.destination_hash)+" completed", RNS.LOG_DEBUG)
self.alive = True
self.last_heard = time.time()
else:
RNS.log("Resource transfer for LXMF peer sync failed to "+str(self.destination), RNS.LOG_DEBUG)
if self.link != None:
self.link.teardown()
else:
if self.currently_transferring_messages == None:
RNS.log(f"Sync transfer completed on {self}, but transferred message index was unavailable. Aborting.", RNS.LOG_ERROR)
if self.link != None: self.link.teardown()
self.link = None
self.state = LXMPeer.IDLE
for transient_id in self.currently_transferring_messages:
self.add_handled_message(transient_id)
self.remove_unhandled_message(transient_id)
if self.link != None: self.link.teardown()
self.link = None
self.state = LXMPeer.IDLE
rate_str = ""
if self.current_sync_transfer_started != None:
self.sync_transfer_rate = (resource.get_transfer_size()*8)/(time.time()-self.current_sync_transfer_started)
rate_str = f" at {RNS.prettyspeed(self.sync_transfer_rate)}"
RNS.log(f"Syncing {len(self.currently_transferring_messages)} messages to peer {RNS.prettyhexrep(self.destination_hash)} completed{rate_str}", RNS.LOG_VERBOSE)
self.alive = True
self.last_heard = time.time()
self.offered += len(self.last_offer)
self.outgoing += len(self.currently_transferring_messages)
self.tx_bytes += resource.get_data_size()
self.currently_transferring_messages = None
self.current_sync_transfer_started = None
if self.sync_strategy == self.STRATEGY_PERSISTENT:
if self.unhandled_message_count > 0: self.sync()
else:
RNS.log("Resource transfer for LXMF peer sync failed to "+str(self.destination), RNS.LOG_VERBOSE)
if self.link != None: self.link.teardown()
self.link = None
self.state = LXMPeer.IDLE
self.currently_transferring_messages = None
self.current_sync_transfer_started = None
def link_established(self, link):
self.link.identify(self.router.identity)
@ -267,17 +542,105 @@ class LXMPeer:
self.sync()
def link_closed(self, link):
self.link = None
self.link = None
self.state = LXMPeer.IDLE
def handle_message(self, transient_id):
if not transient_id in self.handled_messages and not transient_id in self.unhandled_messages:
# TODO: Remove at some point
RNS.log("The message "+RNS.prettyhexrep(transient_id)+" was added to distribution queue for "+RNS.prettyhexrep(self.destination_hash), RNS.LOG_EXTREME)
self.unhandled_messages[transient_id] = self.router.propagation_entries[transient_id]
def queued_items(self):
return len(self.handled_messages_queue) > 0 or len(self.unhandled_messages_queue) > 0
def __str__(self):
if self.destination_hash:
return RNS.prettyhexrep(self.destination_hash)
def queue_unhandled_message(self, transient_id):
self.unhandled_messages_queue.append(transient_id)
def queue_handled_message(self, transient_id):
self.handled_messages_queue.append(transient_id)
def process_queues(self):
if len(self.unhandled_messages_queue) > 0 or len(self.handled_messages_queue) > 0:
handled_messages = self.handled_messages
unhandled_messages = self.unhandled_messages
while len(self.handled_messages_queue) > 0:
transient_id = self.handled_messages_queue.pop()
if not transient_id in handled_messages: self.add_handled_message(transient_id)
if transient_id in unhandled_messages: self.remove_unhandled_message(transient_id)
while len(self.unhandled_messages_queue) > 0:
transient_id = self.unhandled_messages_queue.pop()
if not transient_id in handled_messages and not transient_id in unhandled_messages:
self.add_unhandled_message(transient_id)
del handled_messages, unhandled_messages
@property
def handled_messages(self):
pes = self.router.propagation_entries.copy()
hm = list(filter(lambda tid: self.destination_hash in pes[tid][4], pes))
self._hm_count = len(hm); del pes
self._hm_counts_synced = True
return hm
@property
def unhandled_messages(self):
pes = self.router.propagation_entries.copy()
um = list(filter(lambda tid: self.destination_hash in pes[tid][5], pes))
self._um_count = len(um); del pes
self._um_counts_synced = True
return um
@property
def handled_message_count(self):
if not self._hm_counts_synced: self._update_counts()
return self._hm_count
@property
def unhandled_message_count(self):
if not self._um_counts_synced: self._update_counts()
return self._um_count
@property
def acceptance_rate(self):
return 0 if self.offered == 0 else (self.outgoing/self.offered)
def _update_counts(self):
if not self._hm_counts_synced:
hm = self.handled_messages; del hm
if not self._um_counts_synced:
um = self.unhandled_messages; del um
def add_handled_message(self, transient_id):
if transient_id in self.router.propagation_entries:
if not self.destination_hash in self.router.propagation_entries[transient_id][4]:
self.router.propagation_entries[transient_id][4].append(self.destination_hash)
self._hm_counts_synced = False
def add_unhandled_message(self, transient_id):
if transient_id in self.router.propagation_entries:
if not self.destination_hash in self.router.propagation_entries[transient_id][5]:
self.router.propagation_entries[transient_id][5].append(self.destination_hash)
self._um_count += 1
def remove_handled_message(self, transient_id):
if transient_id in self.router.propagation_entries:
if self.destination_hash in self.router.propagation_entries[transient_id][4]:
self.router.propagation_entries[transient_id][4].remove(self.destination_hash)
self._hm_counts_synced = False
def remove_unhandled_message(self, transient_id):
if transient_id in self.router.propagation_entries:
if self.destination_hash in self.router.propagation_entries[transient_id][5]:
self.router.propagation_entries[transient_id][5].remove(self.destination_hash)
self._um_counts_synced = False
@property
def name(self):
if type(self.metadata) != dict: return None
else:
return "<Unknown>"
if not PN_META_NAME in self.metadata: return None
else:
try: return self.metadata[PN_META_NAME].decode("utf-8")
except: return None
def __str__(self):
if self.destination_hash: return RNS.prettyhexrep(self.destination_hash)
else: return "<Unknown>"

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,26 @@
import RNS
import RNS.vendor.umsgpack as msgpack
import os
import time
import base64
import multiprocessing
import LXMF.LXStamper as LXStamper
from .LXMF import APP_NAME, compression_support_from_app_data
from threading import Lock
from .LXMF import APP_NAME
class LXMessage:
DRAFT = 0x00
GENERATING = 0x00
OUTBOUND = 0x01
SENDING = 0x02
SENT = 0x04
DELIVERED = 0x08
REJECTED = 0xFD
CANCELLED = 0xFE
FAILED = 0xFF
states = [DRAFT, OUTBOUND, SENDING, SENT, DELIVERED, FAILED]
states = [GENERATING, OUTBOUND, SENDING, SENT, DELIVERED, REJECTED, CANCELLED, FAILED]
UNKNOWN = 0x00
PACKET = 0x01
@ -32,19 +39,33 @@ class LXMessage:
DESTINATION_LENGTH = RNS.Identity.TRUNCATED_HASHLENGTH//8
SIGNATURE_LENGTH = RNS.Identity.SIGLENGTH//8
TICKET_LENGTH = RNS.Identity.TRUNCATED_HASHLENGTH//8
# LXMF overhead is 111 bytes per message:
# Default ticket expiry is 3 weeks, with an
# additional grace period of 5 days, allowing
# for timekeeping inaccuracies. Tickets will
# automatically renew when there is less than
# 14 days to expiry.
TICKET_EXPIRY = 21*24*60*60
TICKET_GRACE = 5*24*60*60
TICKET_RENEW = 14*24*60*60
TICKET_INTERVAL = 1*24*60*60
COST_TICKET = 0x100
# LXMF overhead is 112 bytes per message:
# 16 bytes for destination hash
# 16 bytes for source hash
# 64 bytes for Ed25519 signature
# 8 bytes for timestamp
# 7 bytes for msgpack structure
LXMF_OVERHEAD = 2*DESTINATION_LENGTH + SIGNATURE_LENGTH + 8 + 7
# 8 bytes for msgpack structure
TIMESTAMP_SIZE = 8
STRUCT_OVERHEAD = 8
LXMF_OVERHEAD = 2*DESTINATION_LENGTH + SIGNATURE_LENGTH + TIMESTAMP_SIZE + STRUCT_OVERHEAD
# With an MTU of 500, the maximum amount of data
# we can send in a single encrypted packet is
# 383 bytes.
ENCRYPTED_PACKET_MDU = RNS.Packet.ENCRYPTED_MDU
# 391 bytes.
ENCRYPTED_PACKET_MDU = RNS.Packet.ENCRYPTED_MDU + TIMESTAMP_SIZE
# The max content length we can fit in LXMF message
# inside a single RNS packet is the encrypted MDU, minus
@ -53,7 +74,7 @@ class LXMessage:
# field of the packet, therefore we also add the length
# of a destination hash to the calculation. With default
# RNS and LXMF parameters, the largest single-packet
# LXMF message we can send is 288 bytes. If a message
# LXMF message we can send is 295 bytes. If a message
# is larger than that, a Reticulum link will be used.
ENCRYPTED_PACKET_MAX_CONTENT = ENCRYPTED_PACKET_MDU - LXMF_OVERHEAD + DESTINATION_LENGTH
@ -63,13 +84,13 @@ class LXMessage:
LINK_PACKET_MDU = RNS.Link.MDU
# Which means that we can deliver single-packet LXMF
# messages with content of up to 320 bytes over a link.
# messages with content of up to 319 bytes over a link.
# If a message is larger than that, LXMF will sequence
# and transfer it as a RNS resource over the link instead.
LINK_PACKET_MAX_CONTENT = LINK_PACKET_MDU - LXMF_OVERHEAD
# For plain packets without encryption, we can
# fit up to 369 bytes of content.
# fit up to 368 bytes of content.
PLAIN_PACKET_MDU = RNS.Packet.PLAIN_MDU
PLAIN_PACKET_MAX_CONTENT = PLAIN_PACKET_MDU - LXMF_OVERHEAD + DESTINATION_LENGTH
@ -90,56 +111,84 @@ class LXMessage:
else:
return "<LXMessage>"
def __init__(self, destination, source, content = "", title = "", fields = None, desired_method = None, destination_hash = None, source_hash = None):
def __init__(self, destination, source, content = "", title = "", fields = None, desired_method = None, destination_hash = None, source_hash = None, stamp_cost=None, include_ticket=False):
if isinstance(destination, RNS.Destination) or destination == None:
self.__destination = destination
if destination != None:
self.destination_hash = destination.hash
else:
self.destination_hash = destination_hash
else:
raise ValueError("LXMessage initialised with invalid destination")
self.__destination = destination
if destination != None: self.destination_hash = destination.hash
else: self.destination_hash = destination_hash
else: raise ValueError("LXMessage initialised with invalid destination")
if isinstance(source, RNS.Destination) or source == None:
self.__source = source
if source != None:
self.source_hash = source.hash
else:
self.source_hash = source_hash
else:
raise ValueError("LXMessage initialised with invalid source")
self.__source = source
if source != None: self.source_hash = source.hash
else: self.source_hash = source_hash
else: raise ValueError("LXMessage initialised with invalid source")
self.set_title_from_string(title)
self.set_content_from_string(content)
if title == None: title = ""
if type(title) == bytes: self.set_title_from_bytes(title)
else: self.set_title_from_string(title)
if type(content) == bytes: self.set_content_from_bytes(content)
else: self.set_content_from_string(content)
self.set_fields(fields)
self.payload = None
self.timestamp = None
self.signature = None
self.hash = None
self.packed = None
self.progress = None
self.state = LXMessage.DRAFT
self.method = LXMessage.UNKNOWN
self.payload = None
self.timestamp = None
self.signature = None
self.hash = None
self.transient_id = None
self.packed = None
self.auto_compress = True
self.state = LXMessage.GENERATING
self.method = LXMessage.UNKNOWN
self.progress = 0.0
self.rssi = None
self.snr = None
self.q = None
self.stamp = None
self.stamp_cost = stamp_cost
self.stamp_value = None
self.stamp_valid = False
self.stamp_checked = False
self.propagation_stamp = None
self.propagation_stamp_value = None
self.propagation_stamp_valid = False
self.propagation_target_cost = None
self.defer_stamp = True
self.defer_propagation_stamp = True
self.outbound_ticket = None
self.include_ticket = include_ticket
self.propagation_packed = None
self.paper_packed = None
self.incoming = False
self.source_blackholed = False
self.signature_validated = False
self.unverified_reason = None
self.ratchet_id = None
self.representation = LXMessage.UNKNOWN
self.desired_method = desired_method
self.delivery_attempts = 0
self.transport_encrypted = False
self.transport_encryption = None
self.ratchet_id = None
self.packet_representation = None
self.resource_representation = None
self.__delivery_destination = None
self.__delivery_callback = None
self.failed_callback = None
self.__pn_encrypted_data = None
self.__persist_lock = Lock()
self.failed_callback = None
self.deferred_stamp_generating = False
def set_title_from_string(self, title_string):
self.title = title_string.encode("utf-8")
@ -157,16 +206,31 @@ class LXMessage:
self.content = content_bytes
def content_as_string(self):
return self.content.decode("utf-8")
try:
return self.content.decode("utf-8")
except Exception as e:
RNS.log(f"{self} could not decode message content as string: {e}")
return None
def set_fields(self, fields):
if isinstance(fields, dict) or fields == None:
self.fields = fields
self.fields = fields or {}
else:
raise ValueError("LXMessage property \"fields\" can only be dict or None")
def get_fields(self):
return self.__fields
return self.fields
@property
def destination(self):
return self.__destination
@destination.setter
def destination(self, destination):
self.set_destination(destination)
def get_destination(self):
return self.destination
def set_destination(self, destination):
if self.destination == None:
@ -177,8 +241,16 @@ class LXMessage:
else:
raise ValueError("Cannot reassign destination on LXMessage")
def get_destination(self):
return self.__destination
@property
def source(self):
return self.__source
@source.setter
def source(self, source):
self.set_source(source)
def get_source(self):
return self.source
def set_source(self, source):
if self.source == None:
@ -189,9 +261,6 @@ class LXMessage:
else:
raise ValueError("Cannot reassign source on LXMessage")
def get_source(self):
return self.__source
def set_delivery_destination(self, delivery_destination):
self.__delivery_destination = delivery_destination
@ -201,10 +270,91 @@ class LXMessage:
def register_failed_callback(self, callback):
self.failed_callback = callback
def pack(self):
def validate_stamp(self, target_cost, tickets=None):
if tickets != None:
for ticket in tickets:
try:
if self.stamp == RNS.Identity.truncated_hash(ticket+self.message_id):
RNS.log(f"Stamp on {self} validated by inbound ticket", RNS.LOG_DEBUG) # TODO: Remove at some point
self.stamp_value = LXMessage.COST_TICKET
return True
except Exception as e:
RNS.log(f"Error while validating ticket: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
if self.stamp == None:
return False
else:
workblock = LXStamper.stamp_workblock(self.message_id)
if LXStamper.stamp_valid(self.stamp, target_cost, workblock):
RNS.log(f"Stamp on {self} validated", RNS.LOG_DEBUG) # TODO: Remove at some point
self.stamp_value = LXStamper.stamp_value(workblock, self.stamp)
return True
else:
return False
def get_stamp(self, timeout=None):
# If an outbound ticket exists, use this for
# generating a valid stamp.
if self.outbound_ticket != None and type(self.outbound_ticket) == bytes and len(self.outbound_ticket) == LXMessage.TICKET_LENGTH:
generated_stamp = RNS.Identity.truncated_hash(self.outbound_ticket+self.message_id)
self.stamp_value = LXMessage.COST_TICKET
RNS.log(f"Generated stamp with outbound ticket {RNS.hexrep(self.outbound_ticket)} for {self}", RNS.LOG_DEBUG) # TODO: Remove at some point
return generated_stamp
# If no stamp cost is required, we can just
# return immediately.
elif self.stamp_cost == None:
self.stamp_value = None
return None
# If a stamp was already generated, return
# it immediately.
elif self.stamp != None:
return self.stamp
# Otherwise, we will need to generate a
# valid stamp according to the cost that
# the receiver has specified.
else:
generated_stamp, value = LXStamper.generate_stamp(self.message_id, self.stamp_cost)
if generated_stamp:
self.stamp_value = value
self.stamp_valid = True
return generated_stamp
else:
return None
def get_propagation_stamp(self, target_cost, timeout=None):
# If a stamp was already generated, return
# it immediately.
if self.propagation_stamp != None:
return self.propagation_stamp
# Otherwise, we will need to generate a
# valid stamp according to the cost that
# the propagation node has specified.
else:
self.propagation_target_cost = target_cost
if self.propagation_target_cost == None:
raise ValueError("Cannot generate propagation stamp without configured target propagation cost")
if not self.transient_id: self.pack()
generated_stamp, value = LXStamper.generate_stamp(self.transient_id, target_cost, expand_rounds=LXStamper.WORKBLOCK_EXPAND_ROUNDS_PN)
if generated_stamp:
self.propagation_stamp = generated_stamp
self.propagation_stamp_value = value
self.propagation_stamp_valid = True
return generated_stamp
else:
return None
def pack(self, payload_updated=False):
if not self.packed:
if self.timestamp == None:
self.timestamp = time.time()
if self.timestamp == None: self.timestamp = time.time()
self.propagation_packed = None
self.paper_packed = None
@ -217,28 +367,40 @@ class LXMessage:
hashed_part += msgpack.packb(self.payload)
self.hash = RNS.Identity.full_hash(hashed_part)
self.message_id = self.hash
if not self.defer_stamp:
self.stamp = self.get_stamp()
if self.stamp != None: self.payload.append(self.stamp)
signed_part = b""
signed_part += hashed_part
signed_part += self.hash
self.signature = self.__source.sign(signed_part)
self.signature_validated = True
packed_payload = msgpack.packb(self.payload)
self.packed = b""
self.packed += self.__destination.hash
self.packed += self.__source.hash
self.packed += self.signature
packed_payload = msgpack.packb(self.payload)
self.packed += packed_payload
self.packed_size = len(self.packed)
content_size = len(packed_payload)
content_size = len(packed_payload)-LXMessage.TIMESTAMP_SIZE-LXMessage.STRUCT_OVERHEAD
# If no desired delivery method has been defined,
# one will be chosen according to these rules:
if self.desired_method == None:
self.desired_method = LXMessage.DIRECT
# TODO: Expand rules to something more intelligent
# If opportunistic delivery was requested, check
# that message will fit within packet size limits
if self.desired_method == LXMessage.OPPORTUNISTIC:
if self.__destination.type == RNS.Destination.SINGLE:
if content_size > LXMessage.ENCRYPTED_PACKET_MAX_CONTENT:
RNS.log(f"Opportunistic delivery was requested for {self}, but content of length {content_size} exceeds packet size limit. Falling back to link-based delivery.", RNS.LOG_DEBUG)
self.desired_method = LXMessage.DIRECT
# Set delivery parameters according to delivery method
if self.desired_method == LXMessage.OPPORTUNISTIC:
if self.__destination.type == RNS.Destination.SINGLE:
single_packet_content_limit = LXMessage.ENCRYPTED_PACKET_MAX_CONTENT
@ -246,7 +408,7 @@ class LXMessage:
single_packet_content_limit = LXMessage.PLAIN_PACKET_MAX_CONTENT
if content_size > single_packet_content_limit:
raise TypeError("LXMessage desired opportunistic delivery method, but content exceeds single-packet size.")
raise TypeError(f"LXMessage desired opportunistic delivery method, but content of length {content_size} exceeds single-packet content limit of {single_packet_content_limit}.")
else:
self.method = LXMessage.OPPORTUNISTIC
self.representation = LXMessage.PACKET
@ -264,8 +426,14 @@ class LXMessage:
elif self.desired_method == LXMessage.PROPAGATED:
single_packet_content_limit = LXMessage.LINK_PACKET_MAX_CONTENT
encrypted_data = self.__destination.encrypt(self.packed[LXMessage.DESTINATION_LENGTH:])
self.propagation_packed = msgpack.packb([time.time(), [self.packed[:LXMessage.DESTINATION_LENGTH]+encrypted_data]])
if self.__pn_encrypted_data == None or payload_updated:
self.__pn_encrypted_data = self.__destination.encrypt(self.packed[LXMessage.DESTINATION_LENGTH:])
self.ratchet_id = self.__destination.latest_ratchet_id
lxmf_data = self.packed[:LXMessage.DESTINATION_LENGTH]+self.__pn_encrypted_data
self.transient_id = RNS.Identity.full_hash(lxmf_data)
if self.propagation_stamp != None: lxmf_data += self.propagation_stamp
self.propagation_packed = msgpack.packb([time.time(), [lxmf_data]])
content_size = len(self.propagation_packed)
if content_size <= single_packet_content_limit:
@ -279,6 +447,7 @@ class LXMessage:
paper_content_limit = LXMessage.PAPER_MDU
encrypted_data = self.__destination.encrypt(self.packed[LXMessage.DESTINATION_LENGTH:])
self.ratchet_id = self.__destination.latest_ratchet_id
self.paper_packed = self.packed[:LXMessage.DESTINATION_LENGTH]+encrypted_data
content_size = len(self.paper_packed)
@ -293,25 +462,34 @@ class LXMessage:
def send(self):
self.determine_transport_encryption()
self.determine_compression_support()
if self.method == LXMessage.OPPORTUNISTIC:
self.__as_packet().send().set_delivery_callback(self.__mark_delivered)
lxm_packet = self.__as_packet()
lxm_packet.send().set_delivery_callback(self.__mark_delivered)
self.progress = 0.50
self.ratchet_id = lxm_packet.ratchet_id
self.state = LXMessage.SENT
elif self.method == LXMessage.DIRECT:
self.state = LXMessage.SENDING
if self.representation == LXMessage.PACKET:
receipt = self.__as_packet().send()
lxm_packet = self.__as_packet()
receipt = lxm_packet.send()
self.ratchet_id = self.__delivery_destination.link_id
if receipt:
receipt.set_delivery_callback(self.__mark_delivered)
receipt.set_timeout_callback(self.__link_packet_timed_out)
self.progress = 0.50
else:
if self.__delivery_destination:
self.__delivery_destination.teardown()
elif self.representation == LXMessage.RESOURCE:
self.resource_representation = self.__as_resource()
self.ratchet_id = self.__delivery_destination.link_id
self.progress = 0.10
elif self.method == LXMessage.PROPAGATED:
self.state = LXMessage.SENDING
@ -321,14 +499,28 @@ class LXMessage:
if receipt:
receipt.set_delivery_callback(self.__mark_propagated)
receipt.set_timeout_callback(self.__link_packet_timed_out)
self.progress = 0.50
else:
self.__delivery_destination.teardown()
elif self.representation == LXMessage.RESOURCE:
self.resource_representation = self.__as_resource()
self.progress = 0.10
def determine_compression_support(self):
app_data = RNS.Identity.recall_app_data(self.destination_hash)
if app_data: self.auto_compress = compression_support_from_app_data(app_data)
else: self.auto_compress = True
###### TODO: Remove debug logging
if app_data: RNS.log(f"Set compression support from app data to: {self.auto_compress}", RNS.LOG_DEBUG)
else: RNS.log(f"Defaulting compression support to {self.auto_compress}", RNS.LOG_DEBUG)
######
def determine_transport_encryption(self):
# TODO: These descriptions are old and outdated.
# Update the transport encryption descriptions to
# account for ratchets and other changes.
if self.method == LXMessage.OPPORTUNISTIC:
if self.__destination.type == RNS.Destination.SINGLE:
self.transport_encrypted = True
@ -369,56 +561,67 @@ class LXMessage:
def __mark_delivered(self, receipt = None):
RNS.log("Received delivery notification for "+str(self), RNS.LOG_DEBUG)
self.state = LXMessage.DELIVERED
self.progress = 1.0
if self.__delivery_callback != None and callable(self.__delivery_callback):
try:
self.__delivery_callback(self)
except Exception as e:
RNS.log("An error occurred in the external delivery callback for "+str(message), RNS.LOG_ERROR)
RNS.log("An error occurred in the external delivery callback for "+str(self), RNS.LOG_ERROR)
RNS.trace_exception(e)
def __mark_propagated(self, receipt = None):
RNS.log("Received propagation success notification for "+str(self), RNS.LOG_DEBUG)
self.state = LXMessage.SENT
self.progress = 1.0
if self.__delivery_callback != None and callable(self.__delivery_callback):
try:
self.__delivery_callback(self)
except Exception as e:
RNS.log("An error occurred in the external delivery callback for "+str(message), RNS.LOG_ERROR)
RNS.log("An error occurred in the external delivery callback for "+str(self), RNS.LOG_ERROR)
RNS.trace_exception(e)
def __mark_paper_generated(self, receipt = None):
RNS.log("Paper message generation succeeded for "+str(self), RNS.LOG_DEBUG)
self.state = LXMessage.PAPER
self.progress = 1.0
if self.__delivery_callback != None and callable(self.__delivery_callback):
try:
self.__delivery_callback(self)
except Exception as e:
RNS.log("An error occurred in the external delivery callback for "+str(message), RNS.LOG_ERROR)
RNS.log("An error occurred in the external delivery callback for "+str(self), RNS.LOG_ERROR)
RNS.trace_exception(e)
def __resource_concluded(self, resource):
if resource.status == RNS.Resource.COMPLETE:
self.__mark_delivered()
else:
resource.link.teardown()
self.state = LXMessage.OUTBOUND
if resource.status == RNS.Resource.REJECTED:
self.state = LXMessage.REJECTED
elif self.state != LXMessage.CANCELLED:
resource.link.teardown()
self.state = LXMessage.OUTBOUND
def __propagation_resource_concluded(self, resource):
if resource.status == RNS.Resource.COMPLETE:
self.__mark_propagated()
else:
resource.link.teardown()
self.state = LXMessage.OUTBOUND
if self.state != LXMessage.CANCELLED:
resource.link.teardown()
self.state = LXMessage.OUTBOUND
def __link_packet_timed_out(self, packet_receipt):
if packet_receipt:
packet_receipt.destination.teardown()
self.state = LXMessage.OUTBOUND
if self.state != LXMessage.CANCELLED:
if packet_receipt:
packet_receipt.destination.teardown()
self.state = LXMessage.OUTBOUND
def __update_transfer_progress(self, resource):
self.progress = resource.get_progress()
self.progress = 0.10 + (resource.get_progress()*0.90)
def __as_packet(self):
if not self.packed:
@ -447,10 +650,8 @@ class LXMessage:
if not self.__delivery_destination.status == RNS.Link.ACTIVE:
raise ConnectionError("Tried to synthesize resource for LXMF message on a link that was not active")
self.progress = 0.0
if self.method == LXMessage.DIRECT:
return RNS.Resource(self.packed, self.__delivery_destination, callback = self.__resource_concluded, progress_callback = self.__update_transfer_progress)
return RNS.Resource(self.packed, self.__delivery_destination, callback = self.__resource_concluded, progress_callback = self.__update_transfer_progress, auto_compress=self.auto_compress)
elif self.method == LXMessage.PROPAGATED:
return RNS.Resource(self.propagation_packed, self.__delivery_destination, callback = self.__propagation_resource_concluded, progress_callback = self.__update_transfer_progress)
else:
@ -470,24 +671,31 @@ class LXMessage:
return msgpack.packb(container)
def write_to_directory(self, directory_path):
file_name = RNS.hexrep(self.hash, delimit=False)
file_path = directory_path+"/"+file_name
tmp_path = file_path+".tmp."+str(os.getpid() or time.time())+"."+RNS.hexrep(os.urandom(8), delimit=False)
try:
file = open(file_path, "wb")
file.write(self.packed_container())
file.close()
with self.__persist_lock:
try:
with open(tmp_path, "wb") as file:
file.write(self.packed_container())
file.flush()
try: os.fsync(file.fileno())
except OSError as e: RNS.log(f"Error while waiting for persist fsync for {self}: {e}", RNS.LOG_WARNING)
return file_path
os.replace(tmp_path, file_path)
return file_path
except Exception as e:
RNS.log("Error while writing LXMF message to file \""+str(file_path)+"\". The contained exception was: "+str(e), RNS.LOG_ERROR)
return None
except Exception as e:
try:
if os.path.exists(tmp_path): os.unlink(tmp_path)
except Exception as e: RNS.log(f"Error while cleaning temporary file {tmp_path} for {self}: {e}", RNS.LOG_ERROR)
RNS.log(f"Error while writing LXMF message to file \"{file_path}\". The contained exception was: {e}", RNS.LOG_ERROR)
return None
def as_uri(self, finalise=True):
self.determine_transport_encryption()
if not self.packed:
self.pack()
@ -499,6 +707,7 @@ class LXMessage:
lxm_uri = LXMessage.URI_SCHEMA+"://"+encoded_bytes.decode("utf-8").replace("=","")
if finalise:
self.determine_transport_encryption()
self.__mark_paper_generated()
return lxm_uri
@ -507,7 +716,6 @@ class LXMessage:
raise TypeError("Attempt to represent LXM with non-paper delivery method as URI")
def as_qr(self):
self.determine_transport_encryption()
if not self.packed:
self.pack()
@ -522,6 +730,7 @@ class LXMessage:
data = self.as_uri(finalise=False),
)
self.determine_transport_encryption()
self.__mark_paper_generated()
return qr
@ -540,22 +749,31 @@ class LXMessage:
source_hash = lxmf_bytes[LXMessage.DESTINATION_LENGTH:2*LXMessage.DESTINATION_LENGTH]
signature = lxmf_bytes[2*LXMessage.DESTINATION_LENGTH:2*LXMessage.DESTINATION_LENGTH+LXMessage.SIGNATURE_LENGTH]
packed_payload = lxmf_bytes[2*LXMessage.DESTINATION_LENGTH+LXMessage.SIGNATURE_LENGTH:]
unpacked_payload = msgpack.unpackb(packed_payload)
# Extract stamp from payload if included
if len(unpacked_payload) > 4:
stamp = unpacked_payload[4]
unpacked_payload = unpacked_payload[:4]
packed_payload = msgpack.packb(unpacked_payload)
else:
stamp = None
hashed_part = b"" + destination_hash + source_hash + packed_payload
message_hash = RNS.Identity.full_hash(hashed_part)
signed_part = b"" + hashed_part + message_hash
unpacked_payload = msgpack.unpackb(packed_payload)
timestamp = unpacked_payload[0]
title_bytes = unpacked_payload[1]
content_bytes = unpacked_payload[2]
fields = unpacked_payload[3]
destination_identity = RNS.Identity.recall(destination_hash)
destination_identity = RNS.Identity.recall(destination_hash, _no_use=True)
if destination_identity != None:
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "delivery")
else:
destination = None
source_identity = RNS.Identity.recall(source_hash)
source_identity = RNS.Identity.recall(source_hash, _no_use=True)
if source_identity != None:
source = RNS.Destination(source_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "delivery")
else:
@ -572,7 +790,9 @@ class LXMessage:
desired_method = original_method)
message.hash = message_hash
message.message_id = message.hash
message.signature = signature
message.stamp = stamp
message.incoming = True
message.timestamp = timestamp
message.packed = lxmf_bytes
@ -580,6 +800,10 @@ class LXMessage:
message.set_title_from_bytes(title_bytes)
message.set_content_from_bytes(content_bytes)
try:
if source_identity != None: message.source_blackholed = RNS.Reticulum.get_instance().is_blackholed(source_identity)
except Exception as e: RNS.log(f"Could not determine message source blackhole status: {e}", RNS.LOG_WARNING)
try:
if source:
if source.identity.validate(signature, signed_part):

523
LXMF/LXStamper.py 100644
View File

@ -0,0 +1,523 @@
import RNS
import RNS.vendor.umsgpack as msgpack
import os
import sys
import time
import math
import itertools
import contextlib
import multiprocessing
WORKBLOCK_EXPAND_ROUNDS = 3000
WORKBLOCK_EXPAND_ROUNDS_PN = 1000
WORKBLOCK_EXPAND_ROUNDS_PEERING = 25
STAMP_SIZE = RNS.Identity.HASHLENGTH//8
PN_VALIDATION_POOL_MIN_SIZE = 256
USE_WORKER_MANAGER = False
active_jobs = {}
if sys.version_info[0] >= 3 and sys.version_info[1] >= 14:
USE_WORKER_MANAGER = True
@contextlib.contextmanager
def worker_context(ctx, stamp_cost, workblock, message_id):
stop_event = ctx.Event()
result_queue = ctx.Queue(1)
rounds_queue = ctx.Queue()
active_jobs[message_id] = [stop_event, result_queue]
job_procs = []
try: yield stop_event, result_queue, rounds_queue, job_procs
finally:
if message_id in active_jobs: del active_jobs[message_id]
stop_event.set()
for p in job_procs:
if p.is_alive(): p.terminate()
for p in job_procs: p.join(timeout=0.5)
result_queue.close()
result_queue.join_thread()
rounds_queue.close()
rounds_queue.join_thread()
stop_event.clear()
def stamp_workblock(material, expand_rounds=WORKBLOCK_EXPAND_ROUNDS):
wb_st = time.time()
workblock = b""
for n in range(expand_rounds):
workblock += RNS.Cryptography.hkdf(length=256,
derive_from=material,
salt=RNS.Identity.full_hash(material+msgpack.packb(n)),
context=None)
wb_time = time.time() - wb_st
# RNS.log(f"Stamp workblock size {RNS.prettysize(len(workblock))}, generated in {round(wb_time*1000,2)}ms", RNS.LOG_DEBUG)
return workblock
def stamp_value(workblock, stamp):
value = 0
bits = 256
material = RNS.Identity.full_hash(workblock+stamp)
i = int.from_bytes(material, byteorder="big")
while ((i & (1 << (bits - 1))) == 0):
i = (i << 1)
value += 1
return value
def stamp_valid(stamp, target_cost, workblock):
target = 0b1 << 256-target_cost
result = RNS.Identity.full_hash(workblock+stamp)
if int.from_bytes(result, byteorder="big") > target: return False
else: return True
def validate_peering_key(peering_id, peering_key, target_cost):
workblock = stamp_workblock(peering_id, expand_rounds=WORKBLOCK_EXPAND_ROUNDS_PEERING)
if not stamp_valid(peering_key, target_cost, workblock): return False
else: return True
def validate_pn_stamp(transient_data, target_cost):
from .LXMessage import LXMessage
if len(transient_data) <= LXMessage.LXMF_OVERHEAD+STAMP_SIZE: return None, None, None, None
else:
lxm_data = transient_data[:-STAMP_SIZE]
stamp = transient_data[-STAMP_SIZE:]
transient_id = RNS.Identity.full_hash(lxm_data)
workblock = stamp_workblock(transient_id, expand_rounds=WORKBLOCK_EXPAND_ROUNDS_PN)
if not stamp_valid(stamp, target_cost, workblock): return None, None, None, None
else:
value = stamp_value(workblock, stamp)
return transient_id, lxm_data, value, stamp
def validate_pn_stamps_job_simple(transient_list, target_cost):
validated_messages = []
for transient_data in transient_list:
transient_id, lxm_data, value, stamp_data = validate_pn_stamp(transient_data, target_cost)
if transient_id: validated_messages.append([transient_id, lxm_data, value, stamp_data])
return validated_messages
def validate_pn_stamps_job_multip(transient_list, target_cost):
cores = multiprocessing.cpu_count()
pool_count = min(cores, math.ceil(len(transient_list) / PN_VALIDATION_POOL_MIN_SIZE))
RNS.log(f"Validating {len(transient_list)} stamps using {pool_count} processes...", RNS.LOG_VERBOSE)
with multiprocessing.get_context("spawn").Pool(pool_count) as p:
validated_entries = p.starmap(validate_pn_stamp, zip(transient_list, itertools.repeat(target_cost)))
RNS.log(f"Validation pool completed for {len(transient_list)} stamps", RNS.LOG_VERBOSE)
return [e for e in validated_entries if e[0] != None]
def validate_pn_stamps(transient_list, target_cost):
non_mp_platform = RNS.vendor.platformutils.is_android()
if len(transient_list) <= PN_VALIDATION_POOL_MIN_SIZE or non_mp_platform: return validate_pn_stamps_job_simple(transient_list, target_cost)
else: return validate_pn_stamps_job_multip(transient_list, target_cost)
def generate_stamp(message_id, stamp_cost, expand_rounds=WORKBLOCK_EXPAND_ROUNDS):
RNS.log(f"Generating stamp with cost {stamp_cost} for {RNS.prettyhexrep(message_id)}...", RNS.LOG_DEBUG)
workblock = stamp_workblock(message_id, expand_rounds=expand_rounds)
start_time = time.time()
stamp = None
rounds = 0
value = 0
if RNS.vendor.platformutils.is_windows() or RNS.vendor.platformutils.is_darwin(): stamp, rounds = job_simple(stamp_cost, workblock, message_id)
elif RNS.vendor.platformutils.is_android(): stamp, rounds = job_android(stamp_cost, workblock, message_id)
else:
if USE_WORKER_MANAGER: stamp, rounds = job_linux_managed(stamp_cost, workblock, message_id)
else: stamp, rounds = job_linux(stamp_cost, workblock, message_id)
duration = time.time() - start_time
speed = rounds/duration
if stamp != None: value = stamp_value(workblock, stamp)
RNS.log(f"Stamp with value {value} generated in {RNS.prettytime(duration)}, {rounds} rounds, {int(speed)} rounds per second", RNS.LOG_DEBUG)
return stamp, value
def cancel_work(message_id):
if RNS.vendor.platformutils.is_windows() or RNS.vendor.platformutils.is_darwin():
try:
if message_id in active_jobs:
active_jobs[message_id] = True
except Exception as e:
RNS.log("Error while terminating stamp generation workers: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
elif RNS.vendor.platformutils.is_android():
try:
if message_id in active_jobs:
active_jobs[message_id] = True
except Exception as e:
RNS.log("Error while terminating stamp generation workers: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
else:
try:
if message_id in active_jobs:
stop_event = active_jobs[message_id][0]
result_queue = active_jobs[message_id][1]
stop_event.set()
result_queue.put(None)
active_jobs.pop(message_id)
except Exception as e:
RNS.log("Error while terminating stamp generation workers: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
def job_simple(stamp_cost, workblock, message_id):
# A simple, single-process stamp generator.
# should work on any platform, and is used
# as a fall-back, in case of limited multi-
# processing and/or acceleration support.
platform = RNS.vendor.platformutils.get_platform()
RNS.log(f"Running stamp generation on {platform}, work limited to single CPU core. This will be slower than ideal.", RNS.LOG_WARNING)
rounds = 0
pstamp = os.urandom(256//8)
st = time.time()
active_jobs[message_id] = False;
def sv(s, c, w):
target = 0b1<<256-c; m = w+s
result = RNS.Identity.full_hash(m)
if int.from_bytes(result, byteorder="big") > target: return False
else: return True
while not sv(pstamp, stamp_cost, workblock) and not active_jobs[message_id]:
pstamp = os.urandom(256//8); rounds += 1
if rounds % 2500 == 0:
speed = rounds / (time.time()-st)
RNS.log(f"Stamp generation running. {rounds} rounds completed so far, {int(speed)} rounds per second", RNS.LOG_DEBUG)
if active_jobs[message_id] == True:
pstamp = None
active_jobs.pop(message_id)
return pstamp, rounds
def job_linux_managed(stamp_cost, workblock, message_id):
ctx = multiprocessing.get_context("fork")
cores = multiprocessing.cpu_count()
jobs = cores if cores <= 12 else int(cores/2)
allow_kill = True
stamp = None
total_rounds = 0
with worker_context(ctx, stamp_cost, workblock, message_id) as (stop_event, result_queue, rounds_queue, job_procs):
RNS.log(f"Starting {jobs} stamp generation workers", RNS.LOG_DEBUG)
def job(stop_event, sc, wb, worker_id):
terminated = False
rounds = 0
pstamp = os.urandom(256//8)
def sv(s, c, w):
target = 0b1<<256-c; m = w+s
result = RNS.Identity.full_hash(m)
if int.from_bytes(result, byteorder="big") > target: return False
else: return True
while not stop_event.is_set() and not sv(pstamp, sc, wb):
pstamp = os.urandom(256//8); rounds += 1
if not stop_event.is_set():
stop_event.set()
try: result_queue.put_nowait(pstamp)
except: pass
try: rounds_queue.put_nowait(rounds)
except: pass
for jpn in range(jobs):
p = ctx.Process(target=job, args=(stop_event, stamp_cost, workblock, jpn), daemon=True)
job_procs.append(p)
p.start()
try: stamp = result_queue.get(timeout=None)
except Exception as e:
RNS.log(f"Failed to get result from workers: {e}", RNS.LOG_ERROR)
stamp = None
# Collect any potential spurious
# results from worker queue.
try:
while True: result_queue.get_nowait()
except: pass
for j in range(jobs):
nrounds = 0
try:
nrounds = rounds_queue.get(timeout=2)
except Exception as e:
RNS.log(f"Failed to get round stats part {j}: {e}", RNS.LOG_ERROR)
total_rounds += nrounds
all_exited = False
exit_timeout = time.time() + 5
while time.time() < exit_timeout:
if not any(p.is_alive() for p in job_procs):
all_exited = True
break
time.sleep(0.1)
if not all_exited:
RNS.log("Stamp generation IPC timeout, possible worker deadlock. Terminating remaining processes.", RNS.LOG_ERROR)
if allow_kill:
for j in range(jobs):
process = job_procs[j]
process.kill()
else:
return None
else:
for j in range(jobs):
process = job_procs[j]
process.join()
# RNS.log(f"Joined {j} / {process}", RNS.LOG_DEBUG) # TODO: Remove
return stamp, total_rounds
def job_linux(stamp_cost, workblock, message_id):
allow_kill = True
stamp = None
total_rounds = 0
cores = multiprocessing.cpu_count()
jobs = cores if cores <= 12 else int(cores/2)
stop_event = multiprocessing.Event()
result_queue = multiprocessing.Queue(1)
rounds_queue = multiprocessing.Queue()
def job(stop_event, pn, sc, wb):
terminated = False
rounds = 0
pstamp = os.urandom(256//8)
def sv(s, c, w):
target = 0b1<<256-c; m = w+s
result = RNS.Identity.full_hash(m)
if int.from_bytes(result, byteorder="big") > target:
return False
else:
return True
while not stop_event.is_set() and not sv(pstamp, sc, wb):
pstamp = os.urandom(256//8); rounds += 1
if not stop_event.is_set():
stop_event.set()
result_queue.put(pstamp)
rounds_queue.put(rounds)
job_procs = []
RNS.log(f"Starting {jobs} stamp generation workers", RNS.LOG_DEBUG)
for jpn in range(jobs):
process = multiprocessing.get_context("fork").Process(target=job, kwargs={"stop_event": stop_event, "pn": jpn, "sc": stamp_cost, "wb": workblock}, daemon=True)
job_procs.append(process)
process.start()
active_jobs[message_id] = [stop_event, result_queue]
stamp = result_queue.get()
# Collect any potential spurious
# results from worker queue.
try:
while True: result_queue.get_nowait()
except: pass
for j in range(jobs):
nrounds = 0
try:
nrounds = rounds_queue.get(timeout=2)
except Exception as e:
RNS.log(f"Failed to get round stats part {j}: {e}", RNS.LOG_ERROR)
total_rounds += nrounds
all_exited = False
exit_timeout = time.time() + 5
while time.time() < exit_timeout:
if not any(p.is_alive() for p in job_procs):
all_exited = True
break
time.sleep(0.1)
if not all_exited:
RNS.log("Stamp generation IPC timeout, possible worker deadlock. Terminating remaining processes.", RNS.LOG_ERROR)
if allow_kill:
for j in range(jobs):
process = job_procs[j]
process.kill()
else:
return None
else:
for j in range(jobs):
process = job_procs[j]
process.join()
# RNS.log(f"Joined {j} / {process}", RNS.LOG_DEBUG) # TODO: Remove
return stamp, total_rounds
def job_android(stamp_cost, workblock, message_id):
# Semaphore support is flaky to non-existent on
# Android, so we need to manually dispatch and
# manage workloads here, while periodically
# checking in on the progress.
stamp = None
start_time = time.time()
total_rounds = 0
rounds_per_worker = 1000
use_nacl = False
try:
import nacl.encoding
import nacl.hash
use_nacl = True
except:
pass
if use_nacl:
def full_hash(m):
return nacl.hash.sha256(m, encoder=nacl.encoding.RawEncoder)
else:
def full_hash(m):
return RNS.Identity.full_hash(m)
def sv(s, c, w):
target = 0b1<<256-c
m = w+s
result = full_hash(m)
if int.from_bytes(result, byteorder="big") > target:
return False
else:
return True
wm = multiprocessing.Manager()
jobs = multiprocessing.cpu_count()
def job(procnum=None, results_dict=None, wb=None, sc=None, jr=None):
# RNS.log(f"Worker {procnum} starting for {jr} rounds...") # TODO: Remove
try:
rounds = 0
found_stamp = None
while True:
pstamp = os.urandom(256//8)
rounds += 1
if sv(pstamp, sc, wb):
found_stamp = pstamp
break
if rounds >= jr:
# RNS.log(f"Worker {procnum} found no result in {rounds} rounds") # TODO: Remove
break
results_dict[procnum] = [found_stamp, rounds]
except Exception as e:
RNS.log(f"Stamp generation worker error: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
active_jobs[message_id] = False;
RNS.log(f"Dispatching {jobs} workers for stamp generation...", RNS.LOG_DEBUG) # TODO: Remove
results_dict = wm.dict()
while stamp == None and active_jobs[message_id] == False:
job_procs = []
try:
for pnum in range(jobs):
pargs = {"procnum":pnum, "results_dict": results_dict, "wb": workblock, "sc":stamp_cost, "jr":rounds_per_worker}
process = multiprocessing.Process(target=job, kwargs=pargs)
job_procs.append(process)
process.start()
for process in job_procs:
process.join()
for j in results_dict:
r = results_dict[j]
total_rounds += r[1]
if r[0] != None:
stamp = r[0]
if stamp == None:
elapsed = time.time() - start_time
speed = total_rounds/elapsed
RNS.log(f"Stamp generation running. {total_rounds} rounds completed so far, {int(speed)} rounds per second", RNS.LOG_DEBUG)
except Exception as e:
RNS.log(f"Stamp generation job error: {e}")
RNS.trace_exception(e)
active_jobs.pop(message_id)
return stamp, total_rounds
# def stamp_value_linear(workblock, stamp):
# value = 0
# bits = 256
# material = RNS.Identity.full_hash(workblock+stamp)
# s = int.from_bytes(material, byteorder="big")
# return s.bit_count()
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
RNS.log("No cost argument provided", RNS.LOG_ERROR)
exit(1)
else:
try: cost = int(sys.argv[1])
except Exception as e:
RNS.log(f"Invalid cost argument provided: {e}", RNS.LOG_ERROR)
exit(1)
rounds = 1
if len(sys.argv) > 2:
try: rounds = int(sys.argv[2])
except Exception as e:
RNS.log(f"Invalid rounds argument provided: {e}", RNS.LOG_ERROR)
exit(1)
RNS.loglevel = RNS.LOG_DEBUG
for i in range(rounds):
RNS.log("", RNS.LOG_DEBUG)
RNS.log(f"Round {i+1} of {rounds}", RNS.LOG_DEBUG)
RNS.log("Testing LXMF stamp generation", RNS.LOG_DEBUG)
message_id = os.urandom(32)
generate_stamp(message_id, cost)
RNS.log("", RNS.LOG_DEBUG)
RNS.log("Testing propagation stamp generation", RNS.LOG_DEBUG)
message_id = os.urandom(32)
generate_stamp(message_id, cost, expand_rounds=WORKBLOCK_EXPAND_ROUNDS_PN)
RNS.log("", RNS.LOG_DEBUG)
RNS.log("Testing peering key generation", RNS.LOG_DEBUG)
message_id = os.urandom(32)
generate_stamp(message_id, cost, expand_rounds=WORKBLOCK_EXPAND_ROUNDS_PEERING)
# transient_list = []
# st = time.time(); count = 10000
# for i in range(count): transient_list.append(os.urandom(256))
# validate_pn_stamps(transient_list, 5)
# dt = time.time()-st; mps = count/dt
# RNS.log(f"Validated {count} PN stamps in {RNS.prettytime(dt)}, {round(mps,1)} m/s", RNS.LOG_DEBUG)

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ import os
import glob
from .LXMessage import LXMessage
from .LXMRouter import LXMRouter
from .LXMF import *
from ._version import __version__

View File

@ -1 +1 @@
__version__ = "0.3.4"
__version__ = "1.1.0"

33
MIRROR.md 100644
View File

@ -0,0 +1,33 @@
This repository is a public mirror. All potential future development is happening elsewhere.
I am stepping back from all public-facing interaction with this project. Reticulum has always been primarily my work, and continuing in the current public, internet-facing model is no longer sustainable.
The software remains available for use as-is. Occasional updates may appear at unpredictable intervals, but there will be no support, no responses to issues, no discussions, and no community management in this or any other public venue. If it doesn't work for you, it doesn't work. That is the entire extent of available troubleshooting assistance I can offer you.
If you've followed this project for a while, you already know what this means. You know who designed, wrote and tested this, and you know how many years of my life it took. You'll also know about both my particular challenges and strengths, and how I believe anything worth building needs to be built and maintained with our own hands.
Seven months ago, I said I needed to step back, that I was exhausted, and that I needed to recover. I believed a public resolve would be enough to effectuate that, but while striving to get just a few more useful features and protocols out, the unproductive requests and demands also ramped up, and I got pulled back into the same patterns and draining interactions that I'd explicitly said I couldn't sustain anymore.
So here's what you might have already guessed: I'm done playing the game by rules I can't win at.
Everything you need is right here, and by any sensible measure, it's done. Anyone who wants to invest the time, skill and persistence can build on it, or completely re-imagine it with different priorities. That was always the point.
The people who actually contributed - you know who you are, and you know I mean it when I say: Thank you. All of you who've used this to build something real - that was the goal, and you did it without needing me to hold your hand.
The rest of you: You have what you need. Use it or don't. I am not going to be the person who explains it to you anymore.
This is not a temporary break. It's not "see you after some rest", but a recognition that the current model is fundamentally incompatible with my life, my health, and my reality.
If you want to support continued work, you can do so at the donation links listed in this repository. But please understand, that this is not purchasing support or guaranteeing updates. It is support for work that happens on my timeline, according to my capacity, which at the moment is not what it was.
If you want Reticulum to continue evolving, you have the power to make that happen. The protocol is public domain. The code is open source. Everything you need is right here. I've provided the tools, but building what comes next is not my responsibility anymore. It's yours.
To the small group of people who has actually been here, and understood what this work was and what it cost - you already know where to find me if it actually matters.
To everyone else: This is where we part ways. No hard feelings. It's just time.
---
असतो मा सद्गमय
तमसो मा ज्योतिर्गमय
मृत्योर्मा अमृतं गमय

View File

@ -16,10 +16,20 @@ create_symlinks:
-ln -s ../../LXMF ./LXMF/Utilities/LXMF
build_wheel:
python3 setup.py sdist bdist_wheel
python3 setup.py bdist_wheel
release: remove_symlinks build_wheel create_symlinks
build_sdist:
python3 setup.py sdist
build_spkg: remove_symlinks build_sdist create_symlinks
release: remove_symlinks build_wheel build_spkg create_symlinks
upload:
@echo Ready to publish release over Reticulum
@read VOID
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/lxmf create $$(python setup.py --getversion):dist --name lxmf
upload-pip:
@echo Uploading to PyPi...
twine upload dist/*
twine upload dist/*.whl dist/*.tar.gz

View File

@ -1,14 +1,23 @@
# Lightweight Extensible Message Format
LXMF is a simple and flexible messaging format and delivery protocol that allows a wide variety of implementations, while using as little bandwidth as possible. It is built on top of [Reticulum](https://reticulum.network) and offers zero-conf message routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports.
LXMF is a simple and flexible messaging format and delivery protocol that allows a wide variety of implementations, while using as little bandwidth as possible. It is built on top of [Reticulum](a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=reticulum) and offers zero-conf message routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports.
LXMF is efficient enough that it can deliver messages over extremely low-bandwidth systems such as packet radio or LoRa. Encrypted LXMF messages can also be encoded as QR-codes or text-based URIs, allowing completely analog *paper message* transport.
User-facing clients built on LXMF include:
- [Sideband](https://unsigned.io/sideband)
- [MeshChat](https://github.com/liamcottle/reticulum-meshchat)
- [Nomad Network](https://unsigned.io/nomadnet)
- [Nexus Messenger](https://github.com/HarlekinSimplex/nexus_messenger)
Community-provided tools and utilities for LXMF include:
- [LXMFy](https://lxmfy.quad4.io/)
- [LXMF-Bot](https://github.com/randogoth/lxmf-bot)
- [LXMF Messageboard](https://github.com/chengtripp/lxmf_messageboard)
- [LXMEvent](https://github.com/faragher/LXMEvent)
- [RangeMap](https://github.com/faragher/RangeMap)
- [LXMF Tools](https://github.com/SebastianObi/LXMF-Tools)
## Structure
@ -76,7 +85,6 @@ lxm_router = LXMF.LXMRouter()
message = LXMF.LXMessage(destination, source, "This is a short, simple message.")
lxm_router.handle_outbound(message)
```
The LXM Router then handles the heavy lifting, such as message packing, encryption, delivery confirmation, path lookup, routing, retries and failure notifications.
@ -85,11 +93,11 @@ The LXM Router then handles the heavy lifting, such as message packing, encrypti
LXMF uses encryption provided by [Reticulum](https://reticulum.network), and thus uses end-to-end encryption by default. The delivery method of a message will influence which transport encryption scheme is used.
- A message can be delivered opportunistically, embedded in a single Reticulum packet. In this cases the message will be opportunistically routed through the network, and will be encrypted with _ephemeral_ keys derived with _ECDH_ on _Curve25519_. This mode offers Perfect Forward Secrecy.
- If a message is delivered over a Reticulum link (which is the default method), the message will be encrypted with ephemeral AES-128 keys derived with ECDH on Curve25519. This mode offers forward secrecy.
- If a message is delivered to the Reticulum GROUP destination type, the message will be transported using _AES-128_ encryption.
- A message can be delivered opportunistically, embedded in a single Reticulum packet. In this cases the message will be opportunistically routed through the network, and will be encrypted with per-packet AES-128 keys derived with ECDH on Curve25519.
- If a message is delivered over a Reticulum link (which is the default method), the message will be encrypted with _ephemeral_ keys derived with _ECDH_ on _Curve25519_. This mode offers Perfect Forward Secrecy.
- If a message is delivered to the Reticulum GROUP destination type, the message will be encrypted using the symmetric AES-128 key of the GROUP destination.
## Wire Format & Overhead
@ -102,6 +110,10 @@ Assuming the default Reticulum configuration, the binary wire-format is as follo
The complete message overhead for LXMF is only 111 bytes, which in return gives you timestamped, digitally signed, infinitely extensible, end-to-end encrypted, zero-conf routed, minimal-infrastructure messaging that's easy to use and build applications with.
## Code Examples
Before writing your own programs using LXMF, you need to have a basic understanding of how the [Reticulum](https://reticulum.network) protocol and API works. Please see the [Reticulum Manual](https://reticulum.network/manual/). For a few simple examples of how to send and receive messages with LXMF, please see the [receiver example](./docs/example_receiver.py) and the [sender example](./docs/example_sender.py) included in this repository.
## Example Paper Message
You can try out the paper messaging functionality by using the following QR code. It is a paper message sent to the LXMF address `6b3362bd2c1dbf87b66a85f79a8d8c75`. To be able to decrypt and read the message, you will need to import the following Reticulum Identity to an LXMF messaging app:
@ -110,18 +122,16 @@ You can try out the paper messaging functionality by using the following QR code
The [Sideband](https://unsigned.io/sideband) application allows you to do this easily. After you have imported the identity into an app of your choice, you can scan the following QR code and open it in the app, where it will be decrypted and added as a message.
<p align="center"><img width="50%" src="./docs/paper_msg_test.png"/></p>
[Paper message QR code](a8d24177d946de4f1f0a0fe1af9a1338:/page/blob.mu`g=reticulum|r=lxmf|ref=HEAD|path=docs/paper_msg_test.png)
You can also find the entire message in <a href="lxm://azNivSwdv4e2aoX3mo2MdTAozuI7BlzrLlHULmnVgpz3dNT9CMPVwgywzCJP8FVogj5j_kU7j7ywuvBNcr45kRTrd19c3iHenmnSDe4VEd6FuGsAiT0Khzl7T81YZHPTDhRNp0FdhDE9AJ7uphw7zKMyqhHHxOxqrYeBeKF66gpPxDceqjsOApvsSwggjcuHBx9OxOBy05XmnJxA1unCKgvNfOFYc1T47luxoY3c0dLOJnJPwZuFRytx2TXlQNZzOJ28yTEygIfkDqEO9mZi5lgev7XZJ0DvgioQxMIyoCm7lBUzfq66zW3SQj6vHHph7bhr36dLOCFgk4fZA6yia2MlTT9KV66Tn2l8mPNDlvuSAJhwDA_xx2PN9zKadCjo9sItkAp8r-Ss1CzoUWZUAyT1oDw7ly6RrzGBG-e3eM3CL6u1juIeFiHby7_3cON-6VTUuk4xR5nwKlFTu5vsYMVXe5H3VahiDSS4Q1aqX7I">this link</a>:
You can also find the entire message in this link:
`lxm://azNivSwdv4e2aoX3mo2MdTAozuI7BlzrLlHULmnVgpz3dNT9CMPVwgywzCJP8FVogj5j_kU7j7ywuvBNcr45kRTrd19c3iHenmnSDe4VEd6FuGsAiT0Khzl7T81YZHPTDhRNp0FdhDE9AJ7uphw7zKMyqhHHxOxqrYeBeKF66gpPxDceqjsOApvsSwggjcuHBx9OxOBy05XmnJxA1unCKgvNfOFYc1T47luxoY3c0dLOJnJPwZuFRytx2TXlQNZzOJ28yTEygIfkDqEO9mZi5lgev7XZJ0DvgioQxMIyoCm7lBUzfq66zW3SQj6vHHph7bhr36dLOCFgk4fZA6yia2MlTT9KV66Tn2l8mPNDlvuSAJhwDA_xx2PN9zKadCjo9sItkAp8r-Ss1CzoUWZUAyT1oDw7ly6RrzGBG-e3eM3CL6u1juIeFiHby7_3cON-6VTUuk4xR5nwKlFTu5vsYMVXe5H3VahiDSS4Q1aqX7I`
```
lxm://azNivSwdv4e2aoX3mo2MdTAozuI7BlzrLlHULmnVgpz3dNT9CMPVwgywzCJP8FVogj5j_kU7j7ywuvBNcr45kRTrd19c3iHenmnSDe4VEd6FuGsAiT0Khzl7T81YZHPTDhRNp0FdhDE9AJ7uphw7zKMyqhHHxOxqrYeBeKF66gpPxDceqjsOApvsSwggjcuHBx9OxOBy05XmnJxA1unCKgvNfOFYc1T47luxoY3c0dLOJnJPwZuFRytx2TXlQNZzOJ28yTEygIfkDqEO9mZi5lgev7XZJ0DvgioQxMIyoCm7lBUzfq66zW3SQj6vHHph7bhr36dLOCFgk4fZA6yia2MlTT9KV66Tn2l8mPNDlvuSAJhwDA_xx2PN9zKadCjo9sItkAp8r-Ss1CzoUWZUAyT1oDw7ly6RrzGBG-e3eM3CL6u1juIeFiHby7_3cON-6VTUuk4xR5nwKlFTu5vsYMVXe5H3VahiDSS4Q1aqX7I
```
On operating systems that allow for registering custom URI-handlers, you can click the link, and it will be decoded directly in your LXMF client. This works with Sideband on Android.
## Caveat Emptor
LXMF is beta software, and should be considered experimental. While it has been built with cryptography best practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
## Installation
If you want to try out LXMF, you can install it with pip:
@ -145,14 +155,64 @@ Alternatively, you can use the `pipx` tool to install Reticulum in an isolated e
pipx install lxmf
```
## Daemon Included
The `lxmf` package comes with the `lxmd` program, a fully functional (but lightweight) LXMF message router and propagation node daemon. After installing the `lxmf` package, you can run `lxmd --help` to learn more about the command-line options:
```text
$ lxmd --help
usage: lxmd [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-p] [-i PATH] [-v] [-q] [-s] [--exampleconfig] [--version]
Lightweight Extensible Messaging Daemon
options:
-h, --help show this help message and exit
--config CONFIG path to alternative lxmd config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-p, --propagation-node
run an LXMF Propagation Node
-i PATH, --on-inbound PATH
executable to run when a message is received
-v, --verbose
-q, --quiet
-s, --service lxmd is running as a service and should log to file
--exampleconfig print verbose configuration example to stdout and exit
--version show program's version number and exit
```
Or run `lxmd --exampleconfig` to generate a commented example configuration documenting all the available configuration directives.
## Support LXMF Development
You can help support the continued development of open, free and private communications systems by donating via one of the following channels:
- Monero:
```
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
```
- Bitcoin
```
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
```
- Ethereum
```
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
```
- Liberapay: https://liberapay.com/Reticulum/
- Ko-Fi: https://ko-fi.com/markqvist
## Caveat Emptor
LXMF is beta software, and should be considered experimental. While it has been built with cryptography best practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
## Development Roadmap
LXMF is actively being developed, and the following improvements and features are currently planned for implementation:
- Write and release full API and protocol documentation
- Update examples in readme to actually work
- Content Destinations, and easy to use API for group messaging and discussion threads
- Sneakernet and physical transport functionality
- Content Destinations, and easy to use API for group messaging and discussion threads
- Write and release full API and protocol documentation
- Documenting and possibly expanding LXMF limits and priorities
- Sync affinity based on link speeds and distances, for more intelligently choosing peer sync order
- Markets on LXMF

415
Zen of Reticulum.md 100644
View File

@ -0,0 +1,415 @@
# Zen of Reticulum
## I: The Illusion Of The Center
For the better part of a generation, we have been taught to visualize the digital world through the lens of hierarchy. The mental maps we carry are dominated by a single, misleading image: **The Cloud**.
We imagine the network as a vast, ethereal space "up there" or "out there". A centralized repository of services and data to which we, the lowly clients, must connect. We build our software with this assumption hardcoded into our logic: *There is a server. The server has the authority. The server knows the way. I must find the server to function*.
This is the Client-Server mental model, and it is the primary obstacle to understanding Reticulum.
### Fallacy Of The Cloud
The first step in the Zen of Reticulum is to realize that *there is no cloud*. There is only other people's computers. When you build for the cloud, you are building *for* a landlord. You are accepting that your application's existence is conditional on the permission, uptime, and continued goodwill of a central authority.
In Reticulum, you must shift your thinking from "connecting to" to "being among". Reticulum is not a service you subscribe to - *it is a fabric you inhabit*. There is no "up there". There is only *here* and *there*, and the space between them is peer-to-peer.
### Decentralization Or Uncentralizability?
It is common to hear the word "decentralized" thrown around in modern tech circles. But often, this is merely a marketing term for "slightly distributed centralization". A blockchain with a few dominant miners, or a federated protocol with a few giant servers. *In practice*, it's still centralized. It simply has a few centers instead of one.
Reticulum goes further. It wants **Uncentralizability**.
This is not a wishful political stance, but a foundational mathematical characteristic of the protocol, onto which everything else has been built. Reticulum assumes that every peer on the network is potentially hostile, and every link is potentially compromised. It is designed with no "privileged" nodes. While some nodes may act as Transport Instances - forwarding traffic for others - they do so *blindly*, and they only know about their immediate surroundings, and nothing more. They route based on cryptographic proofs, not on administrative privilege. They cannot see who is talking to whom, nor can they selectively manipulate traffic without breaking their own ability to route entirely.
The system is designed to make hierarchy structurally impossible. You cannot hijack an address, because there is no central registry to hijack. You cannot block a user, because there is no central switch to flip. You can offer paths through the network, but you can't force anyone to use them.
### Death To The Address
To break free of the center, you must also let go of the concept of the "Address".
In the IP world, an address is a location. It is a coordinate in a *deeply hierarchical* and static grid. If you move your computer to a different house, your address changes. If your router reboots, your address might change. Your *identity* is bound to your *location*, and therefore, it is fragile, and easily controlled.
Reticulum abolishes this link between *Identity* and *Location*.
In Reticulum, an address is not a place; it is a **Hash of an Identity**. It is a cryptographic representation of *who* you are, not *where* you are. Because of this, your address is portable. You can take a laptop from a WiFi cafe in Berlin, to a LoRa mesh in the mountains, to a packet radio link on a boat, and your "address" - your *Destination Hash* - never changes.
The network does not route to a place; it routes to a *person* (or a machine). When you send a packet, you are not targeting a coordinate in a grid; you are encrypting a message for a specific entity. The network dynamically discovers where that entity currently resides, and it does so in a way where no one really knows where that entity is actually located physically.
**Consider:**
- **The Old Way:** *"I am at `192.168.1.5`. Come find me"*.
- **The Zen Way:** *"I am `<327c1b2f87c9353e01769b01090b18f2>`. Wherever I am, my peers can reach me"*.
Once you stop thinking about servers and start thinking about portable identities, where everyone can always reach everyone else directly, the illusion of the center fades away. You realize there *is* no center holding the network together. No coordinators or bureaucrats required. The network is simply the sum of its peers, communicating directly, sovereignly, and without a master.
## II: Physics Of Trust
*Paranoia Is A Great Design Principle*
If we accept that there is no center - that the network is a chaotic, peer-to-peer mesh - we are forced to confront a terrifying reality: **There is no one guarding the door**.
In the traditional networking mindset, we rely on the concept of the "trusted core". We assume our local coffee shop WiFi is safe, or that the backbone providers are neutral custodians. We build our security like a castle: strong walls on the outside, soft and trusting on the inside. We use encryption only when we step out into the "wild" internet.
### Hostile Environments
The Zen of Reticulum requires you to invert this. You must assume that *every* environment is hostile. This isn't cynicism, just uncaring physics.
When you transmit information over radio waves, you are shouting into a crowded room. Anyone can listen. When you traverse the internet, your packets pass through routers controlled by strangers, corporations, and state actors. Assuming privacy in this environment without cryptographic protection is not optimism but gross negligence.
Reticulum is built on the premise that every link is tapped, and every peer is a potential adversary. If your system cannot survive an adversary owning the physical layer, it cannot survive at all.
But this is the paradox: By assuming the network is hostile, you make it safe. When you accept the dangers for what they are, they become manageable. When you stop trusting the infrastructure and start trusting the math, you eliminate the single point of failure: Human integrity.
### Encryption Is Not A Feature
In the world of TCP/IP, encryption is an afterthought. It is a layer we slap on top of the protocol (HTTPS, TLS) to patch the security holes of the original design. It is a "feature" you sometimes *enable* for "sensitive data". This is fundamentally flawed, since all data is sensitive.
In Reticulum, encryption is **gravity**.
It is not optional. It is not a plugin. It is the *fundamental force that allows the network to exist*. If you were to strip the encryption from Reticulum, the routing would break. The Transport system uses cryptographic signatures and entropy to verify paths and pass information. If packets were plaintext, intermediate nodes could not prove that a route was valid, nor could endpoints prevent spoofing or tampering.
In Reticulum, the entropy of the encrypted packet *is* the routing logic.
To ask for a version of Reticulum without encryption is like asking for a version of the ocean without liquid. You are not asking for a feature change; you're asking for a different physical universe. We design for a universe where information has mass, structure, and integrity.
### Zero-Trust Architectures
We must unlearn our reliance on **Institutional Trust**.
For decades, we have been trained to trust authorities. We trust a website because a chain of Certificate Authorities (companies we don't know) vouches for it. We trust an app because it is in an app store (run by a corporation we don't control). We trust a message because it comes from a phone number assigned by a telecom. Yet, everything in our digital information sphere today is more untrustworthy and risky than a medieval second-hand underwear market.
Reticulum replaces institutional trust with **Cryptographic Proof**.
In Reticulum, you do not trust a node because it has a nice hostname or because it is listed in a directory. You trust it because it holds the private key corresponding to the Destination Hash you are communicating with. This trust is binary, mathematical, and **absolute**. Either the signature matches, or it does not. There is no "maybe".
This shift moves the power from the institution to the individual. You become the ultimate arbiter of your own trust relationships. You decide which keys to accept, which paths to follow, and which identities to recognize.
**Consider:**
- **The Old Way:** *"I trust this site because the browser says the lock icon is green"*.
- **The Zen Way:** *"I trust this destination because I have verified its hash fingerprint out-of-band, and the math confirms the signature"*.
When you internalize the Physics of Trust, you stop looking for protection from firewalls, VPNs, and Terms of Service agreements. You realize that true security comes from the design of the protocol itself. You can stop trusting the cloud, and you start trusting the code - because you can verify it yourself.
## III: Merits Of Scarcity
*Every Bit Counts*
We have grown addicted to abundance. In the modern digital ecosystem, bandwidth is treated as an endless, flat ocean. We stream high-definition video without a thought, we ship entire libraries of code just to render a single button, and we measure performance in gigabits per second. This abundance has hollowed out our craft. When constraints vanish, efficiency dies, and with it, a certain kind of Clarity and Quality.
Reticulum asks you to step out of the ocean and onto the tightrope.
### The Bandwidth Fallacy
The Zen of Reticulum requires the realization that **5 bits per second is a valid speed**.
To a modern developer, this sounds like paralysis. But there is a profound freedom in limits: When you have a gigabit connection, you can be incredibly sloppy. You can be wasteful. You can push your problems onto the infrastructure. *"Its slow? Get a faster router"*.
But on a high-latency, low-bandwidth link (be it a noisy HF radio channel or a tenuous LoRa hop) you cannot push problems anywhere. You must solve them. The network does not negotiate with waste.
This forces a shift from consumption to interaction. You are no longer, then, consuming a service provided by a fat pipe; you are engaging in a careful negotiation with the physical medium. The medium becomes a partner in the conversation, not just a dumb conduit. You suddenly need to *understand the world to be in it*.
### Cost Of A Byte
In a scarce economy, a byte is not just data, but energy, time, and space.
Every byte you transmit consumes battery life on a solar-powered node. It occupies valuable airtime that could have been used by another peer. It represents a measurable slice of the electromagnetic spectrum.
When you internalize this, you begin to write code differently. You stop asking, "How much data can I send?" and start asking, "What is the *minimum* amount of information required to convey this intent? How can I best utilize my informational entropy?"
This is where the elegance of Reticulum shines. The protocol is designed to strip away the non-essential. A link establishment takes three very small packets. A destination hash fits in 16 bytes. The overhead is vanishingly small, leaving almost the entire channel for the message itself.
**Consider:**
- **The Old Way:** *"I need to send a status update. I'll send a JSON object with metadata, timestamps, and user profile info (15KB)."*
- **The Zen Way:** *"I need to send a status update. I'll send a single byte representing the state code. The context is already known."*
This is of course optimization, but more importantly, *it is a form of respect*. Efficiency in a shared medium is an act of stewardship. By taking only what you need from the network, you leave room for others. The network listens to those who speak with purpose.
### Flow & Time
Scarcity also teaches us about time. We have become addicted to the *synchronous* now - the instant ping, the real-time stream. But Reticulum embraces *asynchronous* time.
When links are intermittent and latency is measured in minutes or hours, "real-time" is an illusion. Reticulum doesn't encourage **Store and Forward** as a mere fallback, but as a primary mode of existence. You write a message, it propagates when it can, and it arrives when it arrives.
This changes the psychological texture of communication. It removes the anxiety of the immediate response. It allows for contemplation. You are not demanding the recipient's attention *right now*; you are placing a gift in their path, to be found when they are ready.
By designing for delay, you design for resilience. You are no longer building a house of cards that collapses when a single packet drops. You are building a stone arch that distributes the load *over time*.
### Liberation From Limits
There is a strange optimism in scarcity. When you are forced to work within strict constraints, you are forced to prioritize. *You* must decide what truly matters. *That* is the real core of agency.
In the infinite fantasy world of The Cloud, everything is urgent, so nothing is. In the economy of Reticulum, the cost of transmission forces you to weigh the value of your message. Do you really need to send that heart beat? Is that photo essential?
When you strip away the noise, what remains is *signal*.
This discipline creates a different kind of developer. It creates a craftsman who understands that the best code is the code you don't have to write. It creates a user who understands that the most powerful message is the one that is *understood*, not the one that is loudest. In the world of Reticulum, you are not a mere consumer of bandwidth; you are an architect of intent.
## IV: Sovereignty Through Infrastructure
**Be Your Own Network**
We live in an era of digital tenancy. We lease our connectivity from ISPs. We rent our storage from cloud providers. We even borrow our identity from social media platforms. We are tenants in a house we did not build, governed by rules we did not write, subject to eviction at the whim of a landlord who has never met us.
The Zen of Reticulum is the realization that you *can* own the house.
### A Carrier-Grade Fallacy
For decades, we have been gaslit into believing that networking is really not just hard, but impossible. It is presented as a dark art reserved for telcos and billionaires, requiring millions of dollars of fiber optics, climate-controlled data centers, and armies of engineers. We are told that building reliable infrastructure is "too complex" for the individual or small organization.
This is a big, fat lie.
Physics is simple. A radio wave needs a transmitter and a receiver. A packet needs a path. The "complexity" of the modern internet is largely bureaucratic - a mountain of billing systems, regulatory capture, and legacy cruft designed to keep the gatekeepers in power.
Reticulum strips away the bureaucracy. It runs on hardware that costs the price of a dinner. It runs on spectrum that is free to use. It demonstrates that a robust, planetary-scale network does not require a Fortune 500 company. It requires only the will to deploy, and the distributed, uncoordinated efforts of many individuals.
### Personal Infrastructure
This is where the rubber meets the road. You can read about Reticulum, you can understand the theory, but the insights only arrive when you plug in a radio and run a Transport Node. Suddenly, you are no longer a consumer. You're an operator.
This shift is subtle but profound. When you run your own infrastructure, the network ceases to be a service that is provided *to* you. It becomes a space that you *inhabit*. You become responsible for the flow of information. You gain an intimate understanding of the medium - the way the weather affects the radio waves, the way the topology changes, the way the packets dance through the ether.
There is a quiet competence that comes from this. You stop asking "Is the internet down?" and start asking "Is *my* links up?" You stop waiting for a technician and start checking the logs. This is a form of strength. To understand the system that carries your words is to be free from the mystery that keeps you dependent.
### The Ability To Disconnect
Why go to the trouble? Why buy the radio, write the config, and leave the Pi running in the corner?
Because the old, centralized network is fragile. And because most of us doesn't even really want to be there anymore.
The internet we rely on today is a chain of single points of failure. Cut the undersea cable, and a continent goes dark. Shut down the power grid, and the cloud evaporates. Deprioritize the "wrong" traffic, and the flow of information is strangled.
Sovereignty is the ability to survive the cut, whether or not that cut was an accident or on purpose.
When you build your own infrastructure, you build a lifeline. Reticulum is designed to function over media that the traditional internet cannot touch - bare wires, battery-powered radios, ad-hoc WiFi meshes. When the grid fails, or the censors arrive, or the bill goes unpaid, your Reticulum network continues to hum.
This is not about "dropping out" of society. It is about building a substrate on which an actual *Society* can function.
**Consider:**
- **The Old Way:** "My connection is slow. I should call my ISP and complain."
- **The Zen Way:** "The path is noisy. I will adjust the antenna or find a better route."
By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone else's megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.
# V: Identity and Nomadism
**A Fluid Self**
In the old world, you are defined by your coordinates. If you are at `34.109.71.5`, you're *here*. If you unplug the cable and walk down the street, you vanish. Your digital self evaporates because it was tethered to the wall. You are a ghost in the endless machinations of gears, levers and transistors, bound to the hardware, and those that own it.
This creates a subtle, constant anxiety. We are terrified of disconnecting because, in the architecture of the old web, disconnecting is a kind of death.
The Zen of Reticulum offers a different way to be.
### Portable Existence
In Reticulum, your identity is not a location, or a username granted by a service. It is a cryptographic key - a complex, unique mathematical signature that exists independently of the physical world. You can carry it only in your mind, if you want to.
Think of it less like a street address and more like a name. *A true name*.
If you travel from Berlin to Tokyo, you do not change your name. You are still you. The people who know you can still recognize you. Reticulum applies this principle to the network layer. Your Destination Hash is **invariant**. It travels with you, stored securely on your device, *immutable as a stone*.
This changes the relationship between you and the machine. You are not "logged into" the network via a specific gateway. You *are* the endpoint. The network does not connect to a place; *it converges on you*.
### Roaming Nodes
This freedom introduces a new concept of time and space: **Nomadism**.
Because your identity is portable, your connectivity can be fluid. You can be sitting at a desk connected to a fiber backbone one moment, and walking through a field connected only to a long-range LoRa mesh the next. To the rest of the network, nothing has changed. Your friends do not need to update your contact info. The messages they send do not bounce back. The network senses the shift in the medium and reroutes the flow of data automatically.
You are no longer a stationary node in a fixed grid. You are a wanderer in a fluid medium.
The interfaces - whether it is WiFi, Ethernet, Packet Radio, or a physical wire - is merely the clothing your node wears. You change it to suit the environment. Underneath, you remain the same. This is the liberation of the protocol. It treats the physical medium as a transient circumstance, not a definition of self.
**Consider:**
- **The Old Way:** *"I lost connection. I have to reconnect to the VPN to tell them where I am now."*
- **The Zen Way:** *"I moved. The network subtly bends to accomodate this new reality."*
### Announcing Presence
How does the network find a wanderer? It listens.
In the IP world, we query directories. We ask a server, "Where is Mark?" The server checks its database and gives us a coordinate. This means that someone, somewhere, is keeping track of you. It assumes and *requires* surveillance.
Reticulum replaces surveillance with **Announces**.
Instead of asking a central authority where you are, you simply state your presence. You broadcast a cryptographic proof: "I am here, and I am who I say I am". This ripples out through the mesh. Your neighbors hear it, update their path tables, and pass it on.
This is a quiet, organic process. It is the digital equivalent of lighting lanterns in the dark. You do not need to chase the light; you let the light find you. It respects your autonomy. You choose when to announce, how often to speak, and to whom. You also choose when to disappear - for but a moment or perpetually.
### Anchor In The Flow
There is a deep peace in this nomadism. It teaches you that stability does not come from standing still. Stability comes from *internal coherence*.
By holding your own private key, you hold your own center of gravity. The world around you; the infrastructure, the topography and the availability of links can all shift chaotically. Storms can knock out towers. Cables can be cut. The internet can go down.
But as long as you possess your key, you possess your identity. The entire infrastructure can be destroyed and rebuilt, and you are still you. Nothing lasts, yet nothing is lost.
You become a sovereign entity moving through the noise, connected not by the rigidity of cables, but by the fluidity of recognition. The network becomes a place you inhabit, rather than a utility you subscribe to: You are at home in the ether.
## VI: Ethics Of The Tool
**Technology With Conscience**
You have unlearned the center. You have accepted the physics of trust. You have embraced the economy of scarcity and the freedom of unbound nomadism. You are standing in a new space. Now, look at the tool in your hand.
In the old world, we were taught that technology is neutral. We are told that "guns don't kill people, people do", or that a component is just a component, indifferent to what its combinatorial potential is. This is a convenient lie. It serves only to allow the builders to wash their hands of responsibility.
But we know better now. We know that **architecture is politics**, and *politics is control*. The way you build a system determines how it will be used. If you build a system optimized for mass surveillance, you *will* get a panopticon. If you build a system optimized for centralized control, you *will* get a dictatorship. If you build a system optimized for extraction, you *will* get a parasite.
The Zen of Reticulum asserts that a tool is never neutral.
On the very contrary: A tool is intent, **crystallized**.
### The Harm Principle
Why does the Reticulum License forbid the software from being used in systems designed to harm humans? Is it not just a restriction on freedom?
It is a restriction on *license*, yes, but it is an expansion of *freedom*.
Building powerful tools without a moral compass is in no way virtuous or commendable, it is plain and simple irresponsibility.
A tool that can easily be used to oppress is a real danger to the user. If you build a network that can be turned against you by a tyrant, you are not free. You are merely waiting for the leash to tighten. By encoding the "Harm Principle" into the legal DNA of the reference implementation, we are building a safeguard. We are stating, clearly and immutably, that *this tool* is for **life**, not for death.
This aligns the software with the interests of humanity. It cements that the network cannot be conscripted into a kill-system, a weaponized drone controller, or a torture device without breaking the license and the law. It is a line drawn in the sand - not by a government or external authority, but by the creators of the tool itself.
**Consider:**
- **The Old Way:** *"It's just software. How people use it is not my problem."*
- **The Zen Way:** *"This software is a habitat. I will not allow it to be used to build a cage."*
It is *your* choice whether to align with this - we are not forcing this stance on anyone. If you choose to align with life over death, with creativity over destruction, we grant you an immensely powerful tool, to own and build with as you please. If you do not, we deny it.
If you do not like this, we most assuredly do not need you here, and you are on your own.
### Public Domain Protocol
This leads to a vital distinction: The difference between the *idea* and the *implementation*.
The protocol - the mathematical rules of how Reticulum works - is dedicated to the Public Domain. It belongs to humanity. **No one can own it**. Anyone can implement it, improve it, or adapt it. This is the core idea of free communication, which itself must be forever free.
But the functional, deployed *reference implementation* - the Python code, the maintenance, the years of labor - has a conscience. This distinction is the engine of sustainability. It allows the protocol to be universal, while ensuring that the specific labor of the builders is not hijacked to undermine the foundational intent of the project itself. From this document, it should be very clear what this intent is.
If you want to build a system with Reticulum that manipulates and damages users for profits or targets missiles, you can use the public domain protocol, and start from scratch. But you cannot take our work. You must do your own. This serves as a pillar of accountability. If you want to build a weapon, *you* go and forge the steel yourself, while the world observes. And when the blood is drawn - it is on **your** hands.
### Preserving Human Agency
We live in an era of predatory extraction. The open-source commons is being scraped, ingested, and regurgitated by machine learning algorithms, whose corporate owners seek to replace the very humans who built those commons. Our code, our words, and our creativity is being used to train systems that are specifically designed to make us obsolete, without offering anything else in return than serfdom and leashes.
Reticulum stands against this.
The license protects the software from being used to feed the beast. It draws a hard line: This tool is for *people*. It is for human-to-human connection. It is not a dataset to be strip-mined for the purpose of building a synthetic overlord, puppeteered by a miniscule conglomerate of controllers.
This is a radical act of preservation. By protecting the code from AI appropriation, we are protecting space for human agency. We are ensuring that there remains a digital realm where the actors are flesh, blood and soul, where decisions are made by minds, not overlords hiding behind models.
When you use Reticulum, you are using a tool that respects you. It does not see you as a product to be tracked. It does not see your data as fuel for an algorithm. It sees you as a sovereign, equal peer.
This changes the foundational premise of using the technology. It restores dignity to the interaction. You are not the user of a service; you are a participant in a mutual covenant. The tool aligns with your autonomy, rather than eroding it.
In this way, ethics is not a restriction, but a foundation. It is the foundation that helps ensure the network will still belong to you tomorrow.
## VII: Design Patterns For Post-IP Systems
**Practical Philosophy for Developers**
The philosophy is useless if it cannot be hammered into code. The metaphors we have explored - nomadism, scarcity, trust - are not just poetry, but real-world engineering constraints. When you sit down to write software for Reticulum, these concepts must shape the very structure of your application.
We are now moving from the *why* to the *how*. This is where the abstract becomes concrete, and where you will see the true depth of the patterns we have been weaving.
### Store & Forward
The web has trained us to be impatient. We write synchronous code. We fire a request and we wait, blocking the UI, holding our breath. If the response doesn't come in 250 milliseconds, we show a spinner. If it doesn't come in five seconds, we show an error. We treat network connectivity as a binary state: either we are "online" or we are "broken".
This is brittle. It is a rejection of reality.
In Reticulum, connectivity is a spectrum, and presence is asynchronous. If at all applicable to your intent, you must design your applications to embrace **Store & Forward**.
Instead of demanding an immediate answer, your application should act as a patient participant. You create a message for someone or something in the mesh. The network holds it. It carries it from node to node, perhaps over hours or days, waiting for the recipient to appear. When they finally surface, the message is delivered. This requires a shift from "request/response" to "event/handler". How exactly you do this is a challenge for you to solve intelligently within your problem domain, but Reticulum-based systems already exist that does this extremely well, and you can use them for inspiration.
**Consider:**
- **The Old Way:** `Connect() -> Send() -> Wait() -> Crash if timeout.`
- **The Zen Way:** `Send() -> Continue living. -> Receive() when it arrives.`
This changes the user experience profoundly. It removes the anxiety of the loading bar. It creates a sense of continuity. The user is not "waiting for the network"; they are interacting with a persistent log of communication that lives in the network itself.
### Naming Is Power
In the IP world, we are slaves to the Domain Name System. We rely on a hierarchy of registrars to map human-readable names to machine-readable addresses. This hierarchy is a choke point. If the registrar revokes your domain, or if the DNS server goes down, you vanish.
Reticulum dissolves this hierarchy with **Hash-based Identity**.
In this design pattern, a name is not a string you look up; it is a cryptographic destination you verify. When you design for Reticulum, you stop asking the user for a URL and start asking for a Destination or Identity Hash.
This feels strange at first. A hash like `<83b7328926fed0d2e6a10a7671f9e237>` looks alien compared to `myfriend.com`. But that alienness is the armor. It **cannot** be spoofed. It **cannot** be censored by a registrar. It is **absolute**.
Designing for this means shifting your UI metaphors. You are no longer browsing a web of pages; you are managing a ledger of keys. You are building an "Address Book" that is actually a keyring. The names are given by the user, and the power stays with them. That hashes look complex is directly analogous to the strengths of the bonds formed by their use. It forces the user to engage in a moment of verification, an out-of-band handshake, which restores the human element of trust that SSL certificates stripped away.
### The Interface Is The Medium
One of the most liberating patterns in Reticulum is **Transport Agnosticism**.
In traditional networking, your code is often littered with transport logic. "Am I on WiFi? Check bandwidth. Am I on Cellular? Check data plan. Am I on Ethernet?". You are constantly micromanaging the pipe.
In Reticulum, you write to the API, and the API writes to the medium. You send a packet to a Destination. You do not care if that packet travels over a TCP tunnel, a LoRa radio wave, or a serial wire interface. That is the stack's concern.
This allows you to write **Universal Applications**.
Imagine a messaging app. You write it once. It works on a laptop connected to fiber. It works on a phone in the city using WiFi. And, without a single line of code changed, it works on a device in the wilderness, talking only to other devices via radio.
The pattern is simple: **Never code to the hardware. Code to the intent.**
**Consider:**
- **The Old Way:** `socket.connect(ip, port)`
- **The Zen Way:** `RNS.Packet(destination, data).send()`
By abstracting the medium, you make your software immortal to changes in infrastructure. The user might switch from a 4G hotspot to a HF modem tomorrow. Your software doesn't need to know. It simply continues the conversation.
### Emergent Patterns
When you combine these patterns - *Store & Forward*, *Hash-based Identity*, and *Transport Agnosticism* - you create software that feels fundamentally different.
It feels *grounded*. It doesn't flicker when the signal drops. It doesn't panic when the server is down. It has weight. It has persistence. It has *relevance*.
You are no longer building a "client" that begs a "server" for attention. You are building an autonomous agent that exists within the mesh. It speaks when it needs to, listens when it can, and carries its identity with it wherever it goes.
This is the culmination of the Zen. The code is not just a set of instructions: It is a behavioral envelope. It is a way of *being* in the network.
## VIII: Fabric Of The Independent
We have stripped away the illusions. We have seen that the center is empty, that trust *must* be hard, that resources are finite, and that we must own our infrastructure. We have seen that tools have ethics and that our identity can move fluidly.
This is a reclaiming of the commons. For too long, we have allowed the most vital substrate of human society - *our ability to speak to one another* - to be colonized by entities that do not share our interests. We have allowed the architecture of our communication to be designed by accountants rather than architects.
We are taking it back. Not by petitioning the masters, but by building the new world within, over, under and around the shell of the old.
### The Work Is Finished
The heavy lifting is done.
The protocol is in the public domain, a gift to humanity that can never be taken away. The software is written, tested, and running on devices scattered across the globe. The manual lies open before you. The source code for the reference implementation is now distributed on hundreds of thousands of devices across the planet. No one can delete or destroy it. The hardware is accessible and abundant.
It was a hard road to get here, but we got here. Now, there is no roadmap committee waiting for approval. There is no venture capital dictating the user experience. There is no CEO to sign off on the next feature release.
There is only you.
The barrier to entry is no longer complexity: It is the mere habit of dependency. You were conditioned to wait. Wait for the app update. Wait for the ISP to fix the line. Wait for the platform to allow the post. Wait for the government to change the policies. Wait for the likes. Wait for the revolution to be televised.
The revolution never was televised.
It is packetized.
### Open Sky
The future of this technology is a construction project.
It looks like a single node on a windowsill, listening to the static. It looks like a message sent to a neighbor, bypassing the noise of the commercial web. It looks like a community mesh that grows, link by link, hop by hop, carried by hands that care more about connection than profit.
You have the blueprints. You have the tools. You have the philosophy. The noise of the old world has fallen away, leaving you with the quiet clarity of the open spectrum.
*Mark, early 2026*

View File

@ -0,0 +1,72 @@
import RNS
import LXMF
import time
required_stamp_cost = 8
enforce_stamps = False
def delivery_callback(message):
global my_lxmf_destination, router
time_string = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp))
signature_string = "Signature is invalid, reason undetermined"
if message.signature_validated:
signature_string = "Validated"
else:
if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID:
signature_string = "Invalid signature"
if message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN:
signature_string = "Cannot verify, source is unknown"
if message.stamp_valid:
stamp_string = "Validated"
else:
stamp_string = "Invalid"
RNS.log("\t+--- LXMF Delivery ---------------------------------------------")
RNS.log("\t| Source hash : "+RNS.prettyhexrep(message.source_hash))
RNS.log("\t| Source instance : "+str(message.get_source()))
RNS.log("\t| Destination hash : "+RNS.prettyhexrep(message.destination_hash))
RNS.log("\t| Destination instance : "+str(message.get_destination()))
RNS.log("\t| Transport Encryption : "+str(message.transport_encryption))
RNS.log("\t| Timestamp : "+time_string)
RNS.log("\t| Title : "+str(message.title_as_string()))
RNS.log("\t| Content : "+str(message.content_as_string()))
RNS.log("\t| Fields : "+str(message.fields))
if message.ratchet_id:
RNS.log("\t| Ratchet : "+str(RNS.Identity._get_ratchet_id(message.ratchet_id)))
RNS.log("\t| Message signature : "+signature_string)
RNS.log("\t| Stamp : "+stamp_string)
RNS.log("\t+---------------------------------------------------------------")
# Optionally, send a reply
# source = my_lxmf_destination
# dest = message.source
# lxm = LXMF.LXMessage(dest, source, "Reply", None, desired_method=LXMF.LXMessage.DIRECT, include_ticket=True)
# router.handle_outbound(lxm)
r = RNS.Reticulum()
router = LXMF.LXMRouter(storagepath="./tmp1", enforce_stamps=enforce_stamps)
identity = RNS.Identity()
my_lxmf_destination = router.register_delivery_identity(identity, display_name="Anonymous Peer", stamp_cost=required_stamp_cost)
router.register_delivery_callback(delivery_callback)
RNS.log("Ready to receive on: "+RNS.prettyhexrep(my_lxmf_destination.hash))
# You can set a propagation node address to test receiving
# messages from a propagation node, instead of directly
# router.set_outbound_propagation_node(bytes.fromhex("e75d9b6a69f82b48b6077cf2242d7499"))
# This loop allows you to execute various actions for testing
# and experimenting with the example scripts.
while True:
input()
RNS.log("Announcing lxmf.delivery destination...")
router.announce(my_lxmf_destination.hash)
# input()
# RNS.log("Requesting messages from propagation node...")
# router.request_messages_from_propagation_node(identity)

View File

@ -0,0 +1,69 @@
import LXMF
import RNS
import time
import random
random_names = ["Tom", "Delilah", "Nancey", "Williams", "Neomi", "Curtis", "Alexa", "Theodora", "Ted", "Dinorah", "Nicol", "Drusilla", "Annalisa", "Verlene", "Latesha", "Tina", "Mia", "Brock", "Timothy", "Philip", "Willian", "Reyna", "Simona", "Mimi", "Stanford", "Ferne", "Catalina", "Lucie", "Jaye", "Natasha", "Willetta", "Isabel", "Esperanza", "Ciara", "Eusebio", "William", "Elma", "Angelica", "Coreen", "Melani", "Jonathan", "Maryland", "Caroline", "Gregg", "Ora", "Jacqui", "Letty", "Roselle", "Oralee", "Angla"]
random_titles = ["Long time", "Hi again", "Hi there", "Test message", "", "", "Something different"]
random_msgs = ["If wishes were horses then beggars might fly. Stuff like that. It's enough to drive you crazy.", "'My ident cards were stolen,' Jason said. 'That fivehundred-dollar bill is yours if you can get me to someone who can replace them. If you're going to do it, do it right now; I'm not going to wait.' Wait to be picked up by a pol or a nat, he thought. Caught here in this rundown dingy hotel.", "A six, no matter what the external circumstances, will always prevail. Because that's the way they genetically defined us.", "'Should be there in an hour,' he called back over his shoulder to Chuck. Then he added, in an afterthought, 'Wonder if the computers finished its run. It was due about now.'. Chuck didnt reply, so George swung round in his saddle. He could just see Chucks face, a white oval turned toward the sky."]
def delivery_callback(message):
pass
r = RNS.Reticulum()
router = LXMF.LXMRouter(storagepath="./tmp2")
router.register_delivery_callback(delivery_callback)
ident = RNS.Identity()
source = router.register_delivery_identity(ident, display_name=random_names[random.randint(0,len(random_names)-1)], stamp_cost=8)
router.announce(source.hash)
RNS.log("Source announced")
print("Recipient: ", end=" ")
recipient_hexhash = input()
recipient_hash = bytes.fromhex(recipient_hexhash)
if not RNS.Transport.has_path(recipient_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.request_path(recipient_hash)
while not RNS.Transport.has_path(recipient_hash):
time.sleep(0.1)
# Recall the server identity
recipient_identity = RNS.Identity.recall(recipient_hash)
dest = RNS.Destination(recipient_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
# This loop allows you to execute various actions for testing
# and experimenting with the example scripts.
while True:
# Create a message that will be sent directly to the
# destination over a Reticulum link:
lxm = LXMF.LXMessage(dest, source, random_msgs[random.randint(0,len(random_msgs)-1)],
random_titles[random.randint(0,len(random_titles)-1)],
desired_method=LXMF.LXMessage.DIRECT, include_ticket=True)
# Or, create an oppertunistic, single-packet message
# for sending without first establishing a link:
# lxm = LXMF.LXMessage(dest, source, "This is a test",
# random_titles[random.randint(0,len(random_titles)-1)],
# desired_method=LXMF.LXMessage.OPPORTUNISTIC, include_ticket=True)
# Or, try sending the message via a propagation node:
# router.set_outbound_propagation_node(bytes.fromhex("e75d9b6a69f82b48b6077cf2242d7499"))
# lxm = LXMF.LXMessage(dest, source, random_msgs[random.randint(0,len(random_msgs)-1)],
# random_titles[random.randint(0,len(random_titles)-1)],
# desired_method=LXMF.LXMessage.PROPAGATED)
# Finally dispatch the message to the LXMF message
# router, which will handle the delivery according
# to the specified message parameters and options:
router.handle_outbound(lxm)
# Wait for user input before starting over
input()

2
requirements.txt 100644
View File

@ -0,0 +1,2 @@
qrcode>=7.4.2
rns>=1.0.0

View File

@ -1,3 +1,4 @@
import sys
import setuptools
with open("README.md", "r") as fh:
@ -5,6 +6,10 @@ with open("README.md", "r") as fh:
exec(open("LXMF/_version.py", "r").read())
if "--getversion" in sys.argv:
print(__version__, end="")
exit(0)
setuptools.setup(
name="lxmf",
version=__version__,
@ -15,9 +20,10 @@ setuptools.setup(
long_description_content_type="text/markdown",
url="https://github.com/markqvist/lxmf",
packages=["LXMF", "LXMF.Utilities"],
license="Reticulum License",
license_files = ("LICENSE"),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
entry_points= {
@ -25,6 +31,6 @@ setuptools.setup(
'lxmd=LXMF.Utilities.lxmd:main',
]
},
install_requires=['rns>=0.6.2'],
python_requires='>=3.7',
install_requires=["rns>=1.4.0"],
python_requires=">=3.7",
)