{
 "number": 36013,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36013",
 "title": "test: Descriptor roundtrip and raw()/ addr() coverage",
 "author": "pablomartin4btc",
 "author_association": "MEMBER",
 "created_at": "2026-08-18T19:29:54Z",
 "updated_at": "2026-09-16T15:01:54Z",
 "age_days": 29,
 "draft": false,
 "labels": [
  "Tests"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e",
 "head_ref": "descriptor/roundtrip-raw-addr-tests",
 "head_repo": "pablomartin4btc/bitcoin",
 "head_history": [
  {
   "t": "2026-08-26T23:30:15Z",
   "sha": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e"
  }
 ],
 "additions": 101,
 "deletions": 8,
 "changed_files": 1,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/36013#issuecomment-5334185063"
     },
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/36013#pullrequestreview-5104606928"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36133,
     "title": "wallet: store multipath descriptor",
     "author": "Sjors"
    },
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    }
   ]
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-18T21:17:54Z",
   "stale": false
  },
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-03T16:56:41Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "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": [
   "jeanpablojp",
   "rustaceanrob",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-27T02:06:21Z",
  "last_reviewer_activity": "2026-09-03T16:56:41Z",
  "last_reviewer": "jeanpablojp",
  "author_silent_days": 21,
  "waiting_on_author_days": 13,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   22838,
   24361,
   31244
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 24361,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "Descriptor unit tests and simplifications"
   },
   {
    "number": 31244,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2025-07-31",
    "title": "descriptors: MuSig2"
   },
   {
    "number": 22838,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2024-08-28",
    "title": "descriptors: Be able to specify change and receiving in a single descriptor string"
   }
  ],
  "conflicts": [
   36133,
   35713
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/test/descriptor_tests.cpp"
 ],
 "body": "Part 1 of 2, reviving #24361.\n\nPorts the two test-only commits from the original PR above, re-applied against the current <code>descriptor_tests.cpp</code> \u2014 the original no longer applies cleanly, since <code>DoCheck()</code>'s signature has changed substantially since the original PR was created back in 2022.\n\n`norm_prv` was dropped, and `op_desc_id`, `spender_nlocktime`, `spender_nsequence`, `preimages`, `expected_prv`/`expected_pub`, and `desc_index` were all added - so the original diff no longer applies...\n\n```\ndiff -u <(git show 15220ec903:src/test/descriptor_tests.cpp | sed -n '/^void DoCheck/,/^{/p') \\\n        <(git show HEAD:src/test/descriptor_tests.cpp | sed -n '/^void DoCheck/,/^{/p')\n```\n\n```diff\n@@ -1,3 +1,7 @@\n-void DoCheck(const std::string& prv, const std::string& pub, const std::string& norm_prv, const std::string& norm_pub, int flags, const std::vector<std::vector<std::string>>& scripts, const std::optional<OutputType>& type, const std::set<std::vector<uint32_t>>& paths = ONLY_EMPTY,\n-    bool replace_apostrophe_with_h_in_prv=false, bool replace_apostrophe_with_h_in_pub=false)\n+void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int flags,\n+             const std::vector<std::vector<std::string>>& scripts, const std::optional<OutputType>& type, std::optional<uint256> op_desc_id = std::nullopt,\n+             const std::set<std::vector<uint32_t>>& paths = ONLY_EMPTY, bool replace_apostrophe_with_h_in_prv=false,\n+             bool replace_apostrophe_with_h_in_pub=false, uint32_t spender_nlocktime=0, uint32_t spender_nsequence=CTxIn::SEQUENCE_FINAL,\n+             std::map<std::vector<uint8_t>, std::vector<uint8_t>> preimages={},\n+             std::optional<std::string> expected_prv = std::nullopt, std::optional<std::string> expected_pub = std::nullopt, int desc_index = 0)\n {\n\n```\n\n- Roundtrip testing that inferring a descriptor from a generated scriptPubKey reproduces the original descriptor's structure (keys stripped from both sides for comparison). Added extension for MuSig key expressions ([#31244](https://github.com/bitcoin/bitcoin/pull/31244)) and BIP389 multipath ([#22838](https://github.com/bitcoin/bitcoin/pull/22838)), both added since 2022, and skipping for multi-branch taproot trees, where sibling order isn't meaningful.\n(For more details check 1st commit body)\n- `Check()` coverage for valid `raw()` and `addr()` descriptors, previously exercised only for invalid inputs and only in the script-to-descriptor direction. Added a new `NO_KEYS` flag, since these descriptors have zero keys and several existing checks assumed at least one.\n(For more details check 2nd commit body)\n\nNo behavior change, test-only.\n\n---\n\nPart 2 (not yet opened): the `ToString()`/`ToPrivateString()` merge from #24361, independent of this PR \u2014 needs more design work before it's ready.",
 "commits": [
  {
   "sha": "d840df45eab021c6a81a7c847ee3a183ac237586",
   "date": "2026-08-18T16:49:52Z",
   "message": "test: Roundtripping of descriptors ignoring keys\n\nAdd DropKeys(), which strips a descriptor string down to structure\nonly (key material replaced with \"<KEY>\", sortedmulti() normalized to\nmulti(), checksum dropped), and use it to check that inferring a\ndescriptor back from a script it generated reproduces the original\ndescriptor's structure.\n\nA few descriptor features need special handling for this comparison\nto hold:\n\n- MuSig key expressions (musig(...)) aren't recognized by the plain\n  key-matching regex. Added a separate pass collapsing musig()\n  expressions (and any trailing derivation path) to <KEY> before the\n  plain-key regex runs, rather than teaching that regex to understand\n  musig()'s internals recursively.\n- BIP389 multipath key expressions (<1;2;3>) in derivation paths.\n  Extended the path-segment pattern to recognize them, which added\n  two capture groups to the key-matching regex and shifted the\n  position of the group the replacement substitutes back in.\n  Computed that position from the regex's own capture count instead\n  of hardcoding it, so it can't silently drift out of sync again.\n- Sibling branches within a taproot script tree have no meaningful\n  order (it's a Merkle tree), so InferDescriptor() reconstructing a\n  multi-leaf tree in a different but equally valid branch order isn't\n  something a plain string comparison can account for. Skipped the\n  check for any taproot descriptor with more than one script leaf,\n  detected via the presence of '{' in the descriptor string (the\n  grammar only emits '{'/'}' for grouping 2+ branches).\n\nCo-authored-by: Pieter Wuille <pieter@wuille.net>"
  },
  {
   "sha": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e",
   "date": "2026-08-26T23:27:27Z",
   "message": "test: raw() and addr() descriptors in unit tests\n\nAdd Check() coverage for valid raw() and addr() descriptors: a raw\nscript, legacy and P2SH addresses, two bech32 v0 addresses, and two\nfuture-witness-version addresses (v1 and v2, both expected\nnon-standard/unsolvable) - verifying checksum handling, solvability,\nscript derivation, and structural roundtripping. Address/script pairs\nverified against a live node's own validateaddress before use.\n\nThese descriptors have no key expressions at all, which DoCheck()\ndidn't previously account for:\n\n- keys_priv.keys is expected to be empty rather than non-empty, gated\n  on a new NO_KEYS flag.\n- GetMaxKeyExpr()/GetKeyCount() are expected to both report zero\n  rather than the usual \"highest index + 1 == count\" relationship.\n  Added the NO_KEYS-aware branch for it.\n- HavePrivateKeys() returns false for a keyless descriptor, not\n  vacuously true - it didn't exist when the original patch was\n  written, so this wasn't previously exercised. Gated\n  have_all_private_keys on NO_KEYS in addition to the existing\n  MISSING_PRIVKEYS.\n- raw()/addr() hardcode ToPrivateString() to always fail, so the\n  existing \"can serialize back to the private version\" check needs\n  skipping for these descriptors too. Extended its MISSING_PRIVKEYS\n  gate to also cover NO_KEYS.\n\nCo-authored-by: Pieter Wuille <pieter@wuille.net>"
  }
 ],
 "timeline": [
  {
   "t": "2026-08-18T21:17:54Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK"
  },
  {
   "t": "2026-08-24T10:10:23Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/test/descriptor_tests.cpp",
   "commit": "5b4c861a60b60af6da9526dab8e563cc7adf1da7",
   "in_reply_to": null,
   "text": "Now that we have a few checks in the `true` part of the `if` branch below, perhaps we can change this to be `BOOST_TEST_INFO_SCOPE` so the descriptor is also printed if `BOOST_CHECK_EQUAL(parse_pub->GetKeyCount(), 0U);` fails."
  },
  {
   "t": "2026-08-24T10:11:13Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/test/descriptor_tests.cpp",
   "commit": "5b4c861a60b60af6da9526dab8e563cc7adf1da7",
   "in_reply_to": null,
   "text": "Same feedback as the comment above, perhaps could be `BOOST_TEST_INFO_SCOPE`"
  },
  {
   "t": "2026-08-24T10:12:26Z",
   "kind": "review",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "5b4c861a60b60af6da9526dab8e563cc7adf1da7",
   "text": "Left a comment regarding `BOOST_TEST_INFO`, which only prints the value the next failing check, not any check in scope"
  },
  {
   "t": "2026-08-26T23:30:15Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e"
  },
  {
   "t": "2026-08-27T02:01:33Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/test/descriptor_tests.cpp",
   "commit": "5b4c861a60b60af6da9526dab8e563cc7adf1da7",
   "in_reply_to": 3842489830,
   "text": "Done in 2d230d8366d2149c71e69a2e2ec65561ca09cd0e, thanks!"
  },
  {
   "t": "2026-08-27T02:01:49Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/test/descriptor_tests.cpp",
   "commit": "5b4c861a60b60af6da9526dab8e563cc7adf1da7",
   "in_reply_to": 3842495148,
   "text": "Done in 2d230d8366d2149c71e69a2e2ec65561ca09cd0e, thanks!"
  },
  {
   "t": "2026-08-27T02:06:21Z",
   "kind": "comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "text": "-<ins>_**Updates**_</ins>:\n- Addressed @rustaceanrob's [feedback](https://github.com/bitcoin/bitcoin/pull/36013#pullrequestreview-5006717185): replaced  `BOOST_TEST_INFO` with `BOOST_TEST_INFO_SCOPE` in https://github.com/bitcoin/bitcoin/commit/2d230d8366d2149c71e69a2e2ec65561ca09cd0e."
  },
  {
   "t": "2026-09-03T16:56:41Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e",
   "text": "Concept ACK\n\nBuilt at the head, ran the unit suite, and checked the six new addresses by decoding them outside of Core."
  },
  {
   "t": "2026-09-03T16:56:41Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/descriptor_tests.cpp",
   "commit": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e",
   "in_reply_to": null,
   "text": "The multipath alternative here seems avoidable. It only exists because the roundtrip comparison uses `pub`, the literal test string. In these checks, `pub1` is expanded, and `DoCheck()` already compares it against `pub`, or the expanded `expected_pub` literal for multipath. With the old `STR_KEYSTEP` and the comparison on `pub1`, the suite passes. Both forms run the same 666 roundtrip checks and produce the same 60 failures under the tested `InferScript()` mutation. `mark_count()` is worth keeping either way. Is the literal intended to catch something `pub1` would not?"
  },
  {
   "t": "2026-09-03T16:56:41Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/descriptor_tests.cpp",
   "commit": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e",
   "in_reply_to": null,
   "text": "nit: this matches `sortedmulti(` but not `sortedmulti_a(`, which `InferMultiA()` gives back as `multi_a`. No vector uses it today, so nothing breaks, but I flipped the existing 1-of-1 `multi_a` vector to `sortedmulti_a`, same script by construction, and only the roundtrip comparison failed, six times, `multi_a` versus `sortedmulti_a`. Allowing an optional `_a` here and keeping it in the replacement fixes it, ran that and the suite went back to green."
  },
  {
   "t": "2026-09-03T16:56:41Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/descriptor_tests.cpp",
   "commit": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e",
   "in_reply_to": null,
   "text": "Could we assert the `ToPrivateString()` contract for `NO_KEYS` here instead of only skipping the block? The header promises a `false` return when no private keys are available at all, which is exactly the `raw()` and `addr()` case. I changed both implementations to return `true` and the whole unit suite stayed green. An `else if (flags & NO_KEYS)` with the two negative checks closes it. I ran that, and the same mutation now fails 14 times, twice per new vector."
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-18T19:29:58Z",
   "action": "labeled",
   "label": "Tests",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-26T23:32:24Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-27T01:09:27Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 8349,
 "text_tokens_estimate": 2087,
 "changed_paths": [
  "src/test/descriptor_tests.cpp"
 ],
 "files": [
  {
   "path": "src/test/descriptor_tests.cpp",
   "add": 101,
   "del": 8
  }
 ],
 "test_lines": 109,
 "git": {
  "head": "2d230d8366d2149c71e69a2e2ec65561ca09cd0e",
  "head_matches_backup": true,
  "base": "f72537037d3350e2974efd760eaa7b04f820880c",
  "commits": [
   {
    "sha": "d840df45ea",
    "subject": "test: Roundtripping of descriptors ignoring keys",
    "files": 1,
    "add": 70,
    "del": 0
   },
   {
    "sha": "2d230d8366",
    "subject": "test: raw() and addr() descriptors in unit tests",
    "files": 1,
    "add": 31,
    "del": 8
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "153889b942b8179e",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}