PHP WebShell

Текущая директория: /opt/BitGoJS/modules/utxo-lib/test/bitgo

Просмотр файла: signature.ts

import * as assert from 'assert';
import { BIP32Interface } from 'bip32';

import { script as bscript, classify, TxOutput } from '../../src';
import { getKeyName } from '../../src/testutil';
import { getNetworkList, getNetworkName, isBitcoin, isMainnet, Network, networks } from '../../src/networks';

import { ScriptType, ScriptType2Of3, scriptTypes2Of3 } from '../../src/bitgo/outputScripts';
import {
  verifySignature,
  UtxoTransaction,
  parseSignatureScript,
  getSignatureVerifications,
  verifySignatureWithPublicKeys,
  verifySignatureWithPublicKey,
  isPlaceholderSignature,
  toTNumber,
} from '../../src/bitgo';

import * as fixtureUtil from '../fixture.util';

import { fixtureKeys } from '../integration_local_rpc/generate/fixtures';
import {
  defaultTestOutputAmount,
  getFullSignedTransaction2Of3,
  getFullSignedTransactionP2shP2pk,
  getHalfSignedTransaction2Of3,
  getPrevOutputs,
  getSignKeyCombinations,
  getUnsignedTransaction2Of3,
} from '../transaction_util';
import { getPrevOutsWithInvalidOutputScript, getTransactionWithHighS } from './signatureModify';
import { normDefault } from '../testutil/normalize';

function getScriptTypes2Of3() {
  // FIXME(BG-66941): p2trMusig2 signing does not work in this test suite yet
  //  because the test suite is written with TransactionBuilder
  return scriptTypes2Of3.filter((scriptType) => scriptType !== 'p2trMusig2');
}

function keyName(k: BIP32Interface): string | undefined {
  return getKeyName(fixtureKeys, k);
}

async function readFixture<T>(
  network: Network,
  scriptType: ScriptType2Of3 | 'p2shP2pk',
  name: string,
  defaultValue: T
): Promise<T> {
  return await fixtureUtil.readFixture(
    `${__dirname}/fixtures/signature/${getNetworkName(network)}/${scriptType}/${name}.json`,
    defaultValue
  );
}

function runTestCheckScriptStructure<TNumber extends number | bigint = number>(
  network: Network,
  scriptType: ScriptType2Of3 | 'p2shP2pk',
  signer1: BIP32Interface,
  signer2?: BIP32Interface,
  amountType: 'number' | 'bigint' = 'number'
) {
  it(
    `has expected script structure [${getNetworkName(network)} ${scriptType} ` +
      `${keyName(signer1)} ${signer2 ? keyName(signer2) : ''} ${amountType}]`,
    async function () {
      let tx;

      if (scriptType === 'p2shP2pk') {
        tx = getFullSignedTransactionP2shP2pk<TNumber>(fixtureKeys, signer1, network, { amountType });
      } else {
        if (!signer2) {
          throw new Error(`must set cosigner`);
        }
        tx = getFullSignedTransaction2Of3<TNumber>(fixtureKeys, signer1, signer2, scriptType, network, { amountType });
      }

      const { script, witness } = tx.ins[0];
      const scriptDecompiled = bscript.decompile(script);
      if (!scriptDecompiled) {
        throw new Error();
      }
      const scriptASM = bscript.toASM(script).split(' ');
      const classifyInput = classify.input(script);
      const classifyWitness = classify.witness(witness);

      let pubScript;
      let classifyPubScript;
      let pubScriptASM;

      let tapscript;
      let tapscriptASM;
      let classifyTapscript;

      if (classifyInput === 'scripthash' || classifyWitness === 'witnessscripthash') {
        if (witness.length) {
          pubScript = witness[witness.length - 1];
        } else {
          pubScript = scriptDecompiled[scriptDecompiled.length - 1] as Buffer;
        }

        classifyPubScript = classify.output(pubScript);
        pubScriptASM = bscript.toASM(pubScript).split(' ');
      } else if (classifyWitness === 'taproot') {
        tapscript = witness[witness.length - 2];
        classifyTapscript = classify.output(tapscript);
        tapscriptASM = bscript.toASM(tapscript).split(' ');
      }

      const structure = {
        publicKeys: fixtureKeys.map((k) => k.publicKey.toString('hex')),
        script: script?.toString('hex'),
        witness: witness?.map((w) => w.toString('hex')),
        scriptASM,
        pubScriptASM,
        tapscriptASM,
        classifyInput,
        classifyWitness,
        classifyPubScript,
        classifyTapscript,
      };

      const fixtureName = ['structure', keyName(signer1), signer2 ? keyName(signer2) : 'none'].join('-');
      fixtureUtil.assertEqualJSON(structure, await readFixture(network, scriptType, fixtureName, structure));
    }
  );
}

