scalebit / OpenTap

A Node.js Lib for tapscript building, provide module, use case and demo
4 stars 0 forks source link

MuSig2 implementation using @cmdcode/musig2 and bitcoinjs-lib #1

Closed iashishanand closed 1 month ago

iashishanand commented 1 month ago

I am trying to implement MuSig transaction using @cmdcode/musig2 and bitcoinjs-lib library. @UIZorrot I have followed your issue thread on bitcoinjs-lib. But I am getting this error constantly.

Error: Mandatory-script-verify-flag-failed (Invalid Schnorr signature)

import * as musig from "@cmdcode/musig2";
import bitcoin from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";
import { Buff } from "@cmdcode/buff";
import { schnorr } from "@noble/curves/secp256k1";
import { Buffer } from "node:buffer";
import { Transaction } from "bitcoinjs-lib";

bitcoin.initEccLib(ecc);
const TESTNET = bitcoin.networks.testnet;

// Convert the public key to x-only format for Taproot
const toXOnly = (pubKey) =>
    pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);

const wallets = [
    {
        name: "alice",
        sec_key: "6ccc11c46751edab5d9ba2acf68d0133fb67b68fa701c2ab8eddd3d98efe5595",
        pub_key: "633d066237862db2292981e8b1e191c15b6a853a8083160076a24168f83a9d57",
        sec_nonce:
            "cab3a3a5bf918e75e6835a37e8988a99b90e15d32efa67a31c5992a1d449754462a3d3468cb0d14c1c3eb4c3c3743c29c821ea2e969fcb847cff6ab137f4555f",
        pub_nonce:
            "33fce8b07af2a03c9ea546c2c231cc8a5668cb28dc95290b3650fdaeaa1ef571e1cc83c729b3838fadc9dd3d6fecf58115e890974e0187d3c75f1ca52cb3759a",
    },
    {
        name: "bob",
        sec_key: "126086e85273e375ec6a059266c92f30ba070696334633624ace6a3bf5777f4b",
        pub_key: "d6be41e01c23fb2c57c1664c58998710f88174a41f1c69fd27be91518a5a84dd",
        sec_nonce:
            "28b75dc04532f69c64ac974eae8d43bd5db60b10410fc4838ae63f80a255108baafbcc53735b698dfce5f57bbbcce233fd9bc220470be343ea2b2b77e17c471f",
        pub_nonce:
            "95f5b0ebb638e5f0d374c17c7fb3d63126a20ffb2ef1b81c2c0afeee04c5e8dc016e291f06ebc7748c7f5e084fd8c12b655b602ebe5001e627334a225fa34350",
    },
];

// Collect public keys and nonces from all signers.
const group_keys = wallets.map((e) => e.pub_key);
const group_nonces = wallets.map((e) => e.pub_nonce);

// Get the combined public key
const { group_pubkey } = musig.get_key_ctx(group_keys);
const internalPubkey = toXOnly(Buffer.from(group_pubkey, "hex"));

// Create a Taproot address from the x-only public key
const p2pktr = bitcoin.payments.p2tr({
    internalPubkey,
    network: TESTNET,
});

console.log("Taproot address : ", p2pktr.address);

const utxo = {
    txid: "c4ce128062c3d0165cbf58be8d02d771a578bb97864cc217a528d8b72a6554af",
    vout: 1,
    value: 500000,
};

const reciever = {
    value: 10000,
    address: p2pktr.address,
};

// Building a new transaction
let transaction = new Transaction();
transaction.version = 2;
transaction.addInput(Buffer.from(utxo.txid, "hex").reverse(), utxo.vout);
transaction.addOutput(bitcoin.address.toOutputScript(reciever.address, TESTNET), reciever.value);

const signatureHash = transaction.hashForWitnessV1(
    0, // Input index
    [p2pktr.output], // Prevouts
    [utxo.value], // Amounts
    Transaction.SIGHASH_DEFAULT // Sighash type
);

let message = signatureHash;
console.log("Message : ", message.toString('hex'));

// Combine all your collected keys into a signing session.
const ctx = musig.get_ctx(group_keys, group_nonces, Buff.from(message));

// Each member creates their own partial signature,
// using their own computed signing session.
const group_sigs = wallets.map((wallet) => {
    return musig.musign(ctx, wallet.sec_key, wallet.sec_nonce);
});

