Skip to content

Commit

Permalink
Support unknown psbt keypairs (#119)
Browse files Browse the repository at this point in the history
* support unknown psbt keypairs

* rename testcase

* cleaning comment

* TxOpts: add allowUnknown flag

* run format

* remove "ESM is broken" lines
  • Loading branch information
louisinger authored Jan 20, 2025
1 parent b77a1f4 commit f866dbb
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 11 deletions.
12 changes: 9 additions & 3 deletions src/psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,8 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
psbtEnum: T,
val: PSBTKeyMapKeys<T>,
cur?: PSBTKeyMapKeys<T>,
allowedFields?: (keyof PSBTKeyMapKeys<T>)[]
allowedFields?: (keyof PSBTKeyMapKeys<T>)[],
allowUnknown?: boolean
): PSBTKeyMapKeys<T> {
const res: PSBTKeyMapKeys<T> = { ...cur, ...val };
// All arguments can be provided as hex
Expand Down Expand Up @@ -517,8 +518,13 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
throw new Error(`Cannot change signed field=${k}`);
}
}
// Remove unknown keys
for (const k in res) if (!psbtEnum[k]) delete res[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;
}

Expand Down
19 changes: 14 additions & 5 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function cloneDeep<T>(obj: T): T {

// Mostly security features, hardened defaults;
// but you still can parse other people tx with unspendable outputs and stuff if you want
export type TxOpts = {
export interface TxOpts {
version?: number;
lockTime?: number;
PSBTVersion?: number;
Expand All @@ -84,7 +84,9 @@ export type TxOpts = {
allowLegacyWitnessUtxo?: boolean;
lowR?: boolean; // Use lowR signatures
customScripts?: CustomScript[]; // UNSAFE: Custom payment scripts
};
// Allow to add additional unknown keys/values to the "unknown" array member
allowUnknown?: boolean;
}

/**
* Internal, exported only for backwards-compat. Use `SigHash` instead.
Expand Down Expand Up @@ -520,7 +522,8 @@ export class Transaction {
input,
this.inputs[idx],
allowedFields,
this.opts.disableScriptCheck
this.opts.disableScriptCheck,
this.opts.allowUnknown
);
}
// Output stuff
Expand Down Expand Up @@ -556,7 +559,7 @@ export class Transaction {
if (script === undefined) script = cur?.script;
let res: psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput> = { ...cur, ...o, amount, script };
if (res.amount === undefined) delete res.amount;
res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields);
res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.allowUnknown);
psbt.PSBTOutputCoder.encode(res);
if (
res.script &&
Expand Down Expand Up @@ -1070,7 +1073,13 @@ export class Transaction {
: P.EMPTY;
if (!equalBytes(thisUnsigned, otherUnsigned))
throw new Error(`Transaction/combine: different unsigned tx`);
this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global);
this.global = psbt.mergeKeyMap(
psbt.PSBTGlobal,
this.global,
other.global,
undefined,
this.opts.allowUnknown
);
for (let i = 0; i < this.inputs.length; i++) this.updateInput(i, other.inputs[i], true);
for (let i = 0; i < this.outputs.length; i++) this.updateOutput(i, other.outputs[i], true);
return this;
Expand Down
13 changes: 10 additions & 3 deletions src/utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export function normalizeInput(
i: psbt.TransactionInputUpdate,
cur?: psbt.TransactionInput,
allowedFields?: (keyof psbt.TransactionInput)[],
disableScriptCheck = false
disableScriptCheck = false,
allowUnknown = false
): psbt.TransactionInput {
let { nonWitnessUtxo, txid } = i;
// String support for common fields. We usually prefer Uint8Array to avoid errors
Expand All @@ -55,7 +56,7 @@ export function normalizeInput(
if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields);
res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown);
psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point

let prevOut;
Expand Down Expand Up @@ -410,7 +411,13 @@ export class _Estimator {
}
const inputKeys = new Set();
this.normalizedInputs = allInputs.map((i) => {
const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck);
const normalized = normalizeInput(
i,
undefined,
undefined,
opts.disableScriptCheck,
opts.allowUnknown
);
inputBeforeSign(normalized); // check fields
const key = `${hex.encode(normalized.txid!)}:${normalized.index}`;
if (!opts.allowSameUtxo && inputKeys.has(key))
Expand Down
32 changes: 32 additions & 0 deletions test/bip174-psbt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,36 @@ should('bip174-psbt: PSBT garbage', () => {
);
});

should('bip174-psbt: PSBT unknown keys', () => {
const tx = hex.decode(
'70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000'
)

const psbtWithoutAllowUnknown = btc.Transaction.fromPSBT(tx);
const psbtWithAllowUnknown = btc.Transaction.fromPSBT(tx, { allowUnknown: true });

const unknown = [
[{ type: 0xff, key: new Uint8Array(Buffer.from('unknownKey1')) }, new Uint8Array(Buffer.from('unknownValue1'))],
[{ type: 0xff, key: new Uint8Array(Buffer.from('unknownKey2')) }, new Uint8Array(Buffer.from('unknownValue2'))],
]

// input unknown key
psbtWithoutAllowUnknown.updateInput(0, { unknown });
psbtWithAllowUnknown.updateInput(0, { unknown });

// output unknown key
psbtWithoutAllowUnknown.updateOutput(0, { unknown });
psbtWithAllowUnknown.updateOutput(0, { unknown });
// verify the unknown key is preserved only if the flag is set
deepStrictEqual(psbtWithAllowUnknown.outputs[0].unknown, unknown);
deepStrictEqual(psbtWithAllowUnknown.inputs[0].unknown, unknown);
deepStrictEqual(psbtWithoutAllowUnknown.outputs[0].unknown, undefined);
deepStrictEqual(psbtWithoutAllowUnknown.inputs[0].unknown, undefined);

// verify the unknown key is preserved with serialization
const psbt2 = btc.Transaction.fromPSBT(psbtWithAllowUnknown.toPSBT());
deepStrictEqual(psbt2.outputs[0].unknown, unknown);
deepStrictEqual(psbt2.inputs[0].unknown, unknown);
});

should.runWhen(import.meta.url);

0 comments on commit f866dbb

Please sign in to comment.