Skip to content

Commit

Permalink
feat: Taproot reverse swaps
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Jan 2, 2024
1 parent f89e6c5 commit f64d6ae
Show file tree
Hide file tree
Showing 17 changed files with 722 additions and 242 deletions.
8 changes: 8 additions & 0 deletions lib/Core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ export const extractClaimPublicKeyFromSwapTree = (
swapTree: Types.SwapTree,
): Buffer => script.decompile(swapTree.claimLeaf.output)![3] as Buffer;

export const extractClaimPublicKeyFromReverseSwapTree = (
swapTree: Types.SwapTree,
): Buffer => script.decompile(swapTree.claimLeaf.output)![6] as Buffer;

export const extractRefundPublicKeyFromReverseSwapTree = (
swapTree: Types.SwapTree,
): Buffer => extractRefundPublicKeyFromSwapTree(swapTree);

export const createMusig = (
ourKeys: ECPairInterface | BIP32Interface,
refundPublicKey: Buffer,
Expand Down
5 changes: 4 additions & 1 deletion lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { OutputType, Scripts } from 'boltz-core';
import { Transaction as LiquidTransaction, confidential } from 'liquidjs-lib';
import commitHash from './Version';
import packageJson from '../package.json';
import { OrderSide } from './consts/Enums';
import ChainClient from './chain/ChainClient';
import { etherDecimals } from './consts/Consts';
import { OrderSide, SwapVersion } from './consts/Enums';

const {
p2trOutput,

Check failure on line 16 in lib/Utils.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20, 3.10)

Property 'p2trOutput' does not exist on type 'typeof import("/home/runner/work/boltz-backend/boltz-backend/node_modules/boltz-core/dist/lib/swap/Scripts")'.

Check failure on line 16 in lib/Utils.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20, 3.10)

Property 'p2trOutput' does not exist on type 'typeof import("/home/runner/work/boltz-backend/boltz-backend/node_modules/boltz-core/dist/lib/swap/Scripts")'.
Expand Down Expand Up @@ -42,6 +42,9 @@ export const generateId = (length = 6): string => {
return id;
};

export const generateSwapId = (version: SwapVersion): string =>
generateId(version === SwapVersion.Legacy ? undefined : 12);

/**
* Stringify any object or array
*/
Expand Down
82 changes: 80 additions & 2 deletions lib/api/v2/routers/SwapRouter.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Request, Response, Router } from 'express';
import Logger from '../../../Logger';
import RouterBase from './RouterBase';
import { getHexString, stringify } from '../../../Utils';
import { SwapVersion } from '../../../consts/Enums';
import Service from '../../../service/Service';
import { getHexString, stringify } from '../../../Utils';
import {
checkPreimageHashLength,
createdResponse,
successResponse,
validateRequest,
} from '../../Utils';
import { SwapVersion } from '../../../consts/Enums';

class SwapRouter extends RouterBase {
constructor(
Expand Down Expand Up @@ -88,6 +88,10 @@ class SwapRouter extends RouterBase {

router.post('/submarine/refund', this.handleError(this.refundSubmarine));

router.post('/reverse', this.handleError(this.createReverse));

router.post('/reverse/claim', this.handleError(this.claimReverse));

return router;
};

Expand Down Expand Up @@ -169,6 +173,80 @@ class SwapRouter extends RouterBase {
partialSignature: getHexString(sig.signature),
});
};

private createReverse = async (req: Request, res: Response) => {
const {
pairId,
pairHash,
orderSide,
referralId,
routingNode,
claimAddress,
preimageHash,
invoiceAmount,
onchainAmount,
claimPublicKey,
} = validateRequest(req.body, [
{ name: 'pairId', type: 'string' },
{ name: 'orderSide', type: 'string' },
{ name: 'preimageHash', type: 'string', hex: true },
{ name: 'claimPublicKey', type: 'string', hex: true },
{ name: 'pairHash', type: 'string', optional: true },
{ name: 'referralId', type: 'string', optional: true },
{ name: 'routingNode', type: 'string', optional: true },
{ name: 'claimAddress', type: 'string', optional: true },
{ name: 'invoiceAmount', type: 'number', optional: true },
{ name: 'onchainAmount', type: 'number', optional: true },
]);

checkPreimageHashLength(preimageHash);

const response = await this.service.createReverseSwap({
pairId,
pairHash,
orderSide,
referralId,
routingNode,
claimAddress,
preimageHash,
invoiceAmount,
onchainAmount,
claimPublicKey,
prepayMinerFee: false,
version: SwapVersion.Taproot,
});

this.logger.verbose(`Created Reverse Swap with id: ${response.id}`);
this.logger.silly(`Reverse swap ${response.id}: ${stringify(response)}`);

createdResponse(res, response);
};