// Combine all the partial signatures into our final signature.
const signature = musig.combine_psigs(ctx, group_sigs);
console.log("Signature : ", signature);

let tapKeySig = Buffer.from(signature, 'hex');

// Check if the signature is valid.
const isValid = schnorr.verify(tapKeySig, message, internalPubkey);
if (isValid) {
    console.log("The signature is valid.");
} else {
    console.log("The signature is NOT valid.");
}

transaction.ins[0].witness = [tapKeySig];

// Broadcasting the transaction
const txHex = transaction.toHex();
console.log(`Broadcasting Transaction Hex: ${txHex}`);

Any help would be appreciated.

UIZorrot commented 1 month ago

I suggest you follow the start_musig_txbuilder() in the tap-node/src/template, it is a working example.

Btw, let tapKeySig = Buffer.from(signature, 'hex'); is not seemed right, delete hex

iashishanand commented 1 month ago

Thanks a lot, deleting hex from let tapKeySig = Buffer.from(signature, 'hex'); worked. Also you have any lead on adaptor signature implementation in javascript or can we use/modify @cmdcode/musig2 library to achieve the same. I am also working on implementing DLC using adaptor signature, but did not find any easy to understand implementation out there.

UIZorrot commented 1 month ago

you welcome. At the best of my knowledge, here is the only repo that use musig2 in bitcoins (I'm actually using the @cmdcode/musig2, but its working in taproot). DLCs is mostly involved with the off-chain psbt, I'm reading the specification, if you are interested, you can join the build of DLCs.

iashishanand commented 1 month ago

Hi @UIZorrot, I tried running a quick experiment on @cmdcode/musig2 can you check and verify whether this is a correct approach. For the purpose of experiment I hardcoded these:

const ADAPTOR_POINT = "633d066237862db2292981e8b1e191c15b6a853a8083160076a24168f83a9d57";
const ADAPTOR_SECRET = "6ccc11c46751edab5d9ba2acf68d0133fb67b68fa701c2ab8eddd3d98efe5595";

I changed the get_challenge() to make the signature invalid by adding adaptor point to group_rx:

export function get_challenge(group_rx, group_pub, message) {
    // Convert group_rx to a point
    const R = pt.lift_x(group_rx);
    // Convert adaptor point to a point
    const T = pt.lift_x(Buff.hex(ADAPTOR_POINT));
    // Add R and T
    const R_plus_T = pt.add(R, T);
    // Convert back to x-coordinate
    const combined_x = pt.to_bytes(R_plus_T).slice(1);

    const grx = convert_32b(combined_x);
    const gpx = convert_32b(group_pub);
    // Create the challenge pre-image
    const preimg = Buff.join([grx, gpx, message]);
    // Return the challenge hash
    return hash340('BIP0340/challenge', preimg);
}

And I changed the implementation for combine_psigs() to add the adaptor secret to the combined signature at end and as well as modifying the group_rx value to make the signature valid:

export function combine_psigs(context, signatures) {
    const { challenge, group_state, group_rx } = context;
    const { parity, tweak } = group_state;
    const sigs = signatures
        .map(e => parse_psig(e))
        .map(e => e.sig);
    const s = combine_s(sigs);
    const e = challenge.big;
    const a = e * parity * tweak;
    const adaptor_secret = Buff.hex(ADAPTOR_SECRET).big;
    const sig = math.mod_n(s + a + adaptor_secret);

    // Convert group_rx to a point
    const R = pt.lift_x(group_rx);
    // Convert adaptor point to a point
    const T = pt.lift_x(Buff.hex(ADAPTOR_POINT));
    // Add R and T
    const R_plus_T = pt.add(R, T);
    // Convert back to x-coordinate
    const combined_rx = pt.to_bytes(R_plus_T).slice(1);

    // Return the combined signature with R+T as the nonce
    return Buff.join([
        keys.convert_32b(combined_rx),
        Buff.big(sig, 32)
    ]);
}

These changes were directly made to the node_modules file, and it's giving weird result sometimes it passes the signature test for MuSig2 as well as @noble/curve library. But sometimes it give that signatures are wrong for the same set of values.

UIZorrot commented 1 month ago

It looks like good, maybe there are some buffer conversion problem?