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) => { - 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) => 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( - 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, + }, +} +`;