cmdruid / musig2

A simple and easy-to-use musig2 library, written in typescript.
https://www.npmjs.com/package/@cmdcode/musig2
Creative Commons Zero v1.0 Universal
6 stars 6 forks source link

Adaptor Signature #3

Open iashishanand opened 3 months ago

iashishanand commented 3 months ago

Hi, I have been wondering if there is any way to modify the library to implement Adaptor Signature, where an adaptor point (T where T = t * G) will be used with the combined nonce to make the signature invalid. But once the adaptor secret (t) is revealed we can add it to the combined signatures' = s + t to make it a valid BIP340 signature. Similar to the public key tweaking mentioned in the image.

adaptor

My ultimate goal is to achieve DLC using this.

cmdruid commented 3 months ago

Try adding a key tweak T to the group pubkey, then have each member create a partial signatures as normal.

The signature can be aggregated without t for verification, and later re-aggregated with t once revealed.

Hopefully that works.

iashishanand commented 3 months ago

Hi @cmdruid I tried running a quick experiment 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.

cmdruid commented 3 months ago

The signatures will randomly pass/fail because the parity of group_rx may flip when you add the tweak. You need to handle the negation properly using an accumulator.

The group pubkey has tweaking fully implemented using an accumulator to track parity and negation. You can specify a number of tweaks when generating the group pubkey, when signing, and when doing signature aggregation.

So you could add a tweak to the group pubkey and sign with it, but keep the tweak secret so the aggregate signature will be missing the tweak. (you can still add the tweak later).

I have the accumulator implemented for tweaking the nonce value, but I did not add an interface for supplying nonce tweaks in the main MusigOptions interface. Do you need to tweak the nonce specifically?

iashishanand commented 3 months ago

Hi @cmdruid,

Thank you for taking the time to review my issues. I've been following resources on implementing DLC and noticed that they all tweak the nonce, which is why I was trying the same approach. If you have any example code demonstrating how to use group public key tweaking to achieve similar results, it would be very helpful.

Additionally, could you guide me on how to resolve the following issue?

The signatures will randomly pass or fail because the parity of group_rx may flip when you add the tweak. You need to handle the negation properly using an accumulator.

Like how can we handle negation and will I need to made changes everywhere.

if (!pt.is_even(R_plus_T)) {
        R_plus_T.y = mod_n(-R_plus_T.y);
        combined_rx = pt.to_bytes(R_plus_T).slice(1);

}

I tried adding above logic it did not work.

cmdruid commented 3 months ago

I have looked into this some more, and came to the conclusion that in order to do this properly, I have to integrate it into the nonce aggregation step.

I hacked together a rough prototype in the development branch. I should have it done in a day or two.

iashishanand commented 3 months ago

Thank you for looking into this further. I will check the development branch.

cmdruid commented 3 months ago

The updated changes are live in the development branch.

Check out the test case here:

https://github.com/cmdruid/musig2/blob/development/test/src/dlc.test.ts

Basically you can pass an array of public keys in the MusigOptions object (labeled nonce_tweaks). These tweaks will be applied to the group pubnonce.

You can verify the un-tweaked signature S and tweaked R values are correct with the verify_adapter_sig method.

Once all the secret values are known, you can add them to the signature using the add_sig_adapters method.

Let me know if you have any questions or run into issues.

iashishanand commented 3 months ago

Hi @cmdruid , I have tested the code. It is performing correctly. Thanks for the help!

iashishanand commented 2 months ago

Hi @cmdruid I tried implementing a DLC prototype using the adaptor signature implementation but I some signature still pass/fails randomly.

Oracle Implementation using https://github.com/cmdruid/crypto-tools:

import { hash, keys, Field, Point } from "@cmdcode/crypto-tools";
import prompts from "prompts";

class Oracle {
  constructor() {
    this.secKey = Field.mod(keys.gen_seckey());
    this.pubKey = Point.from_x(keys.get_pubkey(this.secKey, true));
    this.secNonce = null;
    this.pubNonce = null;
    this.outcomes = [];
  }

  async createNewEvent() {
    const confirmation = await prompts({
      type: "confirm",
      name: "confirm",
      message: "Do you want to create a new event?",
    });

    if (!confirmation.confirm) {
      console.log("Event creation canceled.");
      return;
    }
    this.secNonce = Field.mod(keys.gen_seckey());
    this.pubNonce = Point.from_x(keys.get_pubkey(this.secNonce, true));

    let response = await prompts({
      type: "text",
      name: "outcome",
      message: "Enter the outcome for the event (press enter to finish):",
    });

    while (response.outcome) {
      const adaptorPoint = this.calculateAdaptorPoint(response.outcome);
      this.outcomes.push({
        message: response.outcome,
        adaptor_point: adaptorPoint.hex,
      });
      response = await prompts({
        type: "text",
        name: "outcome",
        message:
          "Enter the next outcome for the event (press enter to finish):",
      });
    }

    return JSON.stringify({
        pub_key: this.pubKey.x.hex,
        pub_nonce: this.pubNonce.hex,
        outcomes: this.outcomes,
      });
  }

  async signOutcome() {
    const confirmation = await prompts({
      type: "confirm",
      name: "confirm",
      message: "Do you want to sign an outcome?",
    });

    if (!confirmation.confirm) {
      console.log("Outcome signing canceled.");
      return;
    }
    if (!this.secNonce) {
      throw new Error("Event not published yet. Call publishEvent() first.");
    }

    const choices = this.outcomes.map((outcome, index) => {
      return { title: outcome.message, value: index };
    });

    const response = await prompts({
      type: "select",
      name: "selectedOutcome",
      message: "Choose the outcome to sign:",
      choices: choices,
    });

    const selectedOutcome = this.outcomes[response.selectedOutcome];

    const encoder = new TextEncoder();
    const msg = encoder.encode(selectedOutcome.message);

    // const msg = Buff.bytes(Buff.hex(selectedOutcome));
    const d = this.secKey.negated;
    const k = this.secNonce.negated.big;
    const ch = hash.hash340(
      "BIP0340/challenge",
      this.pubNonce.x,
      this.pubKey.x,
      msg
    );
    const c = Field.mod(ch);
    const adaptorSecret = Field.mod(k + c.big * d.big);

    const adaptorPoint = this.calculateAdaptorPoint(selectedOutcome.message);

    if (!this.verifyAdaptorPair(adaptorPoint, adaptorSecret)) {
      throw new Error("Adaptor pair verification failed.");
    }

    return adaptorSecret.hex;
  }

  calculateAdaptorPoint(message) {
    if (!this.pubNonce) {
      throw new Error("Event not published yet. Call publishEvent() first.");
    }

    const encoder = new TextEncoder();
    const msg = encoder.encode(message);
    const ch = hash.hash340(
      "BIP0340/challenge",
      this.pubNonce.x,
      this.pubKey.x,
      msg
    );
    const c = Field.mod(ch);
    const eP = this.pubKey.mul(c.big);
    const sG = this.pubNonce.add(eP);
    return sG.x;
  }

  verifyAdaptorPair(adaptorPoint, adaptorSecret) {
    const computedPoint = Field.mod(adaptorSecret).point;
    return computedPoint.x.equals(adaptorPoint);
  }
}

async function testOracle() {
  const oracle = new Oracle();

  const eventDetails = await oracle.createNewEvent();
  console.log("New event created with details:\n", eventDetails);

  const adaptorSecret = await oracle.signOutcome();
  console.log("Adaptor secret:", adaptorSecret);
}

testOracle();

DLC implementation:

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";
import fs from 'fs';
import prompts from 'prompts';

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

// Read config file
const config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
const { wallets, utxos, oracle } = config;

// Calculate total funds from UTXOs
const totalFunds = utxos.reduce((sum, utxo) => sum + utxo.value, 0);

// 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);

// Create a Taproot address from the x-only public key
const p2pktr = bitcoin.payments.p2tr({
    pubkey: Buffer.from(group_pubkey, "hex"),
    network,
});

// Function to create and sign a transaction
async function createAndSignTransaction(outcome) {
    console.log(`Creating transaction for outcome: ${outcome.message}`);

    const receiver = [];
    let remainingFunds = totalFunds;

    // Prompt user for output values for each wallet
    for (let i = 0; i < wallets.length; i++) {
        const wallet = wallets[i];

        const value = await prompts({
            type: 'number',
            name: 'value',
            message: `Enter the value for ${wallet.name}'s output (remaining ${remainingFunds} satoshis):`,
            validate: value => value <= remainingFunds ? true : `Value must not exceed ${remainingFunds} satoshis`
        });

        receiver.push({
            value: value.value,
            address: wallet.address,
        });

        remainingFunds -= value.value;
    }

    // Building a new transaction
    let transaction = new Transaction();
    transaction.version = 2;

    // Add inputs
    for (const input of utxos) {
        transaction.addInput(Buffer.from(input.txid, "hex").reverse(), input.vout);
    }

    // Add outputs
    for (const output of receiver) {
        transaction.addOutput(
            bitcoin.address.toOutputScript(output.address, network),
            output.value
        );
    }

    // Prepare prevouts and amounts for signature hash calculation
    const prevouts = utxos.map(() => p2pktr.output);
    const amounts = utxos.map(input => input.value);

    // Calculate the signature hash for all inputs
    const signatureHashes = utxos.map((_, index) =>
        transaction.hashForWitnessV1(
            index,
            prevouts,
            amounts,
            Transaction.SIGHASH_DEFAULT
        )
    );

    // Configure the musig options to include the key tweak for this specific outcome.
    const options = { nonce_tweaks: [outcome.adaptor_point] };

    const signatures = [];
    const contexts = [];

    // Sign all inputs
    for (let i = 0; i < utxos.length; i++) {
        let message = signatureHashes[i];
        // console.log(`Message for input ${i}: ${message.toString("hex")}`);

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

        // 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);
        signatures.push(signature);

        // Add the signature to the transaction
        transaction.ins[i].witness = [Buffer.from(signature)];
    }

    return { transaction, signatures, contexts };
}

// Create transactions for each outcome
const transactions = {};
for (const outcome of oracle.outcomes) {
    transactions[outcome.message] = await createAndSignTransaction(outcome);
}

// Print transaction hex for all outcomes
console.log("\nTransaction hex for all outcomes:");
for (const [outcome, { transaction }] of Object.entries(transactions)) {
    console.log(`Outcome "${outcome}":`);
    console.log(transaction.toHex());
    console.log(); // Empty line for readability
}

// Prompt user for outcome and adaptor secret
const userInput = await prompts([
    {
        type: 'select',
        name: 'outcome',
        message: 'Select the outcome:',
        choices: oracle.outcomes.map(o => ({ title: o.message, value: o.message })),
    },
    {
        type: 'text',
        name: 'adaptorSecret',
        message: 'Enter the adaptor secret:',
    },
]);

// Get the selected transaction and make it valid
const { transaction: selectedTransaction, signatures, contexts } = transactions[userInput.outcome];
const adaptorSecret = Buffer.from(userInput.adaptorSecret, 'hex');

// Apply adaptor secret to make the transaction valid
for (let i = 0; i < utxos.length; i++) {
    const signature = signatures[i];
    const ctx = contexts[i];
    const adaptedSig = musig.add_sig_adapters(ctx, signature, [adaptorSecret]);

    // Verify the adapted signature
    const isValid = schnorr.verify(adaptedSig, ctx.message, ctx.group_pubkey);
    if (isValid) {
        console.log(`The signature for input ${i} is valid.`);
        selectedTransaction.ins[i].witness = [Buffer.from(adaptedSig)];
    } else {
        console.log(`The signature for input ${i} is NOT valid.`);
    }
}

// Output the final transaction hex
const txHex = selectedTransaction.toHex();
console.log(`Valid Transaction Hex for outcome "${userInput.outcome}": ${txHex}`);

Structure of config.json used to coordinate the process between users and oracle:

{
    "wallets": [
        {
            "name": "Alice",
            "address": "tb1p6ju7ggvuh98atfq9k9ykp48xasxfc5ftwu5elpg5yeyaedejs7dsth8x6v",
            "sec_key": "94da10c04385bac56dcf31d57325efb53e3725412ec2578eb83ae0e602b5cc73",
            "pub_key": "d4b9e4219cb94fd5a405b14960d4e6ec0c9c512b77299f85142649dcb732879b",
            "sec_nonce": "91b0ed858b9df382dfe26c3647b9d52725f84abbae0dd9aacce833e1123fe1109a86cfc5b9a707f7e873f651efa2d1a0d8eb0c6cc63f095d1775108638d18577",
            "pub_nonce": "94a8d3ad0b24e243bd33153517d6e646394d7e0254bd220ece8da52b160d73b08321aff502a4d54d811c955f51b0af3cc47270809527de5b047efe9c8d7e592e"
        },
        {
            "name": "Bob",
            "address": "tb1pxv9zqjexayp3s3cwr4ufnuldvrwj4xj67hgdtmf897uua88vfseqsx27nz",
            "sec_key": "e871a783c5c4a9f34a3b775776eb2b9299b096549b2c4f134298a06884d1762c",
            "pub_key": "330a204b26e90318470e1d7899f3ed60dd2a9a5af5d0d5ed272fb9ce9cec4c32",
            "sec_nonce": "a8e0867f0c83d3d574c647413d87438d43a9ae02833a8e9acd131b284dce477068dd3364af5d9389c3513b1043b92ff94624ba96433e498cbed0605647f70067",
            "pub_nonce": "f4af3b2e0f8285ff4af5a7b010455ebd0fad282860b276d75d3dbed2e924b7614a68776610fa0f546cf5ff04f858442cfbb3cf466b557063304df1ee44e172c9"
        }
    ],
    "utxos": [
        {
            "txid": "a9b7652983dc94d68adc91d8c6322a9ae2173acfa4f071026589920fcce41a8b",
            "vout": 1,
            "value": 50000
        },
        {
            "txid": "eeb464dfeb785d83afbad8cd4b34b95a70f75e4ade4f4e03ce5e36fb603f65dc",
            "vout": 1,
            "value": 50000
        }
    ],
    "oracle": {
        "pub_key": "26df7989486448622ca09fa937e7901d2cebd930635f760f16a44be7b0627736",
        "pub_nonce": "029460c8f315d4cc9dd8a70a9baaf8316a34c7267c742ee520d4ffdcede3482735",
        "outcomes": [
            {
                "message": "rain",
                "adaptor_point": "9cc79823499e3747accb9230927df1f2d9db1548854c651de47310ea93948115"
            },
            {
                "message": "no rain",
                "adaptor_point": "8a144eccc045ec39ea073f457b66aba138fbcdf8fd30e911449a82acd150e68b"
            }
        ]
    }
}
cmdruid commented 2 months ago

If the signature works some of the time but fails randomly (likely 50%) then there is still a parity issue somewhere.

I will review the code and get back to you. In the meantime for the keys.get_seckey() method you are using to create a secret nonce, try setting the boolean flag for even_y to true. This negates the nonce secret in advance, which may fix the issue.

iashishanand commented 2 months ago

Tried making above changes to Oracle's code, but it fails in 1/6 testcases. I believe the issue is with Oracle implementation.

import { hash, keys, Field, Point } from "@cmdcode/crypto-tools";
import prompts from "prompts";

class Oracle {
  constructor() {
    //Set even_y flag to true
    this.secKey = Field.mod(keys.gen_seckey(true));
    this.pubKey = Point.from_x(keys.get_pubkey(this.secKey, true));
    this.secNonce = null;
    this.pubNonce = null;
    this.outcomes = [];
  }

  async createNewEvent() {
    const confirmation = await prompts({
      type: "confirm",
      name: "confirm",
      message: "Do you want to create a new event?",
    });

    if (!confirmation.confirm) {
      console.log("Event creation canceled.");
      return;
    }
    //Set even_y flag to true
    this.secNonce = Field.mod(keys.gen_seckey(true));
    this.pubNonce = Point.from_x(keys.get_pubkey(this.secNonce, true));

    let response = await prompts({
      type: "text",
      name: "outcome",
      message: "Enter the outcome for the event (press enter to finish):",
    });

    while (response.outcome) {
      const adaptorPoint = this.calculateAdaptorPoint(response.outcome);
      this.outcomes.push({
        message: response.outcome,
        adaptor_point: adaptorPoint.hex,
      });
      response = await prompts({
        type: "text",
        name: "outcome",
        message:
          "Enter the next outcome for the event (press enter to finish):",
      });
    }

    return JSON.stringify({
        pub_key: this.pubKey.x.hex,
        pub_nonce: this.pubNonce.hex,
        outcomes: this.outcomes,
      });
  }

  async signOutcome() {
    const confirmation = await prompts({
      type: "confirm",
      name: "confirm",
      message: "Do you want to sign an outcome?",
    });

    if (!confirmation.confirm) {
      console.log("Outcome signing canceled.");
      return;
    }
    if (!this.secNonce) {
      throw new Error("Event not published yet. Call publishEvent() first.");
    }

    const choices = this.outcomes.map((outcome, index) => {
      return { title: outcome.message, value: index };
    });

    const response = await prompts({
      type: "select",
      name: "selectedOutcome",
      message: "Choose the outcome to sign:",
      choices: choices,
    });

    const selectedOutcome = this.outcomes[response.selectedOutcome];

    const encoder = new TextEncoder();
    const msg = encoder.encode(selectedOutcome.message);

    // Remove negation for d and k value. With negation it was giving invalid signature each time.
    const d = this.secKey;
    const k = this.secNonce;
    const ch = hash.hash340(
      "BIP0340/challenge",
      this.pubNonce.x,
      this.pubKey.x,
      msg
    );
    const c = Field.mod(ch);
    const adaptorSecret = Field.mod(k.big + (c.big * d.big));

    const adaptorPoint = this.calculateAdaptorPoint(selectedOutcome.message);

    if (!this.verifyAdaptorPair(adaptorPoint, adaptorSecret)) {
      throw new Error("Adaptor pair verification failed.");
    }

    return adaptorSecret.hex;
  }

  calculateAdaptorPoint(message) {
    if (!this.pubNonce) {
      throw new Error("Event not published yet. Call publishEvent() first.");
    }

    const encoder = new TextEncoder();
    const msg = encoder.encode(message);
    const ch = hash.hash340(
      "BIP0340/challenge",
      this.pubNonce.x,
      this.pubKey.x,
      msg
    );
    const c = Field.mod(ch);
    const eP = this.pubKey.mul(c.big);
    const sG = this.pubNonce.add(eP);
    return sG.x;
  }

  verifyAdaptorPair(adaptorPoint, adaptorSecret) {
    const computedPoint = Field.mod(adaptorSecret).point;
    return computedPoint.x.equals(adaptorPoint);
  }
}

async function testOracle() {
  const oracle = new Oracle();

  const eventDetails = await oracle.createNewEvent();
  console.log("New event created with details:\n", eventDetails);

  const adaptorSecret = await oracle.signOutcome();
  console.log("Adaptor secret:", adaptorSecret);
}

testOracle();

The testcase where it fails:

{
    "wallets": [
        {
            "name": "alice",
            "address": "tb1puqyks3qcy6x5p3vwhglu0h459mhy8c2awcrnvanfdxhd26f7pspqqhz4mv",
            "sec_key": "9ebf004d39c26eb7f9430a86a061c84f895105f6af3601ab9515675c934699fd",
            "pub_key": "e009684418268d40c58eba3fc7deb42eee43e15d760736766969aed5693e0c02",
            "sec_nonce": "1732f1ed2071d5805115471c67aa95f16fb5ce8d89340de05a181eeddd197db368d86d728ba1a329b09f879135e678cdb140aa12ba699980e7ae3d5655f1ea80",
            "pub_nonce": "ea12eca11e04b7b7bb6d64c8cf174ee5d2ced80099ce14bd2e99ab990f8d4725c0ce9c9d7fac29d4dae595ba5554ab4eb79cdb5a6c0ae2c0474b57070ac0ce12"
        },
        {
            "name": "bob",
            "address": "tb1pnaskpu4u95ctqu0dde8j5mrvcy2a8xhwaw6qpkdvcug987wpv8psv0cwx4",
            "sec_key": "118fb1d7a453682bbd36bc0f5d7765aec2d5c614c398ba2b5b222f38cd81bfcb",
            "pub_key": "9f6160f2bc2d30b071ed6e4f2a6c6cc115d39aeeebb400d9acc71053f9c161c3",
            "sec_nonce": "8ea06aa5924f7117329e4c3f27076ac57fe0c81e5228788b5b437d27b00669d5e663068cf50d9859efdd014aa8a3529594fa6fb3a34a7c9f4e654303586775e1",
            "pub_nonce": "c66bf97a6f81e9969a57ddfb9092e5d4997fee2b5f536db229de0bfbf2bc557ffa4140aff4b9a8e838505f10fdf543e2d813be8dd1bf264cee43f8f64e8a53be"
        }
    ],
    "utxos": [
        {
            "txid": "767ed2788c3b904dda6651f19fdd2d77fd67fe98a7546866bffeeeb6effc11ff",
            "vout": 1,
            "value": 5000
        },
        {
            "txid": "5b52b6183e1084e3a4db85e6eae7457cbaa70a62f24fedccdb660de0b1ce94dc",
            "vout": 1,
            "value": 5000
        },
        {
            "txid": "38609dd47bfdb61553e328c06a56f1c134ca8d9725099f44e5409eac3ba3a5f6",
            "vout": 1,
            "value": 50000
        }
    ],
    "oracle": {
        "pub_key": "3a53994dd41c7c485f2fa71369604c5303313c94e330291623b672fdf8cb9a1e",
        "pub_nonce": "02ecc23662f188f89b8e55a15bffeda4a2e48ca98db0f954b50170ee7a68ee08e1",
        "outcomes": [
            {
                "message": "Image",
                "adaptor_point": "9d5e8c79544fe1f95fe8972063bbbdf501365af279802be980bbbfc94dd2e5f8"
            },
            {
                "message": "Video",
                "adaptor_point": "5e8d648b49753a89d66df844ec694479f7f4a9a6183503ac7d2c8a113ab8d6f6"
            }
        ]
    }
}

image

Adaptor secret: 67c87236d52ae942e3241a4cb5465e8b2657d8b7bea64d4d6e54ef9c7dd7dfdf0d

cmdruid commented 2 months ago

I think I found the issue. In my code I am expecting the adapter key to have an even-y coordinate, and in my test case this will always be true because I am using a key generation method that negates keys by default.

However in practice the adapter key can be either parity, and I am not checking the secret to negate it.

I have pushed a fix for this to the development branch. Let me know if this solves the issue!

iashishanand commented 2 months ago

I test the mentioned testcase with the new changes, it's still giving invalid signature. It might be an issue with my oracle implementation.

Tried making above changes to Oracle's code, but it fails in 1/6 testcases. I believe the issue is with Oracle implementation.

import { hash, keys, Field, Point } from "@cmdcode/crypto-tools";
import prompts from "prompts";

class Oracle {
  constructor() {
    //Set even_y flag to true
    this.secKey = Field.mod(keys.gen_seckey(true));
    this.pubKey = Point.from_x(keys.get_pubkey(this.secKey, true));
    this.secNonce = null;
    this.pubNonce = null;
    this.outcomes = [];
  }

  async createNewEvent() {
    const confirmation = await prompts({
      type: "confirm",
      name: "confirm",
      message: "Do you want to create a new event?",
    });

    if (!confirmation.confirm) {
      console.log("Event creation canceled.");
      return;
    }
    //Set even_y flag to true
    this.secNonce = Field.mod(keys.gen_seckey(true));
    this.pubNonce = Point.from_x(keys.get_pubkey(this.secNonce, true));

    let response = await prompts({
      type: "text",
      name: "outcome",
      message: "Enter the outcome for the event (press enter to finish):",
    });

    while (response.outcome) {
      const adaptorPoint = this.calculateAdaptorPoint(response.outcome);
      this.outcomes.push({
        message: response.outcome,
        adaptor_point: adaptorPoint.hex,
      });
      response = await prompts({
        type: "text",
        name: "outcome",
        message:
          "Enter the next outcome for the event (press enter to finish):",
      });
    }

    return JSON.stringify({
        pub_key: this.pubKey.x.hex,
        pub_nonce: this.pubNonce.hex,
        outcomes: this.outcomes,
      });
  }

  async signOutcome() {
    const confirmation = await prompts({
      type: "confirm",
      name: "confirm",
      message: "Do you want to sign an outcome?",
    });

    if (!confirmation.confirm) {
      console.log("Outcome signing canceled.");
      return;
    }
    if (!this.secNonce) {
      throw new Error("Event not published yet. Call publishEvent() first.");
    }

    const choices = this.outcomes.map((outcome, index) => {
      return { title: outcome.message, value: index };
    });

    const response = await prompts({
      type: "select",
      name: "selectedOutcome",
      message: "Choose the outcome to sign:",
      choices: choices,
    });

    const selectedOutcome = this.outcomes[response.selectedOutcome];

    const encoder = new TextEncoder();
    const msg = encoder.encode(selectedOutcome.message);

    // Remove negation for d and k value. With negation it was giving invalid signature each time.
    const d = this.secKey;
    const k = this.secNonce;
    const ch = hash.hash340(
      "BIP0340/challenge",
      this.pubNonce.x,
      this.pubKey.x,
      msg
    );
    const c = Field.mod(ch);
    const adaptorSecret = Field.mod(k.big + (c.big * d.big));

    const adaptorPoint = this.calculateAdaptorPoint(selectedOutcome.message);

    if (!this.verifyAdaptorPair(adaptorPoint, adaptorSecret)) {
      throw new Error("Adaptor pair verification failed.");
    }

    return adaptorSecret.hex;
  }

