Skip to content

Commit 6e89546

Browse files
joschisanclaude
andcommitted
feat(gwv2): shared-crate groundwork for the gatewayv2 gateway
Everything the gatewaydv2 daemon needs from existing crates, kept additive so v1 is untouched at runtime: - fedimint-eventlog/client: per-operation event-log index and replay+tail subscription, bridged so the entry encoding is unchanged - fedimint-gwv2-client: daemon-driven send/receive primitives (start_receive, finalize_send with failure reason, per-operation event emission), richer payment events (preimage, realized ln fee, forfeit signature, destination node) and an optional daemon callback so the v2 daemon can orchestrate payments without the v1 state machines - fedimint-lightning: migrate the LDK client to ldk-node 0.7, pinned to a fork backporting upstream's probing service (lightningdevkit/ldk-node#815) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 096d746 commit 6e89546

15 files changed

Lines changed: 749 additions & 412 deletions

File tree

Cargo.lock

Lines changed: 154 additions & 363 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,14 @@ jsonrpsee-core = "0.24.9"
273273
jsonrpsee-types = "0.24.8"
274274
jsonrpsee-wasm-client = "0.24.9"
275275
jsonrpsee-ws-client = { version = "0.24.9", default-features = false }
276-
ldk-node = { version = "0.6.1", package = "fedimint-ldk-node" }
276+
# fedimint-0.7.0 plus a backport of upstream ldk-node's probing service
277+
# (lightningdevkit/ldk-node#815); switch back to crates.io once a release
278+
# containing the probing service ships.
279+
ldk-node = { package = "fedimint-ldk-node", git = "https://github.com/joschisan/ldk-node", rev = "adf392e4a82923f52e824a9ce5fd27964029bcfc" }
277280
leptos = { version = "0.7.8", default-features = false }
278-
lightning = "0.1.3"
279-
lightning-invoice = { version = "0.33.2", features = ["std"] }
280-
lightning-types = "0.2.0"
281+
lightning = "0.2.0"
282+
lightning-invoice = { version = "0.34.0", features = ["std"] }
283+
lightning-types = "0.3.0"
281284
lnurl-rs = { version = "0.9.0", default-features = false }
282285
lockable = "0.1.1"
283286
lru = "0.16.3"

fedimint-client-module/src/module/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ pub trait ClientContextIface: MaybeSend + MaybeSync {
130130
dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
131131
module_kind: Option<ModuleKind>,
132132
module_id: ModuleInstanceId,
133+
operation: Option<OperationId>,
133134
kind: EventKind,
134135
payload: serde_json::Value,
135136
persist: EventPersistence,
@@ -824,6 +825,34 @@ where
824825
where
825826
E: Event + Send,
826827
Cap: Send,
828+
{
829+
self.log_event_inner(dbtx, None, event).await;
830+
}
831+
832+
/// Like [`Self::log_event`], but additionally tags the event with
833+
/// `operation`, indexing it into the per-operation secondary log so it can
834+
/// be read/streamed via [`crate::ClientContextIface`]-backed
835+
/// `read_operation_events` / `subscribe_operation_events`.
836+
pub async fn log_event_for_operation<E, Cap>(
837+
&self,
838+
dbtx: &mut DatabaseTransaction<'_, Cap>,
839+
operation: OperationId,
840+
event: E,
841+
) where
842+
E: Event + Send,
843+
Cap: Send,
844+
{
845+
self.log_event_inner(dbtx, Some(operation), event).await;
846+
}
847+
848+
async fn log_event_inner<E, Cap>(
849+
&self,
850+
dbtx: &mut DatabaseTransaction<'_, Cap>,
851+
operation: Option<OperationId>,
852+
event: E,
853+
) where
854+
E: Event + Send,
855+
Cap: Send,
827856
{
828857
if <E as Event>::MODULE != Some(<M as ClientModule>::kind()) {
829858
warn!(
@@ -839,6 +868,7 @@ where
839868
&mut dbtx.global_dbtx(self.global_dbtx_access_token).to_ref_nc(),
840869
<E as Event>::MODULE,
841870
self.module_instance_id,
871+
operation,
842872
<E as Event>::KIND,
843873
serde_json::to_value(event).expect("Can't fail"),
844874
<E as Event>::PERSISTENCE,

fedimint-client/src/client.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::sync::Arc;
77
use std::time::{Duration, SystemTime, UNIX_EPOCH};
88

99
use anyhow::{Context as _, anyhow, bail, format_err};
10-
use async_stream::try_stream;
10+
use async_stream::{stream, try_stream};
1111
use bitcoin::key::Secp256k1;
1212
use bitcoin::key::rand::thread_rng;
1313
use bitcoin::secp256k1::{self, PublicKey};
@@ -2347,6 +2347,7 @@ impl Client {
23472347
dbtx: &mut DatabaseTransaction<'_, Cap>,
23482348
kind: EventKind,
23492349
module: Option<(ModuleKind, ModuleInstanceId)>,
2350+
operation: Option<OperationId>,
23502351
payload: Vec<u8>,
23512352
persist: EventPersistence,
23522353
) where
@@ -2359,6 +2360,7 @@ impl Client {
23592360
kind,
23602361
module_kind,
23612362
module_id,
2363+
operation,
23622364
payload,
23632365
persist,
23642366
)
@@ -2483,6 +2485,51 @@ impl Client {
24832485
.await
24842486
}
24852487

2488+
/// One-shot snapshot of every event currently logged for `operation`, in
2489+
/// event-log order, read from the per-operation secondary index.
2490+
pub async fn read_operation_events(&self, operation: OperationId) -> Vec<PersistedLogEntry> {
2491+
self.db
2492+
.begin_transaction_nc()
2493+
.await
2494+
.get_operation_event_log(operation)
2495+
.await
2496+
}
2497+
2498+
/// Stream every event logged for `operation`, in event-log order: history
2499+
/// is replayed first, then new events are yielded as the ordering task
2500+
/// commits them. The stream ends only when the client's event-log ordering
2501+
/// task stops (i.e. on client shutdown).
2502+
pub fn subscribe_operation_events(
2503+
&self,
2504+
operation: OperationId,
2505+
) -> BoxStream<'static, PersistedLogEntry> {
2506+
let db = self.db.clone();
2507+
let mut log_event_added = self.log_event_added_rx.clone();
2508+
2509+
Box::pin(stream! {
2510+
let mut next = EventLogId::LOG_START;
2511+
2512+
loop {
2513+
let entries = db
2514+
.begin_transaction_nc()
2515+
.await
2516+
.get_operation_event_log(operation)
2517+
.await;
2518+
2519+
for entry in entries {
2520+
if next <= entry.id() {
2521+
next = entry.id().next();
2522+
yield entry;
2523+
}
2524+
}
2525+
2526+
if log_event_added.changed().await.is_err() {
2527+
break;
2528+
}
2529+
}
2530+
})
2531+
}
2532+
24862533
pub async fn get_event_log_trimable(
24872534
&self,
24882535
pos: Option<EventLogTrimableId>,
@@ -2707,6 +2754,7 @@ impl ClientContextIface for Client {
27072754
dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
27082755
module_kind: Option<ModuleKind>,
27092756
module_id: ModuleInstanceId,
2757+
operation: Option<OperationId>,
27102758
kind: EventKind,
27112759
payload: serde_json::Value,
27122760
persist: EventPersistence,
@@ -2717,6 +2765,7 @@ impl ClientContextIface for Client {
27172765
dbtx,
27182766
kind,
27192767
module_kind.map(|kind| (kind, module_id)),
2768+
operation,
27202769
serde_json::to_vec(&payload).expect("Serialization can't fail"),
27212770
persist,
27222771
)

fedimint-client/src/client/global_ctx.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ impl IGlobalClientContext for ModuleGlobalClientContext {
109109
dbtx.global_tx(),
110110
kind,
111111
module,
112+
None,
112113
serde_json::to_vec(&payload).expect("Serialization can't fail"),
113114
persist,
114115
)

fedimint-eventlog/src/lib.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
2121
use std::time::Duration;
2222
use std::{fmt, ops};
2323

24-
use fedimint_core::core::{ModuleInstanceId, ModuleKind};
24+
use fedimint_core::core::{ModuleInstanceId, ModuleKind, OperationId};
2525
use fedimint_core::db::{
2626
Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped, NonCommittable,
2727
};
@@ -45,6 +45,16 @@ use tracing::{debug, trace};
4545
pub const DB_KEY_PREFIX_UNORDERED_EVENT_LOG: u8 = 0x3a;
4646
pub const DB_KEY_PREFIX_EVENT_LOG: u8 = 0x39;
4747
pub const DB_KEY_PREFIX_EVENT_LOG_TRIMABLE: u8 = 0x41;
48+
/// Transient bridge, written in the same dbtx as an operation-tagged unordered
49+
/// event: maps the (write-time) [`UnordedEventLogId`] to its `OperationId`.
50+
/// The ordering task consumes it to build
51+
/// [`DB_KEY_PREFIX_EVENT_LOG_BY_OPERATION`] once the final [`EventLogId`] is
52+
/// known, then deletes it.
53+
pub const DB_KEY_PREFIX_UNORDERED_EVENT_OPERATION: u8 = 0x3d;
54+
/// Secondary index for operation-scoped tailing: `(OperationId, EventLogId)` ->
55+
/// the full [`EventLogEntry`], so `subscribe_operation_events` is a cheap range
56+
/// scan without dereferencing back into the main ordered log.
57+
pub const DB_KEY_PREFIX_EVENT_LOG_BY_OPERATION: u8 = 0x3e;
4858

4959
/// Minimum age in ID count for trimable events to be deleted
5060
const TRIMABLE_EVENTLOG_MIN_ID_AGE: u64 = 10_000;
@@ -103,7 +113,7 @@ static UNORDEREDED_EVENT_LOG_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
103113
/// conflicts due the ID allocation. Instead they are picked based on
104114
/// a time and a counter, so they are mostly but not strictly ordered and
105115
/// monotonic, and even more importantly: not contiguous.
106-
#[derive(Debug, Encodable, Decodable)]
116+
#[derive(Debug, Clone, Copy, Encodable, Decodable)]
107117
pub struct UnordedEventLogId {
108118
ts_usecs: u64,
109119
counter: u64,
@@ -446,6 +456,40 @@ impl_db_lookup!(
446456
query_prefix = UnorderedEventLogIdPrefixAll
447457
);
448458

459+
/// Bridge key: the write-time [`UnordedEventLogId`] of an operation-tagged
460+
/// event -> its [`OperationId`]. Written by the operation-aware `log_event`,
461+
/// consumed and deleted by the ordering task.
462+
#[derive(Clone, Debug, Encodable, Decodable)]
463+
pub struct UnorderedEventOperationKey(pub UnordedEventLogId);
464+
465+
impl_db_record!(
466+
key = UnorderedEventOperationKey,
467+
value = OperationId,
468+
db_prefix = DB_KEY_PREFIX_UNORDERED_EVENT_OPERATION,
469+
);
470+
471+
/// Secondary per-operation index key. Ordered by `(operation, event_id)`, so a
472+
/// range scan over one operation yields its events in event-log order.
473+
#[derive(Clone, Debug, Encodable, Decodable)]
474+
pub struct EventLogByOperationKey {
475+
pub operation: OperationId,
476+
pub event_id: EventLogId,
477+
}
478+
479+
#[derive(Clone, Debug, Encodable, Decodable)]
480+
pub struct EventLogByOperationKeyPrefix(pub OperationId);
481+
482+
impl_db_record!(
483+
key = EventLogByOperationKey,
484+
value = EventLogEntry,
485+
db_prefix = DB_KEY_PREFIX_EVENT_LOG_BY_OPERATION,
486+
);
487+
488+
impl_db_lookup!(
489+
key = EventLogByOperationKey,
490+
query_prefix = EventLogByOperationKeyPrefix
491+
);
492+
449493
#[derive(Clone, Debug, Encodable, Decodable)]
450494
pub struct EventLogIdPrefixAll;
451495

@@ -525,6 +569,7 @@ pub trait DBTransactionEventLogExt {
525569
kind: EventKind,
526570
module_kind: Option<ModuleKind>,
527571
module_id: Option<ModuleInstanceId>,
572+
operation: Option<OperationId>,
528573
payload: Vec<u8>,
529574
persist: EventPersistence,
530575
);
@@ -546,12 +591,18 @@ pub trait DBTransactionEventLogExt {
546591
E::KIND,
547592
E::MODULE,
548593
module_id,
594+
None,
549595
serde_json::to_vec(&event).expect("Serialization can't fail"),
550596
<E as Event>::PERSISTENCE,
551597
)
552598
.await;
553599
}
554600

601+
/// All events logged for `operation`, in event-log order, read from the
602+
/// per-operation secondary index. Each returned [`PersistedLogEntry`] is
603+
/// keyed by the entry's ordered [`EventLogId`].
604+
async fn get_operation_event_log(&mut self, operation: OperationId) -> Vec<PersistedLogEntry>;
605+
555606
/// Next [`EventLogId`] to use for new ordered events.
556607
///
557608
/// Used by ordering task, though might be
@@ -586,6 +637,7 @@ where
586637
kind: EventKind,
587638
module_kind: Option<ModuleKind>,
588639
module_id: Option<ModuleInstanceId>,
640+
operation: Option<OperationId>,
589641
payload: Vec<u8>,
590642
persist: EventPersistence,
591643
) {
@@ -623,11 +675,30 @@ where
623675
{
624676
panic!("Trying to overwrite event in the client event log");
625677
}
678+
679+
// Record the operation association so the ordering task can build the
680+
// per-operation secondary index once it assigns the final `EventLogId`.
681+
if let Some(operation) = operation {
682+
self.insert_entry(&UnorderedEventOperationKey(unordered_id), &operation)
683+
.await;
684+
}
685+
626686
self.on_commit(move || {
627687
log_ordering_wakeup_tx.send_replace(());
628688
});
629689
}
630690

691+
async fn get_operation_event_log(&mut self, operation: OperationId) -> Vec<PersistedLogEntry> {
692+
self.find_by_prefix(&EventLogByOperationKeyPrefix(operation))
693+
.await
694+
.map(|(k, v)| PersistedLogEntry {
695+
id: k.event_id,
696+
inner: v,
697+
})
698+
.collect()
699+
.await
700+
}
701+
631702
async fn get_next_event_log_id(&mut self) -> EventLogId {
632703
self.find_by_prefix_sorted_descending(&EventLogIdPrefixAll)
633704
.await
@@ -747,6 +818,14 @@ pub async fn run_event_log_ordering_task(
747818
dbtx.remove_entry(unordered_id).await.is_some(),
748819
"Must never fail to remove entry"
749820
);
821+
822+
// Consume the operation bridge (if any) written alongside this event.
823+
// The per-operation index is keyed by the default log's `EventLogId`,
824+
// so it is only built for non-trimable persisted events below.
825+
let operation = dbtx
826+
.remove_entry(&UnorderedEventOperationKey(*unordered_id))
827+
.await;
828+
750829
if entry.persist() {
751830
// Non-trimable events get persisted in both the default event log
752831
// and trimable event log
@@ -757,6 +836,20 @@ pub async fn run_event_log_ordering_task(
757836
.is_none(),
758837
"Must never overwrite existing event"
759838
);
839+
if let Some(operation) = operation {
840+
assert!(
841+
dbtx.insert_entry(
842+
&EventLogByOperationKey {
843+
operation,
844+
event_id: next_entry_id,
845+
},
846+
&entry.inner,
847+
)
848+
.await
849+
.is_none(),
850+
"Must never overwrite existing per-operation event"
851+
);
852+
}
760853
trace!(target: LOG_CLIENT_EVENT_LOG, ?unordered_id, id=?next_entry_id, "Ordered event log event");
761854
next_entry_id = next_entry_id.next();
762855
}

0 commit comments

Comments
 (0)