Skip to content

core: keep the preimage the list snapshot drops - #69

Merged
jamaljsr merged 1 commit into
mainfrom
core/preserve-send-preimage
Aug 5, 2026
Merged

core: keep the preimage the list snapshot drops#69
jamaljsr merged 1 commit into
mainfrom
core/preserve-send-preimage

Conversation

@Roasbeef

@Roasbeef Roasbeef commented Aug 2, 2026

Copy link
Copy Markdown
Member

In this PR, we stop the SDK throwing away proof of payment.

The daemon reveals a send's preimage exactly once, on the entry it pushes
over the activity stream when the swap settles. Every list read afterwards
returns that same entry with the field empty. Confirmed against a real
regtest send:

"status": "ENTRY_STATUS_COMPLETE",
"progress": {
  "phase_label": "confirmed",
  "payment_hash": "5520a880117c84bcb725772c9f86bedae2050d947e061f29b5b5a1621ea252a4",
  "preimage": ""
}

ActivityStream.noteActivity was consuming that stream entry for its
cursor and discarding the body, then debouncing a list refresh that
overwrote the snapshot. So the one frame carrying the preimage was
dropped, and the refresh behind it guaranteed nothing could recover it.
Entry.progress.preimage is declared on the public type and was
unreachable through any public API.

That matters because a preimage is not decoration. It is the only thing a
caller can show a third party to demonstrate that an invoice actually
settled, since sha256(preimage) equals the payment hash the invoice was
minted against and nobody can produce one without having paid. Verified
end to end on regtest:

preimage:  3048c62adff75738e7181a81b6b8e68e4471aaa1bc814b5ba1834ea6b1285e16
sha256:    37fbac8f8f451e6d53829872fb835b8058985eeedcf16ed7b1e972c2e8d1a7ee
hash:      37fbac8f8f451e6d53829872fb835b8058985eeedcf16ed7b1e972c2e8d1a7ee
VERIFIES:  True

So ActivityStream now records preimages as they go past, keyed by
payment hash, and the refresh puts them back on the entries it lists.
restorePreimages never invents a value it was not given, leaves an entry
that already carries one untouched, returns the input array unchanged when
nothing has been seen, and does not mutate what it was handed. The map is
cleared on stop so a different wallet cannot inherit the last one's
proofs, and holding them only in memory is enough because the stream
replays with includeExisting when it reopens.

Nine tests cover both halves: what the stream remembers and what the
refresh restores. All 204 core tests pass, typecheck is clean.

This was found while building a streaming payments feature on the demo
wallet, where a settled tick has to be provable to a third party. It is
sent on its own because it stands on its own merits and there is no reason
for proof of payment to wait on a UI port.

Note for #58: this touches the refresh path in engine.ts, so it will
want re-applying by hand rather than merging cleanly if that lands first.
activity.ts should be conflict free either way.

Verified end to end under a real browser send

Since this was first opened, the whole path has been exercised from a browser
wallet on signet rather than a synthetic payer: three consecutive L402 ticks,
each one a real send, each preimage arriving on the settle frame and surviving
the list refresh that follows it. The receiving side checked them
independently:

seq 1  sha256(preimage) == payment_hash: True  (1000 sat)
seq 2  sha256(preimage) == payment_hash: True  (1000 sat)
seq 3  sha256(preimage) == payment_hash: True  (1000 sat)

That is the claim this PR rests on, and it is now something we have watched
rather than something we inferred from regtest. Without the change here the
receiving side has nothing to check, since the preimage is gone by the time
any caller can read the entry.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@jamaljsr jamaljsr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested and confirmed it works as advertised. Just found one gap I think we should address.

Comment thread packages/core/src/engine/engine.ts
@jamaljsr
jamaljsr force-pushed the core/preserve-send-preimage branch from 11c2262 to 9b1484a Compare August 5, 2026 18:40
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

In this commit, we stop the SDK throwing away proof of payment.

The daemon reveals a send's preimage exactly once, on the entry it pushes
over the activity stream when the swap settles. Every list read afterwards
returns that same entry with the field empty. Confirmed against a regtest
send: the entry comes back ENTRY_STATUS_COMPLETE, phase "confirmed", and
progress.preimage is "".

The SDK was consuming the stream entry for its cursor and discarding the
body, then debouncing a list refresh that overwrote the snapshot. So the
one frame carrying the preimage was dropped, and the refresh behind it
guaranteed nothing could recover it. The field is declared on the public
Entry type and was unreachable through any public API.

That matters because a preimage is not decoration. It is the only thing a
caller can show a third party to demonstrate that an invoice actually
settled, since sha256(preimage) equals the payment hash the invoice was
minted against and nobody can produce one without having paid.

So ActivityStream now records preimages as they go past, keyed by payment
hash, and the refresh puts them back on the entries it lists.
restorePreimages never invents a value it was not given, leaves an entry
that already carries one untouched, returns the input array unchanged when
nothing has been seen, and does not mutate what it was handed. The map is
cleared on stop so a different wallet cannot inherit the last one's
proofs, and holding them only in memory is enough because the stream
replays with includeExisting when it reopens.
@jamaljsr
jamaljsr force-pushed the core/preserve-send-preimage branch from 9b1484a to 4d76780 Compare August 5, 2026 18:46
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@jamaljsr
jamaljsr merged commit 6b26c3f into main Aug 5, 2026
3 checks passed
@jamaljsr
jamaljsr deleted the core/preserve-send-preimage branch August 5, 2026 18:51
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.

2 participants