bitcoinjs/bitcoinjs-lib

How to recover btc address from txn witness data?

saurabhburade opened this issue · 5 comments

It is impossible for p2tr key spend witnesses. (Since it’s only a signature) but every other type of segwit address can be recovered by its witness stack.

is that ok?

To be specific:

  1. P2WPKH: The last item on the witness stack is the pubkey, HASH160 it and use bech32 with segwit version 0 to encode.
  2. P2WSH: The last item on the witness stack is the witnessScript, SHA256 it and use bech32 with segwit version 0 to encode.
  3. P2TR (script): Follow BIP341 script validation rules up until the point where it compares the final output to the witness program from the previous output. If the transaction is confirmed in a block we can assume this check passed, so therefore we can get the witness program by performing the validation rules and encoding the result using bech32m with segwit version 1.

If you give this a P2TR witness that you know is definitely a P2TR, then it will give you the address if it's a script spend.

Probably buggy though.

import * as bitcoin from 'bitcoinjs-lib';
import * as vint from 'varuint-bitcoin';
import * as ecc from 'tiny-secp256k1';

// Required
bitcoin.initEccLib(ecc);

function p2trScriptToAddress(witness: Buffer[]): string {
  if (!Array.isArray(witness) || !witness.every(b => Buffer.isBuffer(b))) {
    throw new Error('All witness elements must be Buffers');
  }

  const wlen = witness.length;
  if (wlen < 2) {
    throw new Error('Not a p2tr script spend witness');
  }

  let annexOffset = 0;
  if (witness[wlen - 1][0] === 0x50) {
    // has annex
    annexOffset = 1;
  }

  if (wlen < 2 + annexOffset) {
    throw new Error('Not a p2tr script spend witness');
  }

  const controlBlock = witness[wlen - annexOffset - 1];
  if (controlBlock.length < 33 || ((controlBlock.length - 33) / 32) % 1 !== 0) {
    throw new Error('Incorrect Control block length');
  }

  const script = witness[wlen - annexOffset - 2];
  const leafVersion = controlBlock[0] & 0xfe;
  const internalPubkey = controlBlock.subarray(1, 33);

  // Get preimage for tapleaf hash
  const preImageLeafHash = Buffer.allocUnsafe(
    script.length + vint.encodingLength(script.length) + 1,
  );
  preImageLeafHash.writeUInt8(leafVersion, 0);
  vint.encode(script.length, preImageLeafHash, 1);
  script.copy(preImageLeafHash, 1 + vint.encodingLength(script.length));
  // Get tapleaf hash
  let tapLeafHash = bitcoin.crypto.taggedHash('TapLeaf', preImageLeafHash);

  const loops = (controlBlock.length - 33) / 32;
  for (let j = 0; j < loops; j++) {
    const branch = controlBlock.subarray(33 + 32 * j, 65 + 32 * j);

    if (Buffer.compare(tapLeafHash, branch) < 0) {
      tapLeafHash = bitcoin.crypto.taggedHash(
        'TapBranch',
        Buffer.concat([tapLeafHash, branch]),
      );
    } else {
      tapLeafHash = bitcoin.crypto.taggedHash(
        'TapBranch',
        Buffer.concat([branch, tapLeafHash]),
      );
    }
  }

  const final = bitcoin.crypto.taggedHash(
    'TapTweak',
    Buffer.concat([internalPubkey, tapLeafHash]),
  );
  if (!ecc.isPrivate(final)) {
    throw new Error(
      'Rare error. Final taptree hash was higher than curve order',
    );
  }
  const result = ecc.xOnlyPointAddTweak(internalPubkey, final);
  if (result === null) {
    throw new Error('Error when tweaking');
  }

  return bitcoin.address.fromOutputScript(
    Buffer.concat([Buffer.from([0x51, 0x20]), Buffer.from(result.xOnlyPubkey)]),
  );
}

If you're not going to read my replies, posts, and comments, then I am wasting my time here.

Good luck with whatever it is you're doing.

#2088 (comment) (Read this reply to your issue)