From 646a317982b07281117145194525bc586b2f12c4 Mon Sep 17 00:00:00 2001
From: michael1011 <me@michael1011.at>
Date: Mon, 1 Jan 2024 17:25:13 +0100
Subject: [PATCH] chore: miscellaneous fixes (#110)

---
 lib/Boltz.ts                                  |  4 ++
 lib/init.ts                                   |  4 ++
 lib/liquid/Utils.ts                           |  4 +-
 lib/liquid/swap/Claim.ts                      | 10 ++--
 lib/musig/Musig.ts                            | 18 +++---
 lib/swap/Claim.ts                             | 55 +++++++++++--------
 lib/swap/SwapTreeSerializer.ts                | 22 +++++---
 package-lock.json                             |  4 +-
 package.json                                  |  2 +-
 test/integration/liquid/Utils.spec.ts         | 24 ++++++++
 .../liquid/swapTree/SwapTreeClaim.spec.ts     |  8 ++-
 test/integration/musig/Musig.spec.ts          |  2 +-
 .../swapTree/SwapTreeClaim.spec.ts            |  8 ++-
 test/unit/musig/Musig.spec.ts                 | 30 +++++-----
 test/unit/swap/Claim.spec.ts                  |  2 +-
 test/unit/swap/SwapTreeSerializer.spec.ts     | 14 +++++
 .../SwapTreeSerializer.spec.ts.snap           | 26 ++++++++-
 17 files changed, 162 insertions(+), 75 deletions(-)
 create mode 100644 lib/init.ts

diff --git a/lib/Boltz.ts b/lib/Boltz.ts
index 65290b8..6ff1016 100644
--- a/lib/Boltz.ts
+++ b/lib/Boltz.ts
@@ -1,5 +1,7 @@
+import { init } from './init';
 import Musig from './musig/Musig';
 import swapTree from './swap/SwapTree';
+import * as Types from './consts/Types';
 import { targetFee } from './TargetFee';
 import Networks from './consts/Networks';
 import * as Scripts from './swap/Scripts';
@@ -32,6 +34,7 @@ const ContractABIs = {
 
 export {
   Musig,
+  Types,
   Networks,
   OutputType,
   ContractABIs,
@@ -43,6 +46,7 @@ export {
   SwapUtils,
   TaprootUtils,
   SwapTreeSerializer,
+  init,
   swapTree,
   targetFee,
   swapScript,
diff --git a/lib/init.ts b/lib/init.ts
new file mode 100644
index 0000000..34d229b
--- /dev/null
+++ b/lib/init.ts
@@ -0,0 +1,4 @@
+import { initEccLib } from 'bitcoinjs-lib';
+import { TinySecp256k1Interface } from 'bitcoinjs-lib/src/types';
+
+export const init = (eccLib: TinySecp256k1Interface) => initEccLib(eccLib);
diff --git a/lib/liquid/Utils.ts b/lib/liquid/Utils.ts
index 95e7d35..ba64684 100644
--- a/lib/liquid/Utils.ts
+++ b/lib/liquid/Utils.ts
@@ -6,7 +6,9 @@ export const getOutputValue = (
     blindingPrivateKey?: Buffer;
   },
 ): number => {
-  return output.blindingPrivateKey
+  return output.blindingPrivateKey &&
+    output.rangeProof !== undefined &&
+    output.rangeProof.length > 0
     ? Number(
         confidentialLiquid.unblindOutputWithKey(
           output,
diff --git a/lib/liquid/swap/Claim.ts b/lib/liquid/swap/Claim.ts
index 8150dd3..caae259 100644
--- a/lib/liquid/swap/Claim.ts
+++ b/lib/liquid/swap/Claim.ts
@@ -22,14 +22,14 @@ import { reverseBuffer, varuint } from 'liquidjs-lib/src/bufferutils';
 import { ecpair, secp } from '../init';
 import Networks from '../consts/Networks';
 import { getOutputValue } from '../Utils';
+import { getHexString } from '../../Utils';
 import { OutputType } from '../../consts/Enums';
 import { validateInputs } from '../../swap/Claim';
 import { LiquidClaimDetails } from '../consts/Types';
-import { getHexBuffer, getHexString } from '../../Utils';
 import { scriptBuffersToScript } from '../../swap/SwapUtils';
 import { createControlBlock, tapLeafHash, toHashTree } from './TaprooUtils';
 
-const dummyWitness = getHexBuffer('0x21');
+const dummyTaprootSignature = Buffer.alloc(64);
 
 const getSighashType = (type: OutputType) =>
   type === OutputType.Taproot
@@ -171,7 +171,7 @@ export const constructClaimTransaction = (
   for (const [i, utxo] of utxos.entries()) {
     if (utxo.type === OutputType.Taproot) {
       if (utxo.cooperative) {
-        signatures.push(dummyWitness);
+        signatures.push(dummyTaprootSignature);
         continue;
       }
 
@@ -248,9 +248,9 @@ export const constructClaimTransaction = (
 
       if (utxo.type === OutputType.Taproot) {
         if (utxo.cooperative) {
-          // Add a dummy to allow for extraction
+          // Add a dummy to allow for extraction and an accurate fee estimation
           finals.finalScriptWitness = witnessStackToScriptWitness([
-            dummyWitness,
+            dummyTaprootSignature,
           ]);
         } else {
           const tapleaf = isRefund
diff --git a/lib/musig/Musig.ts b/lib/musig/Musig.ts
index fcefdba..e646640 100644
--- a/lib/musig/Musig.ts
+++ b/lib/musig/Musig.ts
@@ -82,24 +82,26 @@ class Musig {
     this.nonceAgg = this.secp.musig.nonceAgg(nonces);
   };
 
-  public aggregateNonces = (nonces: Map<Uint8Array, Uint8Array>) => {
-    const ordered: Uint8Array[] = [];
-
-    if (!nonces.has(this.key.publicKey)) {
-      nonces.set(this.key.publicKey, this.getPublicNonce());
+  public aggregateNonces = (nonces: [Uint8Array, Uint8Array][]) => {
+    if (
+      nonces.find(([keyCmp]) => this.key.publicKey.equals(keyCmp)) === undefined
+    ) {
+      nonces.push([this.key.publicKey, this.getPublicNonce()]);
     }
 
-    if (this.publicKeys.length !== nonces.size) {
+    if (this.publicKeys.length !== nonces.length) {
       throw 'number of nonces != number of public keys';
     }
 
+    const ordered: Uint8Array[] = [];
+
     for (const key of this.publicKeys) {
-      const nonce = nonces.get(key);
+      const nonce = nonces.find(([keyCmp]) => key.equals(keyCmp));
       if (nonce === undefined) {
         throw `could not find nonce for public key ${getHexString(key)}`;
       }
 
-      ordered.push(nonce);
+      ordered.push(nonce[1]);
     }
 
     this.aggregateNoncesOrdered(ordered);
diff --git a/lib/swap/Claim.ts b/lib/swap/Claim.ts
index dd51af2..d538769 100644
--- a/lib/swap/Claim.ts
+++ b/lib/swap/Claim.ts
@@ -13,6 +13,8 @@ import { ClaimDetails } from '../consts/Types';
 import { encodeSignature, scriptBuffersToScript } from './SwapUtils';
 import { createControlBlock, hashForWitnessV1 } from './TaprootUtils';
 
+const dummyTaprootSignature = Buffer.alloc(64);
+
 const isRelevantTaprootOutput = (utxo: Omit<ClaimDetails, 'value'>) =>
   utxo.type === OutputType.Taproot && utxo.cooperative !== true;
 
@@ -117,32 +119,37 @@ export const constructClaimTransaction = (
 
     // Construct and sign the witness for (nested) SegWit inputs
     // When the Taproot output is spent cooperatively, we leave it empty
-    if (utxo.type === OutputType.Taproot && utxo.cooperative !== true) {
-      const tapLeaf = isRefund
-        ? utxo.swapTree!.refundLeaf
-        : utxo.swapTree!.claimLeaf;
-      const sigHash = hashForWitnessV1(
-        utxos,
-        tx,
-        index,
-        tapleafHash(tapLeaf),
-        Transaction.SIGHASH_DEFAULT,
-      );
+    if (utxo.type === OutputType.Taproot) {
+      if (utxo.cooperative !== true) {
+        const tapLeaf = isRefund
+          ? utxo.swapTree!.refundLeaf
+          : utxo.swapTree!.claimLeaf;
+        const sigHash = hashForWitnessV1(
+          utxos,
+          tx,
+          index,
+          tapleafHash(tapLeaf),
+          Transaction.SIGHASH_DEFAULT,
+        );
 
-      const signature = utxo.keys.signSchnorr(sigHash);
-      const witness = isRefund ? [signature] : [signature, utxo.preimage];
+        const signature = utxo.keys.signSchnorr(sigHash);
+        const witness = isRefund ? [signature] : [signature, utxo.preimage];
 
-      tx.setWitness(
-        index,
-        witness.concat([
-          tapLeaf.output,
-          createControlBlock(
-            toHashTree(utxo.swapTree!.tree),
-            tapLeaf,
-            utxo.internalKey!,
-          ),
-        ]),
-      );
+        tx.setWitness(
+          index,
+          witness.concat([
+            tapLeaf.output,
+            createControlBlock(
+              toHashTree(utxo.swapTree!.tree),
+              tapLeaf,
+              utxo.internalKey!,
+            ),
+          ]),
+        );
+      } else {
+        // Stub the signature to allow for accurate fee estimations
+        tx.setWitness(index, [dummyTaprootSignature]);
+      }
     } else if (
       utxo.type === OutputType.Bech32 ||
       utxo.type === OutputType.Compatibility
diff --git a/lib/swap/SwapTreeSerializer.ts b/lib/swap/SwapTreeSerializer.ts
index 8178448..ba5f6b4 100644
--- a/lib/swap/SwapTreeSerializer.ts
+++ b/lib/swap/SwapTreeSerializer.ts
@@ -7,6 +7,11 @@ type SerializedLeaf = {
   output: string;
 };
 
+type SerializedTree = {
+  claimLeaf: SerializedLeaf;
+  refundLeaf: SerializedLeaf;
+};
+
 const serializeLeaf = (leaf: Tapleaf): SerializedLeaf => ({
   version: leaf.version,
   output: getHexString(leaf.output),
@@ -17,14 +22,15 @@ const deserializeLeaf = (leaf: SerializedLeaf): Tapleaf => ({
   output: getHexBuffer(leaf.output),
 });
 
-export const serializeSwapTree = (tree: SwapTree): string =>
-  JSON.stringify({
-    claimLeaf: serializeLeaf(tree.claimLeaf),
-    refundLeaf: serializeLeaf(tree.refundLeaf),
-  });
+export const serializeSwapTree = (tree: SwapTree): SerializedTree => ({
+  claimLeaf: serializeLeaf(tree.claimLeaf),
+  refundLeaf: serializeLeaf(tree.refundLeaf),
+});
 
-export const deserializeSwapTree = (tree: string): SwapTree => {
-  const parsed = JSON.parse(tree);
+export const deserializeSwapTree = (
+  tree: string | SerializedTree,
+): SwapTree => {
+  const parsed = typeof tree === 'string' ? JSON.parse(tree) : tree;
 
   const res = {
     claimLeaf: deserializeLeaf(parsed.claimLeaf),
@@ -36,3 +42,5 @@ export const deserializeSwapTree = (tree: string): SwapTree => {
     tree: swapLeafsToTree(res.claimLeaf, res.refundLeaf),
   };
 };
+
+export { SerializedLeaf, SerializedTree };
diff --git a/package-lock.json b/package-lock.json
index 763fa0a..143fce9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "boltz-core",
-  "version": "1.0.5",
+  "version": "2.0.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "boltz-core",
-      "version": "1.0.5",
+      "version": "2.0.0",
       "license": "AGPL-3.0",
       "dependencies": {
         "@boltz/bitcoin-ops": "^2.0.0",
diff --git a/package.json b/package.json
index 748eb2c..61cfac5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "boltz-core",
-  "version": "1.0.5",
+  "version": "2.0.0",
   "description": "Core library of Boltz",
   "main": "dist/lib/Boltz.js",
   "scripts": {
diff --git a/test/integration/liquid/Utils.spec.ts b/test/integration/liquid/Utils.spec.ts
index 5f0ee71..07cc2a8 100644
--- a/test/integration/liquid/Utils.spec.ts
+++ b/test/integration/liquid/Utils.spec.ts
@@ -31,6 +31,30 @@ describe('Liquid Utils', () => {
     ).toEqual(amount);
   });
 
+  test('should decode unconfidential outputs when a blinding key is provided', async () => {
+    const addr = await elementsClient.getNewAddress();
+    const { scriptPubKey, unconfidentialAddress } =
+      address.fromConfidential(addr);
+
+    const blindingKey = getHexBuffer(
+      await elementsClient.dumpBlindingKey(addr),
+    );
+
+    const amount = 123_321;
+    const tx = Transaction.fromHex(
+      await elementsClient.getRawTransaction(
+        await elementsClient.sendToAddress(unconfidentialAddress, amount),
+      ),
+    );
+
+    expect(
+      getOutputValue({
+        ...tx.outs.find((out) => out.script.equals(scriptPubKey!))!,
+        blindingPrivateKey: blindingKey,
+      }),
+    ).toEqual(amount);
+  });
+
   test('should decode confidential outputs', async () => {
     const addr = await elementsClient.getNewAddress();
     expect(addr.startsWith('el1')).toBeTruthy();
diff --git a/test/integration/liquid/swapTree/SwapTreeClaim.spec.ts b/test/integration/liquid/swapTree/SwapTreeClaim.spec.ts
index 6357615..0cb75ff 100644
--- a/test/integration/liquid/swapTree/SwapTreeClaim.spec.ts
+++ b/test/integration/liquid/swapTree/SwapTreeClaim.spec.ts
@@ -68,10 +68,12 @@ describe.each`
         blindingKey,
       );
 
+      // Check the dummy signature
+      expect(tx.ins[0].witness).toHaveLength(1);
+      expect(tx.ins[0].witness[0].equals(Buffer.alloc(64))).toEqual(true);
+
       const theirNonce = secp.musig.nonceGen(randomBytes(32));
-      musig!.aggregateNonces(
-        new Map([[refundKeys.publicKey, theirNonce.pubNonce]]),
-      );
+      musig!.aggregateNonces([[refundKeys.publicKey, theirNonce.pubNonce]]);
       musig!.initializeSession(
         hashForWitnessV1(Networks.liquidRegtest, [utxo], tx, 0),
       );
diff --git a/test/integration/musig/Musig.spec.ts b/test/integration/musig/Musig.spec.ts
index 79f2824..9aa46c6 100644
--- a/test/integration/musig/Musig.spec.ts
+++ b/test/integration/musig/Musig.spec.ts
@@ -86,7 +86,7 @@ describe('Musig', () => {
 
     // Create signature
     const theirNonce = secp.musig.nonceGen(randomBytes(32));
-    musig.aggregateNonces(new Map([[theirKey.publicKey, theirNonce.pubNonce]]));
+    musig.aggregateNonces([[theirKey.publicKey, theirNonce.pubNonce]]);
     musig.initializeSession(sigHash);
     musig.signPartial();
     musig.addPartial(
diff --git a/test/integration/swapTree/SwapTreeClaim.spec.ts b/test/integration/swapTree/SwapTreeClaim.spec.ts
index 46280d0..0a205fd 100644
--- a/test/integration/swapTree/SwapTreeClaim.spec.ts
+++ b/test/integration/swapTree/SwapTreeClaim.spec.ts
@@ -39,10 +39,12 @@ describe.each`
 
     const tx = constructClaimTransaction([utxo], destinationOutput, 1_000);
 
+    // Check the dummy signature
+    expect(tx.ins[0].witness).toHaveLength(1);
+    expect(tx.ins[0].witness[0].equals(Buffer.alloc(64))).toEqual(true);
+
     const theirNonce = secp.musig.nonceGen(randomBytes(32));
-    musig!.aggregateNonces(
-      new Map([[refundKeys.publicKey, theirNonce.pubNonce]]),
-    );
+    musig!.aggregateNonces([[refundKeys.publicKey, theirNonce.pubNonce]]);
     musig!.initializeSession(hashForWitnessV1([utxo], tx, 0));
     musig!.signPartial();
     musig!.addPartial(
diff --git a/test/unit/musig/Musig.spec.ts b/test/unit/musig/Musig.spec.ts
index 081b8a3..3238715 100644
--- a/test/unit/musig/Musig.spec.ts
+++ b/test/unit/musig/Musig.spec.ts
@@ -170,7 +170,7 @@ describe('Musig', () => {
       [pubKeys[1], secp.musig.nonceGen(randomBytes(32)).pubNonce],
       [pubKeys[2], secp.musig.nonceGen(randomBytes(32)).pubNonce],
     ]);
-    musig.aggregateNonces(nonces);
+    musig.aggregateNonces(Array.from(nonces.entries()));
 
     expect(musig['pubNonces']).toEqual([
       musig.getPublicNonce(),
@@ -192,7 +192,7 @@ describe('Musig', () => {
       ourKey.publicKey,
       ECPair.makeRandom().publicKey,
     ]);
-    expect(() => musig.aggregateNonces(new Map())).toThrow(
+    expect(() => musig.aggregateNonces([])).toThrow(
       'number of nonces != number of public keys',
     );
   });
@@ -205,15 +205,13 @@ describe('Musig', () => {
     ]);
 
     expect(() =>
-      musig.aggregateNonces(
-        new Map([
-          [ourKey.publicKey, musig.getPublicNonce()],
-          [
-            ECPair.makeRandom().publicKey,
-            secp.musig.nonceGen(randomBytes(32)).pubNonce,
-          ],
-        ]),
-      ),
+      musig.aggregateNonces([
+        [ourKey.publicKey, musig.getPublicNonce()],
+        [
+          ECPair.makeRandom().publicKey,
+          secp.musig.nonceGen(randomBytes(32)).pubNonce,
+        ],
+      ]),
     ).toThrow(
       `could not find nonce for public key ${Buffer.from(
         musig['publicKeys'][1],
@@ -527,12 +525,10 @@ describe('Musig', () => {
     }
 
     musig.aggregateNonces(
-      new Map<Uint8Array, Uint8Array>(
-        counterparties.map((party) => [
-          party.key.publicKey,
-          party.nonce.pubNonce,
-        ]),
-      ),
+      counterparties.map((party) => [
+        party.key.publicKey,
+        party.nonce.pubNonce,
+      ]),
     );
 
     musig.initializeSession(toSign);
diff --git a/test/unit/swap/Claim.spec.ts b/test/unit/swap/Claim.spec.ts
index 8c37001..1b340b9 100644
--- a/test/unit/swap/Claim.spec.ts
+++ b/test/unit/swap/Claim.spec.ts
@@ -1,3 +1,4 @@
+import { randomBytes } from 'crypto';
 import { ECPair } from '../Utils';
 import { getHexBuffer } from '../../../lib/Utils';
 import { OutputType } from '../../../lib/consts/Enums';
@@ -5,7 +6,6 @@ import { p2trOutput } from '../../../lib/swap/Scripts';
 import { ClaimDetails } from '../../../lib/consts/Types';
 import { claimDetails, claimDetailsMap } from './ClaimDetails';
 import { constructClaimTransaction } from '../../../lib/swap/Claim';
-import { randomBytes } from 'crypto';
 
 describe('Claim', () => {
   const testClaim = (utxos: ClaimDetails[], fee: number) => {
diff --git a/test/unit/swap/SwapTreeSerializer.spec.ts b/test/unit/swap/SwapTreeSerializer.spec.ts
index 71ebb01..de6b27f 100644
--- a/test/unit/swap/SwapTreeSerializer.spec.ts
+++ b/test/unit/swap/SwapTreeSerializer.spec.ts
@@ -45,4 +45,18 @@ describe('SwapTreeSerializer', () => {
     const serialized = serializeSwapTree(tree);
     expect(deserializeSwapTree(serialized)).toEqual(tree);
   });
+
+  test.each`
+    isLiquid
+    ${false}
+    ${true}
+  `(
+    'should deserialize stringified swap trees (isLiquid: $isLiquid)',
+    ({ isLiquid }) => {
+      const tree = createTree(isLiquid);
+
+      const serialized = JSON.stringify(serializeSwapTree(tree));
+      expect(deserializeSwapTree(serialized)).toEqual(tree);
+    },
+  );
 });
diff --git a/test/unit/swap/__snapshots__/SwapTreeSerializer.spec.ts.snap b/test/unit/swap/__snapshots__/SwapTreeSerializer.spec.ts.snap
index 788882a..a6e5626 100644
--- a/test/unit/swap/__snapshots__/SwapTreeSerializer.spec.ts.snap
+++ b/test/unit/swap/__snapshots__/SwapTreeSerializer.spec.ts.snap
@@ -1,5 +1,27 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`SwapTreeSerializer should serialize swap trees (isLiquid: false) 1`] = `"{"claimLeaf":{"version":192,"output":"a914124b4a204760441dc802c445d4987ba1bd967e6d882083d1d7b47cd4163db23e633b81a3a6906a99a99e5acbbe4df29172f42621668cac"},"refundLeaf":{"version":192,"output":"2010154f49ec6656fd70d28abd9bbb71633da124eda324b1bf2b28dd9686915087ad017bb1"}}"`;
+exports[`SwapTreeSerializer should serialize swap trees (isLiquid: false) 1`] = `
+{
+  "claimLeaf": {
+    "output": "a914124b4a204760441dc802c445d4987ba1bd967e6d882083d1d7b47cd4163db23e633b81a3a6906a99a99e5acbbe4df29172f42621668cac",
+    "version": 192,
+  },
+  "refundLeaf": {
+    "output": "2010154f49ec6656fd70d28abd9bbb71633da124eda324b1bf2b28dd9686915087ad017bb1",
+    "version": 192,
+  },
+}
+`;
 
-exports[`SwapTreeSerializer should serialize swap trees (isLiquid: true) 1`] = `"{"claimLeaf":{"version":196,"output":"a914124b4a204760441dc802c445d4987ba1bd967e6d882083d1d7b47cd4163db23e633b81a3a6906a99a99e5acbbe4df29172f42621668cac"},"refundLeaf":{"version":196,"output":"2010154f49ec6656fd70d28abd9bbb71633da124eda324b1bf2b28dd9686915087ad017bb1"}}"`;
+exports[`SwapTreeSerializer should serialize swap trees (isLiquid: true) 1`] = `
+{
+  "claimLeaf": {
+    "output": "a914124b4a204760441dc802c445d4987ba1bd967e6d882083d1d7b47cd4163db23e633b81a3a6906a99a99e5acbbe4df29172f42621668cac",
+    "version": 196,
+  },
+  "refundLeaf": {
+    "output": "2010154f49ec6656fd70d28abd9bbb71633da124eda324b1bf2b28dd9686915087ad017bb1",
+    "version": 196,
+  },
+}
+`;