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 Jan 28, 2024
1 parent 157d32d commit b4111ad
Show file tree
Hide file tree
Showing 20 changed files with 1,019 additions and 62 deletions.
64 changes: 64 additions & 0 deletions lib/liquid/TreeSort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Tapleaf } from '../consts/Types';
import { LiquidSwapTree } from './consts/Types';

type ProbabilityNode<T> = { probability: number; value: T };

type TreeNode<T> = Tree<T> | T;
type Tree<T> = [TreeNode<T>, TreeNode<T>];

const subSortTree = <T>(nodes: ProbabilityNode<T>[]): TreeNode<T> => {
if (nodes.length === 1) {
return nodes[0].value;
} else if (nodes.length === 2) {
return [nodes[0].value, nodes[1].value];
}

const sum = nodes.reduce((sum, node) => sum + node.probability, 0);

let mid = 0;
let midSum = 0;

while (midSum < sum / 2) {
midSum += nodes[mid].probability;
mid++;
}

return [subSortTree(nodes.slice(0, mid)), subSortTree(nodes.slice(mid))];
};

export const sortTree = <T>(nodes: ProbabilityNode<T>[]): TreeNode<T> =>
subSortTree(nodes.sort((a, b) => b.probability - a.probability));

export const assignTreeProbabilities = (
tree: Omit<LiquidSwapTree, 'tree'>,
): ProbabilityNode<Tapleaf>[] => {
if (tree.covenantClaimLeaf) {
return [
{
probability: 51,
value: tree.covenantClaimLeaf,
},
{
probability: 25,
value: tree.claimLeaf,
},
{
probability: 24,
value: tree.refundLeaf,
},
];
}

return [
{
probability: 51,
value: tree.claimLeaf,
},
{
probability: 49,
value: tree.refundLeaf,
},
];
};

export { Tree, TreeNode, ProbabilityNode };
31 changes: 31 additions & 0 deletions lib/liquid/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ops from '@boltz/bitcoin-ops';
import { crypto, script } from 'bitcoinjs-lib';
import { TxOutput, confidential } from 'liquidjs-lib';
import { confidentialLiquid } from './init';

Expand All @@ -17,3 +19,32 @@ export const getOutputValue = (
)
: confidential.confidentialValueToSatoshi(output.value);
};

const getScriptIntrospectionWitnessScript = (outputScript: Buffer) =>
outputScript.subarray(2, 40);

export const getScriptIntrospectionValues = (
outputScript: Buffer,
): { version: number; script: Buffer } => {
const dec = script.decompile(outputScript)!;

switch (dec[0]) {
case ops.OP_1:
return {
version: 1,
script: getScriptIntrospectionWitnessScript(outputScript),
};

case ops.OP_0:
return {
version: 0,
script: getScriptIntrospectionWitnessScript(outputScript),
};

default:
return {
version: -1,
script: crypto.sha256(outputScript),
};
}
};
6 changes: 6 additions & 0 deletions lib/liquid/consts/Ops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Reference: https://github.com/ElementsProject/elements/blob/master/doc/tapscript_opcodes.md#new-opcodes-for-additional-functionality
export default {
OP_INSPECTOUTPUTSCRIPTPUBKEY: 0xd1,
OP_INSPECTOUTPUTASSET: 0xce,
OP_INSPECTOUTPUTVALUE: 0xcf,
};
12 changes: 9 additions & 3 deletions lib/liquid/consts/Types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { BIP32Interface } from 'bip32';
import { ECPairInterface } from 'ecpair';
import { Transaction, TxOutput } from 'liquidjs-lib';
import { RefundDetails } from '../../consts/Types';
import { RefundDetails, SwapTree, Tapleaf } from '../../consts/Types';

export type LiquidRefundDetails = Omit<RefundDetails, 'value'> &
export type LiquidSwapTree = SwapTree & { covenantClaimLeaf?: Tapleaf };

export type LiquidRefundDetails = Omit<RefundDetails, 'value' | 'swapTree'> &
TxOutput & {
legacyTx?: Transaction;
swapTree?: LiquidSwapTree;
blindingPrivateKey?: Buffer;
};