private claimReverse = async (req: Request, res: Response) => {
const { id, preimage, pubNonce, index, transaction } = validateRequest(
req.body,
[
{ name: 'id', type: 'string' },
{ name: 'index', type: 'number' },
{ name: 'preimage', type: 'string', hex: true },
{ name: 'pubNonce', type: 'string', hex: true },
{ name: 'transaction', type: 'string', hex: true },
],
);

const sig = await this.service.musigSigner.signReverseSwapClaim(
id,
preimage,
pubNonce,
transaction,
index,
);

successResponse(res, {
pubNonce: getHexString(sig.pubNonce),
partialSignature: getHexString(sig.signature),
});
};
}

export default SwapRouter;
53 changes: 43 additions & 10 deletions lib/cli/BoltzApiClient.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,73 @@
import axios from 'axios';
import { SwapUpdateEvent } from '../consts/Enums';

type PartialSignature = {
pubNonce: string;
partialSignature: string;
};

class BoltzApiClient {
public static readonly regtestEndpoint = 'http://127.0.0.1:9001';

constructor(private readonly endpoint = BoltzApiClient.regtestEndpoint) {}

public getStatus = async (
swapId: string,
): Promise<{
status: SwapUpdateEvent;
transaction?: {
hex: string;
};
}> =>
(
await axios.post(`${this.endpoint}/swapstatus`, {
id: swapId,
})
).data;

public getSwapTransaction = async (
swapId: string,
): Promise<{
transactionHex: string;
}> => {
return (
}> =>
(
await axios.post(`${this.endpoint}/getswaptransaction`, {
id: swapId,
})
).data;
};

public refundSwap = async (
public getSwapRefundPartialSignature = async (
swapId: string,
transaction: string,
vin: number,
pubNonce: string,
): Promise<{
pubNonce: string;
partialSignature: string;
}> => {
return (
): Promise<PartialSignature> =>
(
await axios.post(`${this.endpoint}/v2/swap/submarine/refund`, {
pubNonce,
transaction,
id: swapId,
index: vin,
})
).data;
};

public getReverseClaimPartialSignature = async (
swapId: string,
preimage: string,
transaction: string,
vin: number,
pubNonce: string,
): Promise<PartialSignature> =>
(
await axios.post(`${this.endpoint}/v2/swap/reverse/claim`, {
pubNonce,
preimage,
transaction,
id: swapId,
index: vin,
})
).data;
}

export default BoltzApiClient;
export { PartialSignature };
4 changes: 4 additions & 0 deletions lib/cli/BuilderComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ export default {
describe: 'SwapTree of the Swap',
type: 'string',
},
preimage: {
describe: 'preimage of the swap',
type: 'string',
},
};
138 changes: 138 additions & 0 deletions lib/cli/TaprootHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Arguments } from 'yargs';
import { randomBytes } from 'crypto';
import { ECPairInterface } from 'ecpair';
import { Network, Transaction } from 'bitcoinjs-lib';
import { LiquidRefundDetails } from 'boltz-core/lib/liquid';

Check failure on line 5 in lib/cli/TaprootHelper.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20, 3.10)

Cannot find module 'boltz-core/lib/liquid' or its corresponding type declarations.

Check failure on line 5 in lib/cli/TaprootHelper.ts

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20, 3.10)

