PHP WebShell
Текущая директория: /opt/BitGoJS/modules/utxo-lib/test/bitgo/psbt
Просмотр файла: Musig2Util.ts
import * as assert from 'assert';
import {
addReplayProtectionUnspentToPsbt,
addWalletOutputToPsbt,
addWalletUnspentToPsbt,
createPsbtForNetwork,
getExternalChainCode,
getInternalChainCode,
isWalletUnspent,
KeyName,
outputScripts,
parsePsbtInput,
parseSignatureScript2Of3,
ProprietaryKeySubtype,
PSBT_PROPRIETARY_IDENTIFIER,
RootWalletKeys,
scriptTypeForChain,
Tuple,
Unspent,
unspentSum,
UtxoPsbt,
UtxoTransaction,
WalletUnspent,
ChainCode,
} from '../../../src/bitgo';
import {
createKeyPathP2trMusig2,
createOutputScriptP2shP2pk,
createPaymentP2trMusig2,
ScriptType2Of3,
toXOnlyPublicKey,
} from '../../../src/bitgo/outputScripts';
import { mockWalletUnspent, replayProtectionKeyPair } from '../../../src/testutil';
import { bip32, networks } from '../../../src';
import { BIP32Interface } from 'bip32';
import { isPsbtInputFinalized } from '../../../src/bitgo/PsbtUtil';
export const network = networks.bitcoin;
const outputType = 'p2trMusig2';
const CHANGE_INDEX = 100;
const FEE = BigInt(100);
const keys = [1, 2, 3].map((v) => bip32.fromSeed(Buffer.alloc(16, `test/2/${v}`), network)) as BIP32Interface[];
export const rootWalletKeys = new RootWalletKeys([keys[0], keys[1], keys[2]]);
const dummyKey1 = rootWalletKeys.deriveForChainAndIndex(50, 200);
const dummyKey2 = rootWalletKeys.deriveForChainAndIndex(60, 201);
export const dummyTapOutputKey = dummyKey1.user.publicKey.subarray(1, 33);
export const dummyTapInternalKey = dummyKey1.bitgo.publicKey.subarray(1, 33);
export const dummyParticipantPubKeys: Tuple<Buffer> = [dummyKey1.user.publicKey, dummyKey1.backup.publicKey];
export const dummyPubNonce = Buffer.concat([dummyKey2.user.publicKey, dummyKey2.bitgo.publicKey]);
export const dummyAggNonce = Buffer.concat([dummyKey2.backup.publicKey, dummyKey2.bitgo.publicKey]);
export const dummyPrivateKey = dummyKey2.user.privateKey!;
export const dummyPartialSig = dummyKey2.backup.privateKey!;
export const invalidTapOutputKey = Buffer.allocUnsafe(1);
export const invalidTapInputKey = Buffer.allocUnsafe(1);
export const invalidTxHash = Buffer.allocUnsafe(1);
export const invalidParticipantPubKeys: Tuple<Buffer> = [Buffer.allocUnsafe(1), Buffer.allocUnsafe(1)];
export const invalidPartialSig = Buffer.allocUnsafe(1);
export function constructPsbt(
unspents: (Unspent<bigint> & { prevTx?: Buffer })[],
rootWalletKeys: RootWalletKeys,
signer: KeyName,
cosigner: KeyName,
outputs: { chain: ChainCode; index: number; value: bigint }[] | outputScripts.ScriptType2Of3
): UtxoPsbt {
const psbt = createPsbtForNetwork({ network });
if (Array.isArray(outputs)) {
outputs.forEach((output) => addWalletOutputToPsbt(psbt, rootWalletKeys, output.chain, output.index, output.value));
} else {
const total = BigInt(unspentSum<bigint>(unspents, 'bigint'));
addWalletOutputToPsbt(psbt, rootWalletKeys, getInternalChainCode(outputs), CHANGE_INDEX, total - FEE);
}
unspents.forEach((u) => {
if (isWalletUnspent(u)) {
addWalletUnspentToPsbt(psbt, u, rootWalletKeys, signer, cosigner);
} else {
const { redeemScript } = createOutputScriptP2shP2pk(replayProtectionKeyPair.publicKey);
assert.ok(redeemScript);
addReplayProtectionUnspentToPsbt(psbt, u, redeemScript);
}
});
return psbt;
}
export function getUnspents(
inputScriptTypes: ScriptType2Of3[],
rootWalletKeys: RootWalletKeys
): WalletUnspent<bigint>[] {
return inputScriptTypes.map((t, i): WalletUnspent<bigint> => {
if (!outputScripts.isScriptType2Of3(t)) {
throw new Error(`invalid input type ${t}`);
}
const unspent = mockWalletUnspent(network, BigInt('10000000000000000'), {
keys: rootWalletKeys,
chain: getExternalChainCode(t),
vout: i,
});
if (isWalletUnspent(unspent)) {
return unspent;
}
throw new Error('Invalid unspent');
});
}
export function validatePsbtP2trMusig2Input(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
index: number,
unspent: WalletUnspent<bigint>,
spendType: 'keyPath' | 'scriptPath'
): void {
const input = psbt.data.inputs[index];
assert.strictEqual(input.tapBip32Derivation?.length, 2);
let leafHashesCount = 0;
if (spendType === 'keyPath') {
const inputWalletKeys = rootWalletKeys.deriveForChainAndIndex(unspent.chain, unspent.index);
const { internalPubkey, taptreeRoot } = createKeyPathP2trMusig2(inputWalletKeys.publicKeys);
assert.ok(!input.tapLeafScript);
assert.ok(input.tapInternalKey);
assert.ok(input.tapMerkleRoot);
assert.ok(input.tapInternalKey.equals(internalPubkey));
assert.ok(input.tapMerkleRoot.equals(taptreeRoot));
} else {
assert.ok(input.tapLeafScript);
assert.ok(!input.tapInternalKey);
assert.ok(!input.tapMerkleRoot);
leafHashesCount = 1;
}
input.tapBip32Derivation?.forEach((bv) => {
assert.strictEqual(bv.leafHashes.length, leafHashesCount);
});
}
export function validatePsbtP2trMusig2Output(psbt: UtxoPsbt<UtxoTransaction<bigint>>, index: number): void {
const outputWalletKeys = rootWalletKeys.deriveForChainAndIndex(getInternalChainCode(outputType), CHANGE_INDEX);
const payment = createPaymentP2trMusig2(outputWalletKeys.publicKeys);
const output = psbt.data.outputs[index];
assert.ok(!!payment.internalPubkey);
assert.ok(!!output.tapInternalKey);
assert.ok(output.tapInternalKey.equals(payment.internalPubkey));
assert.strictEqual(output.tapBip32Derivation?.length, 3);
output.tapBip32Derivation?.forEach((bv) => {
const leafHashesCount = bv.pubkey.equals(toXOnlyPublicKey(outputWalletKeys.backup.publicKey)) ? 2 : 1;
assert.strictEqual(bv.leafHashes.length, leafHashesCount);
});
}
export function validateNoncesKeyVals(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
index: number,
unspent: WalletUnspent<bigint>
): void {
const keyVals = psbt.getProprietaryKeyVals(index);
const walletKeys = rootWalletKeys.deriveForChainAndIndex(unspent.chain, unspent.index);
const { outputPubkey } = createKeyPathP2trMusig2(walletKeys.publicKeys);
const participantPubKeys = [walletKeys.user.publicKey, walletKeys.bitgo.publicKey];
const nonces = keyVals.filter((kv) => kv.key.subtype === ProprietaryKeySubtype.MUSIG2_PUB_NONCE);
assert.strictEqual(nonces.length, 2);
const nonceKeydata = participantPubKeys.map((p) => {
const keydata = Buffer.alloc(65);
p.copy(keydata);
outputPubkey.copy(keydata, 33);
return keydata;
});
nonces.forEach((kv) => {
assert.strictEqual(kv.key.identifier, PSBT_PROPRIETARY_IDENTIFIER);
assert.strictEqual(kv.value.length, 66);
assert.strictEqual(nonceKeydata.filter((kd) => kd.equals(kv.key.keydata)).length, 1);
});
}
export function validatePartialSigKeyVals(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
index: number,
unspent: WalletUnspent<bigint>
): void {
const keyVals = psbt.getProprietaryKeyVals(index);
const inputWalletKeys = rootWalletKeys.deriveForChainAndIndex(unspent.chain, unspent.index);
const { outputPubkey } = createKeyPathP2trMusig2(inputWalletKeys.publicKeys);
const participantPubKeys = [inputWalletKeys.user.publicKey, inputWalletKeys.bitgo.publicKey];
const partialSigs = keyVals.filter((kv) => kv.key.subtype === ProprietaryKeySubtype.MUSIG2_PARTIAL_SIG);
assert.strictEqual(partialSigs.length, 2);
const partialSigKeydata = participantPubKeys.map((p) => {
const keydata = Buffer.alloc(65);
p.copy(keydata);
outputPubkey.copy(keydata, 33);
return keydata;
});
partialSigs.forEach((kv) => {
assert.strictEqual(kv.key.identifier, PSBT_PROPRIETARY_IDENTIFIER);
assert.strictEqual(kv.value.length, 32);
assert.strictEqual(partialSigKeydata.filter((kd) => kd.equals(kv.key.keydata)).length, 1);
});
}
export function validateParticipantsKeyVals(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
index: number,
unspent: WalletUnspent<bigint>
): void {
const keyVals = psbt.getProprietaryKeyVals(index);
const walletKeys = rootWalletKeys.deriveForChainAndIndex(unspent.chain, unspent.index);
const { internalPubkey, outputPubkey } = createKeyPathP2trMusig2(walletKeys.publicKeys);
const participantPubKeys = [walletKeys.user.publicKey, walletKeys.bitgo.publicKey];
const participantsKeyVals = keyVals.filter(
(kv) => kv.key.subtype === ProprietaryKeySubtype.MUSIG2_PARTICIPANT_PUB_KEYS
);
assert.strictEqual(participantsKeyVals.length, 1);
const kv = participantsKeyVals[0];
assert.strictEqual(kv.key.identifier, PSBT_PROPRIETARY_IDENTIFIER);
assert.ok(Buffer.concat([outputPubkey, internalPubkey]).equals(kv.key.keydata));
const valueMatch = [Buffer.concat(participantPubKeys), Buffer.concat(participantPubKeys.reverse())].some((pks) => {
return pks.equals(kv.value);
});
assert.ok(valueMatch);
}
export function validateFinalizedInput(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
index: number,
unspent: WalletUnspent<bigint>,
spendType?: 'keyPath' | 'scriptPath'
): void {
const input = psbt.data.inputs[index];
assert.ok(isPsbtInputFinalized(input));
if (scriptTypeForChain(unspent.chain) === 'p2trMusig2' && spendType === 'keyPath') {
assert.strictEqual(input.finalScriptWitness?.length, 66);
}
assert.ok(!input.unknownKeyVals?.length);
}
export function validateParsedTaprootKeyPathPsbt(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
index: number,
signature: 'unsigned' | 'halfsigned' | 'fullysigned'
): void {
const parsed = parsePsbtInput(psbt.data.inputs[0]);
assert.ok(parsed.scriptType === 'taprootKeyPathSpend');
assert.strictEqual(parsed.pubScript.length, 34);
assert.strictEqual(parsed.publicKeys.length, 1);
assert.strictEqual(parsed.publicKeys[0].length, 32);
if (signature === 'unsigned') {
assert.strictEqual(parsed.signatures, undefined);
assert.strictEqual(parsed.participantPublicKeys, undefined);
} else {
const expected = signature === 'halfsigned' ? 1 : 2;
assert.strictEqual(parsed.signatures?.length, expected);
parsed.signatures.forEach((sig) => {
assert.strictEqual(sig.length, 32);
});
assert.strictEqual(parsed.participantPublicKeys?.length, expected);
parsed.participantPublicKeys.forEach((pk) => {
assert.strictEqual(pk.length, 33);
});
}
}
export function validateParsedTaprootScriptPathPsbt(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
index: number,
signature: 'unsigned' | 'halfsigned' | 'fullysigned'
): void {
const input = psbt.data.inputs[index];
const parsed = parsePsbtInput(psbt.data.inputs[0]);
assert.ok(parsed.scriptType === 'taprootScriptPathSpend');
assert.ok(input.tapLeafScript);
assert.ok(parsed.pubScript.equals(input.tapLeafScript[0].script));
assert.ok(parsed.controlBlock.equals(input.tapLeafScript[0].controlBlock));
assert.strictEqual(parsed.scriptPathLevel, 1);
assert.strictEqual(parsed.leafVersion, input.tapLeafScript[0].leafVersion);
parsed.publicKeys.forEach((pk) => {
assert.strictEqual(pk.length, 32);
});
if (signature === 'unsigned') {
assert.strictEqual(parsed.signatures, undefined);
} else {
const expected = signature === 'halfsigned' ? 1 : 2;
assert.strictEqual(parsed.signatures?.length, expected);
parsed.signatures.forEach((sig) => {
assert.strictEqual(sig.length, 64);
});
}
}
export function validateParsedTaprootKeyPathTxInput(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
tx: UtxoTransaction<bigint>
): void {
const parsedTxInput = parseSignatureScript2Of3(tx.ins[0]);
assert.ok(parsedTxInput.scriptType === 'taprootKeyPathSpend');
assert.strictEqual(parsedTxInput.signatures.length, 1);
assert.strictEqual(parsedTxInput.signatures[0].length, 64);
}
export function validateParsedTaprootScriptPathTxInput(
psbt: UtxoPsbt<UtxoTransaction<bigint>>,
tx: UtxoTransaction<bigint>,
index: number
): void {
const input = psbt.data.inputs[index];
const parsedTxInput = parseSignatureScript2Of3(tx.ins[0]);
assert.ok(parsedTxInput);
assert.ok(parsedTxInput.scriptType === 'taprootScriptPathSpend');
assert.ok(input.tapLeafScript);
assert.ok(parsedTxInput.pubScript.equals(input.tapLeafScript[0].script));
assert.ok(parsedTxInput.controlBlock.equals(input.tapLeafScript[0].controlBlock));
assert.strictEqual(parsedTxInput.scriptPathLevel, 1);
assert.strictEqual(parsedTxInput.leafVersion, input.tapLeafScript[0].leafVersion);
parsedTxInput.publicKeys.forEach((pk) => {
assert.strictEqual(pk.length, 32);
});
assert.strictEqual(parsedTxInput.signatures?.length, 2);
parsedTxInput.signatures.forEach((sig) => {
assert.strictEqual(sig.length, 64);
});
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!