Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial payment store implementation #13

Merged
merged 7 commits into from
Apr 17, 2023

Conversation

tnull
Copy link
Collaborator

@tnull tnull commented Sep 6, 2022

Based on #8, #9, #10, #11.

This PR implements a PaymentInfoStorage that persists individual PaymentInfo objects after every update, which is done via a KVStorePersister::persist() under the key payments/{payment_hash}.

(So essentially same approach as taken for events and peer storage)

As the data is currently only read from disk upon restart, it still assumes a FilesystemPersister, but may be adapted in the future to support the interface more generically, if KVStorePersister would expose some more functionality.

@tnull tnull force-pushed the 2022-09-initial-payment-store branch 7 times, most recently from 166dafc to d0552ce Compare September 8, 2022 12:31
@tnull tnull force-pushed the 2022-09-initial-payment-store branch from d0552ce to c9ae999 Compare September 26, 2022 10:54
@tnull tnull force-pushed the 2022-09-initial-payment-store branch from c9ae999 to 07187cf Compare February 13, 2023 23:01
@tnull
Copy link
Collaborator Author

tnull commented Feb 13, 2023

Rebased on current version of #11, still needs some more love.

@tnull tnull marked this pull request as draft February 13, 2023 23:02
@tnull tnull force-pushed the 2022-09-initial-payment-store branch 3 times, most recently from 73b3a36 to 3b269c0 Compare March 13, 2023 15:23
@tnull tnull marked this pull request as ready for review March 13, 2023 15:32
@tnull tnull mentioned this pull request Mar 13, 2023
47 tasks
@tnull tnull added this to the 0.1 milestone Mar 13, 2023
@tnull tnull force-pushed the 2022-09-initial-payment-store branch from 3b269c0 to 2cfb300 Compare March 14, 2023 14:02
@tnull tnull mentioned this pull request Mar 14, 2023
@tnull tnull force-pushed the 2022-09-initial-payment-store branch 2 times, most recently from d2d07dd to c2ad64d Compare March 15, 2023 10:19
@jkczyz jkczyz self-requested a review March 16, 2023 17:10
@tnull tnull force-pushed the 2022-09-initial-payment-store branch 2 times, most recently from 3ed20c5 to 2bac77e Compare March 16, 2023 18:15
src/event.rs Outdated Show resolved Hide resolved
src/payment_store.rs Outdated Show resolved Hide resolved
src/io_utils.rs Outdated
@@ -83,3 +86,37 @@ pub(crate) fn read_payment_info(config: &Config) -> Vec<PaymentInfo> {

payments
}