Cannot find module 'boltz-core/lib/liquid' or its corresponding type declarations.
import {
detectSwap,
Musig,
OutputType,
RefundDetails,
SwapTreeSerializer,
TaprootUtils,
Types,
} from 'boltz-core';
import { Network as LiquidNetwork } from 'liquidjs-lib/src/networks';
import { Transaction as LiquidTransaction } from 'liquidjs-lib/src/transaction';
import { TaprootUtils as LiquidTaprootDetails } from 'boltz-core/dist/lib/liquid';
import { getHexBuffer } from '../Utils';
import { ECPair } from '../ECPairHelper';
import { CurrencyType } from '../consts/Enums';
import { PartialSignature } from './BoltzApiClient';
import {
constructClaimTransaction,
setup,
tweakMusig,
zkpMusig,
} from '../Core';
import {
currencyTypeFromNetwork,
getWalletStub,
parseNetwork,
} from './Command';

export const setupCooperativeTransaction = async (
argv: Arguments<any>,
keyExtractionFunc: (tree: Types.SwapTree) => Buffer,
) => {
await setup();

const network = parseNetwork(argv.network);
const currencyType = currencyTypeFromNetwork(argv.network);

const swapTree = SwapTreeSerializer.deserializeSwapTree(argv.swapTree);
const keys = ECPair.fromPrivateKey(getHexBuffer(argv.privateKey));
const theirPublicKey = keyExtractionFunc(swapTree);

const musig = new Musig(zkpMusig, keys, randomBytes(32), [
theirPublicKey,
keys.publicKey,
]);
const tweakedKey = tweakMusig(currencyType, musig, swapTree);

return {
keys,
musig,
network,
tweakedKey,
currencyType,
theirPublicKey,
};
};

export const prepareCooperativeTransaction = <
T extends Transaction | LiquidTransaction,
>(
argv: Arguments<any>,
network: Network | LiquidNetwork,
currencyType: CurrencyType,
keys: ECPairInterface,
tweakedKey: Buffer,
lockupTx: T,
): { tx: T; details: RefundDetails | LiquidRefundDetails } => {
const swapOutput = detectSwap(tweakedKey, lockupTx);
if (swapOutput === undefined) {
throw 'could not find swap output';
}

const details = {
...swapOutput,
keys,
txHash: lockupTx.getHash(),
type: OutputType.Taproot,
cooperative: true,
} as any;
const tx = constructClaimTransaction(
getWalletStub(
currencyType,
network,
argv.destinationAddress,
argv.blindingKey,
),
[details],
argv.destinationAddress,
argv.feePerVbyte,
);

return {
details,
tx: tx as T,
};
};

export const finalizeCooperativeTransaction = <
T extends Transaction | LiquidTransaction,
>(
tx: T,
musig: Musig,
network: Network | LiquidNetwork,
currencyType: CurrencyType,
otherPublicKey: Buffer,
details: RefundDetails | LiquidRefundDetails,
partialSig: PartialSignature,
): T => {
musig.aggregateNonces([[otherPublicKey, getHexBuffer(partialSig.pubNonce)]]);

let hash: Buffer;
if (currencyType === CurrencyType.BitcoinLike) {
hash = TaprootUtils.hashForWitnessV1(
[details] as RefundDetails[],
tx as Transaction,
0,
);
} else {
hash = LiquidTaprootDetails.hashForWitnessV1(
network as LiquidNetwork,
[details] as LiquidRefundDetails[],
tx as LiquidTransaction,
0,
);
}

musig.initializeSession(hash);
musig.signPartial();
musig.addPartial(otherPublicKey, getHexBuffer(partialSig.partialSignature));
tx.setWitness(0, [musig.aggregatePartials()]);

return tx;
};
5 changes: 1 addition & 4 deletions lib/cli/commands/Claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ export const describe = 'claims reverse submarine or chain to chain swaps';

export const builder = {
network: BuilderComponents.network,
preimage: BuilderComponents.preimage,
privateKey: BuilderComponents.privateKey,
redeemScript: BuilderComponents.redeemScript,
rawTransaction: BuilderComponents.rawTransaction,
destinationAddress: BuilderComponents.destinationAddress,
preimage: {
describe: 'preimage of the swap',
type: 'string',
},
feePerVbyte: BuilderComponents.feePerVbyte,
blindingKey: BuilderComponents.blindingKey,
};
Expand Down
Loading

0 comments on commit f64d6ae

Please sign in to comment.