Skip to content

Commit

Permalink
feat: Liquid claim covenant
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Feb 18, 2024
1 parent e4db482 commit 3322160
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 41 deletions.
2 changes: 1 addition & 1 deletion lib/api/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Errors from './Errors';

type ApiArgument = {
name: string;
type: string;
type: 'string' | 'number' | 'boolean' | 'object';
hex?: boolean;
optional?: boolean;
};
Expand Down
8 changes: 7 additions & 1 deletion lib/api/v2/routers/SwapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,8 +722,11 @@ class SwapRouter extends RouterBase {
* addressSignature:
* type: string
* description: Signature of the claim public key of the SHA256 hash of the address for the direct payment
* claimCovenant:
* type: boolean
* default: false
* description: If the claim covenant should be added to the Taproot tree. Only possible when "address" is set
*/

/**
* @openapi
* components:
Expand Down Expand Up @@ -1227,6 +1230,7 @@ class SwapRouter extends RouterBase {
claimAddress,
invoiceAmount,
onchainAmount,
claimCovenant,
claimPublicKey,
addressSignature,
} = validateRequest(req.body, [
Expand All @@ -1240,6 +1244,7 @@ class SwapRouter extends RouterBase {
{ name: 'claimAddress', type: 'string', optional: true },
{ name: 'invoiceAmount', type: 'number', optional: true },
{ name: 'onchainAmount', type: 'number', optional: true },
{ name: 'claimCovenant', type: 'boolean', optional: true },
{ name: 'claimPublicKey', type: 'string', hex: true, optional: true },
{ name: 'addressSignature', type: 'string', hex: true, optional: true },
]);
Expand All @@ -1257,6 +1262,7 @@ class SwapRouter extends RouterBase {
claimAddress,
invoiceAmount,
onchainAmount,
claimCovenant,
claimPublicKey,

userAddress: address,
Expand Down
4 changes: 4 additions & 0 deletions lib/service/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,8 @@ export default {
message: 'swap not eligible for a cooperative claim broadcast',
code: concatErrorCode(ErrorCodePrefix.Service, 39),
}),
INVALID_PARTIAL_SIGNATURE: (): Error => ({
message: 'invalid partial signature',
code: concatErrorCode(ErrorCodePrefix.Service, 40),
}),
};
3 changes: 3 additions & 0 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,8 @@ class Service {
// Address of the user to encode in the invoice memo
userAddress?: string;
userAddressSignature?: Buffer;

claimCovenant?: boolean;
}): Promise<{
id: string;
invoice: string;
Expand Down Expand Up @@ -1584,6 +1586,7 @@ class Service {
claimAddress: args.claimAddress,
preimageHash: args.preimageHash,
claimPublicKey: args.claimPublicKey,
claimCovenant: args.claimCovenant || false,
userAddressSignature: args.userAddressSignature,
});

Expand Down
4 changes: 4 additions & 0 deletions lib/service/cooperative/DeferredClaimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ class DeferredClaimer extends TypedEventEmitter<{
musig.initializeSession(
await hashForWitnessV1(chainCurrency, toClaim.cooperative.transaction, 0),
);
if (!musig.verifyPartial(theirPublicKey, theirPartialSignature)) {
throw Errors.INVALID_PARTIAL_SIGNATURE();
}

musig.addPartial(theirPublicKey, theirPartialSignature);
musig.signPartial();

Expand Down
14 changes: 9 additions & 5 deletions lib/swap/ReverseRoutingHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Errors from './Errors';

type SwapHints = {
invoiceMemo: string;
receivedAmount: number;
bip21?: string;
routingHint?: HopHint[][];
};
Expand Down Expand Up @@ -39,12 +40,17 @@ class ReverseRoutingHints {
},
): SwapHints => {
const invoiceMemo = getSwapMemo(sendingCurrency.symbol, true);
const receivedAmount =
args.onchainAmount -
this.rateProvider.feeProvider.minerFees.get(sendingCurrency.symbol)![
args.version
].reverse.claim;

if (
args.userAddress === undefined ||
args.userAddressSignature === undefined
) {
return { invoiceMemo };
return { invoiceMemo, receivedAmount };
}

try {
Expand All @@ -58,10 +64,7 @@ class ReverseRoutingHints {
const bip21 = this.paymentRequestUtils.encodeBip21(
sendingCurrency.symbol,
args.userAddress,
args.onchainAmount -
this.rateProvider.feeProvider.minerFees.get(sendingCurrency.symbol)![
args.version
].reverse.claim,
receivedAmount,
);

const routingHint = this.encodeRoutingHint(
Expand All @@ -74,6 +77,7 @@ class ReverseRoutingHints {
bip21,
routingHint,
invoiceMemo,
receivedAmount,
};
};

Expand Down
60 changes: 52 additions & 8 deletions lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
swapScript,
swapTree,
} from 'boltz-core';
import {
Feature,
reverseSwapTree as reverseSwapTreeLiquid,
} from 'boltz-core/dist/lib/liquid';
import { randomBytes } from 'crypto';
import { Network as LiquidNetwork } from 'liquidjs-lib/src/networks';
import { Op } from 'sequelize';
import { createMusig, tweakMusig } from '../Core';
import Logger from '../Logger';
Expand Down Expand Up @@ -597,6 +602,8 @@ class SwapManager {

userAddress?: string;
userAddressSignature?: Buffer;

claimCovenant: boolean;
}): Promise<CreatedReverseSwap> => {
const { sendingCurrency, receivingCurrency } = this.getCurrencies(
args.baseCurrency,
Expand Down Expand Up @@ -692,6 +699,7 @@ class SwapManager {
if (isBitcoinLike) {
const { keys, index } = sendingCurrency.wallet.getNewKeys();
const { blocks } = await sendingCurrency.chainClient!.getBlockchainInfo();

result.timeoutBlockHeight = blocks + args.onchainTimeoutBlockDelta;

let outputScript: Buffer;
Expand All @@ -701,13 +709,50 @@ class SwapManager {
case SwapVersion.Taproot: {
result.refundPublicKey = getHexString(keys.publicKey);

tree = reverseSwapTree(
sendingCurrency.type === CurrencyType.Liquid,
args.preimageHash,
args.claimPublicKey!,
keys.publicKey,
result.timeoutBlockHeight,
);
if (args.claimCovenant) {
if (sendingCurrency.type !== CurrencyType.Liquid) {
throw 'claim covenant only supported on Liquid';
}

if (args.userAddress === undefined) {
throw 'userAddress for covenant not specified';
}

try {
sendingCurrency.wallet.decodeAddress(args.userAddress);
} catch (e) {
throw Errors.INVALID_ADDRESS();
}

tree = reverseSwapTreeLiquid(
args.preimageHash,
args.claimPublicKey!,
keys.publicKey,
result.timeoutBlockHeight,
[
{
expectedAmount: hints.receivedAmount,
type: Feature.ClaimCovenant,
assetHash: (
this.walletManager.wallets.get(sendingCurrency.symbol)!
.network as LiquidNetwork
).assetHash,
outputScript: this.walletManager.wallets
.get(sendingCurrency.symbol)!
.decodeAddress(args.userAddress!),
},
],
);
} else {
tree = reverseSwapTree(
sendingCurrency.type === CurrencyType.Liquid,
args.preimageHash,
args.claimPublicKey!,
keys.publicKey,
result.timeoutBlockHeight,
);
}

result.swapTree = SwapTreeSerializer.serializeSwapTree(tree);

const musig = createMusig(keys, args.claimPublicKey!);
Expand Down Expand Up @@ -750,7 +795,6 @@ class SwapManager {
minerFeeInvoice,
node: nodeType,
keyIndex: index,

version: args.version,
fee: args.percentageFee,
invoice: paymentRequest,
Expand Down
34 changes: 17 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"dependencies": {
"@boltz/bolt11": "^1.2.7",
"@google-cloud/storage": "^7.7.0",
"@grpc/grpc-js": "^1.10.0",
"@grpc/grpc-js": "^1.10.1",
"@iarna/toml": "^2.2.5",
"@mattermost/client": "^9.4.0",
"@mattermost/types": "^9.4.0",
Expand All @@ -81,14 +81,14 @@
"bip39": "^3.1.0",
"bitcoinjs-lib": "^6.1.5",
"bolt11": "^1.4.1",
"boltz-core": "^2.1.0",
"boltz-core": "^2.1.1",
"colors": "^1.4.0",
"cors": "^2.8.5",
"cross-os": "^1.5.0",
"csv-parse": "^5.5.3",
"discord.js": "^14.14.1",
"ecpair": "^2.1.0",
"ethers": "^6.11.0",
"ethers": "^6.11.1",
"express": "^4.18.2",
"google-protobuf": "^3.21.2",
"ip-address": "^9.0.5",
Expand Down Expand Up @@ -126,7 +126,7 @@
"@typescript-eslint/parser": "^7.0.1",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jest": "^27.8.0",
"eslint-plugin-node": "^11.1.0",
"git-cliff": "^1.4.0",
"grpc_tools_node_protoc_ts": "^5.3.3",
Expand Down
5 changes: 5 additions & 0 deletions swagger-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,11 @@
"addressSignature": {
"type": "string",
"description": "Signature of the claim public key of the SHA256 hash of the address for the direct payment"
},
"claimCovenant": {
"type": "boolean",
"default": false,
"description": "If the claim covenant should be added to the Taproot tree. Only possible when \"address\" is set"
}
}
},
Expand Down
20 changes: 20 additions & 0 deletions test/integration/service/cooperative/DeferredClaimer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,26 @@ describe('DeferredClaimer', () => {
expect(claimTx.outs).toHaveLength(1);
});

test('should throw when cooperatively broadcasting a submarine swap with invalid partial signature', async () => {
await bitcoinClient.generate(1);
const { swap, preimage, refundKeys } = await createClaimableOutput();

await expect(claimer.deferClaim(swap, preimage)).resolves.toEqual(true);
await claimer.getCooperativeDetails(swap);

const musig = new Musig(secp, refundKeys, randomBytes(32), [
btcWallet.getKeysByIndex(swap.keyIndex!).publicKey,
refundKeys.publicKey,
]);
await expect(
claimer.broadcastCooperative(
swap,
Buffer.from(musig.getPublicNonce()),
randomBytes(32),
),
).rejects.toEqual(Errors.INVALID_PARTIAL_SIGNATURE());
});

test('should throw when cooperatively broadcasting a submarine swap that does not exist', async () => {
await expect(
claimer.broadcastCooperative(
Expand Down

0 comments on commit 3322160

Please sign in to comment.