Repository navigation
Description
Twin: synonymdev/bitkit-ios#1238
Refs: synonymdev/bitkit-core#160, synonymdev/bitkit-core#195, synonymdev/bitkit-core#200, #1696, #1350, #1732
Problem or use case
Bitkit has had no push path since the Blocktank sunset (#1696), which removed registration, decryption and WakeNodeWorker. On master:
FcmService.onMessageReceivedacts only onmessage.notification, so a data message is logged and dropped.onNewTokenonly logs, and nothing reads an FCM token.- The node stops on
ON_STOPunless the user opted in to staying active, and nothing can start it without an Activity.
So Paykit requests are found only by foreground polling, a pubky swap the wallet started cannot be claimed until the user opens the app again (#1835), and a Lightning payment that arrives while the app is closed waits until the user opens it.
synonymdev/bitkit-core#160 added the client for the wake gateway (synonymdev/wake), the service that replaces bitkit-notification-server. Nothing calls it yet. This is new code, not a URL swap.
Blocked by
Phase 1 can start against the gateway's local stack, but it cannot ship until these land:
- Gateway deployment. It is not deployed, and no v1 hostname or audience is chosen (synonymdev/wake#2).
- A producer. Registering alone delivers nothing:
swap.claimableis the closest. Pubky Swaps providers send it since Push a claimable hint when a reverse lockup reaches depth, and wake the owner through a wake grant (#106) coreyphillips/pubky-swap#117, through a grant the owner sends with each reverse swap. bitkit-core does not send that grant yet (feat: let reverse swaps wake the wallet when they can be claimed bitkit-core#200), and a provider calls only gateways on its allowlist, which stays empty until the gateway has a hostname.paykit.syncneeds a way to hand a grant to a peer. The Paykit message for it (paykit.wake_capability) is not specified yet.blocktank.*will not come. Launch is v1 only, ln2 sends onlywakeToTimeoutsince synonymdev/blocktank-lsp-ln2#285, and synonymdev/blocktank-lsp-ln2#284 is parked (synonymdev/wake#2, synonymdev/wake#6).
Proposed solution
Three phases. Each one ships on its own. The Lightning node wake is not planned.
Phase 1: device plumbing
Core and config
- Bump bitkit-core from
0.8.0-rc11to0.8.0-rc13, the first release with the wake fixes from fix(wake): close the client gaps from the gateway review bitkit-core#195. Skip0.8.0-rc12, which has the older wake API. fix: adopt core bip21 validation #1454 already moves to rc13. Wrap the wake calls in a small service onServiceQueue.CORE, like the wrappers inCoreService.kt. - Add a per-network gateway URL to
Env, like the otherwhen (network)getters. - Read the audience from
wakeServerInfo. Never hardcode it.
Credentials
- Generate once and keep in
Keychain: the install id, the encryption key pair and the device secret. Also keep the last registration timestamp. - Reuse them across launches. They belong to this install, so leave them out of VSS backups.
- Do not reuse the old key. Remove the orphaned
PUSH_NOTIFICATION_PRIVATE_KEY.
Registration
- Register when the node is running, from
onNewToken, and when thepk:identity or the topic list changes. Skip it when nothing changed since the last success. - Pass
timestampexplicitly asmax(now, last + 1), using the stored last timestamp. The gateway answers409 stale_registrationto a timestamp older than the install's last registration, or equal to it with different content. Since fix(wake): close the client gaps from the gateway review bitkit-core#195 core's default does this within one process, but it starts over on each launch, so a relaunch in the same second or a clock that steps back would still get a 409. - On
401 stale_timestamp, prepare again with the error'sserverTimeastimestampand sign again once. - Use the production environment. Core rejects
sandboxfor FCM. - The
ln:proof usesLightningService.sign, which has no callers today and noLightningRepowrapper. - Subscribe with patterns (
paykit.*,swap.*), as the wire protocol advises, so a topic added on the server arrives without an app release. Leave outblocktank.*, which nothing will send.
The pk: proof depends on the wallet's identity state. A wallet needs no Pubky profile, so ln: alone is valid.
| Identity state | Identities | Notes |
|---|---|---|
| No profile (the default) | ln: |
Do not use deriveKeys() here: it would register a key the user never published |
| Local identity | ln:, pk: |
The key is in PUBKY_SECRET_KEY |
| Ring-adopted | ln:, pk: |
Read the Ring credential just in time |
| Session without a local key | ln: |
There is no secret to sign with |
PubkyRepo.activeSecretKeyHexis private. Add a narrow method that signs a registration throughwakeSignPubkyProof, rather than exposing the hex.PubkyRepo.publicKeyhas apubkyprefix. Core wantspk:plus the 52 bare characters.- If reading the key fails for a moment, skip the attempt. Do not register
ln:alone, which would droppk:and any grants tied to it.
Receiving
- Serialize
message.datato JSON and pass it towakeDecryptPush. Every v1 wake is a data message. - Then dedupe by
id, drop anything pastdeadline, do the work, and ack. Persist seen ids, because the process can die between pushes and delivery is at least once. - Ack
handledonly when the work is done, since it cancels the fallback alert. Ackno_handleronly for a wake the app shows nothing for, andfailedwhen the work failed. - A v1 wake shows nothing by itself on Android. For
alertandtime_sensitiveurgency, post a local notification with the topic's text fromwakeListTopics(cached, refreshed now and then), then ackhandled. - A
fallback = trueresult has nothing to decrypt or ack. The OS shows it in the background. In the foreground it reachesonMessageReceived, where the app has to post its text. A live presence lease keeps most of these away. - Treat a wake as a prompt to sync. Take amounts, payment state and contact names from the wallet, never from the payload.
- Do not do long work in
onMessageReceived. Enqueue an expedited worker, asWakeNodeWorkerdid. - The gateway's sample config sends fallback alerts on a
wakechannel. The app haschannel_appand the node channel. Add the channel or change the config so they match.
Presence
- Call
wakeSetPresenceonON_START, renewing every 60 s with a 90 s ttl, andwakeClearPresenceonON_STOP.ContentView.ktalready starts and stops Paykit polling at those points.
Unregistering
- Call
wakeUnregisterinWipeWalletUseCase.cleanupRemote, which runs beforekeychain.wipe(). It needs the device secret, and it must stay best effort like the steps around it.
Cleanup
- Skip registration when
Env.isE2eTestis set, as fix: skip push registration in e2e #1245 did, so wiped test installs do not leave dead tokens behind. - Stop logging the FCM token in
onNewToken. Never log the encryption key, the device secret or a grant token. - Fix the stale
WakeNodeWorkermention inAGENTS.md.
Phase 2: swap claim wakes
Needs synonymdev/bitkit-core#200, the pubky swaps route (#1732) and resuming swaps in the background (#1835).
- Owner. After a reverse swap's
SwapAccept, if the provider takes a wake capability (swap-wake-v1), callwakeCreateGrantfor this wallet'spk:identity withswap.claimableand an expiry that covers the claim window. Hand the token to core to send to the provider. It is shown only once. - Revoking. Call
wakeRevokeGrantwhen the swap is claimed or fails. Grants are capped at 30 created per hour per device and 200 live per identity, so keep them per swap and short-lived. - Handling
swap.claimable. A peer wake has no payload and does not name the swap, and wakes on the topic collapse within 30 s, so one wake can stand for several swaps. In a worker, runpubkySwapsAdvancefor every open swap. It checks the lockup on the wallet's own chain view before it claims. Ackhandledonce each claimable swap has its claim broadcast, andfailedotherwise. - Without a wake. The topic has no fallback alert, and the gateway holds it back while presence is live (
only_if_offline), so keep checking open swaps on launch and in the foreground.
Phase 3: Paykit wakes
Needs the Paykit grant message above.
- Handling
paykit.sync. Run the Paykit-only refresh from feat: notify new payment requests while bitkit is in the background #1350 in a worker, with its notification rules, then ackhandled. It does not start the node. Peer wakes carry no payload, sopayloadJsonis null. - Recipient. On linking a peer, call
wakeCreateGrantfor this wallet'spk:identity withpaykit.sync, and send the token over the Encrypted Link. It is shown only once. Keep the grant id per peer, and callwakeRevokeGranton block or unlink. - Reconciling. On launch, use
wakeListGrantsto check that every linked peer has a live grant with more than 30 days left. Creating grants is limited to 30 per hour per device and 200 live per identity, so do this in batches. - Sender. Keep the newest token per link in
Keychain. After writing something for a peer, callwakeSendPeerWakeas best effort: the write must never depend on it. Ongrant_invalid, drop the token. Onrate_limited, skip the wake. wakeListGrantsreportssent_1handsent_24h, which can show a noisy peer.
Not planned: Lightning node wake
Nothing will send blocktank.* (see Blocked by), so no wake arrives for an incoming HTLC, a timeout or a mutual close. If an LSP sends wakes again, WakeNodeWorker at 25e1aa493, the parent of #1696, is the prior art.
Open questions
- Who registers? Before feat: blocktank sunset #1696 every wallet registered after node start, while iOS registered only with notifications on. Registering links the node id, the Pubky key and the push token on the gateway. The suggestion is opt-in: only when notifications are granted, with the Background Payments setting turning it on and off. The prompt reaches only Paykit subscribers today (
NotificationsTimedSheet), so this needs a decision on widening it. - What should Background Payments promise? feat: blocktank sunset #1696 reworded it for Paykit reminders. Wakes add Paykit requests and swap claims, but not Lightning payments that arrive while the app is closed, and the copy should say so.
Done when
- A wallet registers with
ln:, and withpk:when it has one, and registers again after a token rotation with no 409. - With the app closed, a wake on a subscribed topic is decrypted once, handled and acked, and
wake statusshows it acked. - Delivering the same wake twice does the work once.
- A wake past its deadline is dropped.
- An
alerttopic posts a notification with the topic's text. - After a wipe, the old device secret gets a 401.
- E2E builds do not register.
- Unit tests cover parsing, dedupe and ack outcomes, using the shapes in core's
src/modules/wake/test_vectors/pushes.json.
Alternatives considered
- The gateway's legacy v0 route. It exists for builds already in the field. Using it would lose acks, presence,
pk:identities and peer wakes. - Polling only (feat: notify new payment requests while bitkit is in the background #1350). It stays useful as the path for wallets without wakes, but WorkManager runs it at most every 15 minutes, and it cannot help a Lightning receive. feat: notify new payment requests while bitkit is in the background #1350 calls a server-side push out as tracked separately, and this is that work.
Not here
- Pushes for Spark receives. feat: add spark via breez sdk #1697 notes that nothing signals them while the app is closed, and no producer exists for them.
- Blocktank channel orders.
Additional context
- API and examples:
src/modules/wake/README.mdin bitkit-core. - Protocol:
docs/wire-protocol.mdin synonymdev/wake, especially sections 3, 4 and 8. Peer flow:docs/anti-spam.md. - Local testing:
docker compose -f docker/compose.yml --profile dev up -d --build --waitin synonymdev/wake runs the gateway with fake providers, andwake send,wake statusandwake simdrive it. The fakes record pushes rather than deliver them, so a real device needs real Firebase credentials. - The committed
app/google-services.jsonis a placeholder. Confirm which Firebase project each build uses, because the gateway needs that project's service account. - The v1 wire format stays experimental until the apps adopt it, so problems found here should go back to core and the gateway.
- fix(wake): close the client gaps from the gateway review bitkit-core#195 added what core was missing:
wakeSendTestsendswake.testto this device only (the replacement for the old Dev Settings test button),wakeGetDevicereturns what the gateway stored,wakeSetTopicsreturnsunknownTopics, andGatewayRejectedcarriesserverTimeandretryAfterSecs. - The Spark stack (feat: add spark via breez sdk #1697) touches the same
LightningRepoandLightningServicecode, so expect to rebase over it.
Activity