@ledgerhq/hw-app-btc
Version:
Ledger Hardware Wallet Bitcoin Application API
129 lines (124 loc) • 4.98 kB
text/typescript
import { BufferWriter, psbtIn, PsbtV2 } from "@ledgerhq/psbtv2";
/**
* This roughly implements the "input finalizer" role of BIP370 (PSBTv2
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However
* the role is documented in BIP174 (PSBTv0
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki).
*
* Verify that all inputs have a signature, and set inputFinalScriptwitness
* and/or inputFinalScriptSig depending on the type of the spent outputs. Clean
* fields that aren't useful anymore, partial signatures, redeem script and
* derivation paths.
*
* @param psbt The psbt with all signatures added as partial sigs, either
* through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG
*/
export function finalize(psbt: PsbtV2): void {
// First check that each input has a signature
const inputCount = psbt.getGlobalInputCount();
for (let i = 0; i < inputCount; i++) {
const legacyPubkeys = psbt.getInputKeyDatas(i, psbtIn.PARTIAL_SIG);
const taprootSig = psbt.getInputTapKeySig(i);
if (legacyPubkeys.length == 0 && !taprootSig) {
throw Error(`No signature for input ${i} present`);
}
if (legacyPubkeys.length > 0) {
if (legacyPubkeys.length > 1) {
throw Error(`Expected exactly one signature, got ${legacyPubkeys.length}`);
}
if (taprootSig) {
throw Error("Both taproot and non-taproot signatures present.");
}
const isSegwitV0 = !!psbt.getInputWitnessUtxo(i);
const redeemScript = psbt.getInputRedeemScript(i);
const isWrappedSegwit = !!redeemScript;
const signature = psbt.getInputPartialSig(i, legacyPubkeys[0]);
if (!signature) throw new Error("Expected partial signature for input " + i);
if (isSegwitV0) {
const witnessBuf = new BufferWriter();
witnessBuf.writeVarInt(2);
witnessBuf.writeVarInt(signature.length);
witnessBuf.writeSlice(signature);
witnessBuf.writeVarInt(legacyPubkeys[0].length);
witnessBuf.writeSlice(legacyPubkeys[0]);
psbt.setInputFinalScriptwitness(i, witnessBuf.buffer());
if (isWrappedSegwit) {
if (!redeemScript || redeemScript.length == 0) {
throw new Error("Expected non-empty redeemscript. Can't finalize intput " + i);
}
const scriptSigBuf = new BufferWriter();
// Push redeemScript length
scriptSigBuf.writeUInt8(redeemScript.length);
scriptSigBuf.writeSlice(redeemScript);
psbt.setInputFinalScriptsig(i, scriptSigBuf.buffer());
}
} else {
// Legacy input
const scriptSig = new BufferWriter();
writePush(scriptSig, signature);
writePush(scriptSig, legacyPubkeys[0]);
psbt.setInputFinalScriptsig(i, scriptSig.buffer());
}
} else {
// Taproot input
const signature = psbt.getInputTapKeySig(i);
if (!signature) {
throw Error("No taproot signature found");
}
if (signature.length != 64 && signature.length != 65) {
throw Error("Unexpected length of schnorr signature.");
}
const witnessBuf = new BufferWriter();
witnessBuf.writeVarInt(1);
witnessBuf.writeVarSlice(signature);
psbt.setInputFinalScriptwitness(i, witnessBuf.buffer());
}
clearFinalizedInput(psbt, i);
}
}
/**
* Deletes fields that are no longer neccesary from the psbt.
*
* Note, the spec doesn't say anything about removing ouput fields
* like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them
* without actually knowing why. I think we should remove them too.
*/
function clearFinalizedInput(psbt: PsbtV2, inputIndex: number) {
const keyTypes = [
psbtIn.BIP32_DERIVATION,
psbtIn.PARTIAL_SIG,
psbtIn.TAP_BIP32_DERIVATION,
psbtIn.TAP_KEY_SIG,
];
const witnessUtxoAvailable = !!psbt.getInputWitnessUtxo(inputIndex);
const nonWitnessUtxoAvailable = !!psbt.getInputNonWitnessUtxo(inputIndex);
if (witnessUtxoAvailable && nonWitnessUtxoAvailable) {
// Remove NON_WITNESS_UTXO for segwit v0 as it's only needed while signing.
// Segwit v1 doesn't have NON_WITNESS_UTXO set.
// See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#cite_note-7
keyTypes.push(psbtIn.NON_WITNESS_UTXO);
}
psbt.deleteInputEntries(inputIndex, keyTypes);
}
/**
* Writes a script push operation to buf, which looks different
* depending on the size of the data. See
* https://en.bitcoin.it/wiki/Script#Constants
*
* @param buf the BufferWriter to write to
* @param data the Buffer to be pushed.
*/
function writePush(buf: BufferWriter, data: Buffer) {
if (data.length <= 75) {
buf.writeUInt8(data.length);
} else if (data.length <= 256) {
buf.writeUInt8(76);
buf.writeUInt8(data.length);
} else if (data.length <= 256 * 256) {
buf.writeUInt8(77);
const b = Buffer.alloc(2);
b.writeUInt16LE(data.length, 0);
buf.writeSlice(b);
}
buf.writeSlice(data);
}