BRC-256 Payments

Cross-Chain Atomic Swaps Between Wallets

This document lets two BRC-100 wallets swap coins of two different chains, BSV and the chains of draft BRC-213, directly with each other. Nobody holds either side's coins at any point. Either both get paid, or each takes its own coins back after a lock time. Neither wallet has to be online when the other acts. Offer. A maker signs a standing price: it gives

Brandon Cryderman / HandCash (brandongcryderman@gmail.com) Status: Open

How it connects

Relationships

Authoritative content mirror

Full upstream text

View source ↗
Rendered from payments/0256.md at the indexed upstream commit

Abstract

This document lets two BRC-100 wallets swap coins of two different chains, BSV and the chains of draft BRC-213, directly with each other. Nobody holds either side's coins at any point. Either both get paid, or each takes its own coins back after a lock time. Neither wallet has to be online when the other acts.

  1. Offer. A maker signs a standing price: it gives one asset and wants another, in one take or in parts down to a smallest take, until an expiry. Anyone may host or relay the offer. Every wallet checks the signature, the bounds and the expiry itself.
  2. Take. A taker fixes terms for one amount: the swap id, the hash of a secret only it knows, both per-swap keys and two lock times. It derives the maker's per-swap key itself, so it does not wait for an answer.
  3. Lock. The taker locks what it pays at once, on the chain the maker wants, to a hash-locked output (HTLC) that stands for 72 hours. The maker's wallet checks the take whenever it next comes online, within about a day, and once the taker's lock is buried it locks what it gives, on the other chain, for 48 hours.
  4. Claim. The taker claims the maker's lock with the secret, which puts the secret on that chain. The maker reads it there and claims the taker's lock.
  5. Refund. A lock that is not claimed in time goes back to its payer. On the BRC-213 chains OP_CHECKLOCKTIMEVERIFY enforces this. BSV has no time-lock opcode, so a BSV lock's refund branch reads the spending transaction's own nLockTime from its signature preimage and proves that preimage genuine.

Messages between the two wallets are sealed with BRC-78 and travel through a BRC-33 message box. They are store-and-forward. Each wallet acts on what it finds when it next opens its box. A service fee, when a wallet charges one, is a third output on one leg, opened by the same secret. It can be claimed only if the swap happened, and otherwise returns to its payer.

Motivation

Moving value between chains today means trusting a party in the middle: an exchange, a bridge, or a wrapped token's issuer. Each holds the user's coins for a while, and each can fail, freeze or disappear with them.

Two UTXO chains need no middle party. A hash lock on each side, opened by one secret, makes the swap atomic. A time lock on each side makes it safe to walk away. What is missing is agreement on the details two different wallets need in order to swap with each other:

  • the exact scripts on every chain, including BSV, which has no time-lock opcode;
  • who locks first, for how long, and when each side must act, when neither side is online all the time;
  • the messages, their encryption, and where they travel;
  • how an offer is signed, so that a relay or index cannot alter it, and how it is filled in parts;
  • how a fee can be charged without anyone holding the coins.

This document fixes those details. It guarantees that:

  1. Neither wallet ever signs anything that lets the other take its coins without paying.
  2. Every path that locks coins ends in a claim or a refund. Every refusal before a lock ends with nothing moved.
  3. A wallet decides depth, amounts, times and secrets from bytes it checks itself, never from an explorer's answer.
  4. A host can delay or drop messages and offers, but it cannot alter, forge or steal anything.
  5. No step waits on the counterparty being online at the same time. Each wallet needs to come online once within each window of §5.

