psbt: avoid duplicate taproot leaf script keys when merging

m_tap_scripts maps a leaf script to a set of control blocks, but is serialized
one record per control block, keyed by the control block. PSBTInput::Merge
unions it by the map key, so two PSBTs that map the same control block to
different leaf scripts merge into an input serializing the 0x15 key twice.
Duplicate keys are invalid, so combinepsbt hands back a PSBT that can no longer
be decoded. Present since #22558 (v24.0).

Merge the records instead of the map entries, keeping the leaf script already
there, as BIP 174 lets the Combiner pick arbitrarily when conflicts occur. The
control blocks already present are collected once rather than searched for per
incoming record, which would be quadratic in the size of the PSBTs.

Control blocks under a leaf script that both PSBTs carry are now kept as well,
where the map level union dropped them.

The test covers conflicting and non-conflicting merges, including an incoming
leaf script whose control blocks only partly conflict, so records that do not
conflict are not dropped alongside those that do.
This commit is contained in:
Shuvam Pandey
2026-08-20 05:31:02 +05:45
parent 32765aca5c
commit 1cb416397b
2 changed files with 75 additions and 2 deletions

View File

@@ -14,6 +14,7 @@
#include <util/strencodings.h>
#include <algorithm>
#include <set>
using common::PSBTError;
@@ -436,7 +437,17 @@ bool PSBTInput::Merge(const PSBTInput& input)
m_proprietary.insert(input.m_proprietary.begin(), input.m_proprietary.end());
unknown.insert(input.unknown.begin(), input.unknown.end());
m_tap_script_sigs.insert(input.m_tap_script_sigs.begin(), input.m_tap_script_sigs.end());
m_tap_scripts.insert(input.m_tap_scripts.begin(), input.m_tap_scripts.end());
// Merge by control block, the serialized key (BIP 371), to avoid duplicate keys. Keep the
// leaf script already present; BIP 174 lets the Combiner pick arbitrarily on conflict.
std::set<std::vector<unsigned char>> seen_control_blocks;
for (const auto& [_, control_blocks] : m_tap_scripts) {
seen_control_blocks.insert(control_blocks.begin(), control_blocks.end());
}
for (const auto& [leaf, control_blocks] : input.m_tap_scripts) {
for (const auto& control_block : control_blocks) {
if (seen_control_blocks.insert(control_block).second) m_tap_scripts[leaf].insert(control_block);
}
}
m_tap_bip32_paths.insert(input.m_tap_bip32_paths.begin(), input.m_tap_bip32_paths.end());
if (redeem_script.empty() && !input.redeem_script.empty()) redeem_script = input.redeem_script;