Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Liquid Claim covenant #113

Merged
merged 3 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};
13 changes: 12 additions & 1 deletion 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 * as TaprootUtils from './swap/TaprooUtils';
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,
};
58 changes: 44 additions & 14 deletions lib/liquid/swap/Claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ 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';
import { LiquidClaimDetails } from '../consts/Types';
import { ecpair, secp } from '../init';
import { createControlBlock, tapLeafHash, toHashTree } from './TaprooUtils';
import { createControlBlock, tapLeafHash, toHashTree } from './TaprootUtils';

const dummyTaprootSignature = Buffer.alloc(64);

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,
michael1011 marked this conversation as resolved.
Show resolved Hide resolved
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 || 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
Loading