Relationship to other documents

  • Draft BRC-213 (native UTXO-chain coins, #337) supplies each non-BSV chain: its chain id, signature hash, address forms, burial depth D, and the burial check against the wallet's own header chain (§4). A swap leg on such a chain is buried exactly when BRC-213 would call a received coin spendable.
  • BRC-67 (SPV) decides when a BSV lock is buried. BRC-95 Atomic BEEF carries it to the counterparty.
  • BRC-42 / BRC-43 derive the offer key, the per-swap keys and the secret (§3).
  • BRC-33 and BRC-78 carry the settlement messages (§6).
  • BRC-29: a BSV claim or refund pays a BRC-29 child of the wallet's own identity.
  • Draft BRC-254 (market offers, #334) sells an asset for a price on BSV with a SIGHASH_SINGLE | ANYONECANPAY signature. That mechanism needs both sides on one chain. This document covers the case where the two sides are on different chains.
  • Draft BRC-214 (asset profiles, #338): a swap moves value in the bsv and coin:<chain> profiles, and each lock counts against the same payment scope as any other payment in that profile.

Terms

  • Asset: "bsv" or a BRC-213 chain id ("ltc", "bch", "btc", "doge"). Amounts are decimal strings of the asset's base units.
  • Maker: the wallet that signed the offer. It gives give and wants get.
  • Taker: the wallet that takes the offer. It pays get and receives give.
  • Leg: one side's lock. The taker leg is on get, paid by the taker. The maker leg is on give, paid by the maker.
  • Secret: 32 bytes only the taker knows until it claims. Hash: its SHA-256.
  • T1, T2: the lock times of the taker leg and the maker leg, as Unix seconds. T1 is later than T2.
  • Buried: a leg's lock is in a block that the wallet has checked: at depth D on its own header chain for a BRC-213 chain (BRC-213 §4), or with a merkle path that verifies under BRC-67 for BSV.
  • Block time: the timestamp in the header of the block that buries a lock, read from the same checked header.
  • Filled: the sum of pay over the takes of an offer that the maker has taken on and that did not end declined or expired.

Specification

1. Offers

An offer is a JSON object:

{
  "v": 1,
  "maker": "<maker identity key, compressed hex>",
  "give": "ltc",
  "get": "bsv",
  "price": { "give": "28000000", "get": "100000000" },
  "minGet": "3500000",
  "maxGet": "35000000",
  "createdAt": 1800000000000,
  "expiresAt": 1800086400000,
  "nonce": "<32 random bytes, hex>",
  "messagebox": "https://box.example/v1/messagebox",
  "sig": "<DER signature, hex>"
}
  • give and get are different assets.
  • A take pays an amount pay of get and receives floor(pay × price.give / price.get) of give. Every take has pay ≥ minGet, and all takes of one offer together pay at most maxGet.
  • An offer with minGet = maxGet is taken whole, once. An offer with minGet < maxGet fills in parts: it takes any number of takes while what is left, maxGet − filled, is at least minGet.
  • createdAt and expiresAt are Unix milliseconds. An offer stands for at most 24 hours.
  • messagebox is optional. It names the BRC-33 host where takers reach the maker. Without it, a taker uses the host it would use for the maker's identity.

A maker SHOULD set minGet so that the smallest take receives at least 10 times the cost of one claim on give. Otherwise a part could be eaten by its own claim.

Signature. The maker signs SHA-256 of the UTF-8 string "handcash swap offer v1\n" followed by the JSON array [v, maker, give, get, price.give, price.get, minGet, maxGet, createdAt, expiresAt, nonce, messagebox or ""], serialized with no whitespace. The signing key is the BRC-42 child of the maker's identity key under protocol [2, "swap order"], key ID nonce and counterparty anyone.

The protocol is reserved for the wallet. A wallet MUST refuse any application request to derive, sign or reveal a key under it, so that an application holding a signing grant cannot publish offers in the user's name.

Checking. A wallet MUST check an offer before it shows it or takes it, and MUST refuse the offer when any of these fails. "The time" is the current time when showing or taking, and takenAt when the maker checks a take (§4).

  1. Every field has its shape: compressed keys, 64-hex nonce, positive integer amounts, minGet ≤ maxGet, and an https URL for messagebox.
  2. The time is before expiresAt.
  3. createdAt is at most 5 minutes after the time.
  4. expiresAt − createdAt is at most 24 hours.
  5. The signature verifies against the maker's derived public key.

Cancel. A maker withdraws an offer by signing SHA-256 of "handcash swap offer cancel v1\n" + nonce with the same derived key. A host that lists offers MUST accept a cancel only with that signature. A cancel binds takes made after it. A take made before it, which the maker reads later, is still honoured (§4).

Fill report. A maker reports how much of an offer it has taken on by signing SHA-256 of "handcash swap offer filled v1\n" + nonce + "\n" + filled, with filled a decimal string, with the same derived key. A host MUST accept a report only with that signature and with filled ≤ maxGet, and MUST NOT lower a stored filled. A report is advice. The maker's own records decide every take (§4 rule 8), and a taker that acts on a stale report is only declined.

Hosting (informative). Any host can list offers. It verifies each one on publish, may cap how many open offers and publishes a maker can have, and removes offers that expire, are cancelled, or whose reported filled leaves less than minGet. It lists each offer with its last reported filled. A host is never trusted. A wallet checks every listed offer again itself.

2. Lock scripts

A lock pays whoever shows the secret and signs with claimPub. After locktime it returns to its payer. Every key is a compressed secp256k1 key. locktime is Unix seconds, at least 500000000 and at most 2^32 − 1, so that nLockTime reads it as a time.

BRC-213 chains:

OP_IF
  OP_SIZE <32> OP_EQUALVERIFY OP_SHA256 <hash> OP_EQUALVERIFY <claimPub>
OP_ELSE
  <locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP <refundPub>
OP_ENDIF
OP_CHECKSIG

BSV:

OP_IF
  OP_SIZE <32> OP_EQUALVERIFY OP_SHA256 <hash> OP_EQUALVERIFY <claimPub> OP_CHECKSIG
OP_ELSE
  <refund>
OP_ENDIF

where <refund> reads the BIP143 signature preimage of the spending input, which the refund pushes under its signature:

OP_DUP OP_SIZE OP_8 OP_SUB OP_SPLIT OP_4 OP_SPLIT OP_DROP <0x00> OP_CAT OP_BIN2NUM
<locktime> OP_GREATERTHANOREQUAL OP_VERIFY
OP_SIZE <32> OP_SUB OP_SPLIT OP_DROP OP_SIZE OP_4 OP_SUB OP_SPLIT OP_NIP
<0xffffffff> OP_EQUAL OP_NOT OP_VERIFY
OP_HASH256 <reverse 32> <0x00> OP_CAT OP_BIN2NUM
<Gx> OP_ADD <n> OP_MOD <32> OP_NUM2BIN <reverse 32>
<0x3044022079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980220> OP_SWAP OP_CAT
<0x41> OP_CAT <G> OP_CHECKSIGVERIFY
<refundPub> OP_CHECKSIG
  1. The first line takes the preimage's nLockTime (its last 8 bytes are nLockTime and the hash type) as a positive number. The second requires it to be at least locktime.
  2. The third and fourth lines take the input's nSequence (the 4 bytes before the last 40) and require it not to be 0xffffffff, so that nLockTime is enforced by consensus.
  3. The rest proves the preimage is the real one. It builds the signature with private key 1 and nonce 1 over that preimage, r = Gx and s = (hash256(preimage) + Gx) mod n, and checks it with OP_CHECKSIGVERIFY against the generator G. That signature verifies only if the interpreter's own preimage equals the pushed one.
  4. Last, the payer's signature with refundPub.

<reverse 32> is 31 times OP_1 OP_SPLIT followed by 31 times OP_SWAP OP_CAT. <Gx> and <n> are the secp256k1 generator's x coordinate and the group order as minimally encoded script numbers, and <G> is the compressed generator. Miners reject a transaction whose nLockTime is in the future, so no refund is mined before locktime.

OP_SIZE <32> OP_EQUALVERIFY makes the secret exactly 32 bytes on every chain, so a secret that opens one leg always opens the other. <locktime> is a minimally encoded script number.

Asset Output Signature hash
ltc, btc P2WSH of the script BIP143, SIGHASH_ALL
bch P2SH of the script BIP143, SIGHASH_ALL|FORKID
doge P2SH of the script Original signature hash, SIGHASH_ALL
bsv The script itself as the locking script BIP143 with FORKID (0x41)

Spending. A claim or refund spends one or more lock outputs of one transaction to one output of the spender's own.

  • A claim unlocks with <sig> <secret> OP_1 and has nLockTime 0.
  • A refund unlocks with <sig> OP_0 on BRC-213 chains and <sig> <preimage> OP_0 on BSV.
  • A BRC-213 refund has nLockTime equal to locktime. A BSV refund has the first nLockTime at or above locktime, and within 4096 of it, for which the preimage signature of step 3 of every input has an s that is at most n / 2 and at least 2^248, so that it is low-S and exactly 32 bytes. A wallet tries each value in turn.
  • On P2WSH the items are witness items followed by the script. On P2SH the script is pushed last in the scriptSig.
  • Every input has sequence 0xfffffffe. Transactions are version 2 on BRC-213 chains and version 1 on BSV.

Reading the secret. A wallet finds the secret in a claim by taking each 32-byte data item of the spending input (witness items, or scriptSig pushes) and keeping the one whose SHA-256 is the hash. It MUST NOT trust a secret it has not hashed.

3. Keys and the secret

  • Taker's per-swap key. The taker claims and refunds with the BRC-42 child of its identity key under protocol [2, "swap settle"], key ID the swap id and counterparty self.
  • Maker's per-swap key. The maker claims and refunds with the BRC-42 child of its identity key under the same protocol and key ID, with counterparty the taker's identity key. The taker computes its public key, makerPub, as the BRC-42 public child of the maker's identity key with counterparty the maker. Neither needs to ask the other.
  • Secret. The taker's secret is SHA-256 of the 32-byte private key of the BRC-42 child under protocol [2, "swap secret"], key ID the swap id and counterparty self. A wallet restored from its root in the middle of a swap still has every key and the secret.

Both protocols are reserved for the wallet like [2, "swap order"] (§1).

4. Terms and the take

The taker fixes the terms:

{
  "v": 2,
  "swapId": "<16 random bytes, hex>",
  "offer": { "...": "the signed offer, unchanged" },
  "taker": "<taker identity key>",
  "pay": "3500000",
  "receive": "980000",
  "fee": { "leg": "taker", "amount": "17500" },
  "hash": "<SHA-256 of the secret, hex>",
  "takerPub": "<taker per-swap key>",
  "makerPub": "<maker per-swap key, derived by the taker (§3)>",
  "takenAt": 1800000100000,
  "t1": 1800259300,
  "t2": 1800172900
}
  • receive is floor(pay × price.give / price.get), before any fee on the maker leg.
  • fee is the service fee of §8, or null.
  • t1 = floor(takenAt / 1000) + 259200 (72 hours) and t2 = floor(takenAt / 1000) + 172800 (48 hours).

A taker's wallet MUST ask the user to confirm explicitly before it sends a take. The prompt shows what the taker locks, what it receives, the fee, the lock times, and that the maker may take up to the deadline of §5 to lock. For an offer that fills in parts the user chooses pay between minGet and what is left. Once the take is sent the taker locks its leg (§5). It does not wait for an answer.

The maker's check. The maker's wallet checks a take when it reads it, which may be hours after takenAt. Before it locks anything it MUST refuse the take, answering decline with the reason, when any of these fails:

  1. The terms have their shape (malformed).
  2. The offer checks under §1 at takenAt and is the maker's own (offer). The taker is not the maker (own-offer).
  3. pay is within the offer's bounds (out-of-bounds), and receive is exactly what the price gives (wrong-receive).
  4. fee is exactly what §8 gives, or both are null (wrong-fee).
  5. takenAt is at most 5 minutes after the maker's clock, and the maker's clock is before the maker leg's funding deadline of §5 (stale). t1 and t2 are exactly as above (wrong-times).
  6. makerPub is the maker's own per-swap key for this swap and taker (wrong-key).
  7. What the taker receives exceeds the cost of one claim on give (nothing-left).
  8. The sender of the message is taker (sender).
  9. The offer is in the maker's own records and was not cancelled before takenAt (offer-unknown, offer-cancelled), pay still fits what is left of it, maxGet − filled ≥ pay (offer-filled), and the maker can fund its leg (insufficient). A take repeating a swap id the maker already holds is ignored.

A maker reads all waiting takes before any other message and judges them in takenAt order, so that the earliest takes of a part-filled offer are served first, whatever order the host returns.

A wallet SHOULD answer takes of its own open offers without asking the user, because publishing the offer was the consent. A take it accepts adds pay to filled. Once what is left is below minGet, the wallet withdraws the offer from any host. Otherwise it SHOULD send the host a fill report (§1).

Accept. The maker answers accept with makerPub. It is a courtesy. Both wallets hold the same agreed terms, the terms themselves, from the moment the take is sent.

Taking on a take means locking. A maker that takes on a take MUST lock its leg as §5 allows, or decline it. A decline after the taker has locked costs the taker only time. It refunds at t1.

5. Legs

Leg Asset Payer Payee Main output Lock time
Taker offer.get Taker Maker pay t1
Maker offer.give Maker Taker receive, less the fee if it is on this leg t2

Each leg's main output is the lock of §2 with claimPub the payee's per-swap key and refundPub the payer's. A leg that carries the fee has a second lock output with the same hash, refundPub and lock time, and the fee key as claimPub (§8). Both outputs are in the same transaction.

Order and deadlines. All times are measured on the wallet's clock against the agreed terms.

Step Who Allowed when
Fund the taker leg Taker Right after sending the take, within 1 hour of takenAt
Fund the maker leg Maker After the taker leg is buried and checked, and before t2 − 20 h (28 hours after the take)
Claim the maker leg Taker After the maker leg is buried and checked, and before t2 − 3 h
Claim the taker leg Maker Once it knows the secret, until the taker refunds
Refund own leg Payer At or after its lock time, while it is unclaimed

A wallet checks a counterparty's lock before acting on it. The lock transaction hashes to its txid, the named outputs pay exactly the agreed scripts and amounts, and the lock is buried. The maker also refuses a taker lock whose block time is more than 3 hours after takenAt (the 1 hour to fund plus 2 hours for block timestamps). Without that check a taker could send a take with an old takenAt against a cheap old offer. The same 3 hours bound how long a maker waits to learn of the taker's lock. It ends a take without one only after takenAt + 3 h, never at the taker's own 1-hour cutoff. A lock signed in the taker's last minute and announced (or found on chain) late is therefore still honoured, rather than left to wait out t1. Each chain judges refund lock times against its median block time, which trails the clock (about an hour on Bitcoin). A refund refused as non-final is retried until it is accepted. A lock that fails any check is refused, and the wallet does not lock or claim against it. When its own deadline for the next step passes, it waits for its lock time and refunds.

The taker never claims with less than 3 hours left before t2, so its claim lands before the maker may refund. It judges that deadline by two clocks: its own, and the timestamp of the latest block on the chain it claims on (read from the header chain its proofs are checked against). It claims only while both are before t2 − 3 h. With no chain time to read, it waits. A slow device clock therefore cannot send a claim that the maker's refund could race, and a header source running behind cannot stretch the deadline past the device's clock. The maker never funds later than 20 hours before t2, so the taker has 17 hours to come online, see the lock buried and claim. A taker that reaches t2 − 3 h unpaid waits for t1 and refunds. The taker leg's longer lock gives the maker at least 24 hours after t2 to read the secret and claim.

Each wallet must therefore come online once in each window: the maker within 28 hours of the take (to lock) and again within 24 hours of t2 (to claim), and the taker once between the maker's lock and t2 − 3 h (to claim). A wallet SHOULD run its swap service whenever it is open and SHOULD tell its user how long it may stay closed.

Journal. A wallet SHOULD record each lock it funds, and each counterparty lock it may claim, in durable storage before it broadcasts, with enough to claim or refund from its root alone: the agreed terms and the outputs. It SHOULD resume every unfinished swap from that record after a restart or restore.

6. Messages

Messages are JSON objects, sealed with BRC-78 from the sender's identity to the recipient's, and posted to the recipient's BRC-33 box swap-settle. A wallet acknowledges a message only after it has recorded what the message changed.

t From Fields Meaning
take Taker terms The terms (§4)
accept Maker swapId, makerPub The maker took on the take
decline Maker swapId, reason The maker will not lock
funded Payer swapId, leg, txid, main, fee?, beef? A leg is locked: output indexes of the main and fee outputs, and on BSV the lock's Atomic BEEF
secret Taker swapId, secret Courtesy after the taker claims: the maker also reads the secret from the chain
abort Either swapId, reason Nothing was locked and nothing will be

leg is "taker" or "maker". A message whose swapId names no swap the wallet holds, or whose content contradicts the swap's state, is ignored.

Messages are a convenience. Settlement never depends on one arriving. A wallet that misses funded finds a coin-chain lock on the chain at the address its terms fix (searched at most once a minute while it waits, then proven like any lock). A BSV lock is provable only from the payer's BEEF, so a missed BSV funded is resent until it arrives and the swap otherwise ends in refunds. A maker that misses secret reads the secret from the taker's claim. A taker that misses decline refunds at t1 all the same.

7. Outcomes

A swap ends in one of five outcomes:

Outcome Meaning
swapped Both payees claimed
refunded Each payer took its lock back
declined The maker refused before this wallet locked anything
expired A deadline passed before this wallet locked anything
missed The maker was away past t1 and the taker claimed one leg and refunded the other

A taker that locked and was then declined ends refunded. A take that ends declined or expired gives its pay back to the offer's room.

missed is the only outcome that costs a party. It costs the maker, who failed to claim within the 24 hours or more it had. A maker wallet SHOULD warn the user who closes it with a swap in progress, and SHOULD hand its claim to watchtowers (below).

Claim watchtower. A claim's signature does not cover the 32-byte secret, which sits in the unlocking data. Once its own lock is broadcast and the taker's is buried, a maker MAY therefore sign its claim of the taker leg in advance with a placeholder secret, zero the placeholder, and hand the result to watchtowers as a ticket:

{ "v": 1,
  "watch": { "chain": "bsv", "txid": "…", "vout": 0, "script": "<maker lock HTLC script>" },
  "hash": "<sha256 of the secret>",
  "claim": { "chain": "ltc", "hex": "<signed claim, secret zeroed>", "secretAt": 158, "script": "<taker lock HTLC script>" },
  "until": 1800259200 }

A tower watches the maker's lock. When the taker's claim of it shows the secret, the tower writes the secret at secretAt, checks that it hashes to hash, and broadcasts the claim, rebroadcasting until the taker's lock is spent or a while after until. A spend of the maker's lock without the secret means the maker took it back, and the ticket is void. A tower holds no key and decides nothing. The claim pays only the maker, and the worst a tower can do is not broadcast. The maker's wallet keeps the same template, commits the same transaction whenever it runs, and records the template where its recovery data lives, so a restore can take in a claim a tower made. A maker SHOULD use the host of its own message box and at least one more host, never the counterparty's.

8. Service fee (optional)

A wallet may charge a fee on swaps it takes part in, paid only when the swap happens:

  1. Leg. The fee is on the taker leg when offer.get is "bsv", and on the maker leg otherwise. The taker pays it either way: on the taker leg it locks pay + fee, and on the maker leg it receives receive − fee.
  2. Amount. floor(a × 50 / 10000) (0.5%), where a is pay on the taker leg or receive on the maker leg. There is no fee when that is less than 10 times the cost of one claim on the fee leg's chain.
  3. Output. A second lock output on the fee leg (§5), claimable by the fee key with the same secret. If the swap happens, the secret is public and the fee key's holder can claim it. If it does not, the output returns to its payer with the leg's refund, by the same refund branch.
  4. Agreement. The terms carry fee, and the maker's check (§4 rule 4) refuses any other amount, so neither side can change it. Both wallets MUST agree on the fee key before the take. This version has no field for it (Open questions).

No one holds the fee before the swap happens. A fee holder that never claims leaves the output to return to its payer at the lock time, and a wallet SHOULD then refund it.

Fee claimer (informative). A fee holder finds fee outputs to claim by having either wallet report the fee lock: terms version, chain, txid, output indexes, amount, hash, payer and payee keys, and lock time. The report is only a pointer. The claimer rebuilds both scripts, checks the lock on the chain, waits for the sibling main output to be claimed, reads the secret from that claim, and claims the fee. A main output that was refunded means there was no swap, and the claimer leaves the fee output alone.

9. Application access

The swap methods are the wallet's own. An application cannot take, accept, publish, report or cancel an offer, or derive any key under the three reserved protocols. A lock counts against the user's payment scope in its asset's profile (draft BRC-214) like any other payment from that wallet.

10. Version 1 terms

Terms with "v": 1 were the first version. A wallet SHOULD settle version 1 swaps it already holds and MUST NOT propose new ones. They differ as follows:

  • No makerPub in the terms. Both per-swap keys use counterparty self, and the maker sends its key in accept. The taker funds only after accept.
  • t1 = takenAt + 36 h, t2 = takenAt + 12 h. The maker funds before t2 − 8 h; the taker claims before t2 − 3 h. The maker checks takenAt within 5 minutes of its clock either way, and the offer at its own clock.
  • An offer is taken once: the maker refuses any take while its record of the offer is not open.
  • The BSV refund branch is <refundPub> OP_CHECKSIGVERIFY <cosignPub> OP_CHECKSIG, with cosignPub the payee's per-swap key, unlocked by <cosig> <sig> OP_0 with nLockTime equal to locktime. Before broadcasting a BSV lock, its payer builds one refund per lock output (one input, one output to a BRC-29 child of its own identity worth the output less 40 satoshis, nLockTime the lock time, sequence 0xfffffffe) and sends presign (swapId, leg, txid, refunds: [{vout, to, value}]). The payee checks each refund spends an output of the agreed lock, pays at most what it holds and carries the leg's lock time, and answers presigned (swapId, leg, cosigs, DER plus hash type, in refunds order). The payer verifies every co-signature and records the refunds before it broadcasts.

Security considerations

  • Atomicity. One secret opens both legs. The taker reveals it only by claiming the maker leg, before t2. The maker then has until t1, at least 24 hours later, to claim the taker leg. Neither side can claim without paying unless the other stays away past its own deadline.
  • Order of locks. The taker locks first, for longer. A maker that never locks, or declines, costs the taker only its time, and the taker refunds at t1. A maker locks only once the taker's lock is buried and matches the terms.
  • No co-signer. Every refund path, on every chain, needs only the payer's key and the passing of its lock time. Neither side depends on the other answering, and an absent counterparty can never strand a lock.
  • BSV refunds. The refund branch accepts a preimage only if a signature built from it verifies under OP_CHECKSIGVERIFY, which hashes the interpreter's own preimage. A forged preimage with a different nLockTime fails that check. The branch also refuses a final nSequence, so nLockTime is enforced, and miners do not accept a transaction whose nLockTime is in the future.
  • Taking while the maker is away. The maker judges a take as of its signed takenAt, so an offer that expired after the take was made is still honoured. A backdated takenAt gains nothing. The maker refuses a taker lock buried more than 3 hours after takenAt (§5), and a take whose funding deadline has passed (§4 rule 5). The offer's own 24 hours bound how old a price can be taken.
  • Partial fills. The maker's own records decide what is left of an offer. Takes are judged in takenAt order, and a take that no longer fits is declined before the maker locks, so an offer is never over-filled. A host's fill report is advice that only hides used-up offers.
  • Burial. A wallet never locks or claims against an unburied counterparty lock. On BRC-213 chains depth is counted on the wallet's own header chain, so a lying source can delay a swap but never make a shallow lock pass.
  • Clock and fee races. A claim near a deadline can lose a race with a refund. The margins of §5 (20 hours to see and claim after the maker's last funding time, 3 hours to claim, 24 hours between t2 and t1) cover several blocks on every chain here, including congestion and fee bumps. A wallet SHOULD pay at least the current fee rate on every claim.
  • Watchtowers. A ticket shows the swap's outpoints and hash, which the chain shows anyway once the taker claims. A tower cannot redirect the claim (its signature covers the output) or reuse the template (it opens only with the secret, and spends only the taker's lock). A tower that never broadcasts leaves the maker where it was without one.
  • Message hosts. A host can delay, drop or replay messages. Every message is sealed and checked against the swap's state, and a missing message never decides an outcome.
  • Offer abuse. Anyone can take an offer, and a take that the taker never funds costs the maker nothing, because the maker locks only after the taker's lock is buried. A take that ties up part of an offer without funding releases it when it expires.
  • Privacy. Both legs carry the same hash, which links them for anyone watching both chains. This version accepts that link.

Rationale

  • Hash and time locks. They are the only construction that is atomic on chains that cannot read each other. Both sides use scripts every chain here already accepts.
  • A self-proving BSV refund. BSV restored the original opcodes and has no OP_CHECKLOCKTIMEVERIFY. Pushing the signature preimage and proving it with a fixed-key signature reads nLockTime in script, with no new opcode and no counterparty. Version 1 used co-signed refunds instead, which forced a round trip before every BSV lock.
  • 72 and 48 hours. The maker has a day to come online and lock, and the taker most of a day after that to claim, so neither needs to be online at the same time as the other. That is also long enough for the deepest burial here (20 Dogecoin blocks, or 6 at Litecoin's pace) plus broadcast.
  • The taker derives the maker's key. BRC-42 lets each side compute the other's per-swap key from identity keys alone, so the terms are fixed by the taker and need no accept.
  • Fee as an output. A fee taken by a party in the middle would make that party custodial. An output opened by the swap's own secret makes the fee exactly as conditional as the swap.
  • Domain strings. Each signed message (offer, cancel, fill report) has its own tag, so that no signature under the offer key is valid as another.

Open questions

  1. Fee key in the terms. fee should name its claim key (fee.pub) so that wallets of different makers can charge their own fee. The current implementation fixes it.
  2. Linking. Adaptor signatures could replace the hash lock and remove the on-chain link between the legs, where both chains support them.

Implementations

HandCash Desktop and Mobile 1.3.609 (@handcash/wallet-ui):

  • src/wallet/swap/offer.ts: offers, cancel, fill reports and the room left. offerBook.ts: publish, browse with fills, and report fills.
  • htlc.ts: §2 scripts and spends, including the BSV refund's nLockTime search. settle.ts: terms, the take check, partial-fill room, legs, messages and the next-step decision. legChains.ts: each chain's lock, proof, block time and spend. settleRunner.ts: the service. Charts swapMachine (publish whole or in parts), swapOffersMachine (take all or part), swapSettleMachine.
  • The fee is on as in §8, with the fee key fixed to HandCash's, and fee carries no pub.
  • BRC-CLOUD hosts an offer book (/v1/market/swap-offers, with POST /<nonce>/filled), the fee claimer (/v1/swap-fees) and a claim watchtower (POST /v1/swap-tower). claimTower.ts builds, fills and judges tickets. The maker registers at its own message box host and BRC-CLOUD (1.3.613).

References

  • BRC-29: Simple Authenticated BSV P2PKH Payment Protocol
  • BRC-33: PeerServ Message Relay Interface
  • BRC-42: BSV Key Derivation Scheme
  • BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties
  • BRC-67: Simplified Payment Verification
  • BRC-78: Serialization Format for Portable Encrypted Messages
  • BRC-95: Atomic BEEF Transactions
  • BRC-100: Wallet-to-Application Interface
  • BRC-213 (draft, #337): Native UTXO-Chain Coins in a BRC-100 Wallet
  • BRC-214 (draft, #338): Wallet Asset Profiles
  • BRC-254 (draft, #334): Market Offers and Direct Swaps for Wallet Assets
  • BIP143: Transaction Signature Verification for Version 0 Witness Program
  • BIP65: OP_CHECKLOCKTIMEVERIFY