function runTestParseScript<TNumber extends number | bigint = number>(
  network: Network,
  scriptType: ScriptType,
  k1: BIP32Interface,
  k2: BIP32Interface,
  amountType: 'number' | 'bigint' = 'number'
) {
  async function testParseSignedInputs(
    tx: UtxoTransaction<TNumber>,
    name: string,
    expectedScriptType: string | undefined,
    { expectedPlaceholderSignatures }: { expectedPlaceholderSignatures: number }
  ) {
    const parsed = parseSignatureScript(tx.ins[0]);
    assert.strictEqual(
      parsed.scriptType,
      expectedScriptType === 'p2tr' ? 'taprootScriptPathSpend' : expectedScriptType
    );
    const parsed2Of3 = { ...parsed, scriptType: expectedScriptType };
    fixtureUtil.assertEqualJSON(
      parsed2Of3,
      await readFixture(network, scriptType, ['parsed', keyName(k1), keyName(k2), name].join('-'), parsed2Of3)
    );

    if (!parsed.scriptType) {
      return;
    }

    switch (parsed.scriptType) {
      case 'p2shP2pk':
        // we don't parse the signature for this script type
        break;
      case 'p2sh':
      case 'p2shP2wsh':
      case 'p2wsh':
      case 'taprootScriptPathSpend':
        assert.strictEqual(
          parsed.signatures.filter((s) => isPlaceholderSignature(s)).length,
          expectedPlaceholderSignatures
        );
        break;
      default:
        throw new Error(`unexpected scriptType ${(parsed as any).scriptType}`);
    }
  }

  if (scriptType !== 'p2shP2pk') {
    it(`parses half-signed inputs [${getNetworkName(network)} ${scriptType} ${amountType}]`, async function () {
      await testParseSignedInputs(
        getHalfSignedTransaction2Of3<TNumber>(fixtureKeys, k1, k2, scriptType, network, { amountType }),
        'halfSigned',
        scriptType,
        { expectedPlaceholderSignatures: scriptType === 'p2tr' ? 1 : 2 }
      );
    });
  }

  it(`parses full-signed inputs [${getNetworkName(network)} ${scriptType} ${amountType}]`, async function () {
    if (scriptType === 'p2shP2pk') {
      await testParseSignedInputs(
        getFullSignedTransactionP2shP2pk<TNumber>(fixtureKeys, k1, network, { amountType }),
        'fullSigned',
        scriptType,
        { expectedPlaceholderSignatures: 0 }
      );
    } else {
      await testParseSignedInputs(
        getFullSignedTransaction2Of3<TNumber>(fixtureKeys, k1, k2, scriptType, network, { amountType }),
        'fullSigned',
        scriptType,
        { expectedPlaceholderSignatures: 0 }
      );
    }
  });
}