export type LiquidClaimDetails = LiquidRefundDetails & {
export type LiquidClaimDetails = Omit<LiquidRefundDetails, 'keys'> & {
preimage: Buffer;
keys?: ECPairInterface | BIP32Interface;
};
11 changes: 11 additions & 0 deletions lib/liquid/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { getOutputValue } from './Utils';
import * as Utils from './Utils';
import Networks from './consts/Networks';
import ops from './consts/Ops';
import { LiquidClaimDetails, LiquidRefundDetails } from './consts/Types';
import { init } from './init';
import { constructClaimTransaction } from './swap/Claim';
import { constructRefundTransaction } from './swap/Refund';
import reverseSwapTree, {
Feature,
FeatureOption,
} from './swap/ReverseSwapTree';
import * as TaprootUtils from './swap/TaprootUtils';

export {
ops,
Utils,
Feature,
Networks,
TaprootUtils,
FeatureOption,
LiquidClaimDetails,
LiquidRefundDetails,
init,
getOutputValue,
reverseSwapTree,
constructClaimTransaction,
constructRefundTransaction,
};
56 changes: 43 additions & 13 deletions lib/liquid/swap/Claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { reverseBuffer, varuint } from 'liquidjs-lib/src/bufferutils';
import { Network } from 'liquidjs-lib/src/networks';
import { getHexString } from '../../Utils';
import { OutputType } from '../../consts/Enums';
import { validateInputs } from '../../swap/Claim';
import { isRelevantTaprootOutput, validateInputs } from '../../swap/Claim';
import { scriptBuffersToScript } from '../../swap/SwapUtils';
import { getOutputValue } from '../Utils';
import Networks from '../consts/Networks';
Expand All @@ -36,6 +36,29 @@ const getSighashType = (type: OutputType) =>
? Transaction.SIGHASH_DEFAULT
: Transaction.SIGHASH_ALL;

const validateLiquidInputs = (
utxos: LiquidClaimDetails[],
isRefund: boolean,
) => {
validateInputs(utxos);

const taprootInputs = utxos.filter(isRelevantTaprootOutput);

if (isRefund && taprootInputs.some((utxo) => utxo.keys === undefined)) {
throw 'not all Taproot refund inputs have keys';
}

if (
taprootInputs.some(
(utxo) =>
utxo.keys === undefined &&
utxo.swapTree!.covenantClaimLeaf === undefined,
)
) {
throw 'not all Taproot signature claims have keys';
}
};

/**
* Claim swaps
*
Expand All @@ -58,7 +81,7 @@ export const constructClaimTransaction = (
timeoutBlockHeight?: number,
isRefund = false,
): Transaction => {
validateInputs(utxos);
validateLiquidInputs(utxos, isRefund);

if (
utxos.some(
Expand Down Expand Up @@ -133,10 +156,10 @@ export const constructClaimTransaction = (
},
]);

const addFeeOutput = () => {
const addFeeOutput = (isUnblinded = false) => {
updater.addOutputs([
{
amount: fee,
amount: isUnblinded ? fee - 1 : fee,
asset: network.assetHash,
},
]);
Expand All @@ -149,15 +172,16 @@ export const constructClaimTransaction = (
pset.addOutput(
new CreatorOutput(
network.assetHash,
0,
// TODO: figure out flakiness with blinding 0 amount outputs
1,
Buffer.of(ops.OP_RETURN),
ecpair.makeRandom().publicKey,
0,
).toPartialOutput(),
);
}

addFeeOutput();
addFeeOutput(blindingKey === undefined);

blindPset(pset, utxos);
} else {
Expand All @@ -170,15 +194,15 @@ export const constructClaimTransaction = (

for (const [i, utxo] of utxos.entries()) {
if (utxo.type === OutputType.Taproot) {
if (utxo.cooperative) {
if (utxo.cooperative || (!isRefund && utxo.keys === undefined)) {
signatures.push(dummyTaprootSignature);
continue;
}

const leafHash = tapLeafHash(
isRefund ? utxo.swapTree!.refundLeaf : utxo.swapTree!.claimLeaf,
);
const signature = utxo.keys.signSchnorr(
const signature = utxo.keys!.signSchnorr(
pset.getInputPreimage(
i,
getSighashType(utxo.type),
Expand All @@ -194,7 +218,7 @@ export const constructClaimTransaction = (
tapScriptSigs: [
{
signature: signature,
pubkey: toXOnly(utxo.keys.publicKey),
pubkey: toXOnly(utxo.keys!.publicKey),
leafHash,
},
],
Expand All @@ -203,7 +227,7 @@ export const constructClaimTransaction = (
);
} else {
const signature = script.signature.encode(
utxo.keys.sign(pset.getInputPreimage(i, getSighashType(utxo.type))),
utxo.keys!.sign(pset.getInputPreimage(i, getSighashType(utxo.type))),
getSighashType(utxo.type),
);
signatures.push(signature);
Expand All @@ -212,7 +236,7 @@ export const constructClaimTransaction = (
i,
{
partialSig: {
pubkey: utxo.keys.publicKey,
pubkey: utxo.keys!.publicKey,
signature,
},
},
Expand Down Expand Up @@ -253,13 +277,19 @@ export const constructClaimTransaction = (
dummyTaprootSignature,
]);
} else {
const isCovenantClaim = utxo.keys === undefined;

const tapleaf = isRefund
? utxo.swapTree!.refundLeaf
: utxo.swapTree!.claimLeaf;
: isCovenantClaim
? utxo.swapTree!.covenantClaimLeaf!
: utxo.swapTree!.claimLeaf;

const witness = isRefund
? [signatures[i]]
: [signatures[i], utxo.preimage];
: isCovenantClaim
? [utxo.preimage]
: [signatures[i], utxo.preimage];

finals.finalScriptWitness = witnessStackToScriptWitness(
witness.concat([
Expand Down
Loading

0 comments on commit b4111ad

Please sign in to comment.