feat(wallet): add offline ecash receiving with DLEQ verification#1931
feat(wallet): add offline ecash receiving with DLEQ verification#1931GEET3001 wants to merge 1 commit into
Conversation
TheMhv
left a comment
There was a problem hiding this comment.
You need to fix formatting and some errors that you can find running just final-check command.
Can you implement some integration test for this receive_offline function too.
And please, return the wallet tests
6ebe70a to
8c736fd
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1931 +/- ##
==========================================
- Coverage 71.48% 71.43% -0.05%
==========================================
Files 356 356
Lines 73857 73940 +83
==========================================
+ Hits 52798 52821 +23
- Misses 21059 21119 +60 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
0b4239f to
3841334
Compare
|
Integration tests added (as requested by @TheMhv) Added two integration tests in crates/cdk-integration-tests/tests/integration_tests_pure.rs: test_receive_offline_pending_then_finalize — full lifecycle test: token verified offline via DLEQ → stored as PendingReceive → finalized online via finalize_pending_receives() → spendable balance updated Note on dependencies: This PR's offline path relies on keysets being available in the local cache. The correct behavior when the wallet is truly offline (no cached keysets) depends on the metadata cache improvements being worked on in the related issue. Once those land, receive_offline will work seamlessly without any prior online session |
401cc38 to
e9d0499
Compare
8801175 to
8b03dfb
Compare
|
just a heads up, it looks like the recent CI checks are failing due to a runner infrastructure issue. The logs for the Quick Check and Binding tests are all throwing a No space left on device (os error 28) error, which means the runner's disk is currently full. Let me know when the runner has been cleared and I can re-trigger the checks, or feel free to re-trigger them on your end |
eb799db to
27064ef
Compare
30a8a0f to
90fbb30
Compare
cdk-bot
left a comment
There was a problem hiding this comment.
Verified findings approved for disclosure:
- Export receive_offline and finalize_pending_receives on the UniFFI Wallet (high) - FFI consumers cannot use the new offline receive lifecycle through the exported Wallet type, leaving the new public wallet functionality unavailable to Swift/Kotlin bindings.
Unanchored locations included in summary:- crates/cdk-ffi/src/wallet.rs:189
0a09d14 to
77932e6
Compare
cdk-bot
left a comment
There was a problem hiding this comment.
Verified findings approved for disclosure:
- Finalizing all offline receives in one swap can delete unrelated valid pending tokens (high) - A single bad/double-spent offline receive can cause unrelated valid pending offline tokens to be deleted from the wallet, resulting in loss of recoverability for those funds from local wallet state.
Unanchored locations included in summary:- crates/cdk/src/wallet/receive/saga/mod.rs:312
- crates/cdk/src/wallet/receive/saga/compensation.rs:40
d851d2b to
705c7c6
Compare
cdk-bot
left a comment
There was a problem hiding this comment.
Verified findings approved for disclosure:
- Finalizing a rejected offline receive deletes the stored PendingReceive proof (medium) - An offline receive that was accepted and persisted can be silently erased during finalization if the mint definitively rejects the swap, leaving no local record of the token or failure.
- Offline receive rejects valid tokens from inactive keysets (medium) - Offline receive rejects valid tokens from inactive/rotated keysets even though online receive can decode and redeem them.
84844c9 to
dea1e36
Compare
cdk-bot
left a comment
There was a problem hiding this comment.
Verified findings approved for disclosure:
- Offline receive finalization loses token memo and splits one token into per-proof transactions (medium) - Offline-received tokens lose their memo/grouping when finalized; multi-proof tokens create multiple context-less incoming transaction history entries instead of one coherent receive.
dea1e36 to
393e6fd
Compare
393e6fd to
98bbd6e
Compare
Description
Closes #1927
Currently, receiving in the CDK wallet requires the wallet to be online to perform a swap. However, Cashu enables offline receiving pending DLEQ verification and locking conditions. This PR adds that capability end-to-end across the core library, mobile FFI bindings, and CLI.
receive_offline()accepts a token and anOfflineReceiveOptionsstruct containing:min_locktime— the minimum locktime required on the tokenrequire_locked— whether the token must be P2PK locked to protect against double-spend by the sender while offlineThe wallet verifies the token's DLEQ proof and checks these conditions entirely offline. Upon successful verification, the proofs are added to the database in the new
PendingReceivestate. Wallets should display these with a warning that they must be swapped for final settlement.finalize_pending_receives()checks for all proofs inPendingReceivestate and executes the swaps with the mint. This should be called whenever the wallet comes back online.What was added
Core Protocol
PendingReceivestate to theStateenum incashu/nut07OfflineReceiveOptionsstruct (min_locktime,require_locked)receive_offline()method to theWallettrait and core implementation:PendingReceivestate in the databasefinalize_pending_receives()to swap pending proofs with the minttotal_pending_receive_balance()to query the total amount sitting in the pending offline queuereceive_offlinetime;finalize_pending_receivesprocesses each group as a single receive operation and recovers the memo from theOfflinePendingReceivesaga entryDatabase
PENDING_RECEIVEto the proof stateCHECKconstraintFFI Layer (cdk-ffi)
receive_offline(),finalize_pending_receives(), andtotal_pending_receive_balance()over the UniFFI boundary so Swift/Kotlin consumers can use the full offline receive lifecycleProofState::PendingReceiveas a UniFFI enum variantOfflineReceiveOptionsfor mobile callersPendingReceivein all exhaustive match sites (test utils, FFI)CLI (cdk-cli)
--offlineflag to the receive subcommand — routes the token toreceive_offlineinstead of the standard online swapfinalize-receivessubcommand — sweeps allPendingReceiveproofs and swaps them with the mint when the user is back onlineBug Fix
check_spendable.rsthat was introduced whenPendingReceivewas added to theStateenumKnown limitations
receive_offlinedepends on the keyset for the incoming token having been synced during a prior online session. This relies on Add load_from_db to MintMetadataCache #1957'sload_from_dbfallback inMintMetadataCache::load().is_populatedis a coarse boolean — it returnstrueif any keyset is cached, not specifically the one the incoming token uses. If the wallet has never seen that keyset (e.g. the mint rotated keys between the sender minting and the receiver syncing),load_keyset_keysreturnsNone. Scoped fix in this PR: improved error message at theload_keyset_keyscall site directing the user to go online once to sync. Follow-up:is_populatedshould be keyset-id-aware rather than a global boolean.Atomic offline receive:
receive_offlinedoes two DB writes with no transaction between them — a crash between them loses the memo but not funds. Needs a DB transaction API to fix properly.No dismissal API for stuck pending receives — UI wallets have no way to let users clear a
PendingReceiveproof they know is gone.Notes to reviewers
PendingReceiveis kept intentionally separate fromPending(used for in-flight send/melt) to avoid ambiguity in state machine transitions and to make it easy to display pending offline receives distinctly in wallet UIs.CHECK (state IN (...))constraint on the proof table to includePENDING_RECEIVE, preventing constraint violations at runtime.ProofState::PendingReceiveis exposed as a UniFFI enum variant, and all three new methods are callable from mobile/desktop consumers.--offlineflag requires users to explicitly opt-in to offline receives, as bearer tokens accepted offline carry a double-spend risk unlessrequire_lockedis set.Changelog
Added
State::PendingReceive— new proof state for offline-received ecash awaiting final swapWallet::receive_offline(token, options)— verify a token's DLEQ proof offline and store proofs inPendingReceivestateWallet::finalize_pending_receives()— sweep allPendingReceiveproofs and swap them with the mintWallet::total_pending_receive_balance()— query the total pending offline balanceOfflineReceiveOptionsstruct withmin_locktimeandrequire_lockedfieldsPENDING_RECEIVEto the proof state constraintcdk-fficdk-clireceive --offlineflagcdk-clifinalize-receivessubcommandChecklist
just quick-checkbefore committing