{
 "number": 35301,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35301",
 "title": "Silent Payments: Implement bip352 (take 2)",
 "author": "Eunovo",
 "author_association": "MEMBER",
 "created_at": "2026-05-16T03:46:01Z",
 "updated_at": "2026-09-17T01:45:21Z",
 "age_days": 124,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "542475dce0fbaef13871cbb11b5b11992389633f",
 "head_ref": "implement-bip352",
 "head_repo": "Eunovo/bitcoin",
 "head_history": [
  {
   "t": "2026-05-18T16:03:01Z",
   "sha": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9"
  },
  {
   "t": "2026-05-21T14:56:55Z",
   "sha": "804d7ae60bfb4667325b9c4c17f8d8eb12d1e2b6"
  },
  {
   "t": "2026-05-22T05:26:29Z",
   "sha": "fd56daa891ad9370637c8bf7574c1712b6859a20"
  },
  {
   "t": "2026-05-25T09:14:36Z",
   "sha": "80d3a4853a9952b3781cce88d13232dc8bcea447"
  },
  {
   "t": "2026-05-29T11:05:15Z",
   "sha": "43dc84da070a282e3ed90bceefe616baa4a6c4c5"
  },
  {
   "t": "2026-06-07T11:02:40Z",
   "sha": "295ebcaeeb9f71f39c53623d33728975da8ec2e6"
  },
  {
   "t": "2026-06-23T09:58:43Z",
   "sha": "35be12454989519d128c3b1523a31938db061955"
  },
  {
   "t": "2026-06-26T09:45:05Z",
   "sha": "30f6396c7df7d4329e517f5f97dcafe29c68b4e8"
  },
  {
   "t": "2026-07-06T16:45:58Z",
   "sha": "0ab06bda74011e39fa616edbfb7e81f365e8c74c"
  },
  {
   "t": "2026-07-07T12:05:40Z",
   "sha": "36b553a1273b9fdbfeefc3108b387c6dc846ef81"
  },
  {
   "t": "2026-07-23T22:11:40Z",
   "sha": "606c1f4b49bfa882c5dd8cd3abe55476aad4996b"
  },
  {
   "t": "2026-07-24T10:51:18Z",
   "sha": "90c2898fe2206a33e71d854a8f93e2f5709e0688"
  },
  {
   "t": "2026-07-27T09:37:20Z",
   "sha": "dde9ab61b53a60b55f722f2a16bcce33f95b7ba6"
  },
  {
   "t": "2026-08-10T16:13:39Z",
   "sha": "b57cd63d760b96e645004970b38ae8c4e8de0692"
  },
  {
   "t": "2026-08-10T17:19:01Z",
   "sha": "ed66407d72697ee4e00b1ae62f5a6a06d60729f0"
  },
  {
   "t": "2026-08-11T13:43:14Z",
   "sha": "4e7ac8f3e451bb3ee38c5ff8701b39c13ecc6de4"
  },
  {
   "t": "2026-08-12T10:43:21Z",
   "sha": "1894ee3a4a7669330ec0999ce510dc2d92eb1c65"
  },
  {
   "t": "2026-08-15T01:08:24Z",
   "sha": "b905cea56f5ca46f0d7e12643c9dd9045706b155"
  },
  {
   "t": "2026-08-17T17:54:53Z",
   "sha": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c"
  },
  {
   "t": "2026-08-24T16:14:20Z",
   "sha": "12515f645eaf22ecbea6ba0b3827c550f2e656cb"
  },
  {
   "t": "2026-09-02T14:59:10Z",
   "sha": "d3218d87019526c625e0235bf6209f4c78713a4a"
  },
  {
   "t": "2026-09-02T15:17:33Z",
   "sha": "6b5a993e808d35d27b6f23c8e1b36c2601e5e2b6"
  },
  {
   "t": "2026-09-04T14:14:27Z",
   "sha": "a97b2202584f2a7628a277ea15192925e518888e"
  },
  {
   "t": "2026-09-16T13:43:29Z",
   "sha": "542475dce0fbaef13871cbb11b5b11992389633f"
  }
 ],
 "additions": 6946,
 "deletions": 0,
 "changed_files": 10,
 "commit_count": 4,
 "size_bucket": "XL",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/35301#issuecomment-4490760075"
     },
     {
      "login": "rkrux",
      "url": "https://github.com/bitcoin/bitcoin/pull/35301#issuecomment-4490966505"
     },
     {
      "login": "josibake",
      "url": "https://github.com/bitcoin/bitcoin/pull/35301#issuecomment-5280606718"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36257,
     "title": "qa: assert_equals -> assert_true/assert_false",
     "author": "hodlinator"
    },
    {
     "number": 36167,
     "title": "[RFC] Enable `-Wunused`",
     "author": "fanquake"
    },
    {
     "number": 35793,
     "title": "Implement BIP 54 (Consensus Cleanup) without mainnet activation",
     "author": "darosior"
    },
    {
     "number": 35642,
     "title": "headersync: do parameter search at runtime",
     "author": "sipa"
    }
   ]
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-05-19T18:21:52Z",
   "stale": false
  },
  "rkrux": {
   "kind": "concept_ack",
   "hash": "aca8a8f",
   "t": "2026-05-19T18:47:37Z",
   "stale": false
  },
  "josibake": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-13T12:47:34Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 3,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "josibake",
   "rkrux",
   "rustaceanrob",
   "sedited",
   "theStack",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-16T13:58:08Z",
  "last_reviewer_activity": "2026-09-10T09:55:07Z",
  "last_reviewer": "josibake",
  "author_silent_days": 1,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   34225,
   36203
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34225,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-05-15",
    "title": "refactor, key: move `CreateMuSig2{Nonce,PartialSig}` functions to `musig.{h,cpp}` module"
   },
   {
    "number": 36203,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-09",
    "title": "Update secp256k1 subtree to latest master"
   }
  ],
  "conflicts": [
   36257,
   36167,
   35793,
   35642
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/addresstype.cpp",
  "src/addresstype.h",
  "src/bech32.h",
  "src/common/bip352.cpp",
  "src/common/bip352.h",
  "src/kernel/chainparams.h",
  "src/key.cpp",
  "src/key.h",
  "src/key_io.cpp",
  "src/test/bip352_tests.cpp",
  "src/test/data/bip352_send_and_receive_vectors.json",
  "src/test/key_io_tests.cpp",
  "test/functional/rpc_validateaddress.py"
 ],
 "body": "This PR is part of integrating silent payments into Bitcoin Core. It is the second iteration of https://github.com/bitcoin/bitcoin/pull/28122, now based on https://github.com/bitcoin-core/secp256k1/pull/1765.\n\nThis project is tracked in https://github.com/bitcoin/bitcoin/issues/28536.\n\nBIP352\nThis PR focuses strictly on the BIP logic and attempts to separate it from the wallet and transaction implementation details. This is accomplished by working directly with public and private keys, instead of needing a wallet backend and transactions for testing. Labels for the receiver are optional and thus deferred for a later PR.\n\nTest vectors from the BIP are included as unit tests.",
 "commits": [
  {
   "sha": "0dd01902e94af804fa5d81ea8992efc19944eb39",
   "date": "2026-09-16T10:23:49Z",
   "message": "crypto: add read-only method to KeyPair\n\nAdd a method for passing a KeyPair object to secp256k1 functions expecting a secp256k1_keypair.\nThis allows for passing a KeyPair directly to a secp256k1 function without needing to create a\ntemporary secp256k1_keypair object.\n\nCo-authored-by: Rob <rob.netzke@gmail.com>"
  },
  {
   "sha": "a3444fb2d6b485214e4d99273c3b8014c45f665a",
   "date": "2026-09-16T10:23:49Z",
   "message": "Add \"sp\" HRP"
  },
  {
   "sha": "7e0cc68ecaa7d69cd4c04c5ecf6a643bda9b898d",
   "date": "2026-09-16T12:58:44Z",
   "message": "common: add bip352.{h,cpp} secp256k1 module\n\nWrap the silentpayments module from libsecp256k1. This is placed in\ncommon as it is intended to be used by:\n\n  * RPCs: for parsing addresses\n  * Wallet: for sending, receiving, spending silent payments outputs\n  * Node: for creating silent payments indexes for light clients\n\nCo-authored-by: Rob <rob.netzke@gmail.com>"
  },
  {
   "sha": "542475dce0fbaef13871cbb11b5b11992389633f",
   "date": "2026-09-16T12:58:44Z",
   "message": "tests: add BIP352 test vectors as unit tests\n\nUse the test vectors to test sending and receiving. A few cases are not\ncovered here, namely anything that requires testing specific to the\nwallet. For example:\n\n* Taproot script path spending is not tested, as that is better tested in\n  a wallets coin selection / signing logic\n* Re-computing outputs during RBF is not tested, as that is better\n  tested in a wallets RBF logic"
  }
 ],
 "timeline": [
  {
   "t": "2026-05-18T01:43:50Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": null,
   "text": "in 6355a904945d0d5704032e464f20a69a5d82a91b: Since #34225 (commit f36d89f4363a25f7948a0f7096201ef8e15045d8), the sign context can be accessed via `GetSecp256k1SignContext`, i.e. by using this in the API calls below, this line and the symbol visibility change in key.cpp are not needed anymore"
  },
  {
   "t": "2026-05-18T01:59:35Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": null,
   "text": "in 6355a904945d0d5704032e464f20a69a5d82a91b: typo: s/uint156/uint256/"
  },
  {
   "t": "2026-05-18T02:04:03Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9",
   "in_reply_to": null,
   "text": "in 6355a904945d0d5704032e464f20a69a5d82a91b: nit: missing doxygen `@param` entry for `prevouts_summary` above"
  },
  {
   "t": "2026-05-18T02:10:43Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "804d7ae60bfb4667325b9c4c17f8d8eb12d1e2b6",
   "in_reply_to": null,
   "text": "in 6355a904945d0d5704032e464f20a69a5d82a91b:\n```suggestion\n        ret &= secp256k1_ec_pubkey_parse(secp256k1_context_static, &recipient_obj.spend_pubkey, recipients[i].m_spend_pubkey.data(), recipients[i].m_spend_pubkey.size());\n```\nto ensure both `pubkey_parse` are successful (or alternatively, could place an extra `assert(ret)` line after the first call)"
  },
  {
   "t": "2026-05-18T02:21:07Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": null,
   "text": "in 6355a904945d0d5704032e464f20a69a5d82a91b: IIUC, this call could fail if a transaction is scanned where one of the P2TR outputs encodes an invalid x-only pubkey (i.e. not on the curve), so I suppose this should be changed to e.g. `if (!ret) continue;` to avoid a crash (unless we demand from the caller that `tx_outputs` only contain valid x-only pubkeys already)"
  },
  {
   "t": "2026-05-18T02:24:36Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": null,
   "text": "in 6355a904945d0d5704032e464f20a69a5d82a91b: comment doesn't apply"
  },
  {
   "t": "2026-05-18T02:30:10Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "ba4b734ec89d590951574d2714867afab27d1347",
   "in_reply_to": null,
   "text": "in ba4b734ec89d590951574d2714867afab27d1347: nit: could add a corresponding new entry to the comment list a few lines above"
  },
  {
   "t": "2026-05-18T02:37:33Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/bech32.h",
   "commit": "ba4b734ec89d590951574d2714867afab27d1347",
   "in_reply_to": null,
   "text": "in ba4b734ec89d590951574d2714867afab27d1347: pedantic nit: according to BIP-352 the limit is [1023](https://github.com/bitcoin/bips/blob/6deafd07ff5d38c7d69b27530ba55a4ee038cde7/bip-0352.mediawiki?plain=1#L219) (not sure which of the two values make more sense, as I'm not very familiar with BIP-173; for SPV0 it doesn't matter anyways)"
  },
  {
   "t": "2026-05-18T16:03:01Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9"
  },
  {
   "t": "2026-05-18T16:03:42Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": 3255922097,
   "text": "Done."
  },
  {
   "t": "2026-05-18T16:03:51Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": 3255958806,
   "text": "Done."
  },
  {
   "t": "2026-05-18T16:04:02Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9",
   "in_reply_to": 3255968343,
   "text": "Done."
  },
  {
   "t": "2026-05-18T16:04:25Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": 3256003872,
   "text": "Changed to `if (!ret) continue;`"
  },
  {
   "t": "2026-05-18T16:04:39Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "6355a904945d0d5704032e464f20a69a5d82a91b",
   "in_reply_to": 3256010958,
   "text": "Done."
  },
  {
   "t": "2026-05-18T16:06:44Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "804d7ae60bfb4667325b9c4c17f8d8eb12d1e2b6",
   "in_reply_to": 3255981201,
   "text": "I added an `assert(ret);` after the first call, so that it is easier to determine which of the pubkeys is invalid, in the event of a crash."
  },
  {
   "t": "2026-05-18T16:10:29Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/bech32.h",
   "commit": "ba4b734ec89d590951574d2714867afab27d1347",
   "in_reply_to": 3256039588,
   "text": "Changed to `1023` to match the [BIP specification](https://github.com/bitcoin/bips/blob/6deafd07ff5d38c7d69b27530ba55a4ee038cde7/bip-0352.mediawiki?plain=1#L219). AFAICT, there is no reason to use `1024`; the most likely reason for it being `1024` is that the BIP might have stated `1024` and was updated to `1023` at some point in the past."
  },
  {
   "t": "2026-05-18T16:11:37Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "ba4b734ec89d590951574d2714867afab27d1347",
   "in_reply_to": 3256023741,
   "text": "Done."
  },
  {
   "t": "2026-05-19T18:21:52Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK"
  },
  {
   "t": "2026-05-19T18:47:37Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "text": "Concept ACK aca8a8f"
  },
  {
   "t": "2026-05-20T14:01:01Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/kernel/chainparams.h",
   "commit": "602835e08070cab981842ce4cd5065730b3e8c48",
   "in_reply_to": null,
   "text": "in 602835e08070cab981842ce4cd5065730b3e8c48 (and 6b71f145ccfdbfe41f7f666d373efb0a68e30375 ff.): nitty nit: personally, I would slightly prefer to use the plural form in the code base since it's the widely used protocol name\n```suggestion\n    const std::string& SilentPaymentsHRP() const { return silent_payments_hrp; }\n```"
  },
  {
   "t": "2026-05-20T14:35:30Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "6b71f145ccfdbfe41f7f666d373efb0a68e30375",
   "in_reply_to": null,
   "text": "in 6b71f145ccfdbfe41f7f666d373efb0a68e30375: Related to a recent off-band discussion we had, I wonder if we should check the validity of the pubkeys (i.e. following the compressed pubkey format and being on the curve) already at this point, e.g. via\n```\n            if (!scan_pukey.IsFullyValid() || !spend_pubkey.IsFullyValid()) return CNoDestination();\n```\nWe don't do the same when decoding taproot addresses (currently the only other address format that directly encodes public keys, without hashing), but the difference with SP here is that an actual output script can't even be derived in this case, so it could make more sense to reject as early as possible."
  },
  {
   "t": "2026-05-20T14:54:44Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/test/data/bip352_send_and_receive_vectors.json",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "in aca8a8f3da02b925f8975ffa6f31468fafdc2ef9: looks like test vectors .json file needs to be updated (to BIP-352 version 1.1.1, see latest change https://github.com/bitcoin/bips/pull/2142)."
  },
  {
   "t": "2026-05-20T14:56:42Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "in 0287192f099299fb7a0b7f83f5e3bfb3174ed152: unless I'm missing something, there is nothing wrong in checking that the witness stack is non-empty before accessing it, and the TODO could simply be removed"
  },
  {
   "t": "2026-05-20T16:27:35Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "6b71f145ccfdbfe41f7f666d373efb0a68e30375",
   "in_reply_to": null,
   "text": "in 6b71f145ccfdbfe41f7f666d373efb0a68e30375: pedantic nit: the version byte is not part of the `ConvertBits` input, i.e. this should be\n```suggestion\n        // ConvertBits will expand each 8-bit byte into 5-bit chunks,\n        // i.e. 1 + (66 * 8 / 5) = 106.6 -> so we reserve 107\n        data_out.reserve(107);\n```\n(verified that `data_out` has indeed a size of 107 by adding debug outputs)"
  },
  {
   "t": "2026-05-20T16:31:17Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/test/bip352_tests.cpp",
   "commit": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9",
   "in_reply_to": null,
   "text": "in aca8a8f3da02b925f8975ffa6f31468fafdc2ef9: could do a round-trip test in the if body, to also add test coverage for encoding SP addresses, e.g.\n```\n       auto encoded_sp_addr = EncodeDestination(*sp);\n       BOOST_CHECK(encoded_sp_addr == silent_payment_addresses[i].get_str());\n```"
  },
  {
   "t": "2026-05-20T16:35:45Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/test/bip352_tests.cpp",
   "commit": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9",
   "in_reply_to": null,
   "text": "in aca8a8f3da02b925f8975ffa6f31468fafdc2ef9: this function is currently unused"
  },
  {
   "t": "2026-05-21T00:17:58Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "fd56daa891ad9370637c8bf7574c1712b6859a20",
   "in_reply_to": null,
   "text": "in 5b0d46e13980f227908186dfdd529d030ff7400a: I suppose using std::unordered_map for the labels cache would be the better choice for  performance reasons, at least if we ever support a large number of labels (it doesn't matter until actual SP receiving support is implemented though, and can be re-evaluated and benchmarked then)"
  },
  {
   "t": "2026-05-21T14:56:55Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "804d7ae60bfb4667325b9c4c17f8d8eb12d1e2b6"
  },
  {
   "t": "2026-05-21T14:57:16Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/bip352_tests.cpp",
   "commit": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9",
   "in_reply_to": 3275576701,
   "text": "Removed."
  },
  {
   "t": "2026-05-21T14:58:12Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/bip352_tests.cpp",
   "commit": "aca8a8f3da02b925f8975ffa6f31468fafdc2ef9",
   "in_reply_to": 3275551824,
   "text": "I added some tests for Encoding and Decoding `V0SilentPaymentsDestination` to `key_io_tests.cpp` in https://github.com/bitcoin/bitcoin/pull/35301/commits/15a2bdd5a18ac6d543b669f9230ea5bd7352c497"
  },
  {
   "t": "2026-05-21T14:58:27Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "6b71f145ccfdbfe41f7f666d373efb0a68e30375",
   "in_reply_to": 3275529941,
   "text": "Updated."
  },
  {
   "t": "2026-05-21T14:58:46Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3274950527,
   "text": "Removed."
  },
  {
   "t": "2026-05-21T14:59:29Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/data/bip352_send_and_receive_vectors.json",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3274936420,
   "text": "Updated to the test_vectors in https://github.com/bitcoin/bips/pull/2142"
  },
  {
   "t": "2026-05-21T14:59:39Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "6b71f145ccfdbfe41f7f666d373efb0a68e30375",
   "in_reply_to": 3274798692,
   "text": "Done."
  },
  {
   "t": "2026-05-21T15:00:53Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/kernel/chainparams.h",
   "commit": "602835e08070cab981842ce4cd5065730b3e8c48",
   "in_reply_to": 3274544497,
   "text": "Done. I also pluralised the name in other function names, variable names and comments."
  },
  {
   "t": "2026-05-21T15:01:34Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "text": "Does https://github.com/bitcoin-core/secp256k1/pull/1765 need to be merged first for this to be reviewed (and later merged)?"
  },
  {
   "t": "2026-05-21T15:22:21Z",
   "kind": "comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThere are no major API changes expected at this point in [bitcoin-core/secp256k1#1765](https://github.com/bitcoin-core/secp256k1/pull/1765), so I'd say this PR can be already reviewed now. For merging it though, the SP module merge and secp256k1 subtree update have to go in first. Obviously, any review in https://github.com/bitcoin-core/secp256k1/pull/1765 would be much appreciated :)"
  },
  {
   "t": "2026-05-22T05:26:29Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "fd56daa891ad9370637c8bf7574c1712b6859a20"
  },
  {
   "t": "2026-05-22T15:03:48Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "in 5b0d46e13980f227908186dfdd529d030ff7400a: could `assert(ret)` here as well, as this serialization should never fail"
  },
  {
   "t": "2026-05-22T15:07:40Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key.cpp",
   "commit": "5b0d46e13980f227908186dfdd529d030ff7400a",
   "in_reply_to": null,
   "text": "in 5b0d46e13980f227908186dfdd529d030ff7400a: this change isn't needed anymore, since you are using the `GetSecp256k1SignContext()` access function now"
  },
  {
   "t": "2026-05-22T15:11:33Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "7df966bc37fe640660ff8a9cd55a9fd5331527dc",
   "in_reply_to": null,
   "text": "in 7df966bc37fe640660ff8a9cd55a9fd5331527dc: consistency micro-nit: in other destination classes, the `private:` part comes before the public one, so could move this up"
  },
  {
   "t": "2026-05-22T15:13:49Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "7df966bc37fe640660ff8a9cd55a9fd5331527dc",
   "in_reply_to": null,
   "text": "in 7df966bc37fe640660ff8a9cd55a9fd5331527dc: nit: I think this could be simplified to a one-liner\n```suggestion\n        return (a.m_scan_pubkey == b.m_scan_pubkey) && (a.m_spend_pubkey == b.m_spend_pubkey);\n```\n\nwithout changing the logic or performance (didn't verify though)."
  },
  {
   "t": "2026-05-22T15:22:47Z",
   "kind": "review",
   "who": "theStack",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "fd56daa891ad9370637c8bf7574c1712b6859a20",
   "text": "Thanks for the quick follow-up! Left a few more comments, most of them being nits. Mostly reviewed the parts around the SP module API calls so far, will take a closer look at higher-level parts of the protocol (particularly the pubkey extraction logic in `GetPubKeyFromInput`) within the next days."
  },
  {
   "t": "2026-05-25T09:14:36Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "80d3a4853a9952b3781cce88d13232dc8bcea447"
  },
  {
   "t": "2026-05-25T09:59:15Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "7df966bc37fe640660ff8a9cd55a9fd5331527dc",
   "in_reply_to": 3289351639,
   "text": "Done."
  },
  {
   "t": "2026-05-25T09:59:26Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "7df966bc37fe640660ff8a9cd55a9fd5331527dc",
   "in_reply_to": 3289339136,
   "text": "Done."
  },
  {
   "t": "2026-05-25T09:59:43Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key.cpp",
   "commit": "5b0d46e13980f227908186dfdd529d030ff7400a",
   "in_reply_to": 3289316512,
   "text": "Done."
  },
  {
   "t": "2026-05-25T10:00:11Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3289294878,
   "text": "Done."
  },
  {
   "t": "2026-05-25T10:02:46Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "fd56daa891ad9370637c8bf7574c1712b6859a20",
   "in_reply_to": 3277893263,
   "text": "Changed to `std::unordered_map`, but that required that I change the labels cache from `map<CPubKey, uint256>` to `unordered_map<CKeyID, uint256, SaltedSipHasher>` because `CPubKey` doesn't have a hash function suitable for use with `unordered_map`."
  },
  {
   "t": "2026-05-27T00:32:29Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "in 061edd8f427674914d98a22e118122fd8dd0a0c8: currently, SP addresses with (not yet specified) versions 1-30 are already accepted and get shoehorned into `V0SilentPaymentsDestination`s. Is that intentional? I guess it's not and it's fine to only allow V0 destinations for now, but if yes, we should probably add tests for v1-v30 addresses; might be a bit tricky though as the encoding round-trip tests would obviously fail."
  },
  {
   "t": "2026-05-27T23:27:28Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/common/bip352.cpp",
   "commit": "80d3a4853a9952b3781cce88d13232dc8bcea447",
   "in_reply_to": null,
   "text": "`GenerateSilentPaymentsTaprootDestinations()` documents `sp_dests` keys as final `tx.vout` positions, but returns generated outputs under contiguous indexes `0..n-1`.\n\nIf SP outputs are mixed with regular outputs, callers would assign them to the wrong positions; the original map keys should be preserved.\n\nDiff:\n\n```diff\ndiff --git a/src/common/bip352.cpp b/src/common/bip352.cpp\nindex 1580e1f8f8..0ac71d456f 100644\n--- a/src/common/bip352.cpp\n+++ b/src/common/bip352.cpp\n@@ -234,8 +234,11 @@ std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootD\n     bool ret;\n     std::map<size_t, WitnessV1Taproot> tr_dests;\n     std::vector<V0SilentPaymentsDestination> recipients;\n+    std::vector<size_t> positions;\n     recipients.reserve(sp_dests.size());\n-    for (const auto& [_, addr] : sp_dests) {\n+    positions.reserve(sp_dests.size());\n+    for (const auto& [pos, addr] : sp_dests) {\n+        positions.push_back(pos);\n         recipients.push_back(addr);\n     }\n     std::vector<secp256k1_xonly_pubkey> outputs = CreateOutputs(recipients, plain_keys, taproot_keys, smallest_outpoint);\n@@ -245,7 +248,7 @@ std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootD\n         unsigned char xonly_pubkey_bytes[32];\n         ret = secp256k1_xonly_pubkey_serialize(secp256k1_context_static, xonly_pubkey_bytes, &outputs[i]);\n         assert(ret);\n-        tr_dests[i] = WitnessV1Taproot{XOnlyPubKey{xonly_pubkey_bytes}};\n+        tr_dests[positions[i]] = WitnessV1Taproot{XOnlyPubKey{xonly_pubkey_bytes}};\n     }\n     return tr_dests;\n }\n```\n\nTest:\n```diff\ndiff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp\nindex cd5fda38da..bc05fcf107 100644\n--- a/src/test/bip352_tests.cpp\n+++ b/src/test/bip352_tests.cpp\n@@ -27,6 +27,25 @@ CKey ParseHexToCKey(std::string hex) {\n     return output;\n };\n\n+BOOST_AUTO_TEST_CASE(bip352_preserves_requested_output_indexes)\n+{\n+    CKey sender_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000001\");\n+    CKey scan_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000002\");\n+    CKey spend_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000003\");\n+    V0SilentPaymentsDestination sp_dest{scan_key.GetPubKey(), spend_key.GetPubKey()};\n+    std::map<size_t, V0SilentPaymentsDestination> sp_dests{{2, sp_dest}, {5, sp_dest}};\n+    COutPoint smallest_outpoint{Txid::FromHex(\"0000000000000000000000000000000000000000000000000000000000000001\").value(), 0};\n+\n+    auto generated = bip352::GenerateSilentPaymentsTaprootDestinations(sp_dests, {sender_key}, {}, smallest_outpoint);\n+\n+    BOOST_REQUIRE(generated.has_value());\n+    BOOST_CHECK_EQUAL(generated->size(), sp_dests.size());\n+    BOOST_CHECK_EQUAL(generated->count(0), 0);\n+    BOOST_CHECK_EQUAL(generated->count(1), 0);\n+    BOOST_CHECK_EQUAL(generated->count(2), 1);\n+    BOOST_CHECK_EQUAL(generated->count(5), 1);\n+}\n+\n BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)\n {\n     UniValue tests;\n```"
  },
  {
   "t": "2026-05-28T00:08:24Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "`GetSilentPaymentsPrevoutsSummary()` currently still builds scan data when a transaction has an eligible input plus another input spending an unknown SegWit version (>1) prevout.\n\nBIP352 v0 says those transactions [must be skipped entirely](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#cite_note-skip_txs_with_unknown_prevouts_11), so this should return no prevouts summary as soon as any spent prevout is witness v2+.\n\nDiff:\n```diff\ndiff --git a/src/common/bip352.cpp b/src/common/bip352.cpp\nindex 1580e1f8f8..2ccae373c8 100644\n--- a/src/common/bip352.cpp\n+++ b/src/common/bip352.cpp\n@@ -152,6 +152,12 @@ std::optional<PrevoutsSummary> GetSilentPaymentsPrevoutsSummary(const std::vecto\n     for (const CTxIn& txin : vin) {\n         const Coin& coin = coins.at(txin.prevout);\n         Assert(!coin.IsSpent());\n+        int witness_version{0};\n+        std::vector<unsigned char> witness_program;\n+        // BIP352 v0 skips transactions spending future witness versions.\n+        if (coin.out.scriptPubKey.IsWitnessProgram(witness_version, witness_program) && witness_version > 1) {\n+            return std::nullopt;\n+        }\n         tx_outpoints.emplace_back(txin.prevout);\n         auto pubkey = GetPubKeyFromInput(txin, coin.out.scriptPubKey);\n         if (pubkey.has_value()) {\n```\n\nTest:\n```diff\ndiff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp\nindex cd5fda38da..b27823acd4 100644\n--- a/src/test/bip352_tests.cpp\n+++ b/src/test/bip352_tests.cpp\n@@ -27,6 +27,25 @@ CKey ParseHexToCKey(std::string hex) {\n     return output;\n };\n\n+BOOST_AUTO_TEST_CASE(bip352_skips_transactions_spending_unknown_segwit_versions)\n+{\n+    CKey key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000001\");\n+    CPubKey pubkey = key.GetPubKey();\n+    COutPoint eligible_outpoint{Txid::FromHex(\"0000000000000000000000000000000000000000000000000000000000000001\").value(), 0};\n+    COutPoint unknown_segwit_outpoint{Txid::FromHex(\"0000000000000000000000000000000000000000000000000000000000000002\").value(), 0};\n+\n+    CTxIn eligible_input{eligible_outpoint};\n+    eligible_input.scriptWitness.stack.emplace_back(64, 0);\n+    eligible_input.scriptWitness.stack.emplace_back(pubkey.begin(), pubkey.end());\n+\n+    std::map<COutPoint, Coin> coins;\n+    coins[eligible_outpoint] = Coin{CTxOut{{}, GetScriptForDestination(WitnessV0KeyHash{pubkey})}, 0, false};\n+    coins[unknown_segwit_outpoint] = Coin{CTxOut{{}, GetScriptForDestination(WitnessUnknown{2, std::vector<unsigned char>(32, 1)})}, 0, false};\n+\n+    BOOST_REQUIRE(bip352::GetSilentPaymentsPrevoutsSummary({eligible_input}, coins).has_value());\n+    BOOST_CHECK(!bip352::GetSilentPaymentsPrevoutsSummary({eligible_input, CTxIn{unknown_segwit_outpoint}}, coins).has_value());\n+}\n+\n BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)\n {\n     UniValue tests;\n```"
  },
  {
   "t": "2026-05-28T00:31:02Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/key_io.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "Silent Payments decoding looks too permissive: it detects SP addresses by `HRP` prefix and then accepts both `BECH32` and `BECH32M` without requiring `dec.hrp == params.SilentPaymentsHRP()`.\n\nThat can make `spx`... or Bech32-checksummed SP payloads decode as valid SP destinations, while [BIP352 should require the exact SP HRP and Bech32m](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#address-encoding).\n\nDiff:\n```diff\ndiff --git a/src/key_io.cpp b/src/key_io.cpp\nindex 335f02a5ba..e77f616cc7 100644\n--- a/src/key_io.cpp\n+++ b/src/key_io.cpp\n@@ -158,6 +158,14 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par\n             return CNoDestination();\n         }\n         if (is_silent_payment) {\n+            if (dec.hrp != params.SilentPaymentsHRP()) {\n+                error_str = strprintf(\"Invalid or unsupported prefix for Silent Payments address (expected %s, got %s).\", params.SilentPaymentsHRP(), dec.hrp);\n+                return CNoDestination();\n+            }\n+            if (dec.encoding != bech32::Encoding::BECH32M) {\n+                error_str = \"Silent Payments address must use Bech32m checksum\";\n+                return CNoDestination();\n+            }\n             if (!ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, dec.data.begin() + 1, dec.data.end())) {\n                 return CNoDestination();\n             }\n```\n\nTest:\n```diff\ndiff --git a/src/test/data/key_io_invalid.json b/src/test/data/key_io_invalid.json\nindex 0505dc9e8b..13b3fb0526 100644\n--- a/src/test/data/key_io_invalid.json\n+++ b/src/test/data/key_io_invalid.json\n@@ -209,6 +209,12 @@\n     [\n         \"TB1Q3F9WGNXE9ZMTTMDN5VKVKHYZ8Y0LCV72YV7V5LSXTJXEYHNHEHASLYL0TZ\"\n     ],\n+    [\n+        \"spx1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68g37pn04\"\n+    ],\n+    [\n+        \"sp1qq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68gcwu9kg\"\n+    ],\n     [\n         \"sp1qqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2qugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68g25havg\"\n     ],"
  },
  {
   "t": "2026-05-28T00:53:47Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/common/bip352.cpp",
   "commit": "80d3a4853a9952b3781cce88d13232dc8bcea447",
   "in_reply_to": null,
   "text": "`ScanForSilentPaymentsOutputs` skips invalid x-only taproot outputs, but still stores pointers using the original `tx_outputs` index into the compacted `tx_output_objs` vector.\n\nIf an invalid output appears before a valid one, this can take `&tx_output_objs[i]` out of bounds or point at the wrong object during scanning.\n\nDiff:\n```diff\ndiff --git a/src/common/bip352.cpp b/src/common/bip352.cpp\nindex 1580e1f8f8..8d019f3469 100644\n--- a/src/common/bip352.cpp\n+++ b/src/common/bip352.cpp\n@@ -316,19 +316,19 @@ std::optional<std::vector<SilentPaymentsOutput>> ScanForSilentPaymentsOutputs(\n     tx_output_objs.reserve(tx_outputs.size());\n     tx_output_ptrs.reserve(tx_outputs.size());\n\n-    for (size_t i = 0; i < tx_outputs.size(); i++) {\n-        secp256k1_silentpayments_found_output found_output{};\n+    for (const XOnlyPubKey& tx_output : tx_outputs) {\n         secp256k1_xonly_pubkey tx_output_obj;\n-        found_output_objs.push_back(found_output);\n-        found_output_ptrs.push_back(&found_output_objs[i]);\n-        ret = secp256k1_xonly_pubkey_parse(secp256k1_context_static, &tx_output_obj, tx_outputs[i].data());\n+        ret = secp256k1_xonly_pubkey_parse(secp256k1_context_static, &tx_output_obj, tx_output.data());\n         if (!ret) {\n             // It is possible that a P2TR output encodes an invalid x-only pubkey.\n             continue;\n         }\n         tx_output_objs.push_back(tx_output_obj);\n-        tx_output_ptrs.push_back(&tx_output_objs[i]);\n+        tx_output_ptrs.push_back(&tx_output_objs.back());\n+        found_output_objs.emplace_back();\n+        found_output_ptrs.push_back(&found_output_objs.back());\n     }\n+    if (tx_output_ptrs.empty()) return {};\n\n     // Parse the pubkeys into secp pubkey and xonly_pubkey objects\n     ret = secp256k1_ec_pubkey_parse(secp256k1_context_static, &spend_pubkey_obj, recipient_spend_pubkey.data(), recipient_spend_pubkey.size());\n```\n\n```diff\ndiff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp\nindex cd5fda38da..ca9424507d 100644\n--- a/src/test/bip352_tests.cpp\n+++ b/src/test/bip352_tests.cpp\n@@ -197,5 +197,39 @@ BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)\n         }\n     }\n }\n+\n+BOOST_AUTO_TEST_CASE(bip352_scan_skips_invalid_taproot_outputs)\n+{\n+    CKey sender_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000001\");\n+    CKey scan_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000002\");\n+    CKey spend_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000003\");\n+    const COutPoint outpoint{Txid::FromHex(\"0000000000000000000000000000000000000000000000000000000000000001\").value(), 0};\n+\n+    std::map<size_t, V0SilentPaymentsDestination> sp_dests;\n+    sp_dests.emplace(0, V0SilentPaymentsDestination{scan_key.GetPubKey(), spend_key.GetPubKey()});\n+    const auto sp_tr_dests = bip352::GenerateSilentPaymentsTaprootDestinations(sp_dests, {sender_key}, {}, outpoint);\n+    BOOST_REQUIRE(sp_tr_dests.has_value());\n+    const XOnlyPubKey expected_output{sp_tr_dests->begin()->second};\n+\n+    CTxIn txin{outpoint};\n+    const CPubKey sender_pubkey{sender_key.GetPubKey()};\n+    txin.scriptWitness.stack.emplace_back();\n+    txin.scriptWitness.stack.emplace_back(sender_pubkey.begin(), sender_pubkey.end());\n+\n+    std::map<COutPoint, Coin> coins;\n+    coins[outpoint] = Coin{CTxOut{{}, GetScriptForDestination(WitnessV0KeyHash{sender_pubkey})}, 0, false};\n+    const auto prevouts_summary = bip352::GetSilentPaymentsPrevoutsSummary({txin}, coins);\n+    BOOST_REQUIRE(prevouts_summary.has_value());\n+\n+    std::vector<XOnlyPubKey> output_pub_keys;\n+    output_pub_keys.emplace_back(ParseHex(\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"));\n+    output_pub_keys.push_back(expected_output);\n+\n+    std::unordered_map<CKeyID, uint256, SaltedSipHasher> labels;\n+    const auto found_outputs = bip352::ScanForSilentPaymentsOutputs(scan_key, *prevouts_summary, spend_key.GetPubKey(), output_pub_keys, labels);\n+    BOOST_REQUIRE(found_outputs.has_value());\n+    BOOST_REQUIRE_EQUAL(found_outputs->size(), 1);\n+    BOOST_CHECK(found_outputs->front().output == expected_output);\n+}\n BOOST_AUTO_TEST_SUITE_END()\n } // namespace wallet\n```"
  },
  {
   "t": "2026-05-28T00:58:28Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "80d3a4853a9952b3781cce88d13232dc8bcea447",
   "text": "A few review comments:"
  },
  {
   "t": "2026-05-29T11:05:15Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "43dc84da070a282e3ed90bceefe616baa4a6c4c5"
  },
  {
   "t": "2026-05-29T11:07:17Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3307715746,
   "text": "I added a new struct, called `UnknownSilentPaymentsVersion`, to handle versions 1 to 30. I added some valid and invalid addresses with a version greater than zero."
  },
  {
   "t": "2026-05-29T11:07:39Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "80d3a4853a9952b3781cce88d13232dc8bcea447",
   "in_reply_to": 3314444642,
   "text": "Done."
  },
  {
   "t": "2026-05-29T11:07:54Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3314642210,
   "text": "Done."
  },
  {
   "t": "2026-05-29T11:08:08Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "80d3a4853a9952b3781cce88d13232dc8bcea447",
   "in_reply_to": 3314706113,
   "text": "Done."
  },
  {
   "t": "2026-05-29T11:08:45Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3314571955,
   "text": "Done."
  },
  {
   "t": "2026-05-30T20:32:59Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "295ebcaeeb9f71f39c53623d33728975da8ec2e6",
   "in_reply_to": null,
   "text": "in b0a0c54f5925c83728d0081f3009eb947b55895c: I think for this function, passing the label (pubkey) rather than the label tweak makes more sense, so the generator point multiplication (already done in `CreateLabelTweak` via the `secp256k1_silentpayments_label_create` API function) doesn't have to be repeated manually via `.GetPubKey()`. The label tweak only becomes relevant once a SP outputs need to be spent, but shouldn't be necessary in a function for address generation.\n```suggestion\n**\n * @brief Generate a silent payments labeled address.\n *\n * @param recipient                   The recipient's silent payments destination (i.e. scan and spend public keys).\n * @param label                       The label\n * @return V0SilentPaymentsDestination The silent payments destination, with `B_spend -> B_spend + label`.\n *\n * @see CreateLabelTweak(const CKey& scan_key, const int m);\n */\nV0SilentPaymentsDestination GenerateSilentPaymentsLabeledAddress(const V0SilentPaymentsDestination& recipient, const CPubKey& label);\n```"
  },
  {
   "t": "2026-05-30T20:53:45Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b0a0c54f5925c83728d0081f3009eb947b55895c",
   "in_reply_to": null,
   "text": "in b0a0c54f5925c83728d0081f3009eb947b55895c: nit: as it's returning more than only the tweak (I suspect in a past iteration it did only that though; that would explain why the tweak is passed to `GenerateSilentPaymentsLabeledAddress` below currently), could rename the function. Maybe `CreateLabelData` or simply `CreateLabel`?"
  },
  {
   "t": "2026-05-30T21:04:38Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "295ebcaeeb9f71f39c53623d33728975da8ec2e6",
   "in_reply_to": null,
   "text": "in b0a0c54f5925c83728d0081f3009eb947b55895c: could use directly `CPubKey` as the label cache map key type, since by using `CKeyID` additional hashing steps (via `.GetID()`, performing Hash160, i.e. SHA-256 + RIPEMD-160) are involved for every lookup, which I suspect would be slower. The only remaining gain would be a smaller size of the map in memory (20 bytes vs. 33 bytes keys), but I doubt that this would ever matter in practice."
  },
  {
   "t": "2026-05-30T21:10:51Z",
   "kind": "review",
   "who": "theStack",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "43dc84da070a282e3ed90bceefe616baa4a6c4c5",
   "text": "Looks good overall, left just some more findings regarding labels and the label cache. It might be worth it to introduce a dedicated \"serialized label\" type (e.g. `std::array<byte, 33>`) and avoid using `CPubKey`, but this could still be done in a later follow-up PR."
  },
  {
   "t": "2026-06-07T10:35:08Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "295ebcaeeb9f71f39c53623d33728975da8ec2e6",
   "in_reply_to": 3329321393,
   "text": "`CPubKey` doesn't have a hash function for use with `std::unordered_map`. We'll have to check if it's better to use a `CPubKey + std::map<CPubKey, uint256>` or `CKeyID + std::unordered_map<CKeyID, uint256, SaltedSipHasher>`. The current plan is to eventually support at least 100_000 labels with the Bitcoin Core wallet; we can run a benchmark with this in mind."
  },
  {
   "t": "2026-06-07T11:02:40Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "295ebcaeeb9f71f39c53623d33728975da8ec2e6"
  },
  {
   "t": "2026-06-07T11:03:02Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b0a0c54f5925c83728d0081f3009eb947b55895c",
   "in_reply_to": 3329308035,
   "text": "Done."
  },
  {
   "t": "2026-06-09T23:15:10Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "295ebcaeeb9f71f39c53623d33728975da8ec2e6",
   "in_reply_to": 3329273356,
   "text": "Done."
  },
  {
   "t": "2026-06-22T08:49:03Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "295ebcaeeb9f71f39c53623d33728975da8ec2e6",
   "in_reply_to": 3329321393,
   "text": "I created a benchmark to test this [here](https://github.com/Eunovo/bitcoin/blob/implement-bip352-bench/src/bench/bip352.cpp). After 3 runs, the ordered_map cache seems to perform slightly better most of the time:\n```\n\u279c  2025-implement-bip352-receiving git:(4cf3c054f7) build/bin/bench_bitcoin '-filter=BIP352ScanNoMatch.*'\n\n|               ns/op |                op/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|           99,684.29 |           10,031.67 |    0.7% |      0.01 | `BIP352ScanNoMatchNoLabels`\n|           98,520.27 |           10,150.20 |    0.3% |      0.01 | `BIP352ScanNoMatchWith100kLabels`\n|           97,405.70 |           10,266.34 |    0.4% |      0.01 | `BIP352ScanNoMatchWith100kLabels_OrderedMap`\n|          101,070.67 |            9,894.07 |    0.5% |      0.01 | `BIP352ScanNoMatchWith1kLabels`\n|          100,686.00 |            9,931.87 |    0.4% |      0.01 | `BIP352ScanNoMatchWith1kLabels_OrderedMap`\n\u279c  2025-implement-bip352-receiving git:(4cf3c054f7) build/bin/bench_bitcoin '-filter=BIP352ScanNoMatch.*'\n\n|               ns/op |                op/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|          101,105.09 |            9,890.70 |    0.7% |      0.01 | `BIP352ScanNoMatchNoLabels`\n|          102,756.00 |            9,731.79 |    1.6% |      0.01 | `BIP352ScanNoMatchWith100kLabels`\n|          100,361.90 |            9,963.94 |    0.6% |      0.01 | `BIP352ScanNoMatchWith100kLabels_OrderedMap`\n|          101,470.67 |            9,855.06 |    0.7% |      0.01 | `BIP352ScanNoMatchWith1kLabels`\n|          100,385.78 |            9,961.57 |    0.6% |      0.01 | `BIP352ScanNoMatchWith1kLabels_OrderedMap`\n\u279c  2025-implement-bip352-receiving git:(4cf3c054f7) build/bin/bench_bitcoin '-filter=BIP352ScanNoMatch.*'\n\n|               ns/op |                op/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|          101,449.10 |            9,857.16 |    0.7% |      0.01 | `BIP352ScanNoMatchNoLabels`\n|           99,939.60 |           10,006.04 |    0.3% |      0.01 | `BIP352ScanNoMatchWith100kLabels`\n|           98,678.55 |           10,133.92 |    0.5% |      0.01 | `BIP352ScanNoMatchWith100kLabels_OrderedMap`\n|          101,501.11 |            9,852.11 |    0.5% |      0.01 | `BIP352ScanNoMatchWith1kLabels`\n|           99,560.60 |           10,044.13 |    0.2% |      0.01 | `BIP352ScanNoMatchWith1kLabels_OrderedMap`\n\n```\n\nSteps to Reproduce:\n- Clone https://github.com/Eunovo/bitcoin/tree/implement-bip352-bench\n- Build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_BENCH=ON`\n- Run `sudo pyperf system tune`\n- Run `build/bin/bench_bitcoin '-filter=BIP352ScanNoMatch.*' `"
  },
  {
   "t": "2026-06-23T09:58:43Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "35be12454989519d128c3b1523a31938db061955"
  },
  {
   "t": "2026-06-23T09:59:57Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "295ebcaeeb9f71f39c53623d33728975da8ec2e6",
   "in_reply_to": 3329321393,
   "text": "I have reverted back to using `std::map<CPubKey, uint256>` as the labels cache"
  },
  {
   "t": "2026-06-26T09:45:05Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "30f6396c7df7d4329e517f5f97dcafe29c68b4e8"
  },
  {
   "t": "2026-07-01T17:01:00Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "8de4f671236385b7ba9862ef02d21c775eb2a4a5",
   "in_reply_to": null,
   "text": "At this line and in some other places above in the `GetPubKeyFromInput` function, passing in a transaction that is not consensus-valid could lead to a crash in theory. Assuming consensus-validity seems reasonable for the typical SP scanning scenario, but we probably should document it as precondition in the Doxygen header, for belts-and-suspenders? (Not sure if it's realistic, but I'm thinking of e.g. a potential future RPC that allows scanning for individual user-supplied transactions, where this could become an issue)."
  },
  {
   "t": "2026-07-02T08:23:34Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "8de4f671236385b7ba9862ef02d21c775eb2a4a5",
   "in_reply_to": 3507759403,
   "text": "[quoted text omitted]\n\nFunny you should mention this; I recently added this RPC in the sending PR https://github.com/bitcoin/bitcoin/pull/35302/commits/ee27c74b26212bd883636793268fa27b2002ccf0"
  },
  {
   "t": "2026-07-03T06:14:22Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a4771f7651b97f28e29209b4a43794e073953d2e",
   "in_reply_to": null,
   "text": "in a4771f7651b97f28e29209b4a43794e073953d2e: nit: unused include, probably a leftover from an earlier version"
  },
  {
   "t": "2026-07-03T06:17:28Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a4771f7651b97f28e29209b4a43794e073953d2e",
   "in_reply_to": null,
   "text": "in a4771f7651b97f28e29209b4a43794e073953d2e: could assert here that version is in the range [1,30] (or throw if it isn't)"
  },
  {
   "t": "2026-07-03T06:27:24Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.cpp",
   "commit": "a4771f7651b97f28e29209b4a43794e073953d2e",
   "in_reply_to": null,
   "text": "in a4771f7651b97f28e29209b4a43794e073953d2e: could also ensure that both of the passed in public keys are compressed (with corresponding uncompressed keys we would still derive the correct output scripts when sending, but the SP address encoding would be overlong and invalid, AFAICT)."
  },
  {
   "t": "2026-07-03T07:11:03Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "0ab06bda74011e39fa616edbfb7e81f365e8c74c",
   "in_reply_to": null,
   "text": "in 8de4f671236385b7ba9862ef02d21c775eb2a4a5: could use `uint32_t` as type for m here (as implied by the spec and also used in the secp API)"
  },
  {
   "t": "2026-07-03T07:18:19Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "90c2898fe2206a33e71d854a8f93e2f5709e0688",
   "in_reply_to": null,
   "text": "in 8de4f671236385b7ba9862ef02d21c775eb2a4a5: here and in other doxygen headers in the same file: explicitly mentioning the return type is generally not necessary IMHO, as it's visible anyways two lines below"
  },
  {
   "t": "2026-07-03T07:32:09Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "8de4f671236385b7ba9862ef02d21c775eb2a4a5",
   "in_reply_to": null,
   "text": "in 8de4f671236385b7ba9862ef02d21c775eb2a4a5:\n```suggestion\n * destinations are passed in a map where the key indicates their desired position in the final tx.vout array.\n```"
  },
  {
   "t": "2026-07-03T07:57:45Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c",
   "in_reply_to": null,
   "text": "in 8de4f671236385b7ba9862ef02d21c775eb2a4a5: I wonder if this can be simplified by using a vector instead of a map for both the destinations and the created outputs? The relevant grouping (per shared scan key) happens already inside the secp module, there is nothing special about repeated destinations that warrants a map imho"
  },
  {
   "t": "2026-07-03T08:04:25Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "8de4f671236385b7ba9862ef02d21c775eb2a4a5",
   "in_reply_to": 3507759403,
   "text": "[quoted text omitted]\n\nAh funny indeed, I wasn't aware of this. I guess this RPC could currently crash triggered by malicious user input (e.g. with bogus scriptSig and a P2PKH previous output script), as it doesn't consensus-validate the tx before extracting pubkeys?"
  },
  {
   "t": "2026-07-03T08:14:30Z",
   "kind": "review",
   "who": "theStack",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "30f6396c7df7d4329e517f5f97dcafe29c68b4e8",
   "text": "Did another review round and left some more comments below, many of them being nitty (feel free to push back or ignore)."
  },
  {
   "t": "2026-07-06T16:45:58Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "0ab06bda74011e39fa616edbfb7e81f365e8c74c"
  },
  {
   "t": "2026-07-07T11:09:38Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "8de4f671236385b7ba9862ef02d21c775eb2a4a5",
   "in_reply_to": 3507759403,
   "text": "[quoted text omitted]\n\nIt can. I have fixed the problem by removing the Asserts in `GetPubKeyFromInput` that could cause a crash with unexpected scripts."
  },
  {
   "t": "2026-07-07T11:09:59Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a4771f7651b97f28e29209b4a43794e073953d2e",
   "in_reply_to": 3517850379,
   "text": "Done."
  },
  {
   "t": "2026-07-07T11:10:22Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.cpp",
   "commit": "a4771f7651b97f28e29209b4a43794e073953d2e",
   "in_reply_to": 3517896629,
   "text": "Done."
  },
  {
   "t": "2026-07-07T11:26:23Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "90c2898fe2206a33e71d854a8f93e2f5709e0688",
   "in_reply_to": 3518137458,
   "text": "I'll leave as-is since it's already specified."
  },
  {
   "t": "2026-07-07T11:26:33Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "8de4f671236385b7ba9862ef02d21c775eb2a4a5",
   "in_reply_to": 3518209184,
   "text": "Done."
  },
  {
   "t": "2026-07-07T11:26:58Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a4771f7651b97f28e29209b4a43794e073953d2e",
   "in_reply_to": 3517837482,
   "text": "Done."
  },
  {
   "t": "2026-07-07T12:05:40Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "36b553a1273b9fdbfeefc3108b387c6dc846ef81"
  },
  {
   "t": "2026-07-07T12:06:00Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "0ab06bda74011e39fa616edbfb7e81f365e8c74c",
   "in_reply_to": 3518102498,
   "text": "Done."
  },
  {
   "t": "2026-07-07T13:45:01Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c",
   "in_reply_to": 3518344149,
   "text": "The map is not used for grouping outputs; it is used to store the original indicies of destinations so they can be replaced with the taproot scripts later, see https://github.com/bitcoin/bitcoin/pull/35302/commits/676bb5e8be94303c1a67791bd5f16cd018023002#diff-6e06b309cd494ef5da4e78aa0929a980767edd12342137f268b9219167064d13R1504-R1524."
  },
  {
   "t": "2026-07-23T20:13:14Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "@Eunovo ping for rebase :)"
  },
  {
   "t": "2026-07-23T20:45:01Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWill rebase soon."
  },
  {
   "t": "2026-07-23T22:11:40Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "606c1f4b49bfa882c5dd8cd3abe55476aad4996b"
  },
  {
   "t": "2026-07-24T10:51:18Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "90c2898fe2206a33e71d854a8f93e2f5709e0688"
  },
  {
   "t": "2026-07-24T10:53:11Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "Fixed the typos detected by Drahtbot"
  },
  {
   "t": "2026-07-25T10:55:37Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "75950c7c4b9e802cd251b5f827b2eb76ad796479",
   "in_reply_to": null,
   "text": "in 75950c7c4b9e802cd251b5f827b2eb76ad796479: nit: slightly simpler:\n```suggestion\n    bool is_bech32 = is_silent_payment || (ToLower(str.substr(0, params.Bech32HRP().size())) == params.Bech32HRP());\n```"
  },
  {
   "t": "2026-07-25T11:21:28Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/kernel/chainparams.h",
   "commit": "10240e6092f77fda9ef6ccb52b9c3f5a7f345373",
   "in_reply_to": null,
   "text": "in 10240e6092f77fda9ef6ccb52b9c3f5a7f345373: nit: for consistency, could also use the plural form here (related to the earlier comment https://github.com/bitcoin/bitcoin/pull/35301#discussion_r3274544497)\n```suggestion\n    std::string silent_payments_hrp;\n```"
  },
  {
   "t": "2026-07-25T11:33:25Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/test/key_io_tests.cpp",
   "commit": "75950c7c4b9e802cd251b5f827b2eb76ad796479",
   "in_reply_to": null,
   "text": "in 75950c7c4b9e802cd251b5f827b2eb76ad796479: here and a few lines below: I wonder if we could use `BOOST_REQUIRE_MESSAGE` instead, as failing hard if something is wrong with the test vectors data (IIUC that's when the condition would be violated) seems reasonable? the if condition after could be removed then, simplifying the code"
  },
  {
   "t": "2026-07-25T11:34:18Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "75950c7c4b9e802cd251b5f827b2eb76ad796479",
   "in_reply_to": null,
   "text": "in 75950c7c4b9e802cd251b5f827b2eb76ad796479: yocto-nit: inclusive bounds are a tiny bit more readable imho\n```suggestion\n        Assert(version >= 1 && version <= 30);\n```"
  },
  {
   "t": "2026-07-25T11:47:18Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "d7f1998fa52bb63621daa60a04707d2b75ecbfe2",
   "in_reply_to": null,
   "text": "in d7f1998fa52bb63621daa60a04707d2b75ecbfe2: nit: is the `std::optional` wrapping needed here for the scanning function (e.g. for a potential future error condition)? simply returning an empty std::vector in case no output is found or there are no (P2TR) outputs to scan for in the first place seems fine and simpler AFAICT."
  },
  {
   "t": "2026-07-25T12:03:58Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/test/bip352_tests.cpp",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c",
   "in_reply_to": null,
   "text": "in 90c2898fe2206a33e71d854a8f93e2f5709e0688: should add a `BOOST_REQUIRE(sp != nullptr)` before dereferencing to avoid potential UB"
  },
  {
   "t": "2026-07-25T12:07:23Z",
   "kind": "review",
   "who": "theStack",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "90c2898fe2206a33e71d854a8f93e2f5709e0688",
   "text": "Left some nits below, planning to do a final review pass on the secp wrapper commit d7f1998fa52bb63621daa60a04707d2b75ecbfe2 soon."
  },
  {
   "t": "2026-07-25T15:40:40Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c",
   "in_reply_to": 3518344149,
   "text": "Oh I see now, thinking that the map key for the destinations being a counter was a brain fart. As for using vectors instead of maps, I think this would still work if we only allow contiguous recipient indexes, see e.g. https://github.com/theStack/bitcoin/commit/7b4af2f4b589d134c99c85e7d6153f703eb2be60 (Not entirely sure if the index gaps are really needed, probably for wallet sending scenarios where SP outputs and regular outputs are mixed?)."
  },
  {
   "t": "2026-07-27T09:37:20Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "dde9ab61b53a60b55f722f2a16bcce33f95b7ba6"
  },
  {
   "t": "2026-07-27T09:42:59Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "75950c7c4b9e802cd251b5f827b2eb76ad796479",
   "in_reply_to": 3650060370,
   "text": "It is much better. Fixed."
  },
  {
   "t": "2026-07-27T09:43:33Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/key_io_tests.cpp",
   "commit": "75950c7c4b9e802cd251b5f827b2eb76ad796479",
   "in_reply_to": 3650106137,
   "text": "The resulting code is much cleaner. Fixed!"
  },
  {
   "t": "2026-07-27T09:43:40Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "75950c7c4b9e802cd251b5f827b2eb76ad796479",
   "in_reply_to": 3650106883,
   "text": "Done."
  },
  {
   "t": "2026-07-27T09:43:50Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/kernel/chainparams.h",
   "commit": "10240e6092f77fda9ef6ccb52b9c3f5a7f345373",
   "in_reply_to": 3650095255,
   "text": "Fixed."
  },
  {
   "t": "2026-07-27T09:44:20Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "d7f1998fa52bb63621daa60a04707d2b75ecbfe2",
   "in_reply_to": 3650118341,
   "text": "I agree with you. I removed the `std::optional` wrapper."
  },
  {
   "t": "2026-07-27T09:44:32Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/bip352_tests.cpp",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c",
   "in_reply_to": 3650136002,
   "text": "Done."
  },
  {
   "t": "2026-07-30T09:27:41Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b8cb20ae596feada95331257fcf280cd26bb2ffb",
   "in_reply_to": null,
   "text": "in b8cb20ae596feada95331257fcf280cd26bb2ffb: nit:\n```suggestion\n * @return std::optional<PubKey> The public key, or nullopt if not found.\n```\n(or alternatively, could also remove the return type from the doxygen comment)"
  },
  {
   "t": "2026-07-30T09:41:39Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b8cb20ae596feada95331257fcf280cd26bb2ffb",
   "in_reply_to": null,
   "text": "in b8cb20ae596feada95331257fcf280cd26bb2ffb:\n```suggestion\n * @return std::pair<CPubKey, uint256> The label public key and label tweak.\n```\n(or as above, remove the type)"
  },
  {
   "t": "2026-07-30T09:55:40Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b57cd63d760b96e645004970b38ae8c4e8de0692",
   "in_reply_to": null,
   "text": "in b8cb20ae596feada95331257fcf280cd26bb2ffb: consistency nit:\nthe function's parameter names don't match the ones in the implementation (spend_pubkey vs. recipient_spend_pubkey, output_pub_keys vs. tx_outputs); i'd prefer the shorter ones, but no strong opinion"
  },
  {
   "t": "2026-07-30T10:03:43Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "b8cb20ae596feada95331257fcf280cd26bb2ffb",
   "in_reply_to": null,
   "text": "in b8cb20ae596feada95331257fcf280cd26bb2ffb: nit: this line is not strictly needed I think"
  },
  {
   "t": "2026-07-30T10:46:44Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "in dee330e6adee94d5b2d19cc4214694c97af82b42: should set `error_str` here before returning a `CNoDestination`, to avoid triggering an \"Internal bug detected\" error in [`validateaddress`](https://github.com/bitcoin/bitcoin/blob/9611a356035be531d62bfc40879f388d5dc359c4/src/rpc/output_script.cpp#L64). looking at what happens when the same error condition hits for regular (non-SP) bech32(m) addresses [below](https://github.com/bitcoin/bitcoin/blob/9611a356035be531d62bfc40879f388d5dc359c4/src/key_io.cpp#L200-L203), \"Invalid padding in Silent payments address (Bech32m data section)\" might be a possible error string."
  },
  {
   "t": "2026-07-30T10:48:53Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "dee330e6adee94d5b2d19cc4214694c97af82b42",
   "in_reply_to": null,
   "text": "in dee330e6adee94d5b2d19cc4214694c97af82b42: nit: as there are no format args, there is no need for `strprintf`\n```suggestion\n                error_str = \"Invalid Silent payments address\";\n```"
  },
  {
   "t": "2026-08-10T16:13:39Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "b57cd63d760b96e645004970b38ae8c4e8de0692"
  },
  {
   "t": "2026-08-10T16:15:22Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b8cb20ae596feada95331257fcf280cd26bb2ffb",
   "in_reply_to": 3681564973,
   "text": "Done."
  },
  {
   "t": "2026-08-10T16:15:34Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b8cb20ae596feada95331257fcf280cd26bb2ffb",
   "in_reply_to": 3681659591,
   "text": "Done."
  },
  {
   "t": "2026-08-10T16:18:58Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "b8cb20ae596feada95331257fcf280cd26bb2ffb",
   "in_reply_to": 3681798381,
   "text": "Removed."
  },
  {
   "t": "2026-08-10T16:19:34Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "dee330e6adee94d5b2d19cc4214694c97af82b42",
   "in_reply_to": 3682066045,
   "text": "Removed."
  },
  {
   "t": "2026-08-10T17:19:01Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "ed66407d72697ee4e00b1ae62f5a6a06d60729f0"
  },
  {
   "t": "2026-08-10T17:19:56Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3682053708,
   "text": "Done."
  },
  {
   "t": "2026-08-10T17:20:09Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "b57cd63d760b96e645004970b38ae8c4e8de0692",
   "in_reply_to": 3681748298,
   "text": "Fixed."
  },
  {
   "t": "2026-08-10T17:22:07Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "Sorry for the late reply @theStack . I resolved your comments and fixed a few extra nits I noticed. I also added a new functional test to rpc_validateaddress.py\n\nYou can review with `git range-diff master..dde9ab6 master..ed66407`"
  },
  {
   "t": "2026-08-11T11:55:35Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c",
   "in_reply_to": 3518344149,
   "text": "[quoted text omitted]\n\nYes. We preserve the original indexes in a Map because the vector of recipients can contain both SP and non-SP destinations, and the SP destinations are not required to be contiguous."
  },
  {
   "t": "2026-08-11T13:43:14Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "4e7ac8f3e451bb3ee38c5ff8701b39c13ecc6de4"
  },
  {
   "t": "2026-08-11T19:35:26Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "`CreateOutputs` can return an empty output vector when both input-key sets are empty, before calling secp.\n`GenerateSilentPaymentsTaprootDestinations` converts this to `std::nullopt` without the guard, `secp`\u2019s illegal-argument callback can abort the process.\n\n```diff\ndiff --git a/src/common/bip352.cpp b/src/common/bip352.cpp\nindex 2fe80869c8..c0e6b7e774 100644\n--- a/src/common/bip352.cpp\n+++ b/src/common/bip352.cpp\n@@ -200,6 +200,8 @@ std::vector<secp256k1_xonly_pubkey> CreateOutputs(\n     const std::vector<KeyPair>& taproot_keypairs,\n     const COutPoint& smallest_outpoint\n ) {\n+    if (plain_keys.empty() && taproot_keypairs.empty()) return {};\n+\n     bool ret;\n     std::vector<const secp256k1_keypair *> taproot_keypair_ptrs;\n     std::vector<const unsigned char *> plain_key_ptrs;\ndiff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp\nindex c365540d66..487e30b2cd 100644\n--- a/src/test/bip352_tests.cpp\n+++ b/src/test/bip352_tests.cpp\n@@ -227,6 +227,19 @@ BOOST_AUTO_TEST_CASE(bip352_preserves_requested_output_indexes)\n     BOOST_CHECK_EQUAL(generated->count(5), 1);\n }\n\n+BOOST_AUTO_TEST_CASE(bip352_sender_rejects_empty_input_key_set)\n+{\n+    CKey scan_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000002\");\n+    CKey spend_key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000003\");\n+    V0SilentPaymentsDestination sp_dest{scan_key.GetPubKey(), spend_key.GetPubKey()};\n+    std::map<size_t, V0SilentPaymentsDestination> sp_dests{{0, sp_dest}};\n+    COutPoint smallest_outpoint{Txid::FromHex(\"0000000000000000000000000000000000000000000000000000000000000001\").value(), 0};\n+\n+    const auto generated = bip352::GenerateSilentPaymentsTaprootDestinations(sp_dests, {}, {}, smallest_outpoint);\n+\n+    BOOST_CHECK(!generated.has_value());\n+}\n+\n BOOST_AUTO_TEST_CASE(bip352_skips_transactions_spending_unknown_segwit_versions)\n {\n     CKey key = ParseHexToCKey(\"0000000000000000000000000000000000000000000000000000000000000001\");\n\n```"
  },
  {
   "t": "2026-08-11T19:52:19Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/bip352_tests.cpp",
   "commit": "4e7ac8f3e451bb3ee38c5ff8701b39c13ecc6de4",
   "in_reply_to": null,
   "text": "The receive-vector test currently checks only the matched output public key. Maybe it could check priv_key_tweak too?\n\n```diff\ndiff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp\nindex c365540d66..e16a4c1f28 100644\n--- a/src/test/bip352_tests.cpp\n+++ b/src/test/bip352_tests.cpp\n@@ -193,15 +193,18 @@ BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)\n             if (!expected[\"n_outputs\"].isNull()) {\n                 BOOST_CHECK(found_outputs.size() == (size_t)expected[\"n_outputs\"].getInt<int>());\n             } else {\n-                std::vector<XOnlyPubKey> expected_outputs;\n+                std::map<XOnlyPubKey, uint256> expected_outputs;\n                 for (const auto& output : expected[\"outputs\"].getValues()) {\n-                    std::string pubkey_hex = output[\"pub_key\"].get_str();\n-                    expected_outputs.emplace_back(ParseHex(pubkey_hex));\n+                    expected_outputs.emplace(\n+                        XOnlyPubKey{ParseHex(output[\"pub_key\"].get_str())},\n+                        uint256{ParseHex(output[\"priv_key_tweak\"].get_str())});\n                 }\n                 BOOST_TEST_MESSAGE(found_outputs.size());\n-                BOOST_CHECK(found_outputs.size() == expected_outputs.size());\n+                BOOST_REQUIRE_EQUAL(found_outputs.size(), expected_outputs.size());\n                 for (const auto& output : found_outputs) {\n-                    BOOST_CHECK(std::find(expected_outputs.begin(), expected_outputs.end(), output.output) != expected_outputs.end());\n+                    const auto expected_output = expected_outputs.find(output.output);\n+                    BOOST_REQUIRE(expected_output != expected_outputs.end());\n+                    BOOST_CHECK(output.tweak == expected_output->second);\n                 }\n             }\n         }\n```"
  },
  {
   "t": "2026-08-11T20:04:11Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/key_io_tests.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "The `!isSilentPayments` condition skips all parsing checks for the new vectors. Maybe let them run through the same branch and special-case only `IsValidDestination` ?\n\n```diff\ndiff --git a/src/test/key_io_tests.cpp b/src/test/key_io_tests.cpp\nindex e365c2bd53..7609274848 100644\n--- a/src/test/key_io_tests.cpp\n+++ b/src/test/key_io_tests.cpp\n@@ -54,10 +54,14 @@ BOOST_AUTO_TEST_CASE(key_io_valid_parse)\n             // Private key must be invalid public key\n             destination = DecodeDestination(exp_base58string);\n             BOOST_CHECK_MESSAGE(!IsValidDestination(destination), \"IsValid privkey as pubkey:\" + strTest);\n-        } else if (!isSilentPayments) { // TODO remove if condition when silent payments sending is implemented\n-            // Must be a valid destination\n+        } else {\n+            // Must decode to the expected destination\n             destination = DecodeDestination(exp_base58string);\n-            BOOST_CHECK_MESSAGE(IsValidDestination(destination), \"!IsValid:\" + strTest);\n+            if (isSilentPayments) { // TODO remove special case when silent payments sending is implemented\n+                BOOST_CHECK_MESSAGE(!IsValidDestination(destination), \"IsValid silent payments destination:\" + strTest);\n+            } else {\n+                BOOST_CHECK_MESSAGE(IsValidDestination(destination), \"!IsValid:\" + strTest);\n+            }\n\n             // Payload check depends on address type\n             if (isSilentPayments && silentPaymentsVersion == 0) {\n@@ -74,6 +78,7 @@ BOOST_AUTO_TEST_CASE(key_io_valid_parse)\n             }\n\n             // Try flipped case version\n+            const CTxDestination expected_destination{destination};\n             for (char& c : exp_base58string) {\n                 if (c >= 'a' && c <= 'z') {\n                     c = (c - 'a') + 'A';\n@@ -82,7 +87,11 @@ BOOST_AUTO_TEST_CASE(key_io_valid_parse)\n                 }\n             }\n             destination = DecodeDestination(exp_base58string);\n-            BOOST_CHECK_MESSAGE(IsValidDestination(destination) == try_case_flip, \"!IsValid case flipped:\" + strTest);\n+            if (isSilentPayments) {\n+                BOOST_CHECK_MESSAGE((destination == expected_destination) == try_case_flip, \"case flipped mismatch:\" + strTest);\n+            } else {\n+                BOOST_CHECK_MESSAGE(IsValidDestination(destination) == try_case_flip, \"!IsValid case flipped:\" + strTest);\n+            }\n             if (!isSilentPayments && IsValidDestination(destination)) {\n                 CScript script = GetScriptForDestination(destination);\n                 BOOST_CHECK_EQUAL(HexStr(script), HexStr(exp_payload));\n```"
  },
  {
   "t": "2026-08-11T20:24:16Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/key_io_tests.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "`IsValidDestination` currently returns false even for a successfully decoded silent-payment destination.\n\nRequiring `CNoDestination` directly verifies that the malformed string was actually rejected.\n\n```diff\ndiff --git a/src/test/key_io_tests.cpp b/src/test/key_io_tests.cpp\nindex e365c2bd53..d521498de8 100644\n--- a/src/test/key_io_tests.cpp\n+++ b/src/test/key_io_tests.cpp\n@@ -167,7 +167,7 @@ BOOST_AUTO_TEST_CASE(key_io_invalid)\n         for (const auto& chain : {ChainType::MAIN, ChainType::TESTNET, ChainType::SIGNET, ChainType::REGTEST}) {\n             SelectParams(chain);\n             destination = DecodeDestination(exp_base58string);\n-            BOOST_CHECK_MESSAGE(!IsValidDestination(destination), \"IsValid pubkey in mainnet:\" + strTest);\n+            BOOST_CHECK_MESSAGE(std::holds_alternative<CNoDestination>(destination), \"Decoded invalid destination:\" + strTest);\n             privkey = DecodeSecret(exp_base58string);\n             BOOST_CHECK_MESSAGE(!privkey.IsValid(), \"IsValid privkey in mainnet:\" + strTest);\n         }\n```"
  },
  {
   "t": "2026-08-11T20:32:24Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "This will likely fix the CI error.\n\n```diff\ndiff --git a/src/common/bip352.h b/src/common/bip352.h\nindex d3a266b999..98fecc3e0c 100644\n--- a/src/common/bip352.h\n+++ b/src/common/bip352.h\n@@ -6,7 +6,6 @@\n #define BITCOIN_COMMON_BIP352_H\n\n #include <addresstype.h>\n-#include <coins.h>\n #include <crypto/common.h>\n #include <primitives/transaction.h>\n #include <pubkey.h>\n@@ -26,12 +25,14 @@ struct secp256k1_silentpayments_prevouts_summary;\n class CKey;\n class CScript;\n class KeyPair;\n+class Coin;\n\n namespace bip352 {\n\n using PubKey = std::variant<CPubKey, XOnlyPubKey>;\n\n class PrevoutsSummaryImpl;\n+\n class PrevoutsSummary\n {\n private:\ndiff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp\nindex c365540d66..2822889d23 100644\n--- a/src/test/bip352_tests.cpp\n+++ b/src/test/bip352_tests.cpp\n@@ -1,6 +1,7 @@\n #include <common/bip352.h>\n #include <span.h>\n #include <addresstype.h>\n+#include <coins.h>\n #include <policy/policy.h>\n #include <script/solver.h>\n #include <test/data/bip352_send_and_receive_vectors.json.h>\n```"
  },
  {
   "t": "2026-08-12T10:01:24Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/key_io_tests.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3761305667,
   "text": "I'm not sure it's necessary to do this; the `test-each-commit` CI job runs these tests before the \"disable sending to silent payments address\" commit."
  },
  {
   "t": "2026-08-12T10:03:40Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/key_io_tests.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3761444634,
   "text": "Same as in https://github.com/bitcoin/bitcoin/pull/35301#discussion_r3765408057. We specifically want to test that `IsValidDestination` returns `false` here."
  },
  {
   "t": "2026-08-12T10:43:21Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "1894ee3a4a7669330ec0999ce510dc2d92eb1c65"
  },
  {
   "t": "2026-08-12T10:43:38Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3761113042,
   "text": "Done."
  },
  {
   "t": "2026-08-12T10:43:51Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/test/bip352_tests.cpp",
   "commit": "4e7ac8f3e451bb3ee38c5ff8701b39c13ecc6de4",
   "in_reply_to": 3761228254,
   "text": "Done."
  },
  {
   "t": "2026-08-12T10:44:08Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3761496516,
   "text": "Fixed."
  },
  {
   "t": "2026-08-12T22:31:01Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "ca0cbbe768a7f35f1c83d0dd2d52e119cadfe1d6",
   "in_reply_to": null,
   "text": "in ca0cbbe768a7f35f1c83d0dd2d52e119cadfe1d6: nit: seems unnecessary to involve floating-point arithmetic here, could just do\n```suggestion\n        data_out.reserve(1 + CeilDiv(SILENT_PAYMENTS_V0_DATA_SIZE * 8, 5u));\n```\n(also for consistency with existing code, see `WitnessUnknown`'s `operator()` function below)"
  },
  {
   "t": "2026-08-12T23:20:49Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "49ac46511d53fce503ed907e4ba49b7369b6ddb5",
   "in_reply_to": null,
   "text": "in 49ac46511d53fce503ed907e4ba49b7369b6ddb5: missed this in previous review rounds unfortunately: I think we should check the return value here rather than `assert` to avoid a potential crash. This should only ever happen for adversarially chosen spend public keys (calculated backwards such that  [P_k](https://github.com/bitcoin/bips/blob/fe2d6caf85f976794095ac5c60c11ccee81e50b2/bip-0352.mediawiki?plain=1#L349) results in point at infinity for a certain scan secret key and transaction data), so it can't occur for honestly created wallets. Still worthwhile to avoid crashes that could be provoked by e.g. \"example RPCs\" or \"example wallets\" instructing users to try out SP scanning with a given adversarial scan/spend key material."
  },
  {
   "t": "2026-08-12T23:26:11Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "test/functional/rpc_validateaddress.py",
   "commit": "6a5fe78f39b3d8d5d72ad8818ac29270c8e1cb34",
   "in_reply_to": null,
   "text": "in 6a5fe78f39b3d8d5d72ad8818ac29270c8e1cb34: to have test coverage for the \"... sending support is not yet implemented\" error messages, one could move these test cases to the `INVALID_DATA` section rather than commenting them out. The TODO would then be \"move back to `VALID_DATA` section\" rather than \"uncomment\" (which could very easily be missed). E.g.\n\n```diff\ndiff --git a/test/functional/rpc_validateaddress.py b/test/functional/rpc_validateaddress.py\nindex c24b881229..b4c003e341 100755\n--- a/test/functional/rpc_validateaddress.py\n+++ b/test/functional/rpc_validateaddress.py\n@@ -130,7 +130,18 @@ INVALID_DATA = [\n         \"sp1qqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf26rn7\",\n         \"Invalid Silent payments address\",\n         []\n-    )\n+    ),\n+    # TODO move to VALID_DATA when Silent payments sending is enabled\n+    (\n+        \"sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv\", # V0 Silent Payments address\n+        \"This is a valid Silent Payments v0 address, but sending support is not yet implemented.\",\n+        [],\n+    ),\n+    (\n+        \"sp1pq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68t02m0h0s8cwhy\", # V1 Silent Payments address\n+        \"This is a valid Silent Payments v1 address, but sending support is not yet implemented.\",\n+        [],\n+    ),\n ]\n VALID_DATA = [\n     # BIP 350\n@@ -177,16 +188,6 @@ VALID_DATA = [\n         \"bc1pfeessrawgf\",\n         \"51024e73\",\n     ),\n-    # Silent Payments\n-    # TODO uncomment when Silent payments sending is enabled\n-    # (\n-    #     \"sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv\", # V0 Silent Payments address\n-    #     None\n-    # ),\n-    # (\n-    #     \"sp1pq22l5s6l9460ww6t4tkzsy2a7zejurcmzz35pt0ffrzk5erlaykdcqugecjjnjqf7ggq39vl6wexjlm00n66z94v675n7wcux6d2krr68t02m0h0s8cwhy\", # V1 Silent Payments address\n-    #     None\n-    # ),\n ]\n\n```"
  },
  {
   "t": "2026-08-12T23:39:08Z",
   "kind": "review",
   "who": "theStack",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "1894ee3a4a7669330ec0999ce510dc2d92eb1c65",
   "text": "LGTM, modulo one suggested corner-case fix in the SP scanning wrapper, and two nits, see below."
  },
  {
   "t": "2026-08-13T12:47:34Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "Concept ACK\n\nThis is some great looking code! Who wrote take 1? \ud83d\ude09\n\nOverall, looks good, I'm spending a little extra time digging into the parts I'm less familiar with, namely changes in the libsecp256k1 API and subsequently changes in the wrapper code."
  },
  {
   "t": "2026-08-15T01:08:24Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "b905cea56f5ca46f0d7e12643c9dd9045706b155"
  },
  {
   "t": "2026-08-15T01:09:19Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "ca0cbbe768a7f35f1c83d0dd2d52e119cadfe1d6",
   "in_reply_to": 3770824093,
   "text": "Fixed."
  },
  {
   "t": "2026-08-15T01:09:49Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "49ac46511d53fce503ed907e4ba49b7369b6ddb5",
   "in_reply_to": 3771097886,
   "text": "Fixed."
  },
  {
   "t": "2026-08-15T01:10:03Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "test/functional/rpc_validateaddress.py",
   "commit": "6a5fe78f39b3d8d5d72ad8818ac29270c8e1cb34",
   "in_reply_to": 3771119036,
   "text": "Done."
  },
  {
   "t": "2026-08-17T17:54:53Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c"
  },
  {
   "t": "2026-08-17T18:02:25Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "I added a `SilentPaymentsLabel` class that wraps `secp256k1_silentpayments_label` to replace `CPubKey` usage as label. Reviewers can check with `git range-diff master..0bcb949 master..b905cea`"
  },
  {
   "t": "2026-08-19T10:47:17Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "In `_create_outputs`, we check that there are no null keys passed in:\n\n```c\nif (keypairs != NULL) {\n        ARG_CHECK(n_keypairs > 0);\n        for (i = 0; i < n_keypairs; i++) {\n            ARG_CHECK(keypairs[i] != NULL);\n        }\n    } else {\n        ARG_CHECK(n_keypairs == 0);\n    }\n```\n.. so a precondition that the caller must pass valid keys. But we allow `KeyPair::date()` to return a nullptr if it is invalid. I did some poking around at KeyPair and where its used and it really shouldn't have a null state at all. Rather, when we create one with `CKey::ComputeKeyPair` it should return an `optional<kp>`. But that's its own orthogonal refactor.\n\nSince today keypair and ckey can be in an empty/null state, I think we need to check here before calling `_create_outputs`. Something like this a few lines above where we create the key vectors:\n\n```cpp\nfor (const auto& key : plain_keys) {\n    if (!key.IsValid()) return {};\n    plain_key_ptrs.push_back(UCharCast(key.begin()));\n}\n\nfor (const auto& keypair : taproot_keypairs) {\n    if (!keypair.IsValid()) return {};\n    taproot_keypair_ptrs.push_back(reinterpret_cast<const secp256k1_keypair*>(keypair.data()));\n}\n```\n\nThis is the one that stood out to me (when I was reviewing the first commit and saw it returning a nullptr), but there might be other spots. The heuristic we can use to check is:\n\n1) Does the libsecp module `ARG_CHECK` or assert on scenario x (e.g. no null keys)\n2) Is scenario x allowed by the calling code (can my key being in a null state)"
  },
  {
   "t": "2026-08-19T11:05:31Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "You mention in the commit message we don't validate the raw bytes to avoid the parsing cost (fair), but this makes me a bit queasy \ud83d\ude30 I don't have a concrete suggestion yet, but I wonder if we can lock this path down so that its only reachable when we know its being used in a callback which is presumed to reading from pre-validated trusted storage.\n\nOn a different note, I would find this much easier to review if it were folded into the original common commit. It avoids reviewing lines that then get changed again in the next commit and keeps all the context in one place. Just a preference, tho."
  },
  {
   "t": "2026-08-19T11:07:19Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3812391101,
   "text": "EDIT: I see the full picture now. I would suggest updating the commit message to be a bit more clear, and I think we could probably rename the function to be more explicit. This will likely trip up other reviewers, as well."
  },
  {
   "t": "2026-08-19T11:24:52Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "b0526622a1939203a55ea749a1c4c469cfac1dde",
   "in_reply_to": null,
   "text": "I'm not sure inheriting from the a V0Base is what we want here. I haven't thought about this long, but something that would feel more natural to me would be along the lines of:\n\n```cpp\nstruct SilentPaymentAddress {\npublic:\n    static std::optional<SilentPaymentAddress>\n    Decode(std::string_view address, const CChainParams& params);\n\n    uint8_t Version() const;\n    const CPubKey& ScanKey() const;\n    const CPubKey& SpendKey() const;\n    std::span<const unsigned char> ExtensionData() const;\n\nprivate:\n    SilentPaymentAddress(/* validated fields */);\n\n    uint8_t m_version;\n    CPubKey m_scan_key;\n    CPubKey m_spend_key;\n    std::vector<unsigned char> m_extension_data;\n};\n```\n\nBeyond the unknown type, the more I looked at this commit and the special casing required to get this to integrate into tests etc, the more it made me feel like we are forcing a square peg into a round hole (my bad!). A silent payment address is _strictly_ a wallet level construct. It has nothing to do with key_io, and only loosely with CTxDestinations.\n\nIn places where we want both, such as fuzzing wallet RPCs , or RPCs that want to treat an SP address the same as an encoded scriptpubkey, we could and should create a sum type just for that case, e.g.\n\n```cpp\nusing PaymentTarget =\n    std::variant<CTxDestination, SilentPaymentAddress>;\n```\n\nCurious to hear your thoughts, and more than happy to help write the commit. I strongly suspect it will simplify this a lot, or at least make it easier and more explicit to reason about."
  },
  {
   "t": "2026-08-19T11:30:43Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "b0526622a1939203a55ea749a1c4c469cfac1dde",
   "in_reply_to": null,
   "text": "In the world I'm envisioning, we would completely remove silent payments from the DecodeDestination function, because this returns a CTxDestination. This means we will need to introduce a silent payments specific decoder in `common`, for wallets and RPCs. I think this is much better than trying to coerce a silent payment address to a CTxDestination (its not), or try to have DecodeDestination return sum types of actual destinations and wallet payment instructions."
  },
  {
   "t": "2026-08-19T11:32:02Z",
   "kind": "review",
   "who": "josibake",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "0bcb949e5d1ac72089dcef54fa5e56d4c8bf9c7c",
   "text": "Mostly high level stuff , haven't dug deep into nitty gritty details yet"
  },
  {
   "t": "2026-08-20T08:59:50Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3812391101,
   "text": "[quoted text omitted]\n\nI kept it separate in case I need to ditch it.\n\n[quoted text omitted]\nI'm guessing you found the \"private constructor used by friend pattern\". That's what I came up with to restrict its usage. I will rename the function to be more explicit, or maybe I can even come up with something better."
  },
  {
   "t": "2026-08-20T09:13:26Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key_io.cpp",
   "commit": "b0526622a1939203a55ea749a1c4c469cfac1dde",
   "in_reply_to": 3812554401,
   "text": "What are we trying to achieve by removing the silent payments destination from CTxDestination? silent payments destination is still a \"destination\", and CTxDestination represents a destination that we can send money to."
  },
  {
   "t": "2026-08-20T10:02:20Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3812391101,
   "text": "Its a good pattern! I also recently came across the passkey pattern , which aims to achieve a more granular version of the same thing: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/patterns/passkey.md\n\nNot sure which is the best fit here, but I'll take a look at both"
  },
  {
   "t": "2026-08-20T10:36:55Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "ee242399fc9fc8f91cdba491da2c7d03376e7598",
   "in_reply_to": null,
   "text": "A `std::optional<label>` would be more appropriate here, since it is expected that we will receive invalid bytes (not exceptional). [leaving as a note from our call]"
  },
  {
   "t": "2026-08-24T16:14:20Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb"
  },
  {
   "t": "2026-08-24T18:27:21Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "ee242399fc9fc8f91cdba491da2c7d03376e7598",
   "in_reply_to": 3820760983,
   "text": "Done."
  },
  {
   "t": "2026-08-24T18:30:20Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3812391101,
   "text": "I ended up leaving it as-is because I can't implement the pattern without a private constructor anyway, so adding a private factory just created what seemed like an unnecessary step.\n\n[quoted text omitted]\nSeems overkill for this use case. I think the comment and assert is probably sufficient."
  },
  {
   "t": "2026-08-24T18:30:33Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3812276114,
   "text": "Fixed."
  },
  {
   "t": "2026-08-24T18:32:04Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "b0526622a1939203a55ea749a1c4c469cfac1dde",
   "in_reply_to": 3812518574,
   "text": "[quoted text omitted]\n\nI have removed the `UnknownSilentPaymentsDestination` class and converted what was previously the \"V0Base\" to just `SilentPaymentsDestination`"
  },
  {
   "t": "2026-08-25T10:08:21Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "Given this size is known ahead of time, we can force the caller to use the correct number of bytes with `std::span<const unsigned char, CPubKey::COMPRESSED_SIZE>`. I think this was discussed earlier but I don't think there should be any performance cost here."
  },
  {
   "t": "2026-08-25T10:08:45Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": null,
   "text": "Same as here, can be `std::span` with 33 bytes."
  },
  {
   "t": "2026-08-25T10:21:18Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "Something like this would also work:\n```cpp\nfriend void swap(SilentPaymentsLabel& a, SilentPaymentsLabel& b) noexcept {\n    std::swap(a.m_label, b.m_label);\n    std::swap(a.m_vch, b.vch);\n}\n```\n\n```cpp\nSilentPaymentsLabel& SilentPaymentsLabel::operator=(SilentPaymentsLabel other) noexcept {\n    swap(*this, other);\n    return *this;\n}\n```\n\nThat approach might have some performance costs but I think simplifies the code and unifies the copy and move assignment (can delete `operator(SilentPaymentsLabel&&)`"
  },
  {
   "t": "2026-08-25T12:06:06Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "nit: This `ret` value is used as the result for two different parse opts. These could be split into more explicit `bool ec_scan_pub_ret` and `bool ec_spend_pub_ret`. Similar nit for the bool in `GenerateSilentPaymentsTaprootDestinations`"
  },
  {
   "t": "2026-08-25T12:26:50Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": null,
   "text": "IMO it would be useful to distinguish between a secp error and an empty output vector by changing the return type to `optional<vector<>>`"
  },
  {
   "t": "2026-08-25T13:54:21Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "Since this already uses a named constructor, I would introduce a `FromV0` that has an implicit version:\n```diff\ndiff --git a/src/addresstype.cpp b/src/addresstype.cpp\nindex 1a8f658c58..466e898607 100644\n-std::optional<SilentPaymentsDestination> SilentPaymentsDestination::From(\n+std::optional<SilentPaymentsDestination> SilentPaymentsDestination::FromV0(\n+    const CPubKey& scan_pubkey,\n+    const CPubKey& spend_pubkey)\n+{\n+    if (!scan_pubkey.IsFullyValid()  || !scan_pubkey.IsCompressed())  return std::nullopt;\n+    if (!spend_pubkey.IsFullyValid() || !spend_pubkey.IsCompressed()) return std::nullopt;\n+    return SilentPaymentsDestination(/*version=*/0, scan_pubkey, spend_pubkey, /*extension_data=*/{});\n+}\n+\n+std::optional<SilentPaymentsDestination> SilentPaymentsDestination::FromForwardCompatible(\n     uint8_t version,\n     const CPubKey& scan_pubkey,\n     const CPubKey& spend_pubkey,\n-    const std::span<unsigned char>& extention_data\n-) {\n-    if (version >= 31) return std::nullopt;\n-    if (!scan_pubkey.IsFullyValid() || !scan_pubkey.IsCompressed())\n-        return std::nullopt;\n-    if (!spend_pubkey.IsFullyValid() || !spend_pubkey.IsCompressed())\n-        return std::nullopt;\n-    return SilentPaymentsDestination(version, scan_pubkey, spend_pubkey, extention_data);\n+    std::span<const unsigned char> extension_data)\n+{\n+    if (version < 1 || version > 30)                                  return std::nullopt;\n+    if (!scan_pubkey.IsFullyValid()  || !scan_pubkey.IsCompressed())  return std::nullopt;\n+    if (!spend_pubkey.IsFullyValid() || !spend_pubkey.IsCompressed()) return std::nullopt;\n+    if (extension_data.size() > MAX_EXTENSION_DATA_SIZE)              return std::nullopt;\n+    return SilentPaymentsDestination(version, scan_pubkey, spend_pubkey, extension_data);\n }\n```"
  },
  {
   "t": "2026-08-25T14:07:17Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": null,
   "text": "I would consider creating a newtype that forces the caller to provide the change label. A small wrapper that takes the scan key or change label as the only constructors would suffice:\n\n```cpp\nclass SilentPaymentsLabelSet {\nprivate:\n    std::map<SilentPaymentsLabel, uint256> m_labels;\n    SilentPaymentsLabel m_change_label;\n\npublic:\n    explicit SilentPaymentsLabelSet(std::pair<SilentPaymentsLabel, uint256> change)\n        : m_change_label(change.first)\n    {\n        m_labels.emplace(std::move(change.first), change.second);\n    }\n\n    explicit SilentPaymentsLabelSet(const CKey& scan_key)\n        : SilentPaymentsLabelSet(CreateLabel(scan_key, 0)) {}\n\n    void Add(std::pair<SilentPaymentsLabel, uint256> label)\n    {\n        m_labels.emplace(std::move(label.first), label.second);\n    }\n\n    const SilentPaymentsLabel& GetChangeLabel() const LIFETIMEBOUND { return m_change_label; }\n    const std::map<SilentPaymentsLabel, uint256>& AsMap() const LIFETIMEBOUND { return m_labels; }\n};\n```"
  },
  {
   "t": "2026-08-25T14:15:49Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": null,
   "text": "I think this can be condensed to:\n```\nif (a.hash != b.hash) return a.hash < b.hash;\nreturn internal_bswap_32(a.n) < internal_bswap_32(b.n)\n```\nusing `compat/byteswap`"
  },
  {
   "t": "2026-08-25T14:17:26Z",
   "kind": "review",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "text": "Left some review primarily on `bip352.cpp`. In addition to the suggested comments, I have some public API suggestions, feel free to take them or leave them:\n\nSuggestions for public API\n\n```diff\ndiff --git a/src/common/bip352.cpp b/src/common/bip352.cpp\nindex c8a59f03e2..5e0a82605f 100644\n--- a/src/common/bip352.cpp\n+++ b/src/common/bip352.cpp\n@@ -23,6 +23,7 @@\n #include <streams.h>\n #include <uint256.h>\n #include <util/check.h>\n+#include <util/expected.h>\n\n #include <algorithm>\n #include <optional>\n@@ -64,14 +65,14 @@ SilentPaymentsLabel::SilentPaymentsLabel(const secp256k1_silentpayments_label& l\n     assert(ret);\n }\n\n-SilentPaymentsLabel::SilentPaymentsLabel(const unsigned char* label) {\n-    memcpy(m_vch, label, CPubKey::COMPRESSED_SIZE);\n+SilentPaymentsLabel::SilentPaymentsLabel(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> label) {\n+    std::memcpy(m_vch, label.data(), label.size());\n }\n\n-std::optional<SilentPaymentsLabel> SilentPaymentsLabel::FromBytes(const unsigned char* vch33)\n+std::optional<SilentPaymentsLabel> SilentPaymentsLabel::FromBytes(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> vch33)\n {\n     secp256k1_silentpayments_label label_obj;\n-    if (!secp256k1_silentpayments_recipient_label_parse(secp256k1_context_static, &label_obj, vch33)) {\n+    if (!secp256k1_silentpayments_recipient_label_parse(secp256k1_context_static, &label_obj, vch33.data())) {\n         return std::nullopt;\n     }\n     return SilentPaymentsLabel(label_obj);\n@@ -247,7 +248,7 @@ std::optional<PrevoutsSummary> GetSilentPaymentsPrevoutsSummary(const std::vecto\n     return CreateInputPubkeysTweak(pubkeys, xonly_pubkeys, *smallest_outpoint);\n }\n\n-std::vector<secp256k1_xonly_pubkey> CreateOutputs(\n+std::optional<std::vector<secp256k1_xonly_pubkey>> CreateOutputs(\n     const std::vector<SilentPaymentsDestination>& recipients,\n     const std::vector<CKey>& plain_keys,\n     const std::vector<KeyPair>& taproot_keypairs,\n@@ -284,11 +285,11 @@ std::vector<secp256k1_xonly_pubkey> CreateOutputs(\n         generated_output_ptrs.push_back(&generated_outputs[i]);\n     }\n     for (const auto& key : plain_keys) {\n-        if (!key.IsValid()) return {};\n+        if (!Assume(key.IsValid())) return std::nullopt;\n         plain_key_ptrs.push_back(UCharCast(key.begin()));\n     }\n     for (const auto& keypair : taproot_keypairs) {\n-        if (!keypair.IsValid()) return {};\n+        if (!Assume(keypair.IsValid())) return std::nullopt;\n         taproot_keypair_ptrs.push_back(reinterpret_cast<const secp256k1_keypair*>(keypair.data()));\n     }\n\n@@ -304,17 +305,17 @@ std::vector<secp256k1_xonly_pubkey> CreateOutputs(\n         taproot_keypair_ptrs.data(), taproot_keypair_ptrs.size(),\n         plain_key_ptrs.data(), plain_key_ptrs.size()\n     );\n-    if (!ret) return {};\n+    if (!ret) return std::nullopt;\n     return generated_outputs;\n }\n\n-std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootDestinations(const std::map<size_t, SilentPaymentsDestination>& sp_dests, const std::vector<C\nKey>& plain_keys, const std::vector<KeyPair>& taproot_keys, const COutPoint& smallest_outpoint)\n+util::Expected<std::map<size_t, WitnessV1Taproot>, GenerateOutputsError>\n+GenerateSilentPaymentsTaprootDestinations(const std::map<size_t, SilentPaymentsDestination>& sp_dests, const std::vector<CKey>& plain_keys, const std::vector<KeyPair>& tapr\noot_keys, const COutPoint& smallest_outpoint)\n {\n-    if (sp_dests.empty()) return {};\n-    if (smallest_outpoint.IsNull()) return {};\n-    if (plain_keys.empty() && taproot_keys.empty()) return {};\n+    if (sp_dests.empty())                           return util::Unexpected{GenerateOutputsError::NoRecipients};\n+    if (smallest_outpoint.IsNull())                 return util::Unexpected{GenerateOutputsError::NullSmallestOutpoint};\n+    if (plain_keys.empty() && taproot_keys.empty()) return util::Unexpected{GenerateOutputsError::NoInputKeys};\n\n-    bool ret;\n     std::map<size_t, WitnessV1Taproot> tr_dests;\n     std::vector<SilentPaymentsDestination> recipients;\n     recipients.reserve(sp_dests.size());\n@@ -322,14 +323,13 @@ std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootD\n         tr_dests.emplace(i, WitnessV1Taproot());\n         recipients.push_back(addr);\n     }\n-    std::vector<secp256k1_xonly_pubkey> outputs = CreateOutputs(recipients, plain_keys, taproot_keys, smallest_outpoint);\n-    // This will fail if any input pubkey is null or\n-    // inputs were maliciously crafted to sum to zero\n-    if (outputs.empty()) return std::nullopt;\n+    auto outputs = CreateOutputs(recipients, plain_keys, taproot_keys, smallest_outpoint);\n+    if (!outputs) return util::Unexpected{GenerateOutputsError::Secp256k1Failure};\n+\n     size_t output_i{0};\n     for (const auto& [i, addr] : sp_dests) {\n         unsigned char xonly_pubkey_bytes[32];\n-        ret = secp256k1_xonly_pubkey_serialize(secp256k1_context_static, xonly_pubkey_bytes, &outputs[output_i]);\n+        bool ret = secp256k1_xonly_pubkey_serialize(secp256k1_context_static, xonly_pubkey_bytes, &(*outputs)[output_i]);\n         assert(ret);\n         tr_dests[i] = WitnessV1Taproot{XOnlyPubKey{xonly_pubkey_bytes}};\n         output_i++;\n@@ -339,7 +339,7 @@ std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootD\n\n const unsigned char* LabelLookupCallback(const unsigned char* key, const void* context) {\n     auto label_context = static_cast<const std::map<SilentPaymentsLabel, uint256>*>(context);\n-    SilentPaymentsLabel label{key};\n+    SilentPaymentsLabel label{std::span<const unsigned char, CPubKey::COMPRESSED_SIZE>{key, CPubKey::COMPRESSED_SIZE}};\n     auto it = label_context->find(label);\n     if (it != label_context->end()) {\n         return it->second.begin();\ndiff --git a/src/common/bip352.h b/src/common/bip352.h\nindex fa6213ede0..9b027b0d6d 100644\n--- a/src/common/bip352.h\n+++ b/src/common/bip352.h\n@@ -10,7 +10,9 @@\n #include <primitives/transaction.h>\n #include <pubkey.h>\n #include <uint256.h>\n+#include <util/expected.h>\n\n+#include <array>\n #include <compare>\n #include <cstdint>\n #include <cstring>\n@@ -76,11 +78,11 @@ private:\n     //! LabelLookupCallback. It does not parse the label\n     //! bytes to avoid EC-point parse. This is safe because\n     //! the resulting object is only used for comparison.\n-    SilentPaymentsLabel(const unsigned char* label);\n+    explicit SilentPaymentsLabel(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> label);\n\n     //! Parses raw bytes into a fully valid label\n     //! returns std::nullopt if vch33 is not a validly-encoded label.\n-    static std::optional<SilentPaymentsLabel> FromBytes(const unsigned char* vch33);\n+    static std::optional<SilentPaymentsLabel> FromBytes(std::span<const unsigned char, CPubKey::COMPRESSED_SIZE> vch33);\n\n public:\n     SilentPaymentsLabel(const secp256k1_silentpayments_label& label);\n@@ -113,9 +115,9 @@ public:\n     template <typename Stream>\n     static std::optional<SilentPaymentsLabel> Unserialize(Stream& s)\n     {\n-        unsigned char vch[CPubKey::COMPRESSED_SIZE];\n-        s >> std::span{vch, CPubKey::COMPRESSED_SIZE};\n-        return FromBytes(vch);\n+        std::array<unsigned char, CPubKey::COMPRESSED_SIZE> vch;\n+        s >> std::span{vch};\n+        return FromBytes(std::span{vch});\n     }\n\n     const secp256k1_silentpayments_label* Get() const;\n@@ -141,6 +143,13 @@ struct SilentPaymentsOutput {\n  */\n std::optional<PubKey> GetPubKeyFromInput(const CTxIn& txin, const CScript& spk);\n\n+enum class GenerateOutputsError {\n+    NoRecipients,           //!< sp_dests was empty\n+    NullSmallestOutpoint,   //!< no eligible inputs supplied\n+    NoInputKeys,            //!< both plain_keys and taproot_keys were empty\n+    Secp256k1Failure,       //!< inputs summed to zero, or another libsecp256k1 error\n+};\n+\n /**\n  * @brief Generate silent payments taproot destinations.\n  *\n@@ -154,7 +163,8 @@ std::optional<PubKey> GetPubKeyFromInput(const CTxIn& txin, const CScript& spk);\n  * @param smallest_outpoint                   The smallest_outpoint from the transaction inputs.\n  * @return std::map<size_t, WitnessV1Taproot> The generated silent payments taproot destinations.\n  */\n-std::optional<std::map<size_t, WitnessV1Taproot>> GenerateSilentPaymentsTaprootDestinations(const std::map<size_t, SilentPaymentsDestination>& sp_dests, const std::vector<C\nKey>& plain_keys, const std::vector<KeyPair>& taproot_keys, const COutPoint& smallest_outpoint);\n+util::Expected<std::map<size_t, WitnessV1Taproot>, GenerateOutputsError>\n+GenerateSilentPaymentsTaprootDestinations(const std::map<size_t, SilentPaymentsDestination>& sp_dests, const std::vector<CKey>& plain_keys, const std::vector<KeyPair>& tapr\noot_keys, const COutPoint& smallest_outpoint);\n\n /**\n  * @brief Create a silent payments label pair.\ndiff --git a/src/test/bip352_tests.cpp b/src/test/bip352_tests.cpp\nindex eae2b724c5..8e9cf438e9 100644\n--- a/src/test/bip352_tests.cpp\n+++ b/src/test/bip352_tests.cpp\n@@ -91,7 +91,7 @@ BOOST_AUTO_TEST_CASE(bip352_send_and_receive_test_vectors)\n             }\n             auto sp_tr_dests = bip352::GenerateSilentPaymentsTaprootDestinations(sp_dests, keys, taproot_keys, *smallest_outpoint);\n             // This means the inputs summed to zero, which realistically would only happen maliciously. In this case, just move on\n-            if (!sp_tr_dests.has_value()) {\n+            if (!sp_tr_dests) {\n                 // Check that we actually expect zero outputs to be generated for this test\n                 BOOST_CHECK(expected[\"outputs\"].getValues()[0].empty());\n                 continue;\n@@ -222,7 +222,7 @@ BOOST_AUTO_TEST_CASE(bip352_preserves_requested_output_indexes)\n\n     auto generated = bip352::GenerateSilentPaymentsTaprootDestinations(sp_dests, {sender_key}, {}, smallest_outpoint);\n\n-    BOOST_REQUIRE(generated.has_value());\n+    BOOST_REQUIRE(generated);\n     BOOST_CHECK_EQUAL(generated->size(), sp_dests.size());\n     BOOST_CHECK_EQUAL(generated->count(0), 0);\n     BOOST_CHECK_EQUAL(generated->count(1), 0);\n@@ -240,7 +240,8 @@ BOOST_AUTO_TEST_CASE(bip352_sender_rejects_empty_input_key_set)\n\n     const auto generated = bip352::GenerateSilentPaymentsTaprootDestinations(sp_dests, {}, {}, smallest_outpoint);\n\n-    BOOST_CHECK(!generated.has_value());\n+    BOOST_REQUIRE(!generated);\n+    BOOST_CHECK(generated.error() == bip352::GenerateOutputsError::NoInputKeys);\n }\n```"
  },
  {
   "t": "2026-08-26T15:58:11Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3852013982,
   "text": "It does look neat; however, I think it adds unnecessary costs."
  },
  {
   "t": "2026-08-27T12:58:39Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "While reasoning about the API changes @rustaceanrob suggested, I had a few new ideas, and I came up with a different API that has a new `SilentPaymentsReceiver` class that contains all the receiving functions. I pushed this new API to [implement-bip352-all/bip352.h](https://github.com/Eunovo/bitcoin/blob/implement-bip352-alt/src/common/bip352.h) for reviewers to compare against the API on this branch.\n\nIt offers a clear API that is difficult to misuse. The change label is automatically created and checked. Deriving a label and registering it for scanning are no longer separate steps. There is reduced need to thread key material by hand in different functions.\n\ncc: @josibake  @theStack  @rustaceanrob"
  },
  {
   "t": "2026-08-27T14:43:54Z",
   "kind": "comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI like the changes here."
  },
  {
   "t": "2026-08-31T15:40:38Z",
   "kind": "comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nNice, looks like a reasonable API improvement, happy to re-review if you want to adopt it in this PR."
  },
  {
   "t": "2026-09-01T12:34:50Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nFeels much simpler, and also agree that it provides less footguns for the callers and a more clear flow. Good stuff! I think its a good direction to go on this PR."
  },
  {
   "t": "2026-09-02T14:59:10Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "d3218d87019526c625e0235bf6209f4c78713a4a"
  },
  {
   "t": "2026-09-02T14:59:21Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": 3851899929,
   "text": "Done."
  },
  {
   "t": "2026-09-02T15:01:11Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3851896520,
   "text": "This constructor is only used by `LabelLookupCallback` which is called from libsecp and only provides the `unsigned char*`"
  },
  {
   "t": "2026-09-02T15:01:48Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": 3852923326,
   "text": "Done."
  },
  {
   "t": "2026-09-02T15:03:44Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3853682453,
   "text": "I decided not to add another `From` function and instead set sensible defaults for `version` and `extension_data`, and also add a validation rule that forces extension_data to be empty for version 0."
  },
  {
   "t": "2026-09-02T15:06:15Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.h",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": 3853885345,
   "text": "Done."
  },
  {
   "t": "2026-09-02T15:07:12Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "12515f645eaf22ecbea6ba0b3827c550f2e656cb",
   "in_reply_to": 3853801112,
   "text": "I didn't create this class, but I ended up doing an overhaul of the recipient API that takes your suggestions into account; with the new `SilentPaymentsReceiver` class, the change label will always be provided for Scanning."
  },
  {
   "t": "2026-09-02T15:17:33Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "6b5a993e808d35d27b6f23c8e1b36c2601e5e2b6"
  },
  {
   "t": "2026-09-03T08:06:44Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "The \"FreeBSD\" CI failed with this error: \"The self-hosted runner lost communication with the server. Verify the machine is running and has a healthy network connection. Anything in your workflow that terminates the runner process, starves it for CPU/Memory, or blocks its network access can cause this error.\" I can't tell whether this is related to the PR or just a random CI failure."
  },
  {
   "t": "2026-09-03T08:09:46Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "I've implemented the `SilentPaymentReceiver` API on this branch. Reviewers can check with `git range-diff master..12515f6 master..6b5a993`"
  },
  {
   "t": "2026-09-03T14:33:06Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": null,
   "text": "in 5d2b39d23d55c88a334e04ffd6f3a8b36d78209b: for the intended ordering, I think this is missing a `a.m_spend_pubkey > b.m_spend_pubkey` line (i.e. extension data should be last criterion and only matter if both scan and spend pubkeys are equal)"
  },
  {
   "t": "2026-09-03T16:39:43Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/addresstype.cpp",
   "commit": "5d2b39d23d55c88a334e04ffd6f3a8b36d78209b",
   "in_reply_to": null,
   "text": "in 5d2b39d23d55c88a334e04ffd6f3a8b36d78209b: it's a bit unfortunate that the `.IsFullyValid()` check is now done twice per pubkey, once in the public `::From` function and here another time in the constructor. since the ctor is private, I guess it's fine to remove the checks here? (I guess the performance penalty wouldn't be noticeable in practice though)"
  },
  {
   "t": "2026-09-03T16:46:48Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "63cc1d2890e2f25a77533577e1889a52da35235c",
   "in_reply_to": null,
   "text": "in 63cc1d2890e2f25a77533577e1889a52da35235c: readability nit: could be explicit here whether we want to return an error (i.e. `std::nullopt`) or an empty map (not sure if it matters much in practice, I guess the call-sites already prevent calling this function with no destinations). There is a similar instance in ` SilentPaymentsReceiver::Scan`"
  },
  {
   "t": "2026-09-04T14:14:27Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e"
  },
  {
   "t": "2026-09-04T14:15:17Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.h",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "in_reply_to": 3925599682,
   "text": "Done."
  },
  {
   "t": "2026-09-04T14:15:27Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "63cc1d2890e2f25a77533577e1889a52da35235c",
   "in_reply_to": 3926683948,
   "text": "Done."
  },
  {
   "t": "2026-09-04T14:15:42Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/addresstype.cpp",
   "commit": "5d2b39d23d55c88a334e04ffd6f3a8b36d78209b",
   "in_reply_to": 3926624782,
   "text": "I removed the checks."
  },
  {
   "t": "2026-09-07T04:50:38Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "It's currently not stated in the SP module API docs, but these pointers must be NULL if the accompanying size is zero, as otherwise libsecp triggers the illegal_callback (=`abort` by default, leading to a crash). Seems that in all of our CI instances a `std::vector` implementation is used where `.data()` returns nullptr for empty instances, so we are lucky, but the C++ standard doesn't mandate this (TIL, see \"Notes\" on https://en.cppreference.com/cpp/container/vector/data). Thus a construct like e.g. `ptrs.empty() ? nullptr : ptrs.data()` is needed here to be compliant, which is a bit annoying. Same for `_recipient_prevouts_summary_create` below.\n\nOpened https://github.com/bitcoin-core/secp256k1/issues/1930 to discuss whether it makes more sense to fix the docs or drop the \"must be NULL\" requirement for empty key lists."
  },
  {
   "t": "2026-09-09T08:23:39Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3946630066,
   "text": "I see that the \"must be NULL\" requirement has been dropped in https://github.com/bitcoin-core/secp256k1/pull/1932. Looks like I don't need to update the code anymore?"
  },
  {
   "t": "2026-09-09T10:06:51Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/key.h",
   "commit": "bd83107bd948d3188419527b9b92c0645d99b6c4",
   "in_reply_to": null,
   "text": "I don't think we should rely on doc comments for the intended use of these methods. The only use case so far is for this to be used as a `const secp256k1_keypair*`, and I think moving the reintepret cast to this method is easier to reason about (and has no performance cost). I suggest keeping this scoped to silent payments with the following, which should be easier for reviewers to understand the purpose of this commit:\n\n```diff\ndiff --git a/src/common/bip352.cpp b/src/common/bip352.cpp\nindex c77517475e..5fcf808b71 100644\n--- a/src/common/bip352.cpp\n+++ b/src/common/bip352.cpp\n@@ -290,7 +290,7 @@ std::optional<std::vector<secp256k1_xonly_pubkey>> CreateOutputs(\n     }\n     for (const auto& keypair : taproot_keypairs) {\n         if (!keypair.IsValid()) return std::nullopt;\n-        taproot_keypair_ptrs.push_back(reinterpret_cast<const secp256k1_keypair*>(keypair.data()));\n+        taproot_keypair_ptrs.push_back(keypair.AsSecpKeypair());\n     }\n\n     // Serialize the outpoint\ndiff --git a/src/key.h b/src/key.h\nindex 2d1d7a92da..4110c92373 100644\n--- a/src/key.h\n+++ b/src/key.h\n@@ -20,6 +20,7 @@\n\n struct secp256k1_context_struct;\n typedef struct secp256k1_context_struct secp256k1_context;\n+struct secp256k1_keypair;\n\n /**\n  * CPrivKey is a serialized private key, with all parameters included\n@@ -301,18 +302,11 @@ public:\n     friend KeyPair CKey::ComputeKeyPair(const uint256* merkle_root) const;\n     [[nodiscard]] bool SignSchnorr(const uint256& hash, std::span<unsigned char> sig, const uint256& aux) const;\n\n-    /**\n-      * data() is provided as a read-only method for passing a KeyPair object to secp256k1 functions\n-      * expecting a `secp256k1_keypair`. This avoids needing to create a temporary `secp256k1_keypair`\n-      * object by allowing the KeyPair to be passed directly in the following manner:\n-      *\n-      *     reinterpret_cast<const secp256k1_keypair*>(keypair.data())\n-      *\n-      * Recall that `secp256k1_keypair` is an opaque data type, so this method should only be used\n-      * for passing a KeyPair object as a secp256k1_keypair and should never be used to access the\n-      * underlying keypair bytes directly.\n-      */\n-    const unsigned char* data() const { return IsValid() ? m_keypair->data() : nullptr; }\n+    //! Use the underlying keypair as a `secp256k1_keypair`, `nullptr` if invalid.\n+    const secp256k1_keypair* AsSecpKeypair() const\n+    {\n+        return IsValid() ? reinterpret_cast<const secp256k1_keypair*>(m_keypair->data()) : nullptr;\n+    }\n```"
  },
  {
   "t": "2026-09-09T12:27:29Z",
   "kind": "review_comment",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": null,
   "text": "The nested conditional statements here are hard to follow. I suggest using a series of guard statements/early returns that make each case more legible:\n\n```cpp\nstd::optional<PubKey> GetPubKeyFromInput(const CTxIn& txin, const CScript& spk)\n{\n    std::vector<std::vector<unsigned char>> solutions;\n    const TxoutType type = Solver(spk, solutions);\n\n    if (type == TxoutType::WITNESS_V1_TAPROOT) {\n        // BIP-352: skip script-path spends using NUMS-H internal key.\n        const auto& stack = txin.scriptWitness.stack;\n        if (stack.size() > 1) {\n            const bool has_annex = !stack.back().empty() && stack.back()[0] == ANNEX_TAG;\n            const size_t effective = stack.size() - (has_annex ? 1 : 0);\n            if (effective > 1) {\n                const auto& control = stack[effective - 1];\n                if (control.size() < 33) return std::nullopt;\n                if (std::equal(WitnessV1Taproot::NUMS_H.begin(), WitnessV1Taproot::NUMS_H.end(), control.begin() + 1)) {\n                    return std::nullopt;\n                }\n            }\n        }\n        XOnlyPubKey key{solutions[0]};\n        if (!key.IsFullyValid()) return std::nullopt;\n        return PubKey{key};\n    }\n\n    if (type == TxoutType::WITNESS_V0_KEYHASH) {\n        const auto& stack = txin.scriptWitness.stack;\n        if (stack.empty()) return std::nullopt;\n        CPubKey key{stack.back()};\n        if (!key.IsCompressed() || !key.IsFullyValid()) return std::nullopt;\n        return PubKey{key};\n    }\n\n    if (type == TxoutType::PUBKEYHASH) {\n        std::vector<std::vector<unsigned char>> stack;\n        ScriptError serror;\n        if (!EvalScript(stack, txin.scriptSig, MANDATORY_SCRIPT_VERIFY_FLAGS, DUMMY_CHECKER, SigVersion::BASE, &serror)) {\n            return std::nullopt;\n        }\n        if (stack.empty()) return std::nullopt;\n        CPubKey key{stack.back()};\n        if (!key.IsCompressed() || !key.IsFullyValid()) return std::nullopt;\n        return PubKey{key};\n    }\n\n    if (type == TxoutType::SCRIPTHASH) {\n        // P2SH-P2WPKH only: eval scriptSig, verify redeem script is P2WPKH.\n        std::vector<std::vector<unsigned char>> stack;\n        ScriptError serror;\n        if (!EvalScript(stack, txin.scriptSig, MANDATORY_SCRIPT_VERIFY_FLAGS, DUMMY_CHECKER, SigVersion::BASE, &serror)) {\n            return std::nullopt;\n        }\n        if (stack.empty()) return std::nullopt;\n        CScript redeem{stack.back().begin(), stack.back().end()};\n        if (Solver(redeem, solutions) != TxoutType::WITNESS_V0_KEYHASH) return std::nullopt;\n        if (txin.scriptWitness.stack.empty()) return std::nullopt;\n        CPubKey key{txin.scriptWitness.stack.back()};\n        if (!key.IsCompressed() || !key.IsFullyValid()) return std::nullopt;\n        return PubKey{key};\n    }\n\n    return std::nullopt;\n}\n```"
  },
  {
   "t": "2026-09-09T14:45:35Z",
   "kind": "review_comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3946630066,
   "text": "[quoted text omitted]\n\nYes, the line can be kept as-is, you only have to rebase on master (now that #36203 is in)."
  },
  {
   "t": "2026-09-09T17:29:33Z",
   "kind": "review",
   "who": "rustaceanrob",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "a97b2202584f2a7628a277ea15192925e518888e",
   "text": "Left a few comments. I am wondering if the scope here can be reduced to introducing the module and passing the BIP test vectors so we can determine choices like _is this a `CTxDestination`?_ during the send PR. The additional churn in fuzz, unit and functional tests can be difficult to review IMO. I have pushed an alternative branch you may want to check out that removes the `CTxDestination` variant addition and does the minimal amount of changes to add this module and pass the tests https://github.com/rustaceanrob/bitcoin/tree/implement-bip352-alt-branch\n\nI think the current API is very solid and the next stage should determine how it is integrated."
  },
  {
   "t": "2026-09-10T09:55:07Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis is an excellent suggestion. I took a look at this PR again and while reviewing found myself having the same thoughts: the API is in excellent shape, there are a few details here and there that might need cleaning up in the implementation, and the `CTxDestination` stuff feels like a premature commitment. It feels logical to me to move decisions on how to represent a silent payment address to where the PR where it will be used.\n\nI'm also wondering if it makes sense to go ahead and combine the sending and receiving PRs. This will give us a clear end to end on how the type will be used and make testing (and the review of testing) much easier."
  },
  {
   "t": "2026-09-10T12:18:56Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI can confidently state that the resulting PR will be a nightmare to review, with too many issues to debate. I'm considering splitting the receiving PR into an SP descriptor implementation PR and then the actual receiving PR.\n\n[quoted text omitted]\nI can create a PR to my fork that we can use for experimentation.\n\n[quoted text omitted]\nI agree. I'll reduce the scope of this PR."
  },
  {
   "t": "2026-09-16T13:43:29Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f"
  },
  {
   "t": "2026-09-16T13:44:25Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/key.h",
   "commit": "bd83107bd948d3188419527b9b92c0645d99b6c4",
   "in_reply_to": 3967125599,
   "text": "Done."
  },
  {
   "t": "2026-09-16T13:44:44Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/common/bip352.cpp",
   "commit": "542475dce0fbaef13871cbb11b5b11992389633f",
   "in_reply_to": 3968296259,
   "text": "Done."
  },
  {
   "t": "2026-09-16T13:58:08Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "text": "I have reduced the scope of this PR. It no longer adds the SilentPayments destination to CTxDestination,  and the address decoding code has been moved to the bip352 module. The resulting branch is very similar to https://github.com/rustaceanrob/bitcoin/tree/implement-bip352-alt-branch except for a few changes. The commit to disable sending to silent payments destinations has been removed because it is no longer needed.\n\nReviewers can review with `git range-diff master..a97b220 master..542475d`"
  }
 ],
 "labels_log": [
  {
   "t": "2026-05-21T16:40:01Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-25T10:02:27Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-29T12:12:25Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-29T16:24:09Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-19T20:41:45Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-23T12:02:17Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-23T21:09:06Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-23T23:28:13Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-10T17:19:58Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-12T12:08:43Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-02T15:19:04Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-04T15:19:35Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 79522,
 "text_tokens_estimate": 19880,
 "changed_paths": [
  "src/CMakeLists.txt",
  "src/bech32.h",
  "src/common/bip352.cpp",
  "src/common/bip352.h",
  "src/kernel/chainparams.cpp",
  "src/kernel/chainparams.h",
  "src/key.h",
  "src/test/CMakeLists.txt",
  "src/test/bip352_tests.cpp",
  "src/test/data/bip352_send_and_receive_vectors.json"
 ],
 "files": [
  {
   "path": "src/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/bech32.h",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/common/bip352.cpp",
   "add": 504,
   "del": 0
  },
  {
   "path": "src/common/bip352.h",
   "add": 308,
   "del": 0
  },
  {
   "path": "src/kernel/chainparams.cpp",
   "add": 5,
   "del": 0
  },
  {
   "path": "src/kernel/chainparams.h",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/key.h",
   "add": 7,
   "del": 0
  },
  {
   "path": "src/test/CMakeLists.txt",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/test/bip352_tests.cpp",
   "add": 387,
   "del": 0
  },
  {
   "path": "src/test/data/bip352_send_and_receive_vectors.json",
   "add": 5729,
   "del": 0
  }
 ],
 "test_lines": 6118,
 "git": {
  "head": "542475dce0fbaef13871cbb11b5b11992389633f",
  "head_matches_backup": true,
  "base": "7dedc4545a495ab6725a2a36cec2b6a1a9bf8dbf",
  "commits": [
   {
    "sha": "0dd01902e9",
    "subject": "crypto: add read-only method to KeyPair",
    "files": 1,
    "add": 7,
    "del": 0
   },
   {
    "sha": "a3444fb2d6",
    "subject": "Add \"sp\" HRP",
    "files": 2,
    "add": 7,
    "del": 0
   },
   {
    "sha": "7e0cc68eca",
    "subject": "common: add bip352.{h,cpp} secp256k1 module",
    "files": 4,
    "add": 814,
    "del": 0
   },
   {
    "sha": "542475dce0",
    "subject": "tests: add BIP352 test vectors as unit tests",
    "files": 3,
    "add": 6118,
    "del": 0
   }
  ],
  "patch_truncated": true
 },
 "input_hash": "e250d5079b72db75",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}