Beersy
BRC-256Open

Cross-Chain Atomic Swaps Between Wallets

Moving coins between two different blockchains usually means trusting an exchange or bridge to hold your funds while the swap happens, and that middleman can freeze, lose, or steal them. This describes a way for two wallets to trade coins across chains directly, so that either the trade completes for both sides or each party simply gets its own coins back, with nobody ever holding the other's funds and neither side needing to be online at the same moment.

About Payment handshakes, the back and forth that completes a payment.

AlsoLockingMessaging

Brandon Cryderman / HandCashchanged 26 Oct 202625 min read

Reference for an AI

Everything an assistant needs to answer questions about BRC-256 accurately, including what it depends on.

Summary

Why
Cross-chain trades normally require a trusted middleman who holds both sides' coins and can fail, freeze, or disappear with them.
What
BRC-256 defines how two BRC-100 wallets perform an atomic swap of BSV for coins on another chain using hash- and time-locked outputs, with no custodian and no requirement to be online at the same time.
How
A maker signs and posts an offer, a taker derives the maker's key and its own secret via /BRC-43, both sides lock funds in hash- and time-locked outputs on their respective chains, and BRC-78-sealed messages over a message box let each wallet act whenever it next comes online.

What this lets you do

  • Post a standing offer to swap one chain's coins for another's, fillable in parts until it expires
  • Take an offer by fixing a swap id, secret hash, per-swap keys and lock times without waiting for a reply
  • Claim the other side's lock once the secret is revealed on-chain
  • Charge an optional service fee that only pays out if the swap actually completes

Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.

maker wallettaker walletofferlockclaim/refund

The specification

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 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 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 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 () decides when a BSV lock is buried. BRC-95 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 , #338): a swap moves value in the bsv and coin:<chain> profiles, and each lock counts against the same payment 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 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 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 under protocol [2, "swap order"], 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 , 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 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 , 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.

AssetOutputSignature hash
ltc, btcP2WSH of the scriptBIP143, SIGHASH_ALL
bchP2SH of the scriptBIP143, SIGHASH_ALL|FORKID
dogeP2SH of the scriptOriginal signature hash, SIGHASH_ALL
bsvThe script itself as the 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

LegAssetPayerPayeeMain outputLock time
Takeroffer.getTakerMakerpayt1
Makeroffer.giveMakerTakerreceive, less the fee if it is on this legt2

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.

StepWhoAllowed when
Fund the taker legTakerRight after sending the take, within 1 hour of takenAt
Fund the maker legMakerAfter the taker leg is buried and checked, and before t2 − 20 h (28 hours after the take)
Claim the maker legTakerAfter the maker leg is buried and checked, and before t2 − 3 h
Claim the taker legMakerOnce it knows the secret, until the taker refunds
Refund own legPayerAt 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.

tFromFieldsMeaning
takeTakertermsThe terms (§4)
acceptMakerswapId, makerPubThe maker took on the take
declineMakerswapId, reasonThe maker will not lock
fundedPayerswapId, 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
secretTakerswapId, secretCourtesy after the taker claims: the maker also reads the secret from the chain
abortEitherswapId, reasonNothing 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:

OutcomeMeaning
swappedBoth payees claimed
refundedEach payer took its lock back
declinedThe maker refused before this wallet locked anything
expiredA deadline passed before this wallet locked anything
missedThe 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 , 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 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 Payment Protocol
  • BRC-33: PeerServ Message Relay Interface
  • BRC-42:
  • BRC-43: , , Key IDs and Counterparties
  • BRC-67: Simplified Payment Verification
  • BRC-78: Serialization Format for Portable Encrypted Messages
  • BRC-95: Atomic BEEF Transactions
  • BRC-100:
  • 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
Was this helpful?

Search Beersy

Search standards by number, title, author or topic