You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(swaps): bind preimage scheme to (tag || descriptor || index)
Two small but meaningful changes to the v1 derivation message:
- Descriptor folded into the signed message in addition to the tag.
Domain separation now defends against accidental signature reuse
even within this codebase (a future feature that signs a
descriptor-derived hash for some unrelated proof can't repurpose
the same signature). The descriptor is also what would scope the
preimage if the tag ever got reused outside our control.
- u32 little-endian index baked into the message (always 0 today).
Forward-compatibility for multi-preimage-per-descriptor flows
without a scheme bump: when a caller wants two preimages from one
descriptor, they pass index=1 and recovery iterates 0..N.
So the v1 scheme is now:
message = SHA-256( "NArk-Boltz-Preimage-v1"
|| descriptor.ToString()
|| u32_le(index) )
sig = BIP-340-Schnorr(descriptor_key, message, aux_rand=null)
preimage = SHA-256(sig)
DerivePreimageAsync gains an explicit `uint index` parameter; all four
call sites (three Initiate*Swap + the RestoreSwaps recovery branch)
pass index=0. The restore path leaves a comment noting that a future
multi-preimage variant should iterate index=0..MAX here.
Also documents the determinism requirement on IRemoteSignerTransport.SignAsync:
implementations MUST sign with aux_rand=null for remote-signed wallets
to recover outstanding preimages from seed. The contract was implicit
before — implementations that randomise (hardware wallet side-channel
hardening) would silently break recovery; now it's explicit in XML doc.
docs/articles/swaps.md updated to describe the bundled message
construction and the remote-signer caveat.
The preimages we generate for reverse and chain swaps are derived deterministically from the wallet's signing material so that a restored wallet can re-derive them and claim outstanding VHTLCs. The scheme is:
110
+
The preimages we generate for reverse and chain swaps are derived deterministically from the wallet's signing material so that a restored wallet can re-derive them and claim outstanding VHTLCs. The scheme:
sig = BIP-340-Schnorr( descriptor_key, message, aux_rand=null )
115
+
preimage = SHA-256( sig )
114
116
```
115
117
116
-
— signing the constant tag-hash with the receiver descriptor's key. BIP-340 with `aux_rand=null` is deterministic, so the same descriptor + same wallet always produces the same preimage. Recovery: when `RestoreSwaps` rediscovers a reverse swap, it re-derives the candidate preimage, verifies `SHA-256(candidate) == restored.PreimageHash`, and attaches it to the swap's metadata for the sweeper to claim. Hash mismatch (legacy random preimage, or wrong descriptor) leaves the preimage out; `EnrichReverseSwapPreimage` remains the manual fallback.
118
+
The signed input bundles three things:
119
+
120
+
-**Tag** — domain-separates this signature from any other use of the descriptor's key. Versioned (`-v1`) so a future scheme bump can ship as `-v2` while recovery still tries v1 for older swaps.
121
+
-**Descriptor** — scopes the preimage to the specific swap descriptor.
122
+
-**Index** — lets a caller derive multiple preimages from the same descriptor. Always `0` today; baked into v1 so recovery iteration is forward-compatible without a scheme bump.
123
+
124
+
BIP-340 with `aux_rand=null` is deterministic per `(key, message)`, so same `(wallet, descriptor, index)` always yields the same preimage. Recovery: when `RestoreSwaps` rediscovers a reverse swap, it re-derives the candidate preimage with `index=0`, verifies `SHA-256(candidate) == restored.PreimageHash`, and attaches it to the swap's metadata for the sweeper to claim. Hash mismatch (legacy random preimage, or wrong descriptor) leaves the preimage out; `EnrichReverseSwapPreimage` remains the manual fallback.
117
125
118
126
Watch-only wallets (no signer) fall back to a random preimage on create — they don't get the recovery story but they can still execute swaps until they pair a signer.
119
127
128
+
> **Remote signers and determinism.**`IRemoteSignerTransport.SignAsync` MUST use `aux_rand=null` for the recovery scheme to work end-to-end on remote-signed wallets. Implementations that randomise `aux_rand` (e.g. hardware-wallet side-channel hardening) will produce a different signature each call → different preimage → recovery silently fails. See the XML doc on `SignAsync`. Local sources (`Bip39SigningSource`, `NsecSigningSource`) already satisfy this.
Chain swaps can fail to settle when the user funds the lockup with an amount that doesn't match Boltz's original quote, when an LN invoice times out, or when the swap window expires. The SDK handles these cases automatically inside the routine status-poll loop in `BoltzSwapProvider.PollSwapState`:
0 commit comments