function assertVerifySignatureEquals<TNumber extends number | bigint>(
  tx: UtxoTransaction<TNumber>,
  prevOutputs: TxOutput<TNumber>[],
  value: boolean,
  testOutputAmount: TNumber,
  verificationSettings?: {
    publicKey?: Buffer;
    signatureIndex?: number;
  }
) {
  tx.ins.forEach((input, i) => {
    assert.doesNotThrow(() => {
      getSignatureVerifications(tx, i, testOutputAmount, verificationSettings, prevOutputs);
    });
    assert.strictEqual(
      verifySignature(tx, i, testOutputAmount, verificationSettings, prevOutputs),
      value,
      JSON.stringify(verificationSettings)
    );
    if (verificationSettings?.signatureIndex === undefined && verificationSettings?.publicKey) {
      assert.strictEqual(verifySignatureWithPublicKey(tx, i, prevOutputs, verificationSettings.publicKey), value);
    }
  });
}

function checkSignTransaction<TNumber extends number | bigint>(
  tx: UtxoTransaction<TNumber>,
  scriptType: ScriptType2Of3,
  signKeys: BIP32Interface[],
  testOutputAmount: TNumber
) {
  const prevOutputs = getPrevOutputs<TNumber>(scriptType, testOutputAmount, tx.network) as TxOutput<TNumber>[];

  // return true iff there are any valid signatures at all
  assertVerifySignatureEquals<TNumber>(tx, prevOutputs, signKeys.length > 0, testOutputAmount);

  fixtureKeys.forEach((k) => {
    // if publicKey is given, return true iff it is included in signKeys
    assertVerifySignatureEquals<TNumber>(tx, prevOutputs, signKeys.includes(k), testOutputAmount, {
      publicKey: k.publicKey,
    });
  });

  // When transactions are signed, the signatures have the same order as the public keys in the outputScript.
  const orderedSigningKeys = fixtureKeys.filter((fixtureKey) => signKeys.includes(fixtureKey));

  [0, 1, 2].forEach((signatureIndex) => {
    if (scriptType === 'p2tr') {
      // signatureIndex parameter not support for p2tr verification
      return;
    }
    fixtureKeys.forEach((k) => {
      // If no public key is given, return true iff any valid signature with given index exists.
      assertVerifySignatureEquals<TNumber>(tx, prevOutputs, signatureIndex < signKeys.length, testOutputAmount, {
        signatureIndex,
      });

      // If publicKey and signatureIndex are provided only return if both match.
      assertVerifySignatureEquals<TNumber>(
        tx,
        prevOutputs,
        signatureIndex === orderedSigningKeys.indexOf(k),
        testOutputAmount,
        {
          publicKey: k.publicKey,
          signatureIndex,
        }
      );
    });
  });

  tx.ins.forEach((input, i) => {
    const signatureCount = (res: boolean[]) => res.reduce((sum, b) => sum + (b ? 1 : 0), 0);
    const pubkeys = fixtureKeys.map((k) => k.publicKey);
    const verifyResult = verifySignatureWithPublicKeys(tx, i, prevOutputs, pubkeys);
    assert.deepStrictEqual(
      verifyResult,
      fixtureKeys.map((k) => signKeys.includes(k))
    );
    assert.strictEqual(signatureCount(verifyResult), signKeys.length);

    if (signKeys.length > 0) {
      getTransactionWithHighS(tx, i).forEach((txWithHighS) => {
        assert.strictEqual(
          signatureCount(verifySignatureWithPublicKeys<TNumber>(txWithHighS, i, prevOutputs, pubkeys)),
          signKeys.length - 1
        );
      });

      if (scriptType !== 'p2tr' && scriptType !== 'p2trMusig2') {
        assert.throws(
          () =>
            signatureCount(
              verifySignatureWithPublicKeys<TNumber>(tx, i, getPrevOutsWithInvalidOutputScript(prevOutputs, i), pubkeys)
            ),
          /prevout script .* does not match computed script .*/
        );
      }
    }
  });
}

