{
 "number": 36261,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36261",
 "title": "test: cover PSBT unknown field merging",
 "author": "Bicaru20",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-09-15T13:43:15Z",
 "updated_at": "2026-09-16T20:04:45Z",
 "age_days": 2,
 "draft": false,
 "labels": [
  "Tests"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "21e4ea8134d8ca95416e065490d99f82be66bd67",
 "head_ref": "2026-psbt-unknown-merge-coverage",
 "head_repo": "Bicaru20/bitcoin",
 "head_history": [
  {
   "t": "2026-09-15T13:47:21Z",
   "sha": "c6a22e25e636a2e5fbb3a87848a42ca441b7eca9"
  },
  {
   "t": "2026-09-16T16:34:27Z",
   "sha": "1e86423bfc625da8fecb8d68d73e21bcfc1e941b"
  },
  {
   "t": "2026-09-16T16:35:48Z",
   "sha": "21e4ea8134d8ca95416e065490d99f82be66bd67"
  }
 ],
 "additions": 130,
 "deletions": 0,
 "changed_files": 1,
 "commit_count": 1,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "polespinasa",
      "url": "https://github.com/bitcoin/bitcoin/pull/36261#pullrequestreview-5211766951"
     }
    ],
    "approach_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/36261#issuecomment-5703764879"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "polespinasa": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-15T15:30:57Z",
   "stale": false
  },
  "w0xlt": {
   "kind": "approach_ack",
   "hash": null,
   "t": "2026-09-16T20:04:41Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 1,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "polespinasa",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-16T16:39:39Z",
  "last_reviewer_activity": "2026-09-16T20:04:41Z",
  "last_reviewer": "w0xlt",
  "author_silent_days": 0,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   35310
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35310,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "test: cover PSBT unknown field merging"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "test/functional/rpc_psbt.py"
 ],
 "body": "Continuing the work from #35310 gere in agreement with w0xlt\n\nThis PR adds functional coverage for combinepsbt preserving unknown PSBT fields across global, input, and output maps, as suggested [here](https://github.com/bitcoin/bitcoin/pull/34893#issuecomment-4390043746).\n\nThe test covers both PSBTv0 and PSBTv2 by creating valid base PSBTs with `createpsbt`, injecting unknown key-value pairs into two copies, combining them, and asserting that all unknown fields are retained in the decoded result.",
 "commits": [
  {
   "sha": "21e4ea8134d8ca95416e065490d99f82be66bd67",
   "date": "2026-09-16T16:35:06Z",
   "message": "test: cover PSBT unknown field merging\n\nCo-authored-by: bicaru20 <bicaru2@gmail.com>\nCo-authored-by: w0xlt <94266259+w0xlt@users.noreply.github.com>\nCo-authored-by: polespinasa <pol.espinasa@uab.cat>"
  }
 ],
 "timeline": [
  {
   "t": "2026-09-15T13:43:33Z",
   "kind": "comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "text": "There are currently four test cases:\n\n- Combining the PSBTs with unknown keys and fields with the same values. Since it is duplicated, combining these PSBTs should return a PSBT with a single unknown field.\n- Combining the PSBTs with the same unknown keys values and distinct fields values. In thise case the values of the first PSBT win.\n- Combining PSBTs with unknown fields with the same values. Since the keys are diferent, the combined psbt must contain both keys and values (even if the values are the same).\n- As suggested by @polespinasa and @winterrdog, combining PSBTs preserves unknown fields with distinct values. Since everything is different, the combined psbt must containd both keys and values and those must be distinct.\n\nI added all the tests cases in the same function in the test. I can separate each one into different function if the reviwers think it is clearer this way."
  },
  {
   "t": "2026-09-15T13:47:21Z",
   "kind": "force_push",
   "who": "Bicaru20",
   "commit": "c6a22e25e636a2e5fbb3a87848a42ca441b7eca9"
  },
  {
   "t": "2026-09-15T14:59:57Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "test/functional/rpc_psbt.py",
   "commit": "c6a22e25e636a2e5fbb3a87848a42ca441b7eca9",
   "in_reply_to": null,
   "text": "```suggestion\n            # Combining PSBTs with the same unknown keys but distinct values (first PSBT wins).\n```"
  },
  {
   "t": "2026-09-15T15:30:14Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "test/functional/rpc_psbt.py",
   "commit": "21e4ea8134d8ca95416e065490d99f82be66bd67",
   "in_reply_to": null,
   "text": "Just a style nit preference, it is good as is.\n\nThe same blocks with asserts are repeated over and over, I think a helper function could help hiding the asserts and just keeping the minimal information to make each case clear, improving readability.\n\ndiff\n\n```diff\n$ git diff\ndiff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py\nindex 96ada3386e..6d71784a75 100755\n--- a/test/functional/rpc_psbt.py\n+++ b/test/functional/rpc_psbt.py\n@@ -471,7 +471,7 @@ class PSBTTest(BitcoinTestFramework):\n         ])\n\n     def test_combinepsbt_preserves_unknown_fields(self):\n-        self.log.info(\"Test that combining PSBTs preserves unknown fields with the same and with distinct values per map\")\n+        self.log.info(\"Test that combining PSBTs preserves unknown fields with the same and with distinct values per map, and when they are missing from some maps\")\n\n         def unknown_key(key_type, key_data):\n             return bytes([key_type]) + key_data\n@@ -486,6 +486,16 @@ class PSBTTest(BitcoinTestFramework):\n             psbt.o[0].map[key] = output_value\n             return psbt\n\n+        def check_combined_unknowns(*psbts, global_unknown, input_unknown, output_unknown):\n+            # Combine the PSBTs and check that the unknown fields of the global map, the\n+            # first input and the first output are preserved, along with the PSBT version\n+            combined_psbt = self.nodes[0].combinepsbt([psbt.to_base64() for psbt in psbts])\n+            decoded = self.nodes[0].decodepsbt(combined_psbt)\n+            assert_equal(decoded[\"psbt_version\"], psbt_version)\n+            assert_equal(decoded[\"unknown\"], global_unknown)\n+            assert_equal(decoded[\"inputs\"][0][\"unknown\"], input_unknown)\n+            assert_equal(decoded[\"outputs\"][0][\"unknown\"], output_unknown)\n+\n         unknown_key_a = unknown_key(0xf0, bytes.fromhex(\"010203040506070809\"))\n         unknown_key_b = unknown_key(0xf0, bytes.fromhex(\"010203040506070810\"))\n\n@@ -503,79 +513,59 @@ class PSBTTest(BitcoinTestFramework):\n             base_psbt = self.nodes[0].createpsbt(inputs=inputs, outputs=outputs, psbt_version=psbt_version)\n\n             # Combining the PSBTs with unknown keys and fields with the same values\n-            combined_duplicated_psbt = self.nodes[0].combinepsbt([\n-                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a).to_base64(),\n-                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a).to_base64(),\n-            ])\n-            decoded = self.nodes[0].decodepsbt(combined_duplicated_psbt)\n-            assert_equal(decoded[\"psbt_version\"], psbt_version)\n-            assert_equal(decoded[\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a)\n-            ))\n-            assert_equal(decoded[\"inputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a)\n-            ))\n-            assert_equal(decoded[\"outputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a)\n-            ))\n+            check_combined_unknowns(\n+                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a),\n+                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a),\n+                global_unknown=unknown_fields((unknown_key_a, global_value_a)),\n+                input_unknown=unknown_fields((unknown_key_a, global_value_a)),\n+                output_unknown=unknown_fields((unknown_key_a, global_value_a)),\n+            )\n\n             # Combining the PSBTs with the same unknown keys values and distinct fields values\n             # First PSBT wins\n-            combined_psbt_same_keys = self.nodes[0].combinepsbt([\n-                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a).to_base64(),\n-                build_psbt(unknown_key_a, global_value_b, global_value_b, global_value_b).to_base64(),\n-            ])\n-            decoded = self.nodes[0].decodepsbt(combined_psbt_same_keys)\n-            assert_equal(decoded[\"psbt_version\"], psbt_version)\n-            assert_equal(decoded[\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a)\n-            ))\n-            assert_equal(decoded[\"inputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a)\n-            ))\n-            assert_equal(decoded[\"outputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a)\n-            ))\n+            check_combined_unknowns(\n+                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a),\n+                build_psbt(unknown_key_a, global_value_b, global_value_b, global_value_b),\n+                global_unknown=unknown_fields((unknown_key_a, global_value_a)),\n+                input_unknown=unknown_fields((unknown_key_a, global_value_a)),\n+                output_unknown=unknown_fields((unknown_key_a, global_value_a)),\n+            )\n\n             # Combining PSBTs with unknown fields with the same values\n-            combined_psbt_same_fields = self.nodes[0].combinepsbt([\n-                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a).to_base64(),\n-                build_psbt(unknown_key_b, global_value_a, global_value_a, global_value_a).to_base64(),\n-            ])\n-            decoded = self.nodes[0].decodepsbt(combined_psbt_same_fields)\n-            assert_equal(decoded[\"psbt_version\"], psbt_version)\n-            assert_equal(decoded[\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a),\n-                (unknown_key_b, global_value_a),\n-            ))\n-            assert_equal(decoded[\"inputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a),\n-                (unknown_key_b, global_value_a),\n-            ))\n-            assert_equal(decoded[\"outputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a),\n-                (unknown_key_b, global_value_a),\n-            ))\n+            check_combined_unknowns(\n+                build_psbt(unknown_key_a, global_value_a, global_value_a, global_value_a),\n+                build_psbt(unknown_key_b, global_value_a, global_value_a, global_value_a),\n+                global_unknown=unknown_fields(\n+                    (unknown_key_a, global_value_a),\n+                    (unknown_key_b, global_value_a),\n+                ),\n+                input_unknown=unknown_fields(\n+                    (unknown_key_a, global_value_a),\n+                    (unknown_key_b, global_value_a),\n+                ),\n+                output_unknown=unknown_fields(\n+                    (unknown_key_a, global_value_a),\n+                    (unknown_key_b, global_value_a),\n+                ),\n+            )\n\n             # Combining PSBTs preserves unknown fields with distinct values\n-            combined_psbt = self.nodes[0].combinepsbt([\n-                build_psbt(unknown_key_a, global_value_a, input_value_a, output_value_a).to_base64(),\n-                build_psbt(unknown_key_b, global_value_b, input_value_b, output_value_b).to_base64(),\n-            ])\n-            decoded = self.nodes[0].decodepsbt(combined_psbt)\n-            assert_equal(decoded[\"psbt_version\"], psbt_version)\n-            assert_equal(decoded[\"unknown\"], unknown_fields(\n-                (unknown_key_a, global_value_a),\n-                (unknown_key_b, global_value_b),\n-            ))\n-            assert_equal(decoded[\"inputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, input_value_a),\n-                (unknown_key_b, input_value_b),\n-            ))\n-            assert_equal(decoded[\"outputs\"][0][\"unknown\"], unknown_fields(\n-                (unknown_key_a, output_value_a),\n-                (unknown_key_b, output_value_b),\n-            ))\n+            check_combined_unknowns(\n+                build_psbt(unknown_key_a, global_value_a, input_value_a, output_value_a),\n+                build_psbt(unknown_key_b, global_value_b, input_value_b, output_value_b),\n+                global_unknown=unknown_fields(\n+                    (unknown_key_a, global_value_a),\n+                    (unknown_key_b, global_value_b),\n+                ),\n+                input_unknown=unknown_fields(\n+                    (unknown_key_a, input_value_a),\n+                    (unknown_key_b, input_value_b),\n+                ),\n+                output_unknown=unknown_fields(\n+                    (unknown_key_a, output_value_a),\n+                    (unknown_key_b, output_value_b),\n+                ),\n+            )\n\n     def test_sighash_mismatch(self):\n         self.log.info(\"Test sighash type mismatches\")\n\n```"
  },
  {
   "t": "2026-09-15T15:30:57Z",
   "kind": "review",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "c6a22e25e636a2e5fbb3a87848a42ca441b7eca9",
   "text": "Concept ACK\n\nI would add another test case. What if one of the two PSBTs does not have a value for the unknown field. So pass `None`.\n\ndiff\n\n```diff\n$ git diff\ndiff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py\nindex 96ada3386e..76250ae69b 100755\n--- a/test/functional/rpc_psbt.py\n+++ b/test/functional/rpc_psbt.py\n@@ -471,7 +471,7 @@ class PSBTTest(BitcoinTestFramework):\n         ])\n\n     def test_combinepsbt_preserves_unknown_fields(self):\n-        self.log.info(\"Test that combining PSBTs preserves unknown fields with the same and with distinct values per map\")\n+        self.log.info(\"Test that combining PSBTs preserves unknown fields with the same and with distinct values per map.\")\n\n         def unknown_key(key_type, key_data):\n             return bytes([key_type]) + key_data\n@@ -480,10 +480,14 @@ class PSBTTest(BitcoinTestFramework):\n             return {key.hex(): value.hex() for key, value in entries}\n\n         def build_psbt(key, global_value, input_value, output_value):\n+            # A value of None leaves the corresponding map without the unknown field\n             psbt = PSBT.from_base64(base_psbt)\n-            psbt.g.map[key] = global_value\n-            psbt.i[0].map[key] = input_value\n-            psbt.o[0].map[key] = output_value\n+            if global_value is not None:\n+                psbt.g.map[key] = global_value\n+            if input_value is not None:\n+                psbt.i[0].map[key] = input_value\n+            if output_value is not None:\n+                psbt.o[0].map[key] = output_value\n             return psbt\n\n         unknown_key_a = unknown_key(0xf0, bytes.fromhex(\"010203040506070809\"))\n@@ -577,6 +581,24 @@ class PSBTTest(BitcoinTestFramework):\n                 (unknown_key_b, output_value_b),\n             ))\n\n+            # Combining PSBTs with unknown fields missing from the maps of the other PSBT\n+            # The fields of each map are preserved regardless of which PSBT they come from\n+            combined_psbt = self.nodes[0].combinepsbt([\n+                build_psbt(unknown_key_a, global_value_a, None, None).to_base64(),\n+                build_psbt(unknown_key_a, None, input_value_b, output_value_b).to_base64(),\n+            ])\n+            decoded = self.nodes[0].decodepsbt(combined_psbt)\n+            assert_equal(decoded[\"psbt_version\"], psbt_version)\n+            assert_equal(decoded[\"unknown\"], unknown_fields(\n+                (unknown_key_a, global_value_a)\n+            ))\n+            assert_equal(decoded[\"inputs\"][0][\"unknown\"], unknown_fields(\n+                (unknown_key_a, input_value_b)\n+            ))\n+            assert_equal(decoded[\"outputs\"][0][\"unknown\"], unknown_fields(\n+                (unknown_key_a, output_value_b)\n+            ))\n+\n     def test_sighash_mismatch(self):\n         self.log.info(\"Test sighash type mismatches\")\n         self.nodes[0].createwallet(\"sighash_mismatch\")\n\n```"
  },
  {
   "t": "2026-09-16T16:34:27Z",
   "kind": "force_push",
   "who": "Bicaru20",
   "commit": "1e86423bfc625da8fecb8d68d73e21bcfc1e941b"
  },
  {
   "t": "2026-09-16T16:35:48Z",
   "kind": "force_push",
   "who": "Bicaru20",
   "commit": "21e4ea8134d8ca95416e065490d99f82be66bd67"
  },
  {
   "t": "2026-09-16T16:36:18Z",
   "kind": "review_comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "path": "test/functional/rpc_psbt.py",
   "commit": "c6a22e25e636a2e5fbb3a87848a42ca441b7eca9",
   "in_reply_to": 4017048304,
   "text": "Fixed!"
  },
  {
   "t": "2026-09-16T16:37:38Z",
   "kind": "review_comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "path": "test/functional/rpc_psbt.py",
   "commit": "21e4ea8134d8ca95416e065490d99f82be66bd67",
   "in_reply_to": 4017355441,
   "text": "I'd rather keep it as is. In my opinion it is clearer this way. If more reviwers think you way is better I can change it."
  },
  {
   "t": "2026-09-16T16:39:39Z",
   "kind": "comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "text": "Added the new test case that was suggested and addressed the nits.\n\nAlso added @polespinasa as Co-author."
  },
  {
   "t": "2026-09-16T20:04:41Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Approach ACK"
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-15T13:43:19Z",
   "action": "labeled",
   "label": "Tests",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-15T13:48:37Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-15T15:09:50Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-16T16:36:52Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-16T17:40:18Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 13631,
 "text_tokens_estimate": 3407,
 "changed_paths": [
  "test/functional/rpc_psbt.py"
 ],
 "files": [
  {
   "path": "test/functional/rpc_psbt.py",
   "add": 130,
   "del": 0
  }
 ],
 "test_lines": 130,
 "git": {
  "head": "21e4ea8134d8ca95416e065490d99f82be66bd67",
  "head_matches_backup": true,
  "base": "51ddab532cb38213e2258c24c492bc8a392ffc90",
  "commits": [
   {
    "sha": "21e4ea8134",
    "subject": "test: cover PSBT unknown field merging",
    "files": 1,
    "add": 130,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "eed3b6c95a8156a1",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}