/// Provides an interface that allows a previously persisted key to be unpersisted.
pub trait KVStoreUnpersister {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if a persister is the right abstraction. Seems we want some abstract data storage where persistence is more of an implementation detail?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, however, this comes down to a question of naming/terminology, as get/put/del are probably the core operations of a KV datastore. So maybe I should just create one Datastore/KVStore trait that offers all three and extends KVStorePersister, instead of adding a KVStoreAccess trait and have the trait bounds be KVStorePersister + KVStoreUnpersister + KVStoreAccess...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@G8XSU How does VSS deal with removed data?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently vss doesn't have a delete/remove data api.
from my understanding, ldk-node uses del/remove for events, i am not sure if it will be using vss for that.
Also in later commit i see that unpersist was removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently vss doesn't have a delete/remove data api. from my understanding, ldk-node uses del/remove for events, i am not sure if it will be using vss for that. Also in later commit i see that unpersist was removed.

Yes, not only events, also payment metadata etc. I removed the KVStoreUnpersister trait as all now switched over to this much more powerful KVStore trait:

https://github.com/tnull/ldk-node/blob/c3e488be747e8ab4cc1d3cf5aea1d8abf223839d/src/io/mod.rs#L38-L63

src/io_utils.rs Outdated Show resolved Hide resolved
src/io_utils.rs Outdated Show resolved Hide resolved
src/io_utils.rs Outdated
@@ -83,3 +86,37 @@ pub(crate) fn read_payment_info(config: &Config) -> Vec<PaymentInfo> {

payments
}

/// Provides an interface that allows a previously persisted key to be unpersisted.
pub trait KVStoreUnpersister {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@G8XSU How does VSS deal with removed data?

@tnull tnull force-pushed the 2022-09-initial-payment-store branch 2 times, most recently from 8a564ff to 3bf2169 Compare March 31, 2023 12:37
@tnull tnull requested a review from jkczyz March 31, 2023 12:45
@tnull
Copy link
Collaborator Author

tnull commented Mar 31, 2023

So I think from my side this PR should functionally be ready. An addition would be to add a "last_updated" fields to the payment info, but I'd be happy to push that to a smaller follow-up PR. Same goes for the SQLite implementation of KVStore.

@tnull tnull force-pushed the 2022-09-initial-payment-store branch from 5947128 to c3e488b Compare April 3, 2023 13:51
@tnull
Copy link
Collaborator Author

tnull commented Apr 3, 2023

Now added some refactoring to align the returned structs PaymentDetails/ChannelDetails/PeerDetails and exposed methods to remove and query for filtered payment details.

path.as_ref().encode_wide().chain(Some(0)).collect()
}

pub struct FilesystemStore {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this hold for FilesystemStore ::remove though? At the end it checks if the file exists, which could happen if a write happens after fs::remove_file.

src/io/fs_store.rs Show resolved Hide resolved
src/io/mod.rs Outdated Show resolved Hide resolved
src/io/mod.rs Outdated Show resolved Hide resolved
src/io/mod.rs Outdated Show resolved Hide resolved
src/payment_store.rs Outdated Show resolved Hide resolved
src/peer_store.rs Outdated Show resolved Hide resolved
src/lib.rs Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
@tnull tnull force-pushed the 2022-09-initial-payment-store branch from c3e488b to 3380625 Compare April 6, 2023 09:09
@tnull tnull requested a review from jkczyz April 6, 2023 14:54
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash the fixups.

src/io/mod.rs Outdated Show resolved Hide resolved
src/payment_store.rs Outdated Show resolved Hide resolved
tnull added 5 commits April 7, 2023 10:05
`OnceCell` doesn't call `drop`, which makes the spawned
`bitcoind`/`electrsd` instances linger around after our tests have
finished. To fix this, we move them out of `OnceCell` and let every test
that needs them spawn their own instances. This additional let us drop
the `OnceCell` dev dependency.

Additionally, we improve the test robustness by applying most of the
changes from
lightningdevkit/rust-lightning#2033.
@tnull tnull force-pushed the 2022-09-initial-payment-store branch from af71cc6 to 40f82b8 Compare April 7, 2023 08:07
@tnull
Copy link
Collaborator Author

tnull commented Apr 7, 2023

Feel free to squash the fixups.

Alright, tried squashing them in a reasonable way. However, this PR is showing it's age in that it has gotten way to big over time, will keep follow-ups more succinct.

Also added two new commits addressing feedback.

@tnull tnull requested a review from jkczyz April 7, 2023 08:27
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Feel free pass on the nit. Good to squash now.

src/event.rs Outdated Show resolved Hide resolved
tnull added 2 commits April 17, 2023 18:41
Rather than further relying on the upstream
`KVStorePersister`/`KVStoreUnpersister`, we here implement a general
`KVStore` trait that allows access to `Read`s/`TransactionalWrite`s
which may be used to deserialize/serialize data via the
`Readable`/`Writeable` implementations.

Notably `TransactionalWrite` is a `Write` for which the written data
needs to be explictly `commit`ed, asserting that we always persist
either the whole new change or no change at all.

Additionally, we avoid the `Info` umbrella term but opt to introduce `PaymentDetails`
to align naming with upcoming `PeerDetails` and `ChannelDetails`.
@tnull tnull force-pushed the 2022-09-initial-payment-store branch from 40f82b8 to c611c77 Compare April 17, 2023 16:43
@tnull
Copy link
Collaborator Author

tnull commented Apr 17, 2023

Squashed fixups and included the following changes:

diff --git a/src/event.rs b/src/event.rs
index 8b04585..c655bc2 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -341,6 +341,8 @@ where
                                                self.channel_manager.fail_htlc_backwards(&payment_hash);

-                                               let mut update = PaymentDetailsUpdate::new(payment_hash);
-                                               update.status = Some(PaymentStatus::Failed);
+                                               let update = PaymentDetailsUpdate {
+                                                       status: Some(PaymentStatus::Failed),
+                                                       ..PaymentDetailsUpdate::new(payment_hash)
+                                               };
                                                self.payment_store.update(&update).expect("Failed to access payment store");
                                                return;
@@ -377,6 +379,8 @@ where
                                        self.channel_manager.fail_htlc_backwards(&payment_hash);

-                                       let mut update = PaymentDetailsUpdate::new(payment_hash);
-                                       update.status = Some(PaymentStatus::Failed);
+                                       let update = PaymentDetailsUpdate {
+                                               status: Some(PaymentStatus::Failed),
+                                               ..PaymentDetailsUpdate::new(payment_hash)
+                                       };
                                        self.payment_store.update(&update).expect("Failed to access payment store");
                                }
@@ -396,9 +400,11 @@ where
                                match purpose {
                                        PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => {
-                                               let mut update = PaymentDetailsUpdate::new(payment_hash);
-                                               update.preimage = Some(payment_preimage);
-                                               update.secret = Some(Some(payment_secret));
-                                               update.amount_msat = Some(Some(amount_msat));
-                                               update.status = Some(PaymentStatus::Succeeded);
+                                               let update = PaymentDetailsUpdate {
+                                                       preimage: Some(payment_preimage),
+                                                       secret: Some(Some(payment_secret)),
+                                                       amount_msat: Some(Some(amount_msat)),
+                                                       status: Some(PaymentStatus::Succeeded),
+                                                       ..PaymentDetailsUpdate::new(payment_hash)
+                                               };
                                                match self.payment_store.update(&update) {
                                                        Ok(true) => (),
@@ -491,6 +497,8 @@ where
                                );

-                               let mut update = PaymentDetailsUpdate::new(payment_hash);
-                               update.status = Some(PaymentStatus::Failed);
+                               let update = PaymentDetailsUpdate {
+                                       status: Some(PaymentStatus::Failed),
+                                       ..PaymentDetailsUpdate::new(payment_hash)
+                               };
                                self.payment_store.update(&update).expect("Failed to access payment store");
                                self.event_queue

@tnull tnull merged commit 8cdca11 into lightningdevkit:main Apr 17, 2023
tnull added a commit that referenced this pull request Apr 17, 2023
/// the changes won't be persisted.
///
/// [`Writeable`]: lightning::util::ser::Writeable
fn write(&self, namespace: &str, key: &str) -> std::io::Result<Self::Writer>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not sure if KVStore should be a stateful object, keeping track of written buffer till now.
Can we define behavior in case of key overwrite ?

Also if client just uses a write couple of times and never commits, are we left with dangling in-memory footprint of that buffer? (because depending on kvstore impl, one might have to keep in-memory writer object for each key not commited yet)
Moreover, what will be the expectation in case of lost in-memory buffers when commit hasn't been called yet? (due to restart or crash).

Imo, it would make more sense to have Writer buffer out of KVStore and write once we are ready with full buffer for key. It makes for a much cleaner interface and we can avoid some of the missteps.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not sure if KVStore should be a stateful object, keeping track of written buffer till now. Can we define behavior in case of key overwrite ?

You mean in case we have two writers racing the same key? The current policy would be "latest commit win", which should be perfectly fine I think. If we find any issue with that, the TransactionalWrite interface would allow us to implement this differently though.

Also if client just uses a write couple of times and never commits, are we left with dangling in-memory footprint of that buffer? (because depending on kvstore impl, one might have to keep in-memory writer object for each key not commited yet) Moreover, what will be the expectation in case of lost in-memory buffers when commit hasn't been called yet? (due to restart or crash).

Yes, we're keeping the buffers around until committed, which means they a) would be lost on crash and b) take up some memory until we're sure they have been persisted. I don't see any way around that though, be it in this or another interface design? In particular, I don't see how having the persister separate would magically allow us to get rid of the in-memory read/write buffers? Of course, the reader/writer objects are not meant to be kept around forever, but only to be acquired when you need them.

Imo, it would make more sense to have Writer buffer out of KVStore and write once we are ready with full buffer for key. It makes for a much cleaner interface and we can avoid some of the missteps.

As mentioned above, I still don't see a) how the current approach is particularly error-prone b) how having the KVStore::write taking a concrete byte slice argument would fundamentally improve on the current design. Plus, it would lead to a weird asymmetry as KVStore::read would return a generic Read but write would take a raw byte slice...

So I'm happy to be convinced otherwise, but I'm not seeing the issue or that the solution you propose would improve on it functionally or design-wise for that matter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants