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

Get error while sign Multi Sig PSBT use Unisat Wallet #19

Open
FCBtc1116 opened this issue Jul 31, 2024 · 0 comments
Open

Get error while sign Multi Sig PSBT use Unisat Wallet #19

FCBtc1116 opened this issue Jul 31, 2024 · 0 comments

Comments

@FCBtc1116
Copy link

import * as bitcoin from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";
import { Taptree } from "bitcoinjs-lib/src/types";
import { tapleafHash } from "bitcoinjs-lib/src/payments/bip341";

function makeUnspendableInternalKey(provableNonce?: Buffer): Buffer {
  // This is the generator point of secp256k1. Private key is known (equal to 1)
  const G = Buffer.from(
    "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8",
    "hex"
  );
  // This is the hash of the uncompressed generator point.
  // It is also a valid X value on the curve, but we don't know what the private key is.
  // Since we know this X value (a fake "public key") is made from a hash of a well known value,
  // We can prove that the internalKey is unspendable.
  const Hx = bitcoin.crypto.sha256(G);

  if (provableNonce) {
    if (provableNonce.length !== 32) {
      throw new Error(
        "provableNonce must be a 32 byte random value shared between script holders"
      );
    }
    // Using a shared random value, we create an unspendable internalKey
    // P = H + int(hash_taptweak(provableNonce))*G
    // Since we don't know H's private key (see explanation above), we can't know P's private key
    const tapHash = bitcoin.crypto.taggedHash("TapTweak", provableNonce);
    const ret = ecc.xOnlyPointAddTweak(Hx, tapHash);
    if (!ret) {
      throw new Error(
        "provableNonce produced an invalid key when tweaking the G hash"
      );
    }
    return Buffer.from(ret.xOnlyPubkey);
  } else {
    // The downside to using no shared provable nonce is that anyone viewing a spend
    // on the blockchain can KNOW that you CAN'T use key spend.
    // Most people would be ok with this being public, but some wallets (exchanges etc)
    // might not want ANY details about how their wallet works public.
    return Hx;
  }
}

export class TaprootMultisigWallet {
  private leafScriptCache: Buffer | null = null;
  private internalPubkeyCache: Buffer | null = null;
  private paymentCache: bitcoin.Payment | null = null;
  private readonly publicKeyCache: Buffer;
  network: bitcoin.Network;

  constructor(
    /**
     * A list of all the (x-only) pubkeys in the multisig
     */
    private readonly pubkeys: Buffer[],
    /**
     * The number of required signatures
     */
    private readonly requiredSigs: number,
    /**
     * The private key you hold.
     */
    private readonly privateKey: Buffer,
    /**
     * leaf version (0xc0 currently)
     */
    readonly leafVersion: number,
    /**
     * Optional shared nonce. This should be used in wallets where
     * the fact that key-spend is unspendable should not be public,
     * BUT each signer must verify that it is unspendable to be safe.
     */
    private readonly sharedNonce?: Buffer
  ) {
    this.network = bitcoin.networks.bitcoin;

    const pubkey = ecc.pointFromScalar(privateKey);

    if (!pubkey) throw "Invalid Keys";

    this.publicKeyCache = Buffer.from(pubkey);

    // IMPORTANT: Make sure the pubkeys are sorted (To prevent ordering issues between wallet signers)
    this.pubkeys.sort((a, b) => a.compare(b));
  }

  setNetwork(network: bitcoin.Network): this {
    this.network = network;
    return this;
  }

  // Required for Signer interface.
  // Prevent setting by using a getter.
  get publicKey(): Buffer {
    return this.publicKeyCache;
  }

  /**
   * Lazily build the leafScript. A 2 of 3 would look like:
   * key1 OP_CHECKSIG key2 OP_CHECKSIGADD key3 OP_CHECKSIGADD OP_2 OP_GREATERTHANOREQUAL
   */
  get leafScript(): Buffer {
    if (this.leafScriptCache) {
      return this.leafScriptCache;
    }
    const ops = [];
    this.pubkeys.forEach((pubkey) => {
      if (ops.length === 0) {
        ops.push(pubkey);
        ops.push(bitcoin.opcodes.OP_CHECKSIG);
      } else {
        ops.push(pubkey);
        ops.push(bitcoin.opcodes.OP_CHECKSIGADD);
      }
    });
    if (this.requiredSigs > 16) {
      ops.push(bitcoin.script.number.encode(this.requiredSigs));
    } else {
      ops.push(bitcoin.opcodes.OP_1 - 1 + this.requiredSigs);
    }
    ops.push(bitcoin.opcodes.OP_GREATERTHANOREQUAL);

    this.leafScriptCache = bitcoin.script.compile(ops);
    return this.leafScriptCache;
  }

  get internalPubkey(): Buffer {
    if (this.internalPubkeyCache) {
      return this.internalPubkeyCache;
    }
    // See the helper function for explanation
    this.internalPubkeyCache = makeUnspendableInternalKey(this.sharedNonce);
    return this.internalPubkeyCache;
  }

  get scriptTree(): Taptree {
    // If more complicated, maybe it should be cached.
    // (ie. if other scripts are created only to create the tree
    // and will only be stored in the tree.)
    return {
      output: this.leafScript,
    };
  }

  get redeem(): {
    output: Buffer;
    redeemVersion: number;
  } {
    return {
      output: this.leafScript,
      redeemVersion: this.leafVersion,
    };
  }

  private get payment(): bitcoin.Payment {
    if (this.paymentCache) {
      return this.paymentCache;
    }
    this.paymentCache = bitcoin.payments.p2tr({
      internalPubkey: this.internalPubkey,
      scriptTree: this.scriptTree,
      redeem: this.redeem,
      network: this.network,
    });
    return this.paymentCache;
  }

  get output(): Buffer {
    return this.payment.output!;
  }

  get address(): string {
    return this.payment.address!;
  }

  get controlBlock(): Buffer {
    const witness = this.payment.witness!;
    return witness[witness.length - 1];
  }

  verifyInputScript(psbt: bitcoin.Psbt, index: number) {
    if (index >= psbt.data.inputs.length)
      throw new Error("Invalid input index");
    const input = psbt.data.inputs[index];
    if (!input.tapLeafScript) throw new Error("Input has no tapLeafScripts");
    const hasMatch =
      input.tapLeafScript.length === 1 &&
      input.tapLeafScript[0].leafVersion === this.leafVersion &&
      input.tapLeafScript[0].script.equals(this.leafScript) &&
      input.tapLeafScript[0].controlBlock.equals(this.controlBlock);
    if (!hasMatch)
      throw new Error(
        "No matching leafScript, or extra leaf script. Refusing to sign."
      );
  }

  addInput(
    psbt: bitcoin.Psbt,
    hash: string | Buffer,
    index: number,
    value: number
  ) {
    psbt.addInput({
      hash,
      index,
      witnessUtxo: { value, script: this.output },
    });
    psbt.updateInput(psbt.inputCount - 1, {
      tapLeafScript: [
        {
          leafVersion: this.leafVersion,
          script: this.leafScript,
          controlBlock: this.controlBlock,
        },
      ],
    });
  }

  addDummySigs(psbt: bitcoin.Psbt) {
    const leafHash = tapleafHash({
      output: this.leafScript,
      version: this.leafVersion,
    });
    for (const input of psbt.data.inputs) {
      if (!input.tapScriptSig) continue;
      const signedPubkeys = input.tapScriptSig
        .filter((ts) => ts.leafHash.equals(leafHash))
        .map((ts) => ts.pubkey);
      for (const pubkey of this.pubkeys) {
        if (signedPubkeys.some((sPub) => sPub.equals(pubkey))) continue;
        // Before finalizing, every key that did not sign must have an empty signature
        // in place where their signature would be.
        // In order to do this currently we need to construct a dummy signature manually.
        input.tapScriptSig.push({
          // This can be reused for each dummy signature
          leafHash,
          // This is the pubkey that didn't sign
          pubkey,
          // This must be an empty Buffer.
          signature: Buffer.from([]),
        });
      }
    }
  }

  // required for Signer interface
  sign(hash: Buffer, _lowR?: boolean): Buffer {
    return Buffer.from(ecc.sign(hash, this.privateKey));
  }

  // required for Signer interface
  signSchnorr(hash: Buffer): Buffer {
    return Buffer.from(ecc.signSchnorr(hash, this.privateKey));
  }
}
const testTaproot = async () => {
  const sigWalletPubkey1 =
    "026272fe4cf7746c9c3de3d48afc5f27fe4ba052fc8f72913a6020fa970f7f5823";
  const sigWalletAddress1 =
    "tb1pgda5khhwqlc7jmdzn4plca3pa4m7jg38zcspj0mmuyk8hnj5pphskers72";
  const sigWalletPubkey2 =
    "02c032c56d3af8899a15915d52c706c4659bc5ddb88ddf965bb334641ff9498d58";
  const sigWalletAddress2 =
    "tb1p2vsa0qxsn96sulauasfgyyccfjdwp2rzg8h2ejpxcdauulltczuqw02jmj";
  const sigWalletPubkey3 =
    "02df4a77af5c93acb96b9749a84f4e61e1e259304adec248891d66247faddd8c3e";
  const sigWalletAddress3 =
    "tb1pa8h7v54gqy9dww7y248h4j48sltcqjkasz0mk28steqf4ln02geq8s2u42";

  const leafPubkeys: Buffer[] = [
    toXOnly(Buffer.from(sigWalletPubkey1, "hex")),
    toXOnly(Buffer.from(sigWalletPubkey2, "hex")),
    toXOnly(Buffer.from(sigWalletPubkey3, "hex")),
  ];

  const leafKey = bip32.fromSeed(rng(64), bitcoin.networks.testnet);

  const multiSigWallet = new TaprootMultisigWallet(
    leafPubkeys,
    2,
    leafKey.privateKey!,
    LEAF_VERSION_TAPSCRIPT
  ).setNetwork(bitcoin.networks.testnet);

  console.log(multiSigWallet.address);

  const psbt = new bitcoin.Psbt({ network: bitcoin.networks.testnet });

  multiSigWallet.addInput(
    psbt,
    "a878fc53bd543212f1dde7618a4fa05e9c35d71ef88633b4c2c2cc7a5eb14233",
    0,
    100000
  );
  psbt.addOutput({
    value: 50000,
    address: "tb1psh9f9dk4fuhclvlvu929gqrftzpwpxlrcl208k0k3geltnrgh4dscpzwng",
  });

  console.log("psbt", psbt.toHex());
};

I used above code for generate MultiSig Wallet

But after get PSBT, I can not sign use Unisat Wallet.
I tried to sign PSBT like below

unisat.signPsbt("70736274ff01005e02000000017372b04d478556c29beb88e1eeee43cced8318bc95f664c3dbe5daa61067b2bc0000000000ffffffff0150c300000000000022512085ca92b6d54f2f8fb3ece1545400695882e09be3c7d4f3d9f68a33f5cc68bd5b000000000001012ba086010000000000225120b34b172f4ceba5ed36608acfe7e5921e6ff394810fcfabdefe527dac18ba389d6215c0d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee04941a529c9fb3cd7e776d61b6225b6c610e8906fb8faa6c59ac5c3e95b5f82d29d61a529c9fb3cd7e776d61b6225b6c610e8906fb8faa6c59ac5c3e95b5f82d29d66920df4a77af5c93acb96b9749a84f4e61e1e259304adec248891d66247faddd8c3eac20c032c56d3af8899a15915d52c706c4659bc5ddb88ddf965bb334641ff9498d58ba206272fe4cf7746c9c3de3d48afc5f27fe4ba052fc8f72913a6020fa970f7f5823ba539cc00000", {
    autoFinalized:false,
        toSignInputs:[
          {
            index: 0,
            address: "tb1p2vsa0qxsn96sulauasfgyyccfjdwp2rzg8h2ejpxcdauulltczuqw02jmj",
          }
        ]
})

When I try to use above command, I get this error
"Can not sign for input #0 with the key 025321d780d099750e7fbcec128213184c9ae0a86241eeacc826c37bce7febc0b8"
image

Hope to get help about taproot multi-sig wallet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
@FCBtc1116 and others