  calculateAdaptorPoint(message) {
    if (!this.pubNonce) {
      throw new Error("Event not published yet. Call publishEvent() first.");
    }

    const encoder = new TextEncoder();
    const msg = encoder.encode(message);
    const ch = hash.hash340(
      "BIP0340/challenge",
      this.pubNonce.x,
      this.pubKey.x,
      msg
    );
    const c = Field.mod(ch);
    const eP = this.pubKey.mul(c.big);
    const sG = this.pubNonce.add(eP);
    return sG.x;
  }

  verifyAdaptorPair(adaptorPoint, adaptorSecret) {
    const computedPoint = Field.mod(adaptorSecret).point;
    return computedPoint.x.equals(adaptorPoint);
  }
}

async function testOracle() {
  const oracle = new Oracle();

  const eventDetails = await oracle.createNewEvent();
  console.log("New event created with details:\n", eventDetails);

  const adaptorSecret = await oracle.signOutcome();
  console.log("Adaptor secret:", adaptorSecret);
}

testOracle();

The testcase where it fails:

{
    "wallets": [
        {
            "name": "alice",
            "address": "tb1puqyks3qcy6x5p3vwhglu0h459mhy8c2awcrnvanfdxhd26f7pspqqhz4mv",
            "sec_key": "9ebf004d39c26eb7f9430a86a061c84f895105f6af3601ab9515675c934699fd",
            "pub_key": "e009684418268d40c58eba3fc7deb42eee43e15d760736766969aed5693e0c02",
            "sec_nonce": "1732f1ed2071d5805115471c67aa95f16fb5ce8d89340de05a181eeddd197db368d86d728ba1a329b09f879135e678cdb140aa12ba699980e7ae3d5655f1ea80",
            "pub_nonce": "ea12eca11e04b7b7bb6d64c8cf174ee5d2ced80099ce14bd2e99ab990f8d4725c0ce9c9d7fac29d4dae595ba5554ab4eb79cdb5a6c0ae2c0474b57070ac0ce12"
        },
        {
            "name": "bob",
            "address": "tb1pnaskpu4u95ctqu0dde8j5mrvcy2a8xhwaw6qpkdvcug987wpv8psv0cwx4",
            "sec_key": "118fb1d7a453682bbd36bc0f5d7765aec2d5c614c398ba2b5b222f38cd81bfcb",
            "pub_key": "9f6160f2bc2d30b071ed6e4f2a6c6cc115d39aeeebb400d9acc71053f9c161c3",
            "sec_nonce": "8ea06aa5924f7117329e4c3f27076ac57fe0c81e5228788b5b437d27b00669d5e663068cf50d9859efdd014aa8a3529594fa6fb3a34a7c9f4e654303586775e1",
            "pub_nonce": "c66bf97a6f81e9969a57ddfb9092e5d4997fee2b5f536db229de0bfbf2bc557ffa4140aff4b9a8e838505f10fdf543e2d813be8dd1bf264cee43f8f64e8a53be"
        }
    ],
    "utxos": [
        {
            "txid": "767ed2788c3b904dda6651f19fdd2d77fd67fe98a7546866bffeeeb6effc11ff",
            "vout": 1,
            "value": 5000
        },
        {
            "txid": "5b52b6183e1084e3a4db85e6eae7457cbaa70a62f24fedccdb660de0b1ce94dc",
            "vout": 1,
            "value": 5000
        },
        {
            "txid": "38609dd47bfdb61553e328c06a56f1c134ca8d9725099f44e5409eac3ba3a5f6",
            "vout": 1,
            "value": 50000
        }
    ],
    "oracle": {
        "pub_key": "3a53994dd41c7c485f2fa71369604c5303313c94e330291623b672fdf8cb9a1e",
        "pub_nonce": "02ecc23662f188f89b8e55a15bffeda4a2e48ca98db0f954b50170ee7a68ee08e1",
        "outcomes": [
            {
                "message": "Image",
                "adaptor_point": "9d5e8c79544fe1f95fe8972063bbbdf501365af279802be980bbbfc94dd2e5f8"
            },
            {
                "message": "Video",
                "adaptor_point": "5e8d648b49753a89d66df844ec694479f7f4a9a6183503ac7d2c8a113ab8d6f6"
            }
        ]
    }
}

image

Adaptor secret: 67c87236d52ae942e3241a4cb5465e8b2657d8b7bea64d4d6e54ef9c7dd7dfdf0d