function runTestCheckSignatureVerify<TNumber extends number | bigint = number>(
  network: Network,
  scriptType: ScriptType2Of3,
  k1?: BIP32Interface,
  k2?: BIP32Interface,
  amountType: 'number' | 'bigint' = 'number'
) {
  if (k1 && k2) {
    describe(`verifySignature ${getNetworkName(network)} ${scriptType} ${keyName(k1)} ${keyName(
      k2
    )} ${amountType}`, function () {
      it(`verifies half-signed`, function () {
        checkSignTransaction(
          getHalfSignedTransaction2Of3<TNumber>(fixtureKeys, k1, k2, scriptType, network, { amountType }),
          scriptType,
          [k1],
          toTNumber<TNumber>(defaultTestOutputAmount, amountType)
        );
      });

      it(`verifies full-signed`, function () {
        checkSignTransaction(
          getFullSignedTransaction2Of3<TNumber>(fixtureKeys, k1, k2, scriptType, network, { amountType }),
          scriptType,
          [k1, k2],
          toTNumber<TNumber>(defaultTestOutputAmount, amountType)
        );
      });
    });
  } else {
    describe(`verifySignature ${getNetworkName(network)} ${scriptType} ${amountType} unsigned`, function () {
      it(`verifies unsigned`, function () {
        checkSignTransaction(
          getUnsignedTransaction2Of3<TNumber>(fixtureKeys, scriptType, network, { amountType }),
          scriptType,
          [],
          toTNumber<TNumber>(defaultTestOutputAmount, amountType)
        );
      });
    });
  }
}

describe('Signature (scriptTypes2Of3)', function () {
  getNetworkList()
    .filter(isMainnet)
    // The signing and verification methods are largely network-independent so let's focus on a
    // single network to reduce test time.
    // During development it might make sense to test all networks.
    .filter(isBitcoin)
    .forEach((network) => {
      getScriptTypes2Of3().forEach((scriptType) => {
        runTestCheckSignatureVerify(network, scriptType);

        getSignKeyCombinations(2).map(([k1, k2]) => {
          runTestCheckSignatureVerify(network, scriptType, k1, k2);
          runTestCheckScriptStructure(network, scriptType, k1, k2);
          runTestParseScript(network, scriptType, k1, k2);
        });
      });
      getScriptTypes2Of3().forEach((scriptType) => {
        runTestCheckSignatureVerify<bigint>(network, scriptType, undefined, undefined, 'bigint');

        getSignKeyCombinations(2).map(([k1, k2]) => {
          runTestCheckSignatureVerify<bigint>(network, scriptType, k1, k2, 'bigint');
          runTestCheckScriptStructure<bigint>(network, scriptType, k1, k2, 'bigint');
          runTestParseScript<bigint>(network, scriptType, k1, k2, 'bigint');
        });
      });
    });
});

describe('Signature (p2shP2pk)', function () {
  it('sign and parse', function () {
    const signedTransaction = getFullSignedTransactionP2shP2pk(fixtureKeys, fixtureKeys[0], networks.bitcoin);

    signedTransaction.ins.forEach((input) => {
      assert.deepStrictEqual(
        normDefault(parseSignatureScript(input)),
        normDefault({
          scriptType: 'p2shP2pk',
          publicKeys: [fixtureKeys[0].publicKey],
          signatures: [
            '3045022100e637466be405032a633dcef0bd161305fe93d34ffe2aabc4af434d6f265912210220113d7085b1e00435a2583af82b8a4df3fb009a8d279d231351e42f31d6bac74401',
          ],
        })
      );
    });
  });

  runTestCheckScriptStructure(networks.bitcoin, 'p2shP2pk', fixtureKeys[0]);
  runTestCheckScriptStructure<bigint>(networks.bitcoin, 'p2shP2pk', fixtureKeys[0], undefined, 'bigint');
});

Выполнить команду


Для локальной разработки. Не используйте в интернете!