diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 97b22c11e08..e9eae656b51 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -43,7 +43,6 @@ #include #include -#include #include @@ -1929,30 +1928,16 @@ static RPCMethod joinpsbts() merged_psbt.AddOutput(output); } merged_psbt.MergeGlobalXPubs(psbt); + merged_psbt.m_proprietary.insert(psbt.m_proprietary.begin(), psbt.m_proprietary.end()); merged_psbt.unknown.insert(psbt.unknown.begin(), psbt.unknown.end()); } - // Generate list of shuffled indices for shuffling inputs and outputs of the merged PSBT - std::vector input_indices(merged_psbt.inputs.size()); - std::iota(input_indices.begin(), input_indices.end(), 0); - std::vector output_indices(merged_psbt.outputs.size()); - std::iota(output_indices.begin(), output_indices.end(), 0); - - // Shuffle input and output indices lists - std::shuffle(input_indices.begin(), input_indices.end(), FastRandomContext()); - std::shuffle(output_indices.begin(), output_indices.end(), FastRandomContext()); - - PartiallySignedTransaction shuffled_psbt(tx, merged_psbt.GetVersion()); - for (int i : input_indices) { - shuffled_psbt.AddInput(merged_psbt.inputs[i]); - } - for (int i : output_indices) { - shuffled_psbt.AddOutput(merged_psbt.outputs[i]); - } - shuffled_psbt.unknown.insert(merged_psbt.unknown.begin(), merged_psbt.unknown.end()); + // Shuffle the inputs and outputs for privacy + std::shuffle(merged_psbt.inputs.begin(), merged_psbt.inputs.end(), FastRandomContext()); + std::shuffle(merged_psbt.outputs.begin(), merged_psbt.outputs.end(), FastRandomContext()); DataStream ssTx{}; - ssTx << shuffled_psbt; + ssTx << merged_psbt; return EncodeBase64(ssTx); }, }; diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index d8c60fe61a3..ea5e4dfdc36 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -1180,6 +1180,45 @@ class PSBTTest(BitcoinTestFramework): break assert shuffled + # Check that joining preserves global xpub and proprietary records + def global_xpub_key(extended_pubkey): + xpub_data, xpub_version = base58_to_byte(extended_pubkey) + return bytes([PSBT_GLOBAL_XPUB]) + bytes([xpub_version]) + xpub_data + + xpub1 = "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp" + xpub_key1 = global_xpub_key(xpub1) + xpub_key2 = global_xpub_key("tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + xpub_value = b"\x00\x00\x00\x00" # master key fingerprint with an empty derivation path + global_prop_key = bytes([PSBT_GLOBAL_PROPRIETARY]) + b"\x02\x01\x02\x00" # identifier "0102", subtype 0 + global_prop_value = b"\xde\xad\xbe\xef" + + psbt1_obj = PSBT.from_base64(psbt1) + psbt1_obj.g.map[xpub_key1] = xpub_value + psbt1_obj.g.map[global_prop_key] = global_prop_value + psbt2_obj = PSBT.from_base64(psbt2) + psbt2_obj.g.map[xpub_key2] = xpub_value + joined_globals = PSBT.from_base64(self.nodes[0].joinpsbts([psbt1_obj.to_base64(), psbt2_obj.to_base64()])) + assert_equal(joined_globals.g.map[xpub_key1], xpub_value) + assert_equal(joined_globals.g.map[xpub_key2], xpub_value) + assert_equal(joined_globals.g.map[global_prop_key], global_prop_value) + + # Same proprietary key in both PSBTs with different values: the first PSBT's value wins + collide_key = bytes([PSBT_GLOBAL_PROPRIETARY]) + b"\x02\x03\x04\x00" + psbt_first_obj = PSBT.from_base64(psbt1) + psbt_first_obj.g.map[collide_key] = b"\x11\x11\x11\x11" + psbt_second_obj = PSBT.from_base64(psbt2) + psbt_second_obj.g.map[collide_key] = b"\x22\x22\x22\x22" + joined_collision = PSBT.from_base64(self.nodes[0].joinpsbts([psbt_first_obj.to_base64(), psbt_second_obj.to_base64()])) + assert_equal(joined_collision.g.map[collide_key], b"\x11\x11\x11\x11") + + # Same xpub with conflicting origins: the first PSBT's origin is kept, avoiding duplicate keys + conflict_first_obj = PSBT.from_base64(psbt1) + conflict_first_obj.g.map[xpub_key1] = xpub_value + conflict_second_obj = PSBT.from_base64(psbt2) + conflict_second_obj.g.map[xpub_key1] = b"\x11\x11\x11\x11" + joined_conflict = self.nodes[0].joinpsbts([conflict_first_obj.to_base64(), conflict_second_obj.to_base64()]) + assert_equal(self.nodes[0].decodepsbt(joined_conflict)["global_xpubs"], [{"xpub": xpub1, "master_fingerprint": "00000000", "path": "m"}]) + # Newly created PSBT needs UTXOs and updating addr = self.nodes[1].getnewaddress("", "p2sh-segwit") utxo = self.create_outpoints(self.nodes[0], outputs=[{addr: 7}])[0]