Commit graph

9 commits

Author SHA1 Message Date
Håvard Kittelsen
460e113e75 feat: multislot — more than one wallet key per transaction input
`inputPaths` names one HD key per entry, which is the whole story for a P2PKH
input: one input, one signature. A contract input is not like that. Its unlocking
bytecode may carry several sig/pubkey placeholders — an N-of-N agreement, or a
function taking (sig a, pubkey A, sig b, pubkey B) — and nothing in a 3-tuple can
say which placeholder a key fills.

So entries gain an optional fourth element, `slot`, and an input's index is listed
once per placeholder. `slot` defaults to 0, so every existing 3-tuple keeps
meaning exactly what it meant and no current dapp needs a capability check.
Sending `slot > 0`, or repeating an inputIndex, requires the wallet to advertise
`multislot` — an older wallet keeps one key per input and would return a
transaction missing signatures with nothing to say why.

This supersedes the `multislot` branch, which was cut before the
randomTradeSummary revert and still carries the reverted `txSummary` field. The
protocol design there is good and is kept: the placeholder format (65-byte
Schnorr sig push, 33-byte compressed pubkey push, spliced value-for-value so
offsets survive), the capability gate, and the three wallet rules — don't
deduplicate by inputIndex, compute each input's sighash once, reject rather than
under-fill. Two things are changed.

SLOTS ARE POSITIONAL, NOT BY VACANCY

The earlier definition numbered slots by scanning the template for zero-filled
pushes. That renumbers them as they fill, and a template is not always all
zeroes: in the N-of-N case the docs give as motivation, the dapp may have already
written the counterparty's signature into the first position. Verified against
that implementation's own reference fill, on a two-slot input with slot 0
pre-filled:

  asked for slot 1 -> not filled at all (under-fill)
  asked for slot 0 -> writes into the SECOND slot, silently

Here a push already holding a value still occupies its slot, so `slot` means the
same thing to the dapp that built the template and the wallet filling it,
whatever order things happen in. Overwriting a filled slot is an error rather
than a no-op, because discarding a counterparty's signature is not recoverable.

THE SCAN PARSES PUSHES INSTEAD OF SEARCHING FOR BYTES

A hex-substring search for the placeholder pattern can match bytes that merely
sit inside a larger push, splicing a signature into the middle of unrelated data.
findPlaceholders walks the script's push structure (direct pushes and
OP_PUSHDATA1/2/4) and throws on a truncated template rather than guessing at one
it cannot parse.

WHY THE HELPERS ARE IN THE LIBRARY

The placeholder layout is part of the wire contract — the dapp builds the
template, the wallet splices into it, and they must agree byte for byte or the
transaction is silently unspendable. Leaving every wallet to implement the scan
is how that goes wrong, and both failure modes above come from a reasonable
implementation of a reasonable-sounding rule.

fillPlaceholder throws on a missing slot, a wrong-length value, or an already
filled slot; unfilledPlaceholders is what "reject, don't under-fill" checks. That
makes the rule enforceable rather than something to remember.

Deliberately NOT included: the sighash-validation module from the earlier branch.
It is a separate concern — the library does no transaction signing today, so
adding a validator for it is new surface that deserves its own review, and its
signature-detection heuristic needs work (a 65-byte push is treated as a
signature, which an uncompressed public key also is).

23 core tests, including both misplacement cases above, the inside-a-larger-push
false positive, OP_PUSHDATA1 headers, truncated templates, and fill-order
independence.

Docs: protocol.md, extensions.md, wallet.md, dapp.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:37:42 +02:00
Dagur Valberg Johannsson
b96cb8e151
Add chunk transport extension for oversized messages
Splits ProtocolMessages exceeding NIP-44's 65,535-byte plaintext ceiling
across multiple gift-wrapped events. Symmetric (both directions),
fire-and-forget, backward-compatible via a new transport-level
\`extensions\` field on \`dapp_ready\` and \`wallet_ready\`. Resolves the
\"Failed to swap: invalid plaintext size\" error on aggregated swap
requests and enables signed-tx responses up to the 1 MB BCH consensus
limit (~2 MB hex).
2026-04-21 14:58:12 +02:00
jakobsn
e852e70a8e Dedup connection: conn.dappDiscovered = false; 2026-04-03 12:35:50 +00:00
Dagur Valberg Johannsson
08ea3da6aa
Re-send signature request on ready
User may need some time to open their wallet to approve signature
request and miss it.

If we have a active request, re-send it if we see a
wallet_ready signal, suggesting the wallet was just opened.
2026-04-03 13:22:40 +02:00
Dagur Valberg Johannsson
4d1ba8e207
Add 'extensions' to hdwalletv1 protocol
This backward compatible change allows wallets/dapps to add additional
features outside the basic transaction signature support to the
hdwalletv1 protocol.
2026-03-26 10:50:40 +01:00
Dagur Valberg Johannsson
55ff82bbec
Document SIGHASH_ALL requirement 2026-03-18 10:31:06 +01:00
Dagur Valberg Johannsson
81a67825fe
Don't require input path for all inputs
Not all inputs need a signature; only require it for the input paths
that do.
2026-03-16 16:54:32 +01:00
Dagur Valberg Johannsson
8ef04218b4
Add input paths for a signature request
This solves an issue where wallet has to scan address ranges for each
derivation path and match it to locking script to figure out what
private key to use for signature.

This is a waste of effort and unnecessary complex for wallet
implementations since the dapp side already knows what inputs its using.
2026-03-16 15:27:18 +01:00
Dagur Valberg Johannsson
6fce9b47cb
First commit for WizardConnect 2026-03-06 11:38:09 +01:00