@scure/btc-signer
Version:
Audited & minimal library for Bitcoin. Handle transactions, Schnorr, Taproot, UTXO & PSBT
414 lines • 19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RawPSBTV2 = exports.RawPSBTV0 = exports._DebugPSBT = exports._RawPSBTV2 = exports._RawPSBTV0 = exports.PSBTOutputCoder = exports.PSBTInputCoder = exports.PSBTOutputUnsignedKeys = exports.PSBTOutput = exports.PSBTInputUnsignedKeys = exports.PSBTInputFinalKeys = exports.PSBTInput = exports.PSBTGlobal = exports.TaprootControlBlock = void 0;
exports.cleanPSBTFields = cleanPSBTFields;
exports.mergeKeyMap = mergeKeyMap;
const base_1 = require("@scure/base");
const P = require("micro-packed");
const script_ts_1 = require("./script.js");
const utils_ts_1 = require("./utils.js");
// PSBT BIP174, BIP370, BIP371
// Can be 33 or 64 bytes
const PubKeyECDSA = P.validate(P.bytes(null), (pub) => (0, utils_ts_1.validatePubkey)(pub, utils_ts_1.PubT.ecdsa));
const PubKeySchnorr = P.validate(P.bytes(32), (pub) => (0, utils_ts_1.validatePubkey)(pub, utils_ts_1.PubT.schnorr));
const SignatureSchnorr = P.validate(P.bytes(null), (sig) => {
if (sig.length !== 64 && sig.length !== 65)
throw new Error('Schnorr signature should be 64 or 65 bytes long');
return sig;
});
const BIP32Der = P.struct({
fingerprint: P.U32BE,
path: P.array(null, P.U32LE),
});
const TaprootBIP32Der = P.struct({
hashes: P.array(script_ts_1.CompactSizeLen, P.bytes(32)),
der: BIP32Der,
});
// The 78 byte serialized extended public key as defined by BIP 32.
const GlobalXPUB = P.bytes(78);
const tapScriptSigKey = P.struct({ pubKey: PubKeySchnorr, leafHash: P.bytes(32) });
// Complex structure for PSBT fields
// <control byte with leaf version and parity bit> <internal key p> <C> <E> <AB>
const _TaprootControlBlock = P.struct({
version: P.U8, // With parity :(
internalKey: P.bytes(32),
merklePath: P.array(null, P.bytes(32)),
});
exports.TaprootControlBlock = P.validate(_TaprootControlBlock, (cb) => {
if (cb.merklePath.length > 128)
throw new Error('TaprootControlBlock: merklePath should be of length 0..128 (inclusive)');
return cb;
});
// {<8-bit uint depth> <8-bit uint leaf version> <compact size uint scriptlen> <bytes script>}*
const tapTree = P.array(null, P.struct({
depth: P.U8,
version: P.U8,
script: script_ts_1.VarBytes,
}));
const BytesInf = P.bytes(null); // Bytes will conflict with Bytes type
const Bytes20 = P.bytes(20);
const Bytes32 = P.bytes(32);
// versionsRequiringExclusing = !versionsAllowsInclusion (as set)
// {name: [tag, keyCoder, valueCoder, versionsRequiringInclusion, versionsRequiringExclusing, versionsAllowsInclusion, silentIgnore]}
// SilentIgnore: we use some v2 fields for v1 representation too, so we just clean them before serialize
// Tables from BIP-0174 (https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki)
// prettier-ignore
exports.PSBTGlobal = {
unsignedTx: [0x00, false, script_ts_1.RawOldTx, [0], [0], false],
xpub: [0x01, GlobalXPUB, BIP32Der, [], [0, 2], false],
txVersion: [0x02, false, P.U32LE, [2], [2], false],
fallbackLocktime: [0x03, false, P.U32LE, [], [2], false],
inputCount: [0x04, false, script_ts_1.CompactSizeLen, [2], [2], false],
outputCount: [0x05, false, script_ts_1.CompactSizeLen, [2], [2], false],
txModifiable: [0x06, false, P.U8, [], [2], false], // TODO: bitfield
version: [0xfb, false, P.U32LE, [], [0, 2], false],
proprietary: [0xfc, BytesInf, BytesInf, [], [0, 2], false],
};
// prettier-ignore
exports.PSBTInput = {
nonWitnessUtxo: [0x00, false, script_ts_1.RawTx, [], [0, 2], false],
witnessUtxo: [0x01, false, script_ts_1.RawOutput, [], [0, 2], false],
partialSig: [0x02, PubKeyECDSA, BytesInf, [], [0, 2], false],
sighashType: [0x03, false, P.U32LE, [], [0, 2], false],
redeemScript: [0x04, false, BytesInf, [], [0, 2], false],
witnessScript: [0x05, false, BytesInf, [], [0, 2], false],
bip32Derivation: [0x06, PubKeyECDSA, BIP32Der, [], [0, 2], false],
finalScriptSig: [0x07, false, BytesInf, [], [0, 2], false],
finalScriptWitness: [0x08, false, script_ts_1.RawWitness, [], [0, 2], false],
porCommitment: [0x09, false, BytesInf, [], [0, 2], false],
ripemd160: [0x0a, Bytes20, BytesInf, [], [0, 2], false],
sha256: [0x0b, Bytes32, BytesInf, [], [0, 2], false],
hash160: [0x0c, Bytes20, BytesInf, [], [0, 2], false],
hash256: [0x0d, Bytes32, BytesInf, [], [0, 2], false],
txid: [0x0e, false, Bytes32, [2], [2], true],
index: [0x0f, false, P.U32LE, [2], [2], true],
sequence: [0x10, false, P.U32LE, [], [2], true],
requiredTimeLocktime: [0x11, false, P.U32LE, [], [2], false],
requiredHeightLocktime: [0x12, false, P.U32LE, [], [2], false],
tapKeySig: [0x13, false, SignatureSchnorr, [], [0, 2], false],
tapScriptSig: [0x14, tapScriptSigKey, SignatureSchnorr, [], [0, 2], false],
tapLeafScript: [0x15, exports.TaprootControlBlock, BytesInf, [], [0, 2], false],
tapBip32Derivation: [0x16, Bytes32, TaprootBIP32Der, [], [0, 2], false],
tapInternalKey: [0x17, false, PubKeySchnorr, [], [0, 2], false],
tapMerkleRoot: [0x18, false, Bytes32, [], [0, 2], false],
proprietary: [0xfc, BytesInf, BytesInf, [], [0, 2], false],
};
// All other keys removed when finalizing
exports.PSBTInputFinalKeys = [
'txid',
'sequence',
'index',
'witnessUtxo',
'nonWitnessUtxo',
'finalScriptSig',
'finalScriptWitness',
'unknown',
];
// Can be modified even on signed input
exports.PSBTInputUnsignedKeys = [
'partialSig',
'finalScriptSig',
'finalScriptWitness',
'tapKeySig',
'tapScriptSig',
];
// prettier-ignore
exports.PSBTOutput = {
redeemScript: [0x00, false, BytesInf, [], [0, 2], false],
witnessScript: [0x01, false, BytesInf, [], [0, 2], false],
bip32Derivation: [0x02, PubKeyECDSA, BIP32Der, [], [0, 2], false],
amount: [0x03, false, P.I64LE, [2], [2], true],
script: [0x04, false, BytesInf, [2], [2], true],
tapInternalKey: [0x05, false, PubKeySchnorr, [], [0, 2], false],
tapTree: [0x06, false, tapTree, [], [0, 2], false],
tapBip32Derivation: [0x07, PubKeySchnorr, TaprootBIP32Der, [], [0, 2], false],
proprietary: [0xfc, BytesInf, BytesInf, [], [0, 2], false],
};
// Can be modified even on signed input
exports.PSBTOutputUnsignedKeys = [];
const PSBTKeyPair = P.array(P.NULL, P.struct({
// <key> := <keylen> <keytype> <keydata> WHERE keylen = len(keytype)+len(keydata)
key: P.prefix(script_ts_1.CompactSizeLen, P.struct({ type: script_ts_1.CompactSizeLen, key: P.bytes(null) })),
// <value> := <valuelen> <valuedata>
value: P.bytes(script_ts_1.CompactSizeLen),
}));
function PSBTKeyInfo(info) {
const [type, kc, vc, reqInc, allowInc, silentIgnore] = info;
return { type, kc, vc, reqInc, allowInc, silentIgnore };
}
const PSBTUnknownKey = P.struct({ type: script_ts_1.CompactSizeLen, key: P.bytes(null) });
// Key cannot be 'unknown', value coder cannot be array for elements with empty key
function PSBTKeyMap(psbtEnum) {
// -> Record<type, [keyName, ...coders]>
const byType = {};
for (const k in psbtEnum) {
const [num, kc, vc] = psbtEnum[k];
byType[num] = [k, kc, vc];
}
return P.wrap({
encodeStream: (w, value) => {
let out = [];
// Because we use order of psbtEnum, keymap is sorted here
for (const name in psbtEnum) {
const val = value[name];
if (val === undefined)
continue;
const [type, kc, vc] = psbtEnum[name];
if (!kc) {
out.push({ key: { type, key: P.EMPTY }, value: vc.encode(val) });
}
else {
// Low level interface, returns keys as is (with duplicates). Useful for debug
const kv = val.map(([k, v]) => [
kc.encode(k),
vc.encode(v),
]);
// sort by keys
kv.sort((a, b) => (0, utils_ts_1.compareBytes)(a[0], b[0]));
for (const [key, value] of kv)
out.push({ key: { key, type }, value });
}
}
if (value.unknown) {
value.unknown.sort((a, b) => (0, utils_ts_1.compareBytes)(a[0].key, b[0].key));
for (const [k, v] of value.unknown)
out.push({ key: k, value: v });
}
PSBTKeyPair.encodeStream(w, out);
},
decodeStream: (r) => {
const raw = PSBTKeyPair.decodeStream(r);
const out = {};
const noKey = {};
for (const elm of raw) {
let name = 'unknown';
let key = elm.key.key;
let value = elm.value;
if (byType[elm.key.type]) {
const [_name, kc, vc] = byType[elm.key.type];
name = _name;
if (!kc && key.length) {
throw new Error(`PSBT: Non-empty key for ${name} (key=${base_1.hex.encode(key)} value=${base_1.hex.encode(value)}`);
}
key = kc ? kc.decode(key) : undefined;
value = vc.decode(value);
if (!kc) {
if (out[name])
throw new Error(`PSBT: Same keys: ${name} (key=${key} value=${value})`);
out[name] = value;
noKey[name] = true;
continue;
}
}
else {
// For unknown: add key type inside key
key = { type: elm.key.type, key: elm.key.key };
}
// Only keyed elements at this point
if (noKey[name])
throw new Error(`PSBT: Key type with empty key and no key=${name} val=${value}`);
if (!out[name])
out[name] = [];
out[name].push([key, value]);
}
return out;
},
});
}
exports.PSBTInputCoder = P.validate(PSBTKeyMap(exports.PSBTInput), (i) => {
if (i.finalScriptWitness && !i.finalScriptWitness.length)
throw new Error('validateInput: empty finalScriptWitness');
//if (i.finalScriptSig && !i.finalScriptSig.length) throw new Error('validateInput: empty finalScriptSig');
if (i.partialSig && !i.partialSig.length)
throw new Error('Empty partialSig');
if (i.partialSig)
for (const [k] of i.partialSig)
(0, utils_ts_1.validatePubkey)(k, utils_ts_1.PubT.ecdsa);
if (i.bip32Derivation)
for (const [k] of i.bip32Derivation)
(0, utils_ts_1.validatePubkey)(k, utils_ts_1.PubT.ecdsa);
// Locktime = unsigned little endian integer greater than or equal to 500000000 representing
if (i.requiredTimeLocktime !== undefined && i.requiredTimeLocktime < 500000000)
throw new Error(`validateInput: wrong timeLocktime=${i.requiredTimeLocktime}`);
// unsigned little endian integer greater than 0 and less than 500000000
if (i.requiredHeightLocktime !== undefined &&
(i.requiredHeightLocktime <= 0 || i.requiredHeightLocktime >= 500000000))
throw new Error(`validateInput: wrong heighLocktime=${i.requiredHeightLocktime}`);
if (i.tapLeafScript) {
// tap leaf version appears here twice: in control block and at the end of script
for (const [k, v] of i.tapLeafScript) {
if ((k.version & 254) !== v[v.length - 1])
throw new Error('validateInput: tapLeafScript version mimatch');
if (v[v.length - 1] & 1)
throw new Error('validateInput: tapLeafScript version has parity bit!');
}
}
return i;
});
exports.PSBTOutputCoder = P.validate(PSBTKeyMap(exports.PSBTOutput), (o) => {
if (o.bip32Derivation)
for (const [k] of o.bip32Derivation)
(0, utils_ts_1.validatePubkey)(k, utils_ts_1.PubT.ecdsa);
return o;
});
const PSBTGlobalCoder = P.validate(PSBTKeyMap(exports.PSBTGlobal), (g) => {
const version = g.version || 0;
if (version === 0) {
if (!g.unsignedTx)
throw new Error('PSBTv0: missing unsignedTx');
for (const inp of g.unsignedTx.inputs)
if (inp.finalScriptSig && inp.finalScriptSig.length)
throw new Error('PSBTv0: input scriptSig found in unsignedTx');
}
return g;
});
exports._RawPSBTV0 = P.struct({
magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
global: PSBTGlobalCoder,
inputs: P.array('global/unsignedTx/inputs/length', exports.PSBTInputCoder),
outputs: P.array(null, exports.PSBTOutputCoder),
});
exports._RawPSBTV2 = P.struct({
magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
global: PSBTGlobalCoder,
inputs: P.array('global/inputCount', exports.PSBTInputCoder),
outputs: P.array('global/outputCount', exports.PSBTOutputCoder),
});
exports._DebugPSBT = P.struct({
magic: P.magic(P.string(new Uint8Array([0xff])), 'psbt'),
items: P.array(null, P.apply(P.array(P.NULL, P.tuple([P.hex(script_ts_1.CompactSizeLen), P.bytes(script_ts_1.CompactSize)])), P.coders.dict())),
});
function validatePSBTFields(version, info, lst) {
for (const k in lst) {
if (k === 'unknown')
continue;
if (!info[k])
continue;
const { allowInc } = PSBTKeyInfo(info[k]);
if (!allowInc.includes(version))
throw new Error(`PSBTv${version}: field ${k} is not allowed`);
}
for (const k in info) {
const { reqInc } = PSBTKeyInfo(info[k]);
if (reqInc.includes(version) && lst[k] === undefined)
throw new Error(`PSBTv${version}: missing required field ${k}`);
}
}
function cleanPSBTFields(version, info, lst) {
const out = {};
for (const _k in lst) {
const k = _k;
if (k !== 'unknown') {
if (!info[k])
continue;
const { allowInc, silentIgnore } = PSBTKeyInfo(info[k]);
if (!allowInc.includes(version)) {
if (silentIgnore)
continue;
throw new Error(`Failed to serialize in PSBTv${version}: ${k} but versions allows inclusion=${allowInc}`);
}
}
out[k] = lst[k];
}
return out;
}
function validatePSBT(tx) {
const version = (tx && tx.global && tx.global.version) || 0;
validatePSBTFields(version, exports.PSBTGlobal, tx.global);
for (const i of tx.inputs)
validatePSBTFields(version, exports.PSBTInput, i);
for (const o of tx.outputs)
validatePSBTFields(version, exports.PSBTOutput, o);
// We allow only one empty element at the end of map (compat with bitcoinjs-lib bug)
const inputCount = !version ? tx.global.unsignedTx.inputs.length : tx.global.inputCount;
if (tx.inputs.length < inputCount)
throw new Error('Not enough inputs');
const inputsLeft = tx.inputs.slice(inputCount);
if (inputsLeft.length > 1 || (inputsLeft.length && Object.keys(inputsLeft[0]).length))
throw new Error(`Unexpected inputs left in tx=${inputsLeft}`);
// Same for inputs
const outputCount = !version ? tx.global.unsignedTx.outputs.length : tx.global.outputCount;
if (tx.outputs.length < outputCount)
throw new Error('Not outputs inputs');
const outputsLeft = tx.outputs.slice(outputCount);
if (outputsLeft.length > 1 || (outputsLeft.length && Object.keys(outputsLeft[0]).length))
throw new Error(`Unexpected outputs left in tx=${outputsLeft}`);
return tx;
}
function mergeKeyMap(psbtEnum, val, cur, allowedFields, allowUnknown) {
const res = { ...cur, ...val };
// All arguments can be provided as hex
for (const k in psbtEnum) {
const key = k;
const [_, kC, vC] = psbtEnum[key];
const cannotChange = allowedFields && !allowedFields.includes(k);
if (val[k] === undefined && k in val) {
if (cannotChange)
throw new Error(`Cannot remove signed field=${k}`);
delete res[k];
}
else if (kC) {
const oldKV = (cur && cur[k] ? cur[k] : []);
let newKV = val[key];
if (newKV) {
if (!Array.isArray(newKV))
throw new Error(`keyMap(${k}): KV pairs should be [k, v][]`);
// Decode hex in k-v
newKV = newKV.map((val) => {
if (val.length !== 2)
throw new Error(`keyMap(${k}): KV pairs should be [k, v][]`);
return [
typeof val[0] === 'string' ? kC.decode(base_1.hex.decode(val[0])) : val[0],
typeof val[1] === 'string' ? vC.decode(base_1.hex.decode(val[1])) : val[1],
];
});
const map = {};
const add = (kStr, k, v) => {
if (map[kStr] === undefined) {
map[kStr] = [k, v];
return;
}
const oldVal = base_1.hex.encode(vC.encode(map[kStr][1]));
const newVal = base_1.hex.encode(vC.encode(v));
if (oldVal !== newVal)
throw new Error(`keyMap(${key}): same key=${kStr} oldVal=${oldVal} newVal=${newVal}`);
};
for (const [k, v] of oldKV) {
const kStr = base_1.hex.encode(kC.encode(k));
add(kStr, k, v);
}
for (const [k, v] of newKV) {
const kStr = base_1.hex.encode(kC.encode(k));
// undefined removes previous value
if (v === undefined) {
if (cannotChange)
throw new Error(`Cannot remove signed field=${key}/${k}`);
delete map[kStr];
}
else
add(kStr, k, v);
}
res[key] = Object.values(map);
}
}
else if (typeof res[k] === 'string') {
res[k] = vC.decode(base_1.hex.decode(res[k]));
}
else if (cannotChange && k in val && cur && cur[k] !== undefined) {
if (!(0, utils_ts_1.equalBytes)(vC.encode(val[k]), vC.encode(cur[k])))
throw new Error(`Cannot change signed field=${k}`);
}
}
// Remove unknown keys except the "unknown" array if allowUnknown is true
for (const k in res) {
if (!psbtEnum[k]) {
if (allowUnknown && k === 'unknown')
continue;
delete res[k];
}
}
return res;
}
exports.RawPSBTV0 = P.validate(exports._RawPSBTV0, validatePSBT);
exports.RawPSBTV2 = P.validate(exports._RawPSBTV2, validatePSBT);
//# sourceMappingURL=psbt.js.map