wallet: audit trail for external transactions#3129
Conversation
Implements an append-only ledger that makes every decision made by SN's
external wallet mechanics visible and auditable, including the impact on
monetary amounts. The ledger hooks directly into database mutations made by
the ExternalTransaction logic atomically, reusing the same database
transaction to ensure that there are no unresolvable states in the database
at any time.
The ledger's accounting model exists of append-only rows that track:
- signed amounts in a "side" column that records CREDIT (+) and DEBIT (-)
and an "amount" column containing the magnifier in mSats (>= 0)
- monetary movement across:
sources: ACCRUAL and SETTLEMENT, where ACCRUAL represents the invoice side
and SETTLEMENT the payment side of each transaction.
direction: RECEIVE and SEND, representing the user requesting a payment or
paying an invoice.
type: the nature of the action taken
- OBLIGATION: the recording of an invoice
- FULFILLMENT: the recording of a payment
- CORRECTION: a correction to an invoice that was under- or overpaid and
in a final state.
- UNKNOWN: a state we cannot measure (often because of missing
functionality for a protocol) and thus have to make
assumptions about the state
- ERROR: reversals due to technical errors
- TIMEOUT: reversals because no confirmation was given in time
- de-normalized relations over userId, protocolId, walletId and
externalTransactionId
On most paths currently implemented, the ledger will contain only 2 rows: a
single credit row when the invoice was created, and a debit row containing
the outcome, either a fulfillment/unknown settlement or a reversal of the
invoice. Currently, only in case of under- or overpayment are there more
than 2 rows, as the correction row is appended when a transaction reaches
its final state.
SN chooses to trust the final settlement report from the wallet
implementation and thus corrects the invoice amount accordingly, both in
the ExternalTransaction table in the existing implementation, as in the
ledger. The ledger simply records the choice made.
Append-only is guarded by a trigger. Besides inserts, only deletes are
allowed, to support clearing data from a wallet and cascaded deletion of
wallet, protocol and user entities.
Extracts the dynamic time since functionality from wallet logs so that this can be reused.
49b0a25 to
4ed4519
Compare
Implement the audit trail in a view to the user through gql and UI, through the transaction detail page. - gql: add a ledgerEntries and currentBalanceMsats field to the ExternalTransaction type and extend EXTERNAL_TRANSACTION_FIELDS to include those in the fragment - UI: render a by-default hidden accounting header on the transaction details page that shows the entire audit trail on the accrual (billed) and settlement sides to the user. Summarize the effect of each row and the totality of accounting by displaying a running and current open balance amount.
4ed4519 to
2692758
Compare
|
Apologies for the force pushes - I messed up the source branch pick twice in a row. |
- do not assign ledger types to transient errors and unknowns - check terminal state of reasons against STOP_CHECK_REASONS - document which decisions are terminal and which are transient
|
Pushed a bugfix because ledgerType was being assigned too often (on non-terminal states). In some unknown - and maybe in the future, error - edge cases, the reason enum has to be checked against This didn't show up in practice, because the ledgerType was passed to functions that stripped it off again, so I fixed that too for the edge cases. |
Do you have more context for this? Are there partial payment proposals circulating in ln dev circles or is this in anticipation of a feature on SN?
As discussed in chat, I'd like to derive transaction status using this audit trail rather than doing all the check-and-setting we currently have. This approach is far more architecturally sound. My plan is to rewrite #3121 to fit with this audit trail. To make it fit, I expect #3121 to implement:
The main high-level concern I have with this PR is minor. It is UI/UX of the exposed audit trail. I suspect we'll want to do something like |
It's an anticipation of not being able to guarantee that all encapsulating communications protocol implementations (like NWC) honor that constraint in the future. I should have made that more clear in the comment, can amend if this is going to be used. The other states are pure SN decisions, this one is (currently) triggered externally. Database constraints for external data are a pain.
2 reasons:
Understood. The main function of doing accounting on this is because SN making decisions about rainy case scenarios, and have auditable financial integrity on those decisions.
As we discussed initially, I was looking first to just add the accounting to
The second step I envisioned would be to simply add While I do think that there is something to say for separate accounting functionality, a final solution can definitely be a pure state transition table, i.e. something that simply tracks state transitions. Not as free-form as
This may feel more efficient than 2 separate tables, and if there would ever be an export function, Note that for PayIns, this is a different story and there it does matter as there the only source are SN decisions, and thus I decided that the rigidity is not a bad thing to have, if ever extended to that side of the wallet interactions. |
|
Thank you for the explanations! I understand your intent better now.
You're right I think that's sufficient.
I've wanted to do this since forever. It just needs to be async (request, queue, do, store, then notify) and is quite involved. I'm reworking the base for this branch to avoid doing anything this ledger does better (and rehumanizing it ... ugh the siren song of llms). I have a day worth of rounds to make on that but hope to update that PR tonight. |
Word!
Cool. I'll expand doc and comments a bit to make the intent clearer when I rebase. Do you want to go the merged table route or the strict separation of concerns route? I can pretty quickly transform this into either - takes a day on top of yours at most. |
Keeping them separate makes sense. The boundary here is already helpful. It allowed me to simplify the receive stuff a lot. |
92c45c3 to
5059ca8
Compare
|
Closing this in favor of a less complicated approach |
Description
Implements an append-only ledger that makes every decision made by SN's external wallet mechanics visible and auditable, including the impact on monetary amounts. The ledger hooks directly into database mutations made by the ExternalTransaction logic atomically, reusing the same database transaction to ensure that there are no unresolvable states in the database at any time.
The ledger's accounting model exists of append-only rows that track:
userId,protocolId,walletIdandexternalTransactionIdThe data is exposed through gql nested under the ExternalTransaction type (i.e. on querying transaction details) and on the
/wallets/transactions/[id]page. The data is collapsed by default (modeled after "similar items") because it is only interesting when things go wrong.See
docs/dev/external-transaction-auditing.mdfor a little table that shows how types are mapped across source and debit/credit sides.NOTE: proposed against the feature branch!
Screenshots
Pending, unexpired receive request
A settled receive request
A timed out request
Underpayments
Overpayments
When the protocol does not support tracking of settlement
Additional Context
The primary cause of this was me messing with hostile NWC responses to see what happens when something errors out or worsie - spreads lies on the 3rd party wallet-side. Since there is often no other way than optimistic interpretation with any of the current connection protocols - except for the preimage check - the only defense SN has against poor or hostile implementations is tracking. Therefore the main goal of this is to make monetary choices made by SN visible and auditable to a user, preventing drama.
PS: I will propose a separate PR outside of this feature topic to add a modified implementation of nostr-wallet-connect-lnd to the dev env.
Checklist
Are your changes backward compatible? Please answer below:
Yes against
master, but not within a currently active dev env database on thewallet-external-tx-checkfeature branch (cannot backfill things that weren't captured.)On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:
7 - I have tested with CLINK, NWC and CLN. Most notably I haven't tested lnbits because my env isn't set up properly for that.
For frontend changes: Tested on mobile, light and dark mode? Please answer below:
Yes, yes (and light mode hurts eyes), simulated with viewport only.
Did you introduce any new environment variables? If so, call them out explicitly here:
no.
Did you use AI for this? If so, how much did it assist you?
Some. It helped me understand the base branch at the start and plan integration and with the front-end work. The only thing I have kept from the latter is the css.