Skip to content

Commit 1192866

Browse files
jkczyzclaude
andcommitted
Model pending payments as an enum for pre-broadcast splices
Retrying a user-initiated splice across restarts requires persisting the splice intent before handing it to LDK, which happens before negotiation and therefore before any funding transaction exists. The pending-payment record was built around an on-chain PaymentDetails carrying a txid, which cannot represent a splice that has not been broadcast yet. Reshape PendingPaymentDetails into an enum: a PendingSplice variant that holds only the generated PaymentId and the splice intent, and a Tracked variant that is the previous record plus an optional intent retained until the splice locks. Add the SpliceIntent and SpliceKind types the intent needs to resubmit or rebuild the contribution. Wallet writes to the pending store go through a new DataStore::mutate primitive that reads, transforms, and persists an entry under one critical section of the mutation lock, replacing racy read-then-write pairs. The wallet's pending-store writes share one helper whose closure promotes a bare PendingSplice to a Tracked record once a payment exists under its id: a plain payment-tracking merge would silently no-op against the variant, leaving the splice invisible to txid lookups. This is groundwork; nothing constructs a PendingSplice yet. The classify, retry, and wiring that use it follow in subsequent commits. Generated with assistance from Claude Code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6168bb0 commit 1192866

3 files changed

Lines changed: 468 additions & 105 deletions

File tree

src/data_store.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,35 @@ where
223223
Ok(result)
224224
}
225225

226+
/// Atomically transforms the entry for `id` through `f` and persists the result.
227+
///
228+
/// `f` receives the current entry (`None` when absent) and returns the new state to write;
229+
/// returning `None` leaves the store untouched. The read, the closure, and the write share one
230+
/// critical section of the mutation lock, so no concurrent writer can land in between — unlike
231+
/// a separate [`Self::get`] followed by an insert or update. Keep the closure cheap and
232+
/// non-blocking, and do not call back into this store from it.
233+
///
234+
/// Returns the written object, or `None` when the closure declined to write.
235+
pub(crate) async fn mutate<F: FnOnce(Option<&SO>) -> Option<SO>>(
236+
&self, id: &SO::Id, f: F,
237+
) -> Result<Option<SO>, Error> {
238+
let _guard = self.mutation_lock.lock().await;
239+
240+
let new_object = {
241+
let locked_objects = self.objects.lock().expect("lock");
242+
match f(locked_objects.get(id)) {
243+
Some(new_object) => new_object,
244+
None => return Ok(None),
245+
}
246+
};
247+
debug_assert!(new_object.id() == *id, "mutate closure must not change the object's id");
248+
249+
self.persist(&new_object).await?;
250+
let mut locked_objects = self.objects.lock().expect("lock");
251+
locked_objects.insert(new_object.id(), new_object.clone());
252+
Ok(Some(new_object))
253+
}
254+
226255
/// Returns in-memory objects matching `f`.
227256
///
228257
/// The async mutation lock serializes writers, but this synchronous reader cannot wait on it.
@@ -552,6 +581,103 @@ mod tests {
552581
assert!(data_store.get(&new_id).is_none());
553582
}
554583

584+
#[tokio::test]
585+
async fn mutate_inserts_when_absent() {
586+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
587+
let logger = Arc::new(TestLogger::new());
588+
let primary_namespace = "datastore_test_primary".to_string();
589+
let secondary_namespace = "datastore_test_secondary".to_string();
590+
let data_store: DataStore<TestObject, Arc<TestLogger>> = DataStore::new(
591+
Vec::new(),
592+
primary_namespace.clone(),
593+
secondary_namespace.clone(),
594+
Arc::clone(&store),
595+
logger,
596+
);
597+
598+
let id = TestObjectId { id: [42u8; 4] };
599+
let object = TestObject { id, data: [23u8; 3] };
600+
let result = data_store
601+
.mutate(&id, |existing| {
602+
assert!(existing.is_none());
603+
Some(object)
604+
})
605+
.await;
606+
assert_eq!(Ok(Some(object)), result);
607+
608+
assert_eq!(Some(object), data_store.get(&id));
609+
let store_key = id.encode_to_hex_str();
610+
assert!(KVStore::read(&*store, &primary_namespace, &secondary_namespace, &store_key)
611+
.await
612+
.is_ok());
613+
}
614+
615+
#[tokio::test]
616+
async fn mutate_transforms_existing_entry() {
617+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
618+
let logger = Arc::new(TestLogger::new());
619+
let id = TestObjectId { id: [42u8; 4] };
620+
let existing_object = TestObject { id, data: [23u8; 3] };
621+
let data_store: DataStore<TestObject, Arc<TestLogger>> = DataStore::new(
622+
vec![existing_object],
623+
"datastore_test_primary".to_string(),
624+
"datastore_test_secondary".to_string(),
625+
store,
626+
logger,
627+
);
628+
629+
// The closure sees the current entry and derives the new state from it.
630+
let result = data_store
631+
.mutate(&id, |existing| {
632+
let mut new_object = *existing.unwrap();
633+
new_object.data[0] += 1;
634+
Some(new_object)
635+
})
636+
.await;
637+
let expected = TestObject { id, data: [24u8, 23u8, 23u8] };
638+
assert_eq!(Ok(Some(expected)), result);
639+
assert_eq!(Some(expected), data_store.get(&id));
640+
}
641+
642+
#[tokio::test]
643+
async fn mutate_persists_nothing_when_closure_declines() {
644+
let id = TestObjectId { id: [42u8; 4] };
645+
let existing_object = TestObject { id, data: [23u8; 3] };
646+
let data_store = new_failing_data_store(vec![existing_object]);
647+
648+
// Returning `None` must not attempt a write (the store fails all writes) nor touch memory.
649+
let result = data_store
650+
.mutate(&id, |existing| {
651+
assert_eq!(Some(&existing_object), existing);
652+
None
653+
})
654+
.await;
655+
assert_eq!(Ok(None), result);
656+
assert_eq!(Some(existing_object), data_store.get(&id));
657+
}
658+
659+
#[tokio::test]
660+
async fn mutate_does_not_mutate_memory_if_persist_fails() {
661+
let existing_id = TestObjectId { id: [42u8; 4] };
662+
let existing_object = TestObject { id: existing_id, data: [23u8; 3] };
663+
let data_store = new_failing_data_store(vec![existing_object]);
664+
665+
let changed = TestObject { id: existing_id, data: [24u8; 3] };
666+
assert_eq!(
667+
Err(Error::PersistenceFailed),
668+
data_store.mutate(&existing_id, |_| Some(changed)).await
669+
);
670+
assert_eq!(Some(existing_object), data_store.get(&existing_id));
671+
672+
let new_id = TestObjectId { id: [55u8; 4] };
673+
let new_object = TestObject { id: new_id, data: [34u8; 3] };
674+
assert_eq!(
675+
Err(Error::PersistenceFailed),
676+
data_store.mutate(&new_id, |_| Some(new_object)).await
677+
);
678+
assert!(data_store.get(&new_id).is_none());
679+
}
680+
555681
#[tokio::test]
556682
async fn insert_or_update_does_not_mutate_memory_if_persist_fails() {
557683
let existing_id = TestObjectId { id: [42u8; 4] };

0 commit comments

Comments
 (0)