mirror of
https://github.com/ratspeak/rsLXMF
synced 2026-08-12 18:08:14 -04:00
lxmf: check every valid ticket in stamp validation, cull state at load, use announced stamp flexibility
This commit is contained in:
parent
68ad7c8351
commit
20ef834281
4 changed files with 47 additions and 36 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1095,6 +1095,7 @@ dependencies = [
|
|||
"objc2-foundation",
|
||||
"rand 0.8.5",
|
||||
"rns-crypto",
|
||||
"rns-identity",
|
||||
"rns-transport",
|
||||
"rns-wire",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -130,10 +130,13 @@ impl LxmPeer {
|
|||
peer
|
||||
}
|
||||
|
||||
/// Effective minimum stamp cost this peer will accept.
|
||||
/// Effective minimum stamp cost this peer will accept, using the peer's
|
||||
/// announced flexibility when known (Python `LXMPeer.sync`: cost - flex).
|
||||
pub fn minimum_accepted_stamp_cost(&self) -> u8 {
|
||||
match self.stamp_cost {
|
||||
Some(cost) => cost.saturating_sub(PROPAGATION_COST_FLEX),
|
||||
Some(cost) => {
|
||||
cost.saturating_sub(self.stamp_cost_flexibility.unwrap_or(PROPAGATION_COST_FLEX))
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -465,6 +468,13 @@ mod tests {
|
|||
// cost < flex must saturate at 0.
|
||||
peer.stamp_cost = Some(2);
|
||||
assert_eq!(peer.minimum_accepted_stamp_cost(), 0);
|
||||
|
||||
// Announced flexibility overrides the default.
|
||||
peer.stamp_cost = Some(16);
|
||||
peer.stamp_cost_flexibility = Some(5);
|
||||
assert_eq!(peer.minimum_accepted_stamp_cost(), 11);
|
||||
peer.stamp_cost_flexibility = Some(0);
|
||||
assert_eq!(peer.minimum_accepted_stamp_cost(), 16);
|
||||
}
|
||||
|
||||
/// T0-4: an absurd announce-supplied peering cost must fail the bounded
|
||||
|
|
|
|||
|
|
@ -699,9 +699,9 @@ impl LxmRouter {
|
|||
.add(Ticket::new(token, destination_hash, expires));
|
||||
}
|
||||
|
||||
/// Returns the token of the most-recently-added valid ticket for `destination_hash`.
|
||||
/// Returns the token of the first valid ticket for `destination_hash`.
|
||||
///
|
||||
/// Python reference: `LXMRouter.get_outbound_ticket` — LXMRouter.py:1115-1123.
|
||||
/// Python reference: `LXMRouter.get_outbound_ticket` — LXMRouter.py:1058-1064.
|
||||
pub fn get_outbound_ticket(&self, destination_hash: &[u8; 16]) -> Option<[u8; 16]> {
|
||||
let now = now_f64();
|
||||
self.ticket_store
|
||||
|
|
@ -903,6 +903,11 @@ impl LxmRouter {
|
|||
.replace_locally_delivered(persist::load_local_deliveries(state_dir)?);
|
||||
self.propagation_store
|
||||
.replace_locally_processed(persist::load_locally_processed(state_dir)?);
|
||||
// Python cleans tickets and stamp costs at load (LXMRouter.py:258-284).
|
||||
let now = now_f64();
|
||||
self.ticket_store.cull(now);
|
||||
self.outbound_stamp_costs
|
||||
.retain(|_, e| now - e.recorded_at < STAMP_COST_EXPIRY as f64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1173,7 +1178,10 @@ impl LxmRouter {
|
|||
destination_hash: &[u8; 16],
|
||||
) -> bool {
|
||||
let now = now_f64();
|
||||
if let Some(ticket) = self.ticket_store.find(destination_hash, now) {
|
||||
for ticket in self.ticket_store.all() {
|
||||
if &ticket.destination_hash != destination_hash || !ticket.is_valid(now) {
|
||||
continue;
|
||||
}
|
||||
let mut material = Vec::with_capacity(16 + 32);
|
||||
material.extend_from_slice(&ticket.token);
|
||||
material.extend_from_slice(message_id);
|
||||
|
|
@ -2440,6 +2448,24 @@ mod tests {
|
|||
assert_eq!(router.get_inbound_tickets().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_stamp_checks_all_tickets() {
|
||||
let mut router = LxmRouter::new(RouterConfig::default());
|
||||
let dest = [0xAA; 16];
|
||||
let expires = now_f64() + 1000.0;
|
||||
router.remember_ticket(dest, [0x01; 16], expires);
|
||||
router.remember_ticket(dest, [0x02; 16], expires);
|
||||
|
||||
// Stamp derived from the SECOND ticket must still validate.
|
||||
let message_id = [0x33u8; 32];
|
||||
let mut material = Vec::with_capacity(16 + 32);
|
||||
material.extend_from_slice(&[0x02; 16]);
|
||||
material.extend_from_slice(&message_id);
|
||||
let stamp = rns_crypto::sha::truncated_hash(&material);
|
||||
|
||||
assert!(router.validate_stamp_with_tickets(&message_id, stamp.as_ref(), 16, &dest));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancel_outbound() {
|
||||
let mut router = LxmRouter::new(RouterConfig::default());
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
//! LXMF Ticket system: bypass PoW with pre-shared 16-byte tokens.
|
||||
//!
|
||||
//! Trusted peers may exchange tickets that bypass stamp requirements for a
|
||||
//! fixed expiry window. Tickets are single-use and renewable before expiry.
|
||||
//! fixed expiry window. Tickets are reusable until expiry and renewed once
|
||||
//! within `TICKET_RENEW` of expiring.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -33,10 +34,6 @@ impl Ticket {
|
|||
pub fn should_renew(&self, now: f64) -> bool {
|
||||
self.is_valid(now) && (self.expires - now) < TICKET_RENEW as f64
|
||||
}
|
||||
|
||||
pub fn use_ticket(&mut self) {
|
||||
self.used = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -59,17 +56,6 @@ impl TicketStore {
|
|||
.find(|t| &t.destination_hash == destination_hash && t.is_valid(now))
|
||||
}
|
||||
|
||||
/// Find and mark a ticket as used. Returns the token on success.
|
||||
pub fn use_for(&mut self, destination_hash: &[u8; 16], now: f64) -> Option<[u8; 16]> {
|
||||
for ticket in &mut self.tickets {
|
||||
if &ticket.destination_hash == destination_hash && ticket.is_valid(now) {
|
||||
ticket.use_ticket();
|
||||
return Some(ticket.token);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Drop expired and used tickets (past TICKET_GRACE).
|
||||
pub fn cull(&mut self, now: f64) {
|
||||
self.tickets
|
||||
|
|
@ -104,9 +90,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_ticket_used() {
|
||||
// `used` survives only for persisted-state compat; is_valid must
|
||||
// still reject such entries.
|
||||
let mut ticket = Ticket::new([0xAA; 16], [0xBB; 16], 1000.0);
|
||||
assert!(ticket.is_valid(500.0));
|
||||
ticket.use_ticket();
|
||||
ticket.used = true;
|
||||
assert!(!ticket.is_valid(500.0));
|
||||
}
|
||||
|
||||
|
|
@ -135,20 +123,6 @@ mod tests {
|
|||
assert!(found.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ticket_store_use() {
|
||||
let mut store = TicketStore::new();
|
||||
let dest = [0xBB; 16];
|
||||
|
||||
store.add(Ticket::new([0x01; 16], dest, 1000.0));
|
||||
|
||||
let token = store.use_for(&dest, 500.0);
|
||||
assert_eq!(token, Some([0x01; 16]));
|
||||
|
||||
let token = store.use_for(&dest, 500.0);
|
||||
assert!(token.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ticket_store_cull() {
|
||||
let mut store = TicketStore::new();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue