Skip to content

Commit

Permalink
docs: add claim covenant and magic routing hints (#536)
Browse files Browse the repository at this point in the history
* docs: add claim covenant and magic routing hints

* feat: add emojis to summary

* docs: clarify submarine/reverse swap in apiv2

* docs: claim covenants review

* docs: magic routing hints review

* docs: minor wording fixes

---------

Co-authored-by: Kilian <kilian.rausch@gmail.com>
  • Loading branch information
michael1011 and kilrau authored May 1, 2024
1 parent cc21c00 commit bc82896
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 5 deletions.
2 changes: 2 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* [🤖 REST API v1 (deprecated)](api-v1.md)
* [🔁 Swap Types & States](lifecycle.md)
* [🙋‍♂️ Claim & Refund Transactions](claiming-swaps.md)
* [🪄 Magic Routing Hints](magic-routing-hints.md)
* [📜 Claim covenants](claim-covenants.md)
* [🚫 Don't trust. Verify!](dont-trust-verify.md)
* [⏩ 0-conf](0-conf.md)
* [🚢 Backend Deployment](deployment.md)
Expand Down
8 changes: 4 additions & 4 deletions docs/api-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ After the initial subscription confirmation message and whenever a swap status i

Below are some examples covering the flow of a given swap type from beginning to end, using API v2 and its WebSocket.

## Submarine Swap
## Submarine Swap (Chain -> Lightning)

Swap from the Bitcoin mainchain to Lightning.
Example: Swap from the Bitcoin mainchain to Lightning.

{% tabs %}
{% tab title="Typescript" %}
Expand Down Expand Up @@ -360,9 +360,9 @@ func main() {
{% endtab %}
{% endtabs %}

## Reverse Swap
## Reverse Swap (Lightning -> Chain)

Swap from Lightning to the Bitcoin mainchain.
Example: Swap from Lightning to the Bitcoin mainchain.

{% tabs %}
{% tab title="Typescript" %}
Expand Down
130 changes: 130 additions & 0 deletions docs/claim-covenants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
description: >-
To reduce the interactivity requirements of Reverse Swaps, one can make use of
covenants. For instance this allows a mobile wallet provider to claim Reverse
Swaps for its users while they are offline.
---

# 📜 Claim covenants

Covenants are available on [Liquid](https://liquid.net/) in the form of [direction introspection](https://github.com/ElementsProject/elements/blob/master/doc/tapscript\_opcodes.md#new-opcodes-for-additional-functionality). These opcodes allow the script in the witness to inspect the inputs, outputs and other properties of the transaction it is executed in.

## Boltz API

Boltz API clients can ask for a covenant to be included in the Taptree of a Reverse Swap. With that new leaf in the tree, the coins locked for the Reverse Swap can be claimed by revealing the preimage and sending the expected asset with expected amount to an address of the client in the 0th output of the transaction. This is what the script looks like:

```
OP_SIZE
32
OP_EQUALVERIFY
OP_HASH160
<RIPEMD-160 hash of the SHA-256 hash of the preimage>
OP_EQUALVERIFY
0
OP_INSPECTOUTPUTSCRIPTPUBKEY
<version of the output script of the address of the client>
OP_EQUALVERIFY
<output script of the output script of the address of the client>
OP_EQUALVERIFY
0
OP_INSPECTOUTPUTASSET
OP_1
OP_EQUALVERIFY
<asset hash of Liquid Bitcoin>
OP_EQUALVERIFY
0
OP_INSPECTOUTPUTVALUE
OP_DROP
<amount the user is expecting to receive>
OP_EQUAL
```

## Use Cases & Advantages

No signature is needed to sweep the locked funds to the address of the client, which removes the requirement for clients like mobile wallets to be online to sign a claim transaction. Instead, this allows a third party (e.g. the wallet service provider) to claim the Reverse Swap, as the Swap can only be claimed to the user's address. Lightning HTLCs of the Reverse Swap are always resolved in a timely manner, which is the expected behavior for the routing nodes on the route of the Lightning payment and reduces capital requirements in comparison to solutions like [Zaplocker](https://github.com/supertestnet/zaplocker).

## Trust assumptions

When handing over the preimage of a Reverse Swap to a third party like a mobile wallet provider, you have to rely on this party not to collude with the Lightning node that accepts HTLCs for the hold invoice. If that happens, the covenant script path spend would not be executed, but the Lightning HTLCs resolved and eventually, the coins locked on Liquid will be refunded.

To avoid this from happening, the client should have access to multiple servers that enforce covenant claims for them.

## Example code

This example registers a covenant to be claimed with the reference implementation [covclaim](https://github.com/BoltzExchange/covclaim/) running locally at port 1234:

```typescript
import axios from 'axios';
import { crypto } from 'bitcoinjs-lib';
import { SwapTreeSerializer, Types } from 'boltz-core';
import { randomBytes } from 'crypto';
import { ECPairFactory } from 'ecpair';
import * as ecc from 'tiny-secp256k1';

const ECPair = ECPairFactory(ecc);

type CovenantParams = {
claimPublicKey: Buffer;
refundPublicKey: Buffer;
preimage: Buffer;
blindingKey: Buffer;
address: string;
tree: Types.SwapTree;
};

const endpoint = 'https://api.testnet.boltz.exchange';
const covenantEndpoint = 'http://127.0.0.1:1234';

const createSwap = async (): Promise<CovenantParams> => {
const preimage = randomBytes(32);
const claimKeys = ECPair.makeRandom();

const address =
'tlq1qq090wdhdz8s2pydq3g5whw248exqydax2w05sv3fat7dsdz4088rg9yzha2lh8rcr2wq4ek244ug77al8ps27shp59e588azj';

const swapRes = (
await axios.post(`${endpoint}/v2/swap/reverse`, {
address,
from: 'BTC',
to: 'L-BTC',
claimCovenant: true,
invoiceAmount: 10_000,
preimageHash: crypto.sha256(preimage).toString('hex'),
claimPublicKey: claimKeys.publicKey.toString('hex'),
})
).data;

// Verification checks skipped

return {
address,
preimage,
claimPublicKey: claimKeys.publicKey,
blindingKey: Buffer.from(swapRes.blindingKey, 'hex'),
refundPublicKey: Buffer.from(swapRes.refundPublicKey, 'hex'),
tree: SwapTreeSerializer.deserializeSwapTree(swapRes.swapTree),
};
};

const registerCovenant = async (params: CovenantParams) =>
await axios.post(`${covenantEndpoint}/covenant`, {
address: params.address,
preimage: params.preimage.toString('hex'),
tree: SwapTreeSerializer.serializeSwapTree(params.tree),
blindingKey: params.blindingKey.toString('hex'),
claimPublicKey: params.claimPublicKey.toString('hex'),
refundPublicKey: params.refundPublicKey.toString('hex'),
});

(async () => {
try {
const swap = await createSwap();
await registerCovenant(swap);
} catch (e) {
console.error(e);
}
})();
```
151 changes: 151 additions & 0 deletions docs/magic-routing-hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
description: >-
This page describes the mechanism that encodes a BIP21 into the routing hints
section of an invoice, mainly for Boltz-powered wallets like Aqua to pay each
other directly without a Boltz swap.
---

# 🪄 Magic Routing Hints

We do not allow paying invoices for Reverse Swaps with Submarine Swaps as that can constitute a "self-payment", which some Lightning implementations still not allow efficiently. That causes an UX problem for wallets that show users Lightning invoices from Reverse Swaps and allow them to pay invoices with Submarine Swaps. To work around that problem and minimize the number of transactions that need to be done, the Swaps can be skipped entirely and the sender can pay the receiver directly.

The only data that the receiver and sender share is the Lightning invoice. Encoding arbitrary data in an invoice is impractical, therefore we only add a hint that for this invoice one can fetch a chain address of the recipient. This hint is encoded in the routing hints section of the swap's Lightning invoice with a specific channel id. There is no actual channel with this id, just that this channel as routing hint signals to supporting wallets that the magic routing hint feature is enabled by the recipient. The node public key in the routing hint is the same public key with which the receiver would need to sign to enforce the claim of the Reverse Swap.

When creating a Reverse Swap with a magic routing hint, there are a couple extra steps to be done for the receiver:

* pass a chain address of the wallet for the chain on which the Reverse Swap would be claimed in the API call to create swap
* pass a signature of the SHA-256 hash of that address signed by the private key the Reverse Swap would be claimed with
* when the API call returns, check the invoice for the magic routing hint with the specific channel id and the claim public key in the node public key field

When a wallet tries to pay a Lightning invoice via a Submarine Swap, there are two options. Check for the existence of a magic routing hint, if it does not exist, proceed with the Submarine Swap. When one is found:

* parse the public key in the routing hint
* send a [request to our API](https://api.boltz.exchange/swagger#/Reverse/get\_swap\_reverse\_\_invoice\_\_bip21) to fetch the chain address of the recipient
* extract the address from the BIP-21
* hash the address and verify the signature that is also returned in the API call against the public key in the magic routing hint
* either use that address directly or verify the amount and, in the case of Liquid additionally the asset id, of the BIP-21 and use that to pay

## Example code

```typescript
import axios from 'axios';
import { crypto } from 'bitcoinjs-lib';
import bolt11 from 'bolt11';
import { randomBytes } from 'crypto';
import { ECPairFactory } from 'ecpair';
import * as ecc from 'tiny-secp256k1';

const ECPair = ECPairFactory(ecc);

const endpoint = 'https://api.testnet.boltz.exchange';

const magicRoutingHintConstant = '0846c900051c0000';
const lbtcAssetHash =
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49';

const findMagicRoutingHint = (invoice: string) => {
const decodedInvoice = bolt11.decode(invoice);
const routingInfo = decodedInvoice.tags.find(
(tag) => tag.tagName === 'routing_info',
);
if (routingInfo === undefined) {
return { decodedInvoice };
}

const magicRoutingHint = (routingInfo.data as any[]).find(
(hint) => hint.short_channel_id === magicRoutingHintConstant,
);
if (magicRoutingHint === undefined) {
return { decodedInvoice };
}

return { magicRoutingHint, decodedInvoice };
};

const receiverSide = async () => {
const preimage = randomBytes(32);
const claimKeys = ECPair.makeRandom();

const address =
'tlq1qq090wdhdz8s2pydq3g5whw248exqydax2w05sv3fat7dsdz4088rg9yzha2lh8rcr2wq4ek244ug77al8ps27shp59e588azj';

const addressHash = crypto.sha256(Buffer.from(address, 'utf-8'));
const addressSignature = claimKeys.signSchnorr(addressHash);

const swapRes = (
await axios.post(`${endpoint}/v2/swap/reverse`, {
address,
from: 'BTC',
to: 'L-BTC',
invoiceAmount: 10_000,
addressSignature: addressSignature.toString('hex'),
claimPublicKey: claimKeys.publicKey.toString('hex'),
preimageHash: crypto.sha256(preimage).toString('hex'),
})
).data;

// Other verification checks skipped

const { magicRoutingHint } = findMagicRoutingHint(swapRes.invoice);
if (magicRoutingHint === undefined) {
throw 'no magic routing hint';
}

if (magicRoutingHint.pubkey !== claimKeys.publicKey.toString('hex')) {
throw 'invalid public key in magic routing hint';
}

return swapRes.invoice;
};

const senderSide = async (invoice: string) => {
const { magicRoutingHint, decodedInvoice } = findMagicRoutingHint(invoice);
if (magicRoutingHint === undefined) {
// Pay via Swap
console.log('no magic routing hint found');
return;
}

const bip21Res = (
await axios.get(`${endpoint}/v2/swap/reverse/${invoice}/bip21`)
).data;

const receiverPublicKey = ECPair.fromPublicKey(
Buffer.from(magicRoutingHint.pubkey, 'hex'),
);
const receiverSignature = Buffer.from(bip21Res.signature, 'hex');

const bip21Decoded = new URL(bip21Res.bip21);
const bip21Address = bip21Decoded.pathname;

const addressHash = crypto.sha256(Buffer.from(bip21Address, 'utf-8'));

if (!receiverPublicKey.verifySchnorr(addressHash, receiverSignature)) {
throw 'invalid address signature';
}

if (bip21Decoded.searchParams.get('assetid') !== lbtcAssetHash) {
throw 'invalid BIP-21 asset';
}

// Amount in the BIP-21 is the amount the recipient will actually receive
// The invoice amount includes service and swap onchain fees
if (
Number(bip21Decoded.searchParams.get('amount')) * 10 ** 8 >
Number(decodedInvoice.satoshis)
) {
throw 'invalid BIP-21 amount';
}

// Pay on Liquid
};

(async () => {
try {
const invoice = await receiverSide();
await senderSide(invoice);
} catch (e) {
console.error(e);
}
})();
```
2 changes: 1 addition & 1 deletion swagger-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "Boltz API",
"version": "3.5.0"
"version": "3.6.0"
},
"paths": {
"/chain/fees": {
Expand Down

0 comments on commit bc82896

Please sign in to comment.