{
 "number": 36343,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36343",
 "title": "psbt: verify final scripts in FinalizePSBT",
 "author": "Bruce039",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-09-26T09:55:14Z",
 "updated_at": "2026-10-30T11:41:12Z",
 "description_hash": "c1a4f774e2fab155",
 "age_days": 36,
 "draft": false,
 "labels": [
  "PSBT"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "7148c65121cbe7225acc9ab789146c3632f9de97",
 "head_ref": "fix-finalizepsbt-verify-final",
 "head_repo": "Bruce039/bitcoin",
 "head_history": [
  {
   "t": "2026-09-26T10:01:45Z",
   "sha": "417a548cf23460d87d20ea5d3e1c9c7bcc5af03f"
  }
 ],
 "additions": 41,
 "deletions": 1,
 "changed_files": 3,
 "commit_count": 2,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "fametrano",
      "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-5968528317"
     }
    ],
    "concept_ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-6468028476"
     },
     {
      "login": "Bicaru20",
      "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-6521767967"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "fametrano": {
   "kind": "ack",
   "hash": "7148c65121cbe7225acc9ab789146c3632f9de97",
   "t": "2026-10-03T11:02:23Z",
   "stale": false
  },
  "l0rinc": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-28T11:40:07Z",
   "stale": false
  },
  "Bicaru20": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-30T11:41:08Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Bicaru20",
   "fametrano",
   "l0rinc"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-02T15:32:54Z",
  "last_reviewer_activity": "2026-10-30T11:41:08Z",
  "last_reviewer": "Bicaru20",
  "author_silent_days": 30,
  "waiting_on_author_days": 2,
  "days_since_update": 2
 },
 "refs": {
  "mentioned": [
   30357,
   33014
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 30357,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2024-07-16",
    "title": "Fix cases of calls to `FillPSBT` errantly returning `complete=true`"
   },
   {
    "number": 33014,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-07-22",
    "title": "rpc: Fix internal bug in descriptorprocesspsbt when encountering invalid signatures"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "`finalizepsbt` can return `complete: true` and a hex for a PSBT whose `PSBT_IN_FINAL_SCRIPTWITNESS` (or final scriptSig) is invalid.\n\n`FinalizePSBT` counts an input as complete when `SignPSBTInput` succeeds. If the input already has final fields, `PSBTInput::FillSignatureData` marks the sigdata complete and `ProduceSignature` returns true early (sign.cpp:752), before the script is verified. So the existing final fields are trusted as-is.\n\nTo reproduce, take a signed P2WPKH v0 PSBT that has a `non_witness_utxo` and flip one bit of the signature inside the final witness:\n- `finalizepsbt` returns `complete: true` and a hex\n- `analyzepsbt` says the input is not final, `next: finalizer`\n- `testmempoolaccept` on the hex fails with `mempool-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)`\n\nInputs that only have a `witness_utxo` already come out incomplete because of the `require_witness_sig` check, so this affects inputs with a `non_witness_utxo`.\n\n#30357 fixed the same problem in `FillPSBT` by checking `PSBTInputSignedAndVerified`, and #33014 did it for `descriptorprocesspsbt`. This does the same in `FinalizePSBT`. Taproot verification needs every spent output. If one is missing, that input already fails in `SignPSBTInput` with `MISSING_INPUTS` and makes the PSBT incomplete, so the extra check doesn't change the result in that case.\n\nThe new test in `rpc_psbt.py` builds that PSBT and checks that `finalizepsbt` returns `complete: false` with no hex. It fails on master and passes with the fix. `wallet_musig.py` and the `psbt_wallet_tests` / `script_tests` unit tests still pass.\n\nOne behaviour change: a final script that is valid by consensus but non-standard now gives `complete: false`. `FillPSBT` and `descriptorprocesspsbt` already behave that way, since they check with the same standard flags.",
 "commits": [
  {
   "sha": "417a548cf23460d87d20ea5d3e1c9c7bcc5af03f",
   "date": "2026-09-26T10:01:42Z",
   "message": "psbt: verify final scripts in FinalizePSBT\n\nFinalizePSBT counts an input as complete whenever SignPSBTInput succeeds.\nFor an input that already has final_script_sig/final_script_witness,\nFillSignatureData marks the signature data complete and ProduceSignature\nreturns true before running its script check, so the existing final\nfields are never verified. finalizepsbt then returns complete=true and a\nhex that is rejected by the mempool, while analyzepsbt reports the same\ninput as not final.\n\n#30357 fixed this for FillPSBT by using PSBTInputSignedAndVerified. Do\nthe same in FinalizePSBT."
  },
  {
   "sha": "7148c65121cbe7225acc9ab789146c3632f9de97",
   "date": "2026-10-02T15:32:48Z",
   "message": "doc: add release note for #36343"
  }
 ],
 "timeline": [
  {
   "t": "2026-09-26T10:01:45Z",
   "kind": "force_push",
   "who": "Bruce039",
   "commit": "417a548cf23460d87d20ea5d3e1c9c7bcc5af03f",
   "id": "push:417a548cf23460d87d20ea5d3e1c9c7bcc5af03f"
  },
  {
   "t": "2026-10-02T11:51:16Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "Tested ACK 417a548cf23460d87d20ea5d3e1c9c7bcc5af03f\n\nThe new test fails with the one-line fix reverted and passes with it.\n\nI also flipped a signature bit in the final fields of pkh, sh-wpkh, wpkh and taproot key-path inputs, and of the first input of a three-input PSBT whose other inputs were only partially signed. Without the fix, finalizepsbt returned complete=true and a hex whenever the input also had a non_witness_utxo, and testmempoolaccept and generateblock (submit=false) both rejected that hex. With the fix, all of them are complete=false with no hex. Valid, standard signatures give the same result as before.\n\nAs the description says, a final witness that is consensus-valid but non-standard is now complete=false too. I checked with a high-S signature: before, it was extracted when the input had a non_witness_utxo, and now it is not. This matches walletprocesspsbt, so I think it is right, but it may deserve a release note.",
   "id": "c:5951768922",
   "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-5951768922",
   "hash": "49601163533b60a0",
   "edited": null
  },
  {
   "t": "2026-10-02T15:32:54Z",
   "kind": "comment",
   "who": "Bruce039",
   "assoc": "CONTRIBUTOR",
   "text": "Thanks for the thorough testing. Agreed on the release note, added `doc/release-notes-36343.md` in a separate commit (7148c65121) covering the non-standard case. The code commit is unchanged.",
   "id": "c:5955738380",
   "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-5955738380",
   "hash": "27eb40960f53646b",
   "edited": "2026-10-02T15:33:30Z"
  },
  {
   "t": "2026-10-03T11:02:23Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "reACK 7148c65121cbe7225acc9ab789146c3632f9de97\n\nOnly the release note is new since my Tested ACK, and it matches the code.",
   "id": "c:5968528317",
   "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-5968528317",
   "hash": "5736e50f609c027d",
   "edited": null
  },
  {
   "t": "2026-10-28T11:40:07Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Concept ACK",
   "id": "c:6468028476",
   "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-6468028476",
   "hash": "e8334cc43879b73c",
   "edited": null
  },
  {
   "t": "2026-10-30T11:41:08Z",
   "kind": "comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK\nWhile reviweing the code, in the tests I noticed that the field of `next` in `analyzepsbt` still returns `finalizer`. Since the final script_sig or final script_witness failed verification and in the same test we see that when trying to finalize the psbt it fails, I belvie this field on `analyzepsbt` can be missleading and should return  `updater`.\nI though we could modify the `AnalyzePSBT` in `src/node/psbt.cpp` to set the correct message in the `next` field.\nSomething like this:\n\n  ```diff\n  --- a/src/node/psbt.cpp\n  +++ b/src/node/psbt.cpp\n  @@ -70,26 +70,31 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)\n           if (!PSBTInputSignedAndVerified(psbtx, i, &txdata)) {\n               input_analysis.is_final = false;\n\n  -            // Figure out what is missing\n  -            SignatureData outdata;\n  -            const auto sign_result = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, /*options*/{}, &outdata);\n  -            bool complete = sign_result.has_value();\n  -\n  -            // Things are missing\n  -            if (!complete) {\n  -                input_analysis.missing_pubkeys = outdata.missing_pubkeys;\n  -                input_analysis.missing_redeem_script = outdata.missing_redeem_script;\n  -                input_analysis.missing_witness_script = outdata.missing_witness_script;\n  -                input_analysis.missing_sigs = outdata.missing_sigs;\n  -\n  -                // If we are only missing signatures and nothing else, then next is signer\n  -                if (outdata.missing_pubkeys.empty() && outdata.missing_redeem_script.IsNull() && outdata.missing_witness_script.IsNull() && !outdata.missing_sigs.empty()) {\n  -                    input_analysis.next = PSBTRole::SIGNER;\n  +            if (PSBTInputSigned(input)) {\n  +                // Final fields exist AND failed verification. The next role is the UPDATER.\n  +                input_analysis.next = PSBTRole::UPDATER;\n  +            } else {\n  +                // Figure out what is missing\n  +                SignatureData outdata;\n  +                const auto sign_result = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, /*options*/{}, &outdata);\n  +                bool complete = sign_result.has_value();\n  +\n  +                // Things are missing\n  +                if (!complete) {\n  +                    input_analysis.missing_pubkeys = outdata.missing_pubkeys;\n  +                    input_analysis.missing_redeem_script = outdata.missing_redeem_script;\n  +                    input_analysis.missing_witness_script = outdata.missing_witness_script;\n  +                    input_analysis.missing_sigs = outdata.missing_sigs;\n  +\n  +                    // If we are only missing signatures and nothing else, then next is signer\n  +                    if (outdata.missing_pubkeys.empty() && outdata.missing_redeem_script.IsNull() && outdata.missing_witness_script.IsNull() && !outdata.missing_sigs.empty()) {\n  +                        input_analysis.next = PSBTRole::SIGNER;\n  +                    } else {\n  +                        input_analysis.next = PSBTRole::UPDATER;\n  +                    }\n                   } else {\n  -                    input_analysis.next = PSBTRole::UPDATER;\n  +                    input_analysis.next = PSBTRole::FINALIZER;\n                   }\n  -            } else {\n  -                input_analysis.next = PSBTRole::FINALIZER;\n               }\n           } else if (!utxo.IsNull()){\n               input_analysis.is_final = true;\n  diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py\n  index 2353017626..fa34e163ea 100755\n  --- a/test/functional/rpc_psbt.py\n  +++ b/test/functional/rpc_psbt.py\n  @@ -902,7 +902,9 @@ class PSBTTest(BitcoinTestFramework):\n           flawed_psbt.i[0].map[PSBT_IN_FINAL_SCRIPTWITNESS] = valid_witness[:2] + bitflipper(signature) + valid_witness[2 + sig_len:]\n           invalid_sig_psbt = flawed_psbt.to_base64()\n\n  -        assert_equal(self.nodes[0].analyzepsbt(invalid_sig_psbt)[\"inputs\"][0][\"is_final\"], False)\n  +        analysis = self.nodes[0].analyzepsbt(invalid_sig_psbt)\n  +        assert_equal(analysis[\"inputs\"][0][\"is_final\"], False)\n  +        assert_equal(analysis[\"inputs\"][0][\"next\"], \"updater\")\n           result = self.nodes[0].finalizepsbt(invalid_sig_psbt)\n           assert_equal(result[\"complete\"], False)\n           assert_equal(\"hex\" in result, False)\n\n  ```\n\nTo avoid modifying `analyzepsbt`, I think we could modify `SignPSBTInput` so when having inputs final script_sig or final script_witness it verifies that the scripts are valid. In case the verification fails it would return an error which would cause the `next` field in `analyzepsbt` to return `updater` instead of `finalizer`. However, I haven't checked this last case.",
   "id": "c:6521767967",
   "url": "https://github.com/bitcoin/bitcoin/pull/36343#issuecomment-6521767967",
   "hash": "8786dda7d9aa95f5",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-26T09:55:18Z",
   "action": "labeled",
   "label": "PSBT",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-26T18:32:26Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-26T20:03:57Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 8598,
 "text_tokens_estimate": 2149,
 "changed_paths": [
  "doc/release-notes-36343.md",
  "src/psbt.cpp",
  "test/functional/rpc_psbt.py"
 ],
 "files": [
  {
   "path": "doc/release-notes-36343.md",
   "add": 10,
   "del": 0
  },
  {
   "path": "src/psbt.cpp",
   "add": 3,
   "del": 1
  },
  {
   "path": "test/functional/rpc_psbt.py",
   "add": 28,
   "del": 0
  }
 ],
 "test_lines": 28,
 "git": {
  "head": "7148c65121cbe7225acc9ab789146c3632f9de97",
  "head_matches_backup": true,
  "base": "ed7dd7cf4e1561a97edf72eba29a67b14e28c717",
  "patch_id": "60fb24d081fdb2d5",
  "commits": [
   {
    "sha": "417a548cf2",
    "subject": "psbt: verify final scripts in FinalizePSBT",
    "files": 2,
    "add": 31,
    "del": 1
   },
   {
    "sha": "7148c65121",
    "subject": "doc: add release note for #36343",
    "files": 1,
    "add": 10,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "60fb24d081fdb2d5",
 "input_hash": "5c4ec42a8540da93",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}