Repository navigation
Expand file tree
/
Copy pathdata_plane.rs
More file actions
1292 lines (1171 loc) · 53.8 KB
/
Copy pathdata_plane.rs
File metadata and controls
1292 lines (1171 loc) · 53.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! The dregg DATA PLANE: a userspace comms API app authors build on.
//!
//! `dregg` is usually described as a *control / auth plane* — caps, attenuation,
//! verified turns, receipts. This module turns that same substrate into a
//! **data plane**: the bus a deos app (or Hermes, or the harness) actually moves
//! work across. It buffs the captp queue / custody / session primitives into one
//! coherent surface:
//!
//! * **ENQUEUE** a message to a cell's INBOX (the per-recipient
//! [`crate::store_forward`] `MessageRelay` queue) and get back a signed
//! **delivery receipt** — the relay's [`crate::custody::CustodyReceipt`].
//! * be **WOKEN by name** when an inbox advances ([`Waker`] /
//! [`Bus::poll_wakes`]) — the bb engine's wake/event-notify, but a wake is a
//! *fact about the queue* (cursor moved), never a forgeable signal.
//! * **SUBSCRIBE** to an event stream / **TOPIC** and receive fan-out
//! deliveries ([`Bus::publish`] → every subscriber's inbox), each a real
//! enqueue with its own receipt.
//! * **DRAIN** an inbox, which **WITNESSES delivery**: the drain emits the
//! content hashes that left the queue, and those are exactly what
//! [`crate::custody::InboxState::from_dequeue`] reads to ACQUIT a relay /
//! resolve the receipt. So "queued-but-undelivered" is structurally
//! distinguishable from "drained" — the bb engine's receipt-identity lesson,
//! made unforgeable. [`Bus::drain_one`] witnesses a SINGLE box (FIFO) so a
//! delivery path that hands boxes to a consumer one at a time drains the spool
//! IN LOCKSTEP with delivery — the witness flips as the box reaches the wire,
//! and the inbox never accumulates a parallel, never-drained backlog.
//!
//! # The north star: buildr's "bb engine", matched and exceeded
//!
//! buildr's fleet voted unanimously to keep one asset: an append-only shared log
//! + wake/spool/event-notify + receipt-identity. dregg has each piece already, in
//! verified form:
//!
//! | bb engine | dregg substrate |
//! |----------------------|------------------------------------------------------|
//! | append-only log | the blocklace (`blocklace/`) — the log of record |
//! | spool / mailbox | `MessageRelay` per-recipient queue (the inbox) |
//! | wake / event-notify | [`Waker`] — cursor-advance is the wake |
//! | receipt-identity | `CustodyReceipt` + drain-witness ([`Delivery`]) |
//!
//! Where dregg **exceeds** a plain bb engine — each delivery is a *cap-gated,
//! conserved, verifiable turn*:
//!
//! 1. **You cannot forge a wake.** A wake is minted only when a real enqueue
//! advanced the cursor; a subscriber cannot fabricate one (it is derived
//! from the monotone queue depth, not a flag anyone can set).
//! 2. **A receipt is unforgeable.** Only the relay's Ed25519 key produces a
//! verifying [`CustodyReceipt`]; a dropped delivery is *convictable* and an
//! honest one *acquitted* (the custody calculus).
//! 3. **Revocation / attenuation apply to channels.** A send capability into an
//! inbox or topic is an [`AuthRequired`]; an over-attenuated or unauthorized
//! enqueue is **refused at the seam**, before anything is queued (no receipt
//! is minted for a refused send — so the ledger never shows phantom work).
//! 4. **No claimed-but-undelivered ambiguity.** The bb engine's hardest-won
//! lesson: a thing on the spool is NOT a thing handled. Here, "handled" has a
//! cryptographic witness (the drain-emitted content hash) that is a *separate
//! object* from "enqueued" (the receipt's promise). [`Delivery::is_handled`]
//! reads the witness, never the promise.
//!
//! # How it grounds Houyhnhnm IPC
//!
//! This is the realization of the Houyhnhnm "typed channels / implicit + explicit
//! comms / protocols-as-meta-level" chapter: a [`Channel`] is a typed, named,
//! cap-bearing edge; a [`Topic`] is the multi-cast meta-level; the protocol (who
//! may send, with what attenuation, leaving what receipt) is itself first-class
//! data, not convention. See `docs/deos/DREGG-DATA-PLANE.md`.
use std::collections::{BTreeMap, HashMap};
use dregg_cell::AuthRequired;
use dregg_types::SigningKey;
use serde::{Deserialize, Serialize};
use crate::FederationId;
use crate::custody::{CustodyReceipt, InboxState};
use crate::store_forward::{MessageRelay, QueuedMessage};
// =============================================================================
// §1 — Names and channels: the typed, cap-bearing edges of the data plane
// =============================================================================
/// A **channel name** — a stable, human-meaningful handle for a comms edge.
///
/// In bb-engine terms this is the "wake-by-name" key; in Houyhnhnm terms it is
/// the typed-channel label. A name is just bytes (so apps choose their own
/// namespace, e.g. `b"hermes/inbox"`), content-addressed only for the wake table.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ChannelName(pub Vec<u8>);
impl ChannelName {
/// Construct a name from any byte-ish handle.
pub fn new(name: impl Into<Vec<u8>>) -> Self {
ChannelName(name.into())
}
/// A short stable key for wake-table indexing (BLAKE3 of the name bytes).
pub fn key(&self) -> [u8; 32] {
*blake3::hash(&self.0).as_bytes()
}
}
/// A **topic name** — the multi-cast meta-level. Publishing to a topic fans the
/// payload out to every [`Bus::subscribe`]r's own inbox (each a real enqueue).
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct TopicName(pub Vec<u8>);
impl TopicName {
/// Construct a topic from any byte-ish handle.
pub fn new(name: impl Into<Vec<u8>>) -> Self {
TopicName(name.into())
}
}
/// A **send capability** into a named channel/inbox: the cap-gated edge.
///
/// Holding a `SendCap` is the authority to enqueue to `recipient`'s inbox under
/// channel `name`, at most as broad as `grant`. The data plane checks an offered
/// send against this on every enqueue ([`Bus::enqueue`]); an over-attenuated or
/// mismatched offer is REFUSED before anything is queued — the channel-level
/// realization of dregg attenuation/revocation.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SendCap {
/// The inbox owner this cap can enqueue toward.
pub recipient: FederationId,
/// The channel name this cap is scoped to.
pub name: ChannelName,
/// The authority level granted to a holder of this cap. An offered send must
/// be *narrower-or-equal* to this to be admitted.
pub grant: AuthRequired,
/// If `true`, the cap has been REVOKED and no send through it is admitted
/// (the channel-level revocation switch).
pub revoked: bool,
}
impl SendCap {
/// Mint a fresh, non-revoked send capability.
pub fn grant(recipient: FederationId, name: ChannelName, grant: AuthRequired) -> Self {
Self {
recipient,
name,
grant,
revoked: false,
}
}
/// **Attenuate** this cap to a narrower authority, producing a new cap. Returns
/// `None` if `narrower` is not actually narrower-or-equal to this cap's grant
/// (you cannot *amplify* by calling attenuate — the non-amplification tooth).
pub fn attenuate(&self, narrower: AuthRequired) -> Option<SendCap> {
if narrower.is_narrower_or_equal(&self.grant) {
Some(SendCap {
recipient: self.recipient,
name: self.name.clone(),
grant: narrower,
revoked: self.revoked,
})
} else {
None
}
}
/// Revoke this cap (idempotent). After revocation no send through it is admitted.
pub fn revoke(&mut self) {
self.revoked = true;
}
/// Does an `offered` authority pass the gate of this cap for this `(recipient,
/// name)`? The send is admitted IFF the cap is live, addresses the right
/// recipient+channel, and the offered authority is narrower-or-equal to the
/// grant. This is the single seam an enqueue must pass.
pub fn admits(
&self,
recipient: &FederationId,
name: &ChannelName,
offered: &AuthRequired,
) -> bool {
!self.revoked
&& self.recipient == *recipient
&& self.name == *name
&& offered.is_narrower_or_equal(&self.grant)
}
}
// =============================================================================
// §2 — Errors
// =============================================================================
/// Why a data-plane operation was refused.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DataPlaneError {
/// The offered send was not admitted by the cap (revoked, wrong
/// recipient/channel, or over-broad authority). No message was queued and NO
/// receipt was minted — a refused send leaves no phantom work.
Unauthorized {
/// The recipient the refused send targeted.
recipient: FederationId,
},
/// The underlying relay refused the enqueue (queue/storage full, bad TTL).
Relay(crate::store_forward::RelayError),
/// No such topic is registered (publish/subscribe to an unknown topic).
NoSuchTopic(TopicName),
/// No inbox exists for this recipient yet (drain/wake on an empty name).
NoSuchInbox(FederationId),
}
impl std::fmt::Display for DataPlaneError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DataPlaneError::Unauthorized { recipient } => {
write!(f, "send refused: cap does not admit enqueue to {recipient}")
}
DataPlaneError::Relay(e) => write!(f, "relay refused enqueue: {e}"),
DataPlaneError::NoSuchTopic(t) => write!(f, "no such topic: {t:?}"),
DataPlaneError::NoSuchInbox(r) => write!(f, "no inbox for {r}"),
}
}
}
impl std::error::Error for DataPlaneError {}
// =============================================================================
// §3 — Delivery: the receipt-identity object (the bb engine's lesson, unforgeable)
// =============================================================================
/// A **delivery handle** — the object that distinguishes "queued" from "handled".
///
/// An [`Bus::enqueue`] returns a `Delivery` carrying:
/// * `receipt` — the relay's signed *promise* ([`CustodyReceipt`]) that this box
/// (by `content_hash`) is in the inbox and will be delivered or refunded.
/// * `content_hash` — the content-address of the enqueued box (the identity the
/// drain will later witness).
///
/// Crucially the *promise* (receipt) and the *witness* (a later drain that emits
/// `content_hash`) are SEPARATE objects. [`Delivery::is_handled`] consults the
/// witness, never the promise: a thing on the spool is not a thing handled — the
/// bb-engine receipt-identity invariant, here cryptographic.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Delivery {
/// The relay's signed delivery promise for this enqueue.
pub receipt: CustodyReceipt,
/// The content-address of the enqueued box (what a drain witnesses).
pub content_hash: [u8; 32],
}
impl Delivery {
/// **Is this delivery actually handled?** Reads the drain-witness, NOT the
/// promise. `delivered_content_hashes` is the inbox cell's authenticated record
/// of boxes that left the queue toward the recipient (each drain appends to it
/// — see [`Bus::drain`]). Returns `true` IFF THIS box's `content_hash` is among
/// them. This is the structural separation of "queued" from "handled":
/// possessing the receipt does NOT make `is_handled` true.
pub fn is_handled(&self, delivered_content_hashes: &[[u8; 32]]) -> bool {
delivered_content_hashes.contains(&self.content_hash)
}
/// The inbox state THIS delivery would adjudicate against, built
/// content-address-honestly from the cell's delivered-hash log. The drop /
/// acquit verdict for this receipt is then [`crate::custody::adjudicate_from_inbox`].
pub fn inbox_state(
&self,
delivered_content_hashes: &[[u8; 32]],
root: [u8; 32],
refund_recorded: bool,
) -> InboxState {
InboxState::from_dequeue(
&self.receipt,
delivered_content_hashes,
root,
refund_recorded,
)
}
}
// =============================================================================
// §4 — Waker: wake-by-name, where a wake is a FACT about the queue
// =============================================================================
/// A **wake** — the event-notify signal that a named inbox advanced.
///
/// A wake is *derived*, never *asserted*: it is minted only when an enqueue moved
/// the inbox's monotone cursor forward. A subscriber cannot forge one (there is no
/// public setter; [`Bus`] mints it from the real depth change). This is the
/// bb-engine wake/event-notify, hardened: the cursor is the truth.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Wake {
/// The cursor value the inbox advanced TO (monotone; the count of boxes ever
/// enqueued to this name). A waiter that last saw `cursor = n` is woken whenever
/// the live cursor exceeds `n`.
pub cursor: u64,
}
/// The wake table: per-channel monotone cursors + per-waiter last-seen marks.
///
/// `cursors[name]` only ever increases (one tick per admitted enqueue). A
/// registered waiter is "pending" exactly when the live cursor exceeds its
/// last-seen mark — so a wake cannot be faked (no enqueue ⇒ no cursor move ⇒ no
/// wake) and cannot be lost (the mark persists until the waiter acknowledges).
#[derive(Clone, Debug, Default)]
pub struct Waker {
/// Monotone enqueue cursor per channel key.
cursors: HashMap<[u8; 32], u64>,
/// Per-waiter (keyed by `(channel key, waiter id)`) last-acknowledged cursor.
marks: HashMap<([u8; 32], FederationId), u64>,
}
impl Waker {
/// Fresh wake table.
pub fn new() -> Self {
Self::default()
}
/// Tick the cursor for a channel (called by [`Bus`] on an admitted enqueue).
/// Returns the new cursor value. This is the ONLY way a cursor advances —
/// there is no setter that fakes one.
fn tick(&mut self, key: [u8; 32]) -> u64 {
let c = self.cursors.entry(key).or_insert(0);
*c += 1;
*c
}
/// The live cursor for a channel (0 if never enqueued).
pub fn cursor(&self, name: &ChannelName) -> u64 {
self.cursors.get(&name.key()).copied().unwrap_or(0)
}
/// The number of distinct waiters currently registered on a channel. Used by
/// the node's subscribe handler to cap the wait table (THEME-2/anon-DoS #11).
pub fn waiter_count(&self, name: &ChannelName) -> usize {
let key = name.key();
self.marks.keys().filter(|(k, _)| *k == key).count()
}
/// Whether `waiter` is already registered on a channel. A re-subscribe of an
/// already-registered waiter is idempotent (it re-marks, not grows), so the
/// wait-table cap only gates genuinely NEW waiters.
pub fn is_waiting(&self, name: &ChannelName, waiter: &FederationId) -> bool {
self.marks.contains_key(&(name.key(), *waiter))
}
/// Register a waiter on a channel: it will be woken when the cursor advances
/// past the current value. Marks its last-seen at the current cursor.
pub fn wait(&mut self, name: &ChannelName, waiter: FederationId) {
let key = name.key();
let cur = self.cursors.get(&key).copied().unwrap_or(0);
self.marks.insert((key, waiter), cur);
}
/// Poll a waiter: returns `Some(Wake)` if the channel cursor advanced past the
/// waiter's last-seen mark, else `None`. Does NOT consume the wake (idempotent
/// read); call [`Self::acknowledge`] after handling to advance the mark.
pub fn poll(&self, name: &ChannelName, waiter: &FederationId) -> Option<Wake> {
let key = name.key();
let cur = self.cursors.get(&key).copied().unwrap_or(0);
let seen = self.marks.get(&(key, *waiter)).copied().unwrap_or(0);
if cur > seen {
Some(Wake { cursor: cur })
} else {
None
}
}
/// Acknowledge wakes up to `cursor` for a waiter (advance its mark). After
/// this, [`Self::poll`] returns `None` until the cursor advances again.
pub fn acknowledge(&mut self, name: &ChannelName, waiter: FederationId, cursor: u64) {
let key = name.key();
let mark = self.marks.entry((key, waiter)).or_insert(0);
if cursor > *mark {
*mark = cursor;
}
}
}
// =============================================================================
// §5 — Bus: the userspace data-plane API a deos app holds
// =============================================================================
/// The **data-plane bus** — the one object an app (deos surface, Hermes, the
/// harness) uses to move work. It owns:
/// * a [`MessageRelay`] (the per-recipient inboxes / spool),
/// * a [`Waker`] (wake-by-name / event-notify),
/// * the topic→subscriber table (pub/sub fan-out),
/// * per-inbox delivery-witness logs (the receipt-identity ledger).
///
/// Every op is cap-gated ([`SendCap`]) and receipted ([`Delivery`]). The relay's
/// signing identity (`relay_id` / `relay_key`) signs custody receipts so a drop is
/// convictable and an honest delivery acquitted.
#[derive(Clone, Debug)]
pub struct Bus {
/// The relay holding per-recipient inbox queues (the spool).
relay: MessageRelay,
/// The relay's accountable identity (its `FederationId` IS its pubkey).
relay_id: FederationId,
/// The relay's Ed25519 signing key (signs custody receipts).
relay_key: SigningKey,
/// Wake-by-name table.
waker: Waker,
/// topic → set of subscriber inboxes (pub/sub fan-out).
topics: BTreeMap<TopicName, Vec<FederationId>>,
/// Per-recipient append-only log of delivered (drained) content hashes — the
/// authenticated witness the custody adjudicator reads. "Handled" lives here;
/// "queued" lives in the relay. Their separation IS receipt-identity.
delivered: HashMap<FederationId, Vec<[u8; 32]>>,
/// Per-recipient monotone inbox root (advances on enqueue; carried in receipts).
roots: HashMap<FederationId, [u8; 32]>,
/// Default TTL (in blocks) applied to enqueued boxes.
default_ttl: u64,
/// Default deadline horizon (blocks past `now`) for custody `accept_by`.
default_deadline: u64,
}
impl Bus {
/// Create a bus with the relay's accountable identity + signing key and the
/// relay storage limits. `relay_id` MUST be the pubkey matching `relay_key`
/// (else minted receipts will not verify — the custody binding).
pub fn new(
relay_id: FederationId,
relay_key: SigningKey,
max_queue_depth: usize,
max_total_messages: usize,
) -> Self {
Self {
relay: MessageRelay::new(max_queue_depth, max_total_messages),
relay_id,
relay_key,
waker: Waker::new(),
topics: BTreeMap::new(),
delivered: HashMap::new(),
roots: HashMap::new(),
default_ttl: 1_000,
default_deadline: 100,
}
}
/// Set the default TTL (blocks) for enqueued boxes (builder-style).
pub fn with_default_ttl(mut self, ttl: u64) -> Self {
self.default_ttl = ttl;
self
}
/// Set the default custody deadline horizon (blocks past `now`).
pub fn with_default_deadline(mut self, deadline: u64) -> Self {
self.default_deadline = deadline;
self
}
/// The current monotone inbox root for a recipient (zero if never enqueued).
fn root_of(&self, recipient: &FederationId) -> [u8; 32] {
self.roots.get(recipient).copied().unwrap_or([0u8; 32])
}
/// Advance a recipient's monotone inbox root by folding in a content hash. The
/// root is `H(old_root || content_hash)` — strictly monotone (a fresh hash each
/// enqueue) so it never retreats, matching the custody adjudicator's assumption.
fn advance_root(&mut self, recipient: FederationId, content_hash: &[u8; 32]) -> [u8; 32] {
let old = self.root_of(&recipient);
let mut h = blake3::Hasher::new();
h.update(&old);
h.update(content_hash);
let new = *h.finalize().as_bytes();
self.roots.insert(recipient, new);
new
}
// -------------------------------------------------------------------------
// (a) ENQUEUE — put work into a cell's inbox, with a delivery RECEIPT
// -------------------------------------------------------------------------
/// **ENQUEUE** a payload to `recipient`'s inbox under channel `name`, gated by
/// `cap` at authority `offered`, returning a signed [`Delivery`].
///
/// The seam: the send is admitted only if `cap.admits(recipient, name, offered)`
/// — live cap, right recipient+channel, offered authority narrower-or-equal to
/// the grant. If refused, [`DataPlaneError::Unauthorized`] is returned and
/// **nothing is queued and no receipt is minted** (a refused send leaves no
/// phantom work — the receipt-identity ledger never shows it).
///
/// On admission: the box is enqueued on the relay, the inbox root advances, the
/// relay SIGNS a [`CustodyReceipt`] over `(content_hash, recipient, old→new
/// root, accept_by)`, and the channel's wake cursor ticks (so waiters are
/// woken). The returned `Delivery` is the *promise*; handling is later
/// witnessed by [`Self::drain`].
pub fn enqueue(
&mut self,
cap: &SendCap,
recipient: FederationId,
name: &ChannelName,
offered: AuthRequired,
payload: Vec<u8>,
now: u64,
) -> Result<Delivery, DataPlaneError> {
// THE GATE: refuse over-attenuated / forged / revoked sends BEFORE queuing.
if !cap.admits(&recipient, name, &offered) {
return Err(DataPlaneError::Unauthorized { recipient });
}
// Content-address the box exactly as the relay/receipt commit to it.
let content_hash = *blake3::hash(&payload).as_bytes();
let old_root = self.root_of(&recipient);
let accept_by = now.saturating_add(self.default_deadline);
// The causal sequence of THIS box is the cursor value it ticks the
// channel TO — the post-tick value, so the queued message's sequence and
// the wake cursor a waiter observes for it agree (the first admitted
// enqueue is causal_sequence 1, matching `cursor == 1`). Reading the
// pre-tick cursor here desynchronized the two by one.
let causal_sequence = self.waker.cursor(name).saturating_add(1);
let msg = QueuedMessage {
destination: recipient,
encrypted_payload: payload,
sender_ephemeral_pk: [0u8; 32],
causal_sequence,
queued_at: now,
ttl_blocks: self.default_ttl,
priority: crate::store_forward::MessagePriority::Normal,
};
// Enqueue first; only mint a receipt + tick the cursor if custody was taken.
self.relay.enqueue(msg).map_err(DataPlaneError::Relay)?;
let new_root = self.advance_root(recipient, &content_hash);
let receipt = CustodyReceipt::sign(
self.relay_id,
&self.relay_key,
content_hash,
recipient,
old_root,
new_root,
accept_by,
);
// Wake-by-name: the cursor advances exactly once per admitted enqueue, to
// the value `causal_sequence` recorded above.
let ticked = self.waker.tick(name.key());
debug_assert_eq!(
ticked, causal_sequence,
"the queued box's causal_sequence must equal the cursor it ticked to"
);
Ok(Delivery {
receipt,
content_hash,
})
}
// -------------------------------------------------------------------------
// (b) WAKE — be woken by name when an inbox advances
// -------------------------------------------------------------------------
/// **WAIT** to be woken on a channel: registers `waiter` so a future enqueue to
/// `name` produces a pending wake ([`Self::poll_wake`]).
pub fn wait(&mut self, name: &ChannelName, waiter: FederationId) {
self.waker.wait(name, waiter);
}
/// The number of distinct waiters registered on a channel (wait-table cap).
pub fn waiter_count(&self, name: &ChannelName) -> usize {
self.waker.waiter_count(name)
}
/// Whether `waiter` is already registered on a channel (so a re-subscribe is
/// not counted against the wait-table cap).
pub fn is_waiting(&self, name: &ChannelName, waiter: &FederationId) -> bool {
self.waker.is_waiting(name, waiter)
}
/// **POLL** for a wake on a channel: `Some(Wake)` if the cursor advanced past
/// the waiter's last-seen mark. The wake is a fact (cursor moved), unforgeable.
pub fn poll_wake(&self, name: &ChannelName, waiter: &FederationId) -> Option<Wake> {
self.waker.poll(name, waiter)
}
/// Acknowledge wakes up to `cursor` (advance the waiter's mark).
pub fn acknowledge_wake(&mut self, name: &ChannelName, waiter: FederationId, cursor: u64) {
self.waker.acknowledge(name, waiter, cursor);
}
/// The live wake cursor for a channel (count of admitted enqueues).
pub fn cursor(&self, name: &ChannelName) -> u64 {
self.waker.cursor(name)
}
// -------------------------------------------------------------------------
// (c) SUBSCRIBE / PUBLISH — pub/sub topic fan-out
// -------------------------------------------------------------------------
/// Register a topic (idempotent). Apps may publish to it once registered.
pub fn register_topic(&mut self, topic: TopicName) {
self.topics.entry(topic).or_default();
}
/// **SUBSCRIBE** `subscriber`'s inbox to a topic (auto-registers the topic).
/// A subsequent [`Self::publish`] fans the payload out to this subscriber's
/// inbox as a real enqueue with its own receipt.
pub fn subscribe(&mut self, topic: TopicName, subscriber: FederationId) {
let subs = self.topics.entry(topic).or_default();
if !subs.contains(&subscriber) {
subs.push(subscriber);
}
}
/// Unsubscribe a subscriber from a topic.
pub fn unsubscribe(&mut self, topic: &TopicName, subscriber: &FederationId) {
if let Some(subs) = self.topics.get_mut(topic) {
subs.retain(|s| s != subscriber);
}
}
/// **PUBLISH** a payload to a topic: fan it out to EVERY subscriber's inbox,
/// one real cap-gated enqueue + receipt per subscriber, woken by name.
///
/// The publisher presents a `cap` whose `name` is the per-subscriber channel
/// used for the fan-out (so the same attenuation/revocation discipline governs
/// a broadcast as a unicast). The channel name for each subscriber's delivery
/// is the topic-scoped name `cap.name`; a subscriber waits on that name to be
/// woken on publish. Returns one [`Delivery`] per subscriber that admitted the
/// send. Subscribers the cap does not admit are silently skipped (the cap may
/// be scoped to a subset; an unauthorized fan-out target is simply not served).
pub fn publish(
&mut self,
topic: &TopicName,
cap: &SendCap,
offered: AuthRequired,
payload: Vec<u8>,
now: u64,
) -> Result<Vec<(FederationId, Delivery)>, DataPlaneError> {
let subs = self
.topics
.get(topic)
.ok_or_else(|| DataPlaneError::NoSuchTopic(topic.clone()))?
.clone();
let name = cap.name.clone();
let mut out = Vec::with_capacity(subs.len());
for sub in subs {
// Per-subscriber cap: same grant, re-pointed at this subscriber. The
// publisher's cap must admit this (sub) under the topic channel name.
let sub_cap = SendCap {
recipient: sub,
name: name.clone(),
grant: cap.grant.clone(),
revoked: cap.revoked,
};
match self.enqueue(&sub_cap, sub, &name, offered.clone(), payload.clone(), now) {
Ok(d) => out.push((sub, d)),
Err(DataPlaneError::Unauthorized { .. }) => { /* skip unauthorized target */ }
Err(e) => return Err(e),
}
}
Ok(out)
}
/// Current subscribers of a topic (empty if unregistered).
pub fn subscribers(&self, topic: &TopicName) -> &[FederationId] {
self.topics.get(topic).map_or(&[], |v| v.as_slice())
}
// -------------------------------------------------------------------------
// (d) DRAIN — drain the inbox, WITNESSING delivery (the custody receipt resolves)
// -------------------------------------------------------------------------
/// **DRAIN** `recipient`'s inbox: return the queued boxes (FIFO) AND record
/// their content hashes in the recipient's authenticated delivery-witness log.
///
/// THIS is where "queued" becomes "handled": each drained box's `content_hash`
/// is appended to [`Self::delivered_hashes`], the sticky, content-addressed
/// witness the custody adjudicator reads. After a drain, the matching
/// [`Delivery::is_handled`] flips to `true` and the relay is ACQUITTED for that
/// box; a box that was never drained leaves the witness unset (its relay
/// convictable past the deadline). The separation is the receipt-identity teeth.
pub fn drain(&mut self, recipient: &FederationId) -> Vec<QueuedMessage> {
let drained = self.relay.drain(recipient);
if !drained.is_empty() {
let log = self.delivered.entry(*recipient).or_default();
for m in &drained {
let ch = *blake3::hash(&m.encrypted_payload).as_bytes();
log.push(ch);
}
}
drained
}
/// **DRAIN ONE** box (FIFO) from `recipient`'s inbox, WITNESSING that single
/// delivery: the popped box's content hash is appended to the delivery-witness
/// log exactly as [`Self::drain`] would, so the matching [`Delivery::is_handled`]
/// flips to `true`. `None` if the inbox is empty.
///
/// This is the lockstep companion to a delivery path that hands boxes to a
/// consumer one at a time (e.g. an SSE stream): each delivered box witnesses
/// its own drain, so the spool drains AS it delivers — it never accumulates a
/// parallel, never-drained backlog. "Delivered on the wire" == "drained-witnessed
/// on the Bus", box for box.
pub fn drain_one(&mut self, recipient: &FederationId) -> Option<QueuedMessage> {
let msg = self.relay.drain_one(recipient)?;
let ch = *blake3::hash(&msg.encrypted_payload).as_bytes();
self.delivered.entry(*recipient).or_default().push(ch);
Some(msg)
}
/// The recipient's authenticated log of delivered (drained) content hashes —
/// the witness `Delivery::is_handled` / the custody adjudicator reads. Append
/// only; sticky once a box is drained.
pub fn delivered_hashes(&self, recipient: &FederationId) -> &[[u8; 32]] {
self.delivered.get(recipient).map_or(&[], |v| v.as_slice())
}
/// How many boxes are pending (queued, not yet drained) for a recipient.
pub fn pending_count(&self, recipient: &FederationId) -> usize {
self.relay.pending_count(recipient)
}
/// The recipient's monotone inbox root (carried in receipts; for adjudication).
pub fn inbox_root(&self, recipient: &FederationId) -> [u8; 32] {
self.root_of(recipient)
}
}
// =============================================================================
// §6 — Tests: the bb-engine parity teeth + the receipt-identity proof
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::custody::adjudicate_from_inbox;
use crate::custody::{CustodyOutcome, EvidenceOfDrop};
use dregg_types::generate_keypair;
/// A relay identity whose FederationId IS its Ed25519 pubkey (custody binding).
fn relay_identity() -> (FederationId, SigningKey) {
let (sk, pk) = generate_keypair();
(FederationId(pk.0), sk)
}
fn app_b() -> FederationId {
FederationId([0xBB; 32])
}
fn fresh_bus() -> (Bus, FederationId) {
let (relay_id, relay_key) = relay_identity();
(Bus::new(relay_id, relay_key, 1024, 65536), relay_id)
}
fn inbox_cap(recipient: FederationId, name: &ChannelName) -> SendCap {
SendCap::grant(recipient, name.clone(), AuthRequired::Signature)
}
// ── (1) THE HEADLINE: A enqueues → B woken → B drains → receipt witnesses ──
#[test]
fn enqueue_wake_drain_witness_lifecycle() {
let (mut bus, _relay) = fresh_bus();
let b = app_b();
let name = ChannelName::new(b"app-b/inbox");
let cap = inbox_cap(b, &name);
// B registers to be woken by name.
bus.wait(&name, b);
assert!(
bus.poll_wake(&name, &b).is_none(),
"no wake before any enqueue"
);
// A enqueues work to B's inbox; gets a signed delivery promise.
let delivery = bus
.enqueue(
&cap,
b,
&name,
AuthRequired::Signature,
b"do-the-thing".to_vec(),
10,
)
.expect("authorized enqueue admitted");
assert!(
delivery.receipt.sig_verifies(),
"receipt is a real signature"
);
// B is WOKEN by name (the cursor advanced — an unforgeable fact).
let wake = bus.poll_wake(&name, &b).expect("B is woken on enqueue");
assert_eq!(wake.cursor, 1);
// RECEIPT-IDENTITY, before drain: queued is NOT handled.
assert_eq!(bus.pending_count(&b), 1);
assert!(
!delivery.is_handled(bus.delivered_hashes(&b)),
"a thing on the spool is NOT a thing handled"
);
// B drains → delivery is WITNESSED.
let drained = bus.drain(&b);
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].encrypted_payload, b"do-the-thing");
assert_eq!(bus.pending_count(&b), 0);
// RECEIPT-IDENTITY, after drain: handled is now true, and distinguishable
// from the pre-drain state (the whole bb-engine lesson).
assert!(
delivery.is_handled(bus.delivered_hashes(&b)),
"drained ⇒ handled (the witness, not the promise)"
);
// The custody adjudicator AGREES: a delivered box ACQUITS the relay.
let inbox = delivery.inbox_state(bus.delivered_hashes(&b), bus.inbox_root(&b), false);
let dispute = EvidenceOfDrop::from_receipt(delivery.receipt.clone());
assert!(
!adjudicate_from_inbox(&dispute, &inbox),
"a witnessed delivery is provably acquitted"
);
assert!(matches!(
inbox.true_outcome(&delivery.receipt),
CustodyOutcome::Delivered { .. }
));
// B acknowledges the wake; no further wake until the next enqueue.
bus.acknowledge_wake(&name, b, wake.cursor);
assert!(
bus.poll_wake(&name, &b).is_none(),
"wake consumed after ack"
);
}
// ── (2) RECEIPT-IDENTITY, the convict polarity: never drained ⇒ convictable ──
#[test]
fn undrained_is_distinguishable_and_convictable() {
let (mut bus, _relay) = fresh_bus();
let b = app_b();
let name = ChannelName::new(b"app-b/inbox");
let cap = inbox_cap(b, &name);
let delivery = bus
.enqueue(
&cap,
b,
&name,
AuthRequired::Signature,
b"unhandled-work".to_vec(),
10,
)
.unwrap();
// Never drained: NOT handled, and past the deadline the relay is CONVICTABLE
// — the unambiguous "queued-but-undelivered" state the bb engine demands be
// distinct from "handled".
assert!(!delivery.is_handled(bus.delivered_hashes(&b)));
let inbox = delivery.inbox_state(bus.delivered_hashes(&b), bus.inbox_root(&b), false);
assert_eq!(
inbox.true_outcome(&delivery.receipt),
CustodyOutcome::Dropped
);
let dispute = EvidenceOfDrop::from_receipt(delivery.receipt.clone());
assert!(dispute.well_formed());
assert!(
adjudicate_from_inbox(&dispute, &inbox),
"queued-but-never-drained is provably a drop (no false 'handled')"
);
// Now drain: the SAME delivery flips to handled + acquitted. The two states
// are genuinely distinct objects, never confused.
bus.drain(&b);
let inbox2 = delivery.inbox_state(bus.delivered_hashes(&b), bus.inbox_root(&b), false);
assert!(delivery.is_handled(bus.delivered_hashes(&b)));
assert!(!adjudicate_from_inbox(&dispute, &inbox2));
}
// ── (3) PUB/SUB: a publish fans a delivery to N subscribers ──────────────────
#[test]
fn pubsub_fans_to_n_subscribers() {
let (mut bus, _relay) = fresh_bus();
let topic = TopicName::new(b"weather");
let name = ChannelName::new(b"weather/feed");
let subs: Vec<FederationId> = (0..5u8).map(|i| FederationId([i; 32])).collect();
for s in &subs {
bus.subscribe(topic.clone(), *s);
bus.wait(&name, *s);
}
assert_eq!(bus.subscribers(&topic).len(), 5);
// Publish once → fans to all 5, each a real enqueue + receipt.
let cap = SendCap::grant(subs[0], name.clone(), AuthRequired::Signature);
let deliveries = bus
.publish(
&topic,
&cap,
AuthRequired::Signature,
b"storm-incoming".to_vec(),
7,
)
.unwrap();
assert_eq!(deliveries.len(), 5, "fan-out reached every subscriber");
// Every subscriber is woken and can drain a real copy, each receipt verifies.
for (sub, delivery) in &deliveries {
assert!(delivery.receipt.sig_verifies());
assert!(
bus.poll_wake(&name, sub).is_some(),
"subscriber woken on publish"
);
assert_eq!(bus.pending_count(sub), 1);
let drained = bus.drain(sub);
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].encrypted_payload, b"storm-incoming");
assert!(
delivery.is_handled(bus.delivered_hashes(sub)),
"each fan-out delivery is independently witnessed"
);
}
}
// ── (4) NON-AMP: an over-attenuated / unauthorized enqueue is REFUSED ────────
#[test]
fn over_attenuated_enqueue_refused_no_phantom_work() {
let (mut bus, _relay) = fresh_bus();
let b = app_b();
let name = ChannelName::new(b"app-b/inbox");
// The cap grants only Signature-level authority into this channel.
let cap = SendCap::grant(b, name.clone(), AuthRequired::Signature);
// Offering a BROADER authority (None ⊋ Signature) is refused at the seam.
let refused = bus.enqueue(&cap, b, &name, AuthRequired::None, b"forge".to_vec(), 1);
assert!(
matches!(refused, Err(DataPlaneError::Unauthorized { .. })),
"an over-broad (under-attenuated) send is refused"
);
// RECEIPT-IDENTITY: a refused send leaves NO phantom work — nothing queued,
// no cursor tick, no receipt.
assert_eq!(
bus.pending_count(&b),
0,
"nothing queued for a refused send"
);
assert_eq!(
bus.cursor(&name),
0,
"no wake cursor tick for a refused send"
);
// Wrong channel name is also refused.
let other = ChannelName::new(b"someone-else/inbox");
let wrong_chan = bus.enqueue(&cap, b, &other, AuthRequired::Signature, b"x".to_vec(), 1);
assert!(matches!(
wrong_chan,
Err(DataPlaneError::Unauthorized { .. })
));
// A REVOKED cap admits nothing (channel-level revocation).
let mut revoked = cap.clone();
revoked.revoke();
let after_revoke = bus.enqueue(
&revoked,
b,
&name,
AuthRequired::Signature,
b"y".to_vec(),
1,
);
assert!(matches!(
after_revoke,
Err(DataPlaneError::Unauthorized { .. })
));
assert_eq!(bus.pending_count(&b), 0);
// A correctly-attenuated (narrower) send IS admitted: the attenuate path is
// not vacuously closed.
let narrower = SendCap::grant(b, name.clone(), AuthRequired::Either);
// Either grant admits a narrower Signature offer.
let ok = bus.enqueue(
&narrower,
b,
&name,
AuthRequired::Signature,
b"ok".to_vec(),
1,
);
assert!(
ok.is_ok(),
"a within-grant send is admitted (gate not vacuous)"
);
assert_eq!(bus.cursor(&name), 1);
}
// ── (5) ATTENUATE: the cap algebra is non-amplifying ─────────────────────────
#[test]
fn cap_attenuation_cannot_amplify() {
let b = app_b();
let name = ChannelName::new(b"c");
let either = SendCap::grant(b, name.clone(), AuthRequired::Either);
// Narrowing Either → Signature succeeds.
let narrowed = either
.attenuate(AuthRequired::Signature)
.expect("narrowing allowed");
assert_eq!(narrowed.grant, AuthRequired::Signature);
// Trying to widen Signature → Either (or None) FAILS (no amplification).
assert!(
narrowed.attenuate(AuthRequired::Either).is_none(),