{
 "number": 35569,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35569",
 "title": "Encapsulation for CTransaction",
 "author": "purpleKarrot",
 "author_association": "MEMBER",
 "created_at": "2026-06-19T15:52:03Z",
 "updated_at": "2026-09-16T22:57:37Z",
 "age_days": 90,
 "draft": false,
 "labels": [
  "Refactoring",
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "402500baad826efff3dbe52c75694d93121e1b06",
 "head_ref": "transaction-abstraction",
 "head_repo": "purpleKarrot/bitcoin",
 "head_history": [
  {
   "t": "2026-06-20T03:42:41Z",
   "sha": "853db1904952d014b9db7b4f0f10ad243b415176"
  },
  {
   "t": "2026-06-21T15:55:57Z",
   "sha": "de03c01e31949548abb09fc16ef1f415487a3845"
  },
  {
   "t": "2026-06-21T16:25:46Z",
   "sha": "18b0efc5adfafca3755a2220a3d830d3ebe9d32b"
  },
  {
   "t": "2026-06-22T12:37:11Z",
   "sha": "f71da0125757ce1fdfe9ca79cb4f2afd2dd74196"
  },
  {
   "t": "2026-06-22T13:04:37Z",
   "sha": "926618a85c78d8c91d625098b744297c30091bd9"
  },
  {
   "t": "2026-06-22T15:02:37Z",
   "sha": "693c41a8993ea3151392233ace931fb6bf73b968"
  },
  {
   "t": "2026-06-22T15:06:08Z",
   "sha": "c2dbec1ed279d73c76a28d23d02fcc3bdd433a1f"
  },
  {
   "t": "2026-06-26T15:11:36Z",
   "sha": "1472e6e52c480b9692a2701ba1cc1e3e0b9e4b3e"
  },
  {
   "t": "2026-06-26T15:18:51Z",
   "sha": "a2b339547f6c8acc34ec18d6b44abf0d728a9886"
  },
  {
   "t": "2026-07-27T14:38:33Z",
   "sha": "6d6c33787f0f7b4065abcf4d294230f340070ec2"
  },
  {
   "t": "2026-08-03T14:58:01Z",
   "sha": "7280a562ebebf56858caeac9d3d28736437c47b8"
  },
  {
   "t": "2026-08-03T15:13:48Z",
   "sha": "10485b67784de75809173efc753d926793cbfe25"
  },
  {
   "t": "2026-08-06T08:04:43Z",
   "sha": "ef3a175126164d05c82a0b41d166a523fa4c9459"
  },
  {
   "t": "2026-08-14T20:34:57Z",
   "sha": "13b62f714a08e4a87f226a1ec232d9de4dbbe505"
  },
  {
   "t": "2026-08-21T03:50:37Z",
   "sha": "2a700a6723b9f54c0cfac64b594c5ec175bf833d"
  },
  {
   "t": "2026-08-24T10:59:38Z",
   "sha": "402500baad826efff3dbe52c75694d93121e1b06"
  }
 ],
 "additions": 610,
 "deletions": 514,
 "changed_files": 93,
 "commit_count": 7,
 "size_bucket": "XL",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "josibake",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-5398007216"
     },
     {
      "login": "alexanderwiederin",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#pullrequestreview-5151473784"
     }
    ],
    "concept_nack": [
     {
      "login": "ajtowns",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4843836472"
     }
    ],
    "concept_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4763201910"
     },
     {
      "login": "janb84",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#pullrequestreview-4543756475"
     },
     {
      "login": "willcl-ark",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-5277953565"
     },
     {
      "login": "ryanofsky",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-5293506167"
     }
    ],
    "approach_nack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/35569#pullrequestreview-4621677109"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36015,
     "title": "txorphanage: bound orphan memory by storing transactions serialized",
     "author": "brunoerg"
    },
    {
     "number": 35793,
     "title": "Implement BIP 54 (Consensus Cleanup) without mainnet activation",
     "author": "darosior"
    },
    {
     "number": 35786,
     "title": "wallet: drop spent parents redundant cache invalidation and notification",
     "author": "furszy"
    },
    {
     "number": 35716,
     "title": "wallet: Replace mapWallet and wtxOrdered with a boost::multi_index",
     "author": "achow101"
    },
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    },
    {
     "number": 35675,
     "title": "mining: add block template manager",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35671,
     "title": "mining: add TxCollection to bandwidth-efficiently validate external block templates",
     "author": "Sjors"
    },
    {
     "number": 35662,
     "title": "script: prevent stale sighash caches across transactions",
     "author": "l0rinc"
    },
    {
     "number": 35581,
     "title": "node: add block template manager and track waitNext fee inflow",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35570,
     "title": "refactor: Change some validation.cpp methods to return BlockValidationState",
     "author": "optout21"
    },
    {
     "number": 35511,
     "title": "RFC: consensus: Make `CAmount` a class",
     "author": "hodlinator"
    },
    {
     "number": 34864,
     "title": "coins: tighten cache entry state invariants",
     "author": "l0rinc"
    },
    {
     "number": 33922,
     "title": "mining: add getMemoryLoad() and track template non-mempool memory footprint",
     "author": "Sjors"
    },
    {
     "number": 32575,
     "title": "consensus: Remove special treatment for single threaded script checking",
     "author": "fjahr"
    },
    {
     "number": 31252,
     "title": "rpc: print P2WSH and P2SH redem Script in getrawtransaction and getblock",
     "author": "polespinasa"
    },
    {
     "number": 29843,
     "title": "policy: Allow non-standard scripts with -acceptnonstdtxn=1 (test nets only)",
     "author": "ajtowns"
    },
    {
     "number": 29491,
     "title": "[EXPERIMENTAL] Schnorr batch verification for blocks",
     "author": "fjahr"
    },
    {
     "number": 27865,
     "title": "wallet: Track no-longer-spendable TXOs separately",
     "author": "achow101"
    }
   ]
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-06-21T20:29:44Z",
   "stale": false
  },
  "josibake": {
   "kind": "ack",
   "hash": null,
   "t": "2026-08-24T16:12:38Z",
   "stale": false
  },
  "janb84": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-06-22T11:56:35Z",
   "stale": false
  },
  "ajtowns": {
   "kind": "concept_nack",
   "hash": null,
   "t": "2026-06-30T13:11:22Z",
   "stale": false
  },
  "l0rinc": {
   "kind": "approach_nack",
   "hash": null,
   "t": "2026-07-03T01:03:06Z",
   "stale": false
  },
  "willcl-ark": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-13T08:31:53Z",
   "stale": false
  },
  "ryanofsky": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-14T12:53:27Z",
   "stale": false
  },
  "alexanderwiederin": {
   "kind": "ack",
   "hash": "402500baad826efff3dbe52c75694d93121e1b06",
   "t": "2026-09-09T09:31:18Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 2,
  "stale_ack": 0,
  "concept_ack": 4,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 1,
  "approach_nack": 1
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 2,
  "distinct_reviewers": [
   "ajtowns",
   "alexanderwiederin",
   "fanquake",
   "hodlinator",
   "janb84",
   "jonatack",
   "josibake",
   "l0rinc",
   "maflcko",
   "mzumsande",
   "ryanofsky",
   "sedited",
   "sipa",
   "theuni",
   "w0xlt",
   "willcl-ark"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-24T20:24:11Z",
  "last_reviewer_activity": "2026-09-09T09:31:18Z",
  "last_reviewer": "alexanderwiederin",
  "author_silent_days": 23,
  "waiting_on_author_days": 8,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   8580,
   35904,
   36015
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35904,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Factoring out a stateless, side-effect free validation library"
   },
   {
    "number": 8580,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2016-12-05",
    "title": "Make CTransaction actually immutable"
   },
   {
    "number": 36015,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "txorphanage: bound orphan memory by storing transactions serialized"
   }
  ],
  "conflicts": [
   36015,
   35793,
   35786,
   35716,
   35713,
   35675,
   35671,
   35662,
   35581,
   35570,
   35511,
   34864,
   33922,
   32575,
   31252,
   29843,
   29491,
   27865
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp",
  "contrib/devtools/bitcoin-tidy/observers-base.cpp",
  "contrib/devtools/bitcoin-tidy/use-observers.cpp",
  "src/consensus/tx_verify.cpp",
  "src/policy/policy.h",
  "src/policy/truc_policy.h",
  "src/primitives/transaction.h"
 ],
 "body": "`CTransaction` currently exposes its data members publicly and marks them `const`. This makes the implementation details of the type part of its interface, while using `const` data members to enforce immutability has several undesirable consequences.\n\nThis PR encapsulates the data members behind public observer functions. The existing users of `CTransaction` are migrated to the observers, after which the data members are made private and non-`const`.\n\nThe refactoring is deliberately split into three steps:\n\n1. Add observer functions corresponding to the existing data members.\n2. Migrate all users from direct data-member access to the observers.\n3. Make the data members private.\n\nThe migration is automated with a new `bitcoin-tidy` `use-observers` check. This both makes the large mechanical change straightforward and provides a reusable tool for similar refactorings.\n\nThe split into separate commits allows reviewers to review handwritten changes independently of the automated changes. It also simplifies rebasing: if the mechanical migration conflicts, individual files can be reset to their new base versions and the migration tool can be run on them again. This works because the old interface remains available until the final commit, so the reset code is still valid while the mechanical migration is reapplied.\n\nThis PR does not change the semantics of `CTransaction`. In particular, regular value semantics are intentionally left for a subsequent change.\n\n### Why Encapsulation? Why `CTransaction`?\n\nPublic data members make the representation of a type part of its API. Once access goes through observers, the representation can change without requiring its users to change as well.\n\nThis establishes a clean boundary between the transaction's data and functionality that operates on it. In particular, it allows serialization/deserialization and other functionality to be separated from the type itself in subsequent refactorings.\n\nThis work is part of the broader interface-segregation direction described in #35904, but is not a prerequisite for the stateless validation library proposed there. The encapsulation has value independently of that work.\n\n`CTransaction` is a good type to start this migration with: it is widely used, its interface is relatively simple, and its data members are already effectively immutable. This allows the migration to establish the pattern and tooling without first requiring changes to the semantics of the type.",
 "commits": [
  {
   "sha": "f5a5b041984477110ab041f13cf9af0e6d3c78cb",
   "date": "2026-08-24T10:31:00Z",
   "message": "bitcoin-tidy: Add use-observers check\n\nAdd a check to bitcoin-tidy that detects when an annotated data member\nis accessed outside of a member function and rewrites that access with\nan observer function."
  },
  {
   "sha": "0d6480b1f21869a99f58595abc927cc9bec2c4e1",
   "date": "2026-08-24T10:31:07Z",
   "message": "attributes: Add USE_OBSERVER annotation helper"
  },
  {
   "sha": "ff4aed823addc728da11d55136a7c712dc00cdde",
   "date": "2026-08-24T10:31:09Z",
   "message": "CTransaction: Add observer member functions\n\nMake sure that for each data member of `CTransaction`, there exists\na public member function that is marked `const` and returns the data\nmember either by value or by reference to const.\nMake sure to add the same member functions to `CMutableTransaction`\nfor symmetry, so that they can be used in generic code."
  },
  {
   "sha": "79319eb9e0e4171c160c9509a5bdb993a77fd9c5",
   "date": "2026-08-24T10:31:10Z",
   "message": "CTransaction: Annotate data members with their corresponding observer"
  },
  {
   "sha": "6b99660bee7e415d8aa7572a6073eb2dbc40ae1e",
   "date": "2026-08-24T10:58:27Z",
   "message": "bitcoin-tidy: Apply use-observers fixup\n\nPerform an automated replacement of all direct accesses of\n`CTransaction`'s data members, each with it's associated\nobserver function.\n\nProduce the changeset with the following commands:\n\n```sh\nexport CC=\"$(which clang)\"\nexport CXX=\"$(which clang++)\"\ncmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DBUILD_GUI=ON -DBUILD_KERNEL_LIB=ON -DBUILD_UTIL_CHAINSTATE=ON\ncmake -B build/tidy -S contrib/devtools/bitcoin-tidy\ncmake --build build\ncmake --build build/tidy\nrun-clang-tidy -p build -load='build/tidy/libbitcoin-tidy.so' -checks='-*,bitcoin-use-observers' -fix\n```"
  },
  {
   "sha": "0c8354114779c5a53e2af20972d1c781ef148bcd",
   "date": "2026-08-24T10:58:46Z",
   "message": "policy: Use explicit version type"
  },
  {
   "sha": "402500baad826efff3dbe52c75694d93121e1b06",
   "date": "2026-08-24T10:58:47Z",
   "message": "CTransaction: Make data members private\n\nNow that `CTransaction`'s data members are no longer accessed\noutside of member functions, they can be made non-`const` and\n`private`."
  }
 ],
 "timeline": [
  {
   "t": "2026-06-19T16:51:09Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nCan you put all the parts relevant to reviewers into the PR description?"
  },
  {
   "t": "2026-06-20T03:42:41Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "853db1904952d014b9db7b4f0f10ad243b415176"
  },
  {
   "t": "2026-06-20T05:12:39Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nshould be `clang++`. But before I push that change and trigger another rebuild, I will wait for more comments.\n\nThe linter fails validating the `scripted_diff` due to `cmake` and `run-clang-tidy` not being found. How can this be fixed?"
  },
  {
   "t": "2026-06-20T16:07:44Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis doesn't seem like a good idea at all to me. If you want an object that can represent different transactions over its lifetime, you should be using `CMutableTransaction`, and afaics we don't want that for things like the mempool, or for anything we reference via a `CTransactionRef`.\n\nNote that the comment (\"CTransaction is not actually immutable; deserialization and assignment are implemented, and bypass the constness.\") was made incorrect by #8580 in 2016."
  },
  {
   "t": "2026-06-21T11:34:35Z",
   "kind": "comment",
   "who": "janb84",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI do not think you can do this, as far as I can tell the linter container image does not contain cmake.  Drop the scripted diff validation ?"
  },
  {
   "t": "2026-06-21T15:55:57Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "de03c01e31949548abb09fc16ef1f415487a3845"
  },
  {
   "t": "2026-06-21T16:24:31Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nJust to make sure there is no misunderstanding here. All builtin types in C++ as well as nearly all types in the standard library have regular semantics. You are saying that is a bad design, right?\n\n[quoted text omitted]\nSo instead, the mempool uses `CTransactionRef`, an object that can not only represent different transactions over its lifetime, but can also change whether it actually holds a transaction or not. I don't get how that is better."
  },
  {
   "t": "2026-06-21T16:25:46Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "18b0efc5adfafca3755a2220a3d830d3ebe9d32b"
  },
  {
   "t": "2026-06-21T20:29:44Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK"
  },
  {
   "t": "2026-06-22T06:54:22Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nNo, I said exactly what I intended to say: making `CTransaction` mutable is a bad idea. We already have `CMutableTransaction` for cases where mutability is needed.\n\n[quoted text omitted]\nPassing a CTransactionRef around prevents the recipient from modifying it (ignoring const casts), but doesn't prevent the creator from doing so.\n\n```c++\n    std::shared_ptr<CTransaction> foo;\n    vRecv >> TX_WITH_WITNESS(foo);\n\n    ProcessTransaction(foo); // add to mempool\n\n    foo->version = 42; // mempool copy is automatically updated, how neat!\n```"
  },
  {
   "t": "2026-06-22T09:28:05Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "Concept ACK\n\nMoving the data members behind observers, vs having the class layout define the API seems a solid improvement.\n\n[quoted text omitted]\nPorque no los dos? Turns out, afaict, there is a C++ idiom for this: handle/body idiom with shared immutable representation! What I mean by this is we want to make the exposed transaction type a _regular value handle_ to **immutable transaction data**[^1]. Concretely, something like this:\n\n```cpp\nclass CTransaction {\npublic:\n    CTransaction(const CTransaction&) = default;\n    CTransaction(CTransaction&&) noexcept = default;\n    CTransaction& operator=(const CTransaction&) = default;\n    CTransaction& operator=(CTransaction&&) noexcept = default;\n\n    // the observers added in this PR\n   ...\n\nprivate:\n    std::shared_ptr<const TransactionData> m_data;\n};\n```\n\n`TransactionData` would  of course contain `vin`, `vout`, `version`, `nLockTime`, cached hashes, etc., and would be immutable after construction from `CMutableTransaction` or deserialisation. Now assignment has normal value semantics:\n\n```cpp\nauto a = tx1;\nauto b = a;\na = tx2; // b still observes tx1\n```\n\nThat is different from mutating a shared transaction object:\n\n```cpp\nauto p = std::shared_ptr<CTransaction>{...};\nProcessTransaction(p);\n*p = tx2; // bad: existing users may observe changed transaction contents\n```\n\nWhile we still use `using CTransactionRef = std::shared_ptr<const CTransaction>`, perhaps its best to have `CTransaction` assignment deleted, or the ref type needs to be updated so the shared thing is the immutable `TransactionData`, not the assignable wrapper.\n\nBased on my understanding of the linked blogs, this seems in line with their stated goals. We can get rid of the \"class layout is the API,\" eventually allowing serialisation/deserialisation to move behind an explicit construction/codec boundary as described in the blogs. It also plays nice with the vocabulary type direction: the eventual public transaction type can be regular and idiomatic, while preserving the invariant of once a transaction contents are published/shared, they are immutable.\n\n[^1]: https://stackoverflow.com/questions/18709647/shared-pointer-to-an-immutable-type-has-value-semantics"
  },
  {
   "t": "2026-06-22T09:50:13Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "@josibake, exactly. As highlighted in my blog post, the next refactoring step would be moving the `std::shared_ptr<const>` behind the API boundary. This would result in the exact same runtime indirection as currently, it is just a different level of abstraction. However,\n\n[quoted text omitted]\nfor what benefit? It is pointing to a `const` object. The pointer can be changed to point to a different object (or no object at all) but the shared pointee can not be changed. There can be no spooky action at a distance."
  },
  {
   "t": "2026-06-22T11:17:35Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThe benefit is removing a footgun while refactoring. As long as `CTransactionRef` shares the `CTransaction` wrapper, making that wrapper assignable leaves a possible non-const-alias footgun. So we either keep assignment deleted during that transition, or move the shared state behind the API boundary first.\n\nOnce we reach stage 7 detailed in your bitcoin-tidy blog (which more or less matches my earlier comment, perhaps sans a few details), `CTransaction` is the value handle and the shared state is behind the API boundary and thus default assignment is fine (and desirable!) because it only rebinds to the handle."
  },
  {
   "t": "2026-06-22T11:55:35Z",
   "kind": "review_comment",
   "who": "janb84",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "18b0efc5adfafca3755a2220a3d830d3ebe9d32b",
   "in_reply_to": null,
   "text": "```suggestion\n    return std::accumulate(tx.GetOutputs().cbegin(), tx.GetOutputs().cend(), CAmount{0}, [](CAmount sum, const auto& txout) { return sum + txout.nValue; });\n```\n\nIntentional skipped ? (It's ok as is)"
  },
  {
   "t": "2026-06-22T11:56:35Z",
   "kind": "review",
   "who": "janb84",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "18b0efc5adfafca3755a2220a3d830d3ebe9d32b",
   "text": "Concept ACK\n\n[quoted text omitted]\nI second this approach. Also reaching \"stage 7\" can take a while, if ever reached."
  },
  {
   "t": "2026-06-22T12:18:55Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "18b0efc5adfafca3755a2220a3d830d3ebe9d32b",
   "in_reply_to": 3451979776,
   "text": "Interesting catch. The `clang-tidy` check was not fired on this code and making the data members private did not break it. Apparently, the function template is never instantiated with `CTransaction`; only with `CMutableTransaction`. There is also `CTransaction::GetValueOut()`."
  },
  {
   "t": "2026-06-22T12:28:55Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI get it now. `CTransactionRef` being a `shared_ptr<const>` can only prevent mutation through that handle, it cannot prove that no non-const alias exists. Yes, as long as `shared_ptr<const>` is used, it would be better to explicitly disallow both assignment operators. I'll update that."
  },
  {
   "t": "2026-06-22T12:37:11Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "f71da0125757ce1fdfe9ca79cb4f2afd2dd74196"
  },
  {
   "t": "2026-06-22T13:04:37Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "926618a85c78d8c91d625098b744297c30091bd9"
  },
  {
   "t": "2026-06-22T14:50:44Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "926618a85c78d8c91d625098b744297c30091bd9",
   "in_reply_to": null,
   "text": "As part of reviewing this, I nerd sniped myself into reading about move semantics and had the thought: are vectors treated differently than fixed length values? Turns out they are! So for `uint256` moves are copy like, but for vin/vout, being vectors, move could actually mean move the underlying storage. In light of that, I think we should delete the move constructer during the transition.\n\nConcretely, if vin/vout are no longer const then this would be a memberwise move. Shouldn't matter for the moved to transaction, but the moved from one could be left with `m_witness_hash`, `m_has_witness` etc members that still describe the pre-moved state, but the vin/vouts of the moved from could now be empty.\n\nObviously, this would no longer be a problem once `CTransaction` represents a value reference to an immutable struct, at which point the move constructer can be added back along with the assignment operation.\n\nI applied this suggestion locally and everything compiled, so doesn't seem to break any existing behaviour."
  },
  {
   "t": "2026-06-22T15:01:48Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "926618a85c78d8c91d625098b744297c30091bd9",
   "in_reply_to": 3453169670,
   "text": "Good point! But instead of deleting the move constructor, we should simply not provide it."
  },
  {
   "t": "2026-06-22T15:02:37Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "693c41a8993ea3151392233ace931fb6bf73b968"
  },
  {
   "t": "2026-06-22T15:06:08Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "c2dbec1ed279d73c76a28d23d02fcc3bdd433a1f"
  },
  {
   "t": "2026-06-23T08:09:14Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "I find the commit message in [a35a2b1](https://github.com/bitcoin/bitcoin/pull/35569/commits/a35a2b108acf355293443d5ad42cbf84152a3363) a bit confusing, specifically: \"Make sure to add the same member functions to `CMutableTransaction` for symmetry, so that they can be used in generic code.\"\n\nIt seems clear from the PR description and the code that the observer is not concerned with `CMutableTransaction` in this PR. Is this a stale commit message or a directive for a follow-up? If its a directive for a follow-up, I think it could be made more clear, e.g., \"CMutableTransaction should have the the same member functions to allow both classes to be used in generic code. This is deferred to a follow-up.\""
  },
  {
   "t": "2026-06-23T08:35:57Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "18b0efc5adfafca3755a2220a3d830d3ebe9d32b",
   "in_reply_to": 3451979776,
   "text": "I don't see anything that would prevent this from being used with `CTransaction`, though? Seems we could handle this case cleanly by recognising `TxType` in a template argument as actually a `CTx`. This would avoid more broadly rewriting vout, vin, version, etc in other places."
  },
  {
   "t": "2026-06-23T09:01:03Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "@josibake, adding the observers to `CMutableTransaction` cannot be deferred to a follow-up. Adding them to both `CTransaction` and `CMutableTransaction` is a precondition for the automatic refactoring that follows. The reason is that we have code like this:\n\n```cpp\ntemplate <typename TxType>\nvoid foo(const TxType& tx) {\n  for (const auto& in : tx.vin) {\n    ...\n  }\n  ...\n}\n```\n\nIf that `.vin` is refactored to `.GetInputs()`, the function template can no longer be instantiated with `CMutableTransaction` unless the observer is added there as well."
  },
  {
   "t": "2026-06-23T11:42:31Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "@purpleKarrot thanks for clarifying, I realised I was confusing myself. I do think a nice polish would be to update the `tx-observer` code to handle the template case. I started tinkering with it as a way of getting more familiar with the code and came up with this:\n\n```diff\ndiff --git a/contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp b/contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp\nindex 57a8824c0c..379a550dec 100644\n--- a/contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp\n+++ b/contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp\n@@ -6,6 +6,7 @@\n #include \"observers.h\"\n\n #include <clang-tidy/ClangTidyModule.h>\n+#include <clang-tidy/ClangTidyModuleRegistry.h>\n\n class BitcoinModule final : public clang::tidy::ClangTidyModule\n {\ndiff --git a/contrib/devtools/bitcoin-tidy/observers-base.cpp b/contrib/devtools/bitcoin-tidy/observers-base.cpp\nindex 075e2c1d70..0b2c73eb5a 100644\n--- a/contrib/devtools/bitcoin-tidy/observers-base.cpp\n+++ b/contrib/devtools/bitcoin-tidy/observers-base.cpp\n@@ -1,5 +1,6 @@\n #include \"observers-base.h\"\n\n+#include <clang/AST/ExprCXX.h>\n #include <clang/Lex/Lexer.h>\n\n using namespace clang;\n@@ -13,10 +14,34 @@ void ObserversBase::registerMatchers(MatchFinder* Finder)\n       member(fieldDecl(hasParent(cxxRecordDecl(hasName(ClassName))))))\n       .bind(\"member\"),\n     this);\n+  Finder->addMatcher(\n+    cxxDependentScopeMemberExpr(\n+      anyOf(hasMemberName(\"vin\"),\n+            hasMemberName(\"vout\"),\n+            hasMemberName(\"version\"),\n+            hasMemberName(\"nLockTime\")),\n+      unless(hasAncestor(functionDecl(hasName(\"UnserializeTransaction\")))))\n+      .bind(\"dependent_member\"),\n+    this);\n }\n\n void ObserversBase::check(MatchFinder::MatchResult const& Result)\n {\n+  if (auto const* DME = Result.Nodes.getNodeAs<CXXDependentScopeMemberExpr>(\"dependent_member\")) {\n+    if (DME->isImplicitAccess()) {\n+      return;\n+    }\n+\n+    auto It = MemberToAccessor.find(DME->getMember().getAsString());\n+    if (It == MemberToAccessor.end()) {\n+      return;\n+    }\n+\n+    diag(DME->getMemberLoc(), \"replace direct member access with accessor\")\n+      << FixItHint::CreateReplacement(DME->getMemberLoc(), It->second);\n+    return;\n+  }\n+\n   auto const* ME = Result.Nodes.getNodeAs<MemberExpr>(\"member\");\n   if (!ME || ME->isImplicitAccess()) {\n     return;\n```\n\nI ran this and it does catch the case pointed out by @janb84.\n\nThis, however, is a non-blocking suggestion and rather a nice to have, or even just a review tool. I mainly did this to convince myself there weren't other template functions that could take both `CMutableTransaction` and `CTransaction` that had been missed. In doing so, I ended up convincing myself its not really an issue considering there is no current usage of `CTransaction` in these template functions and if someone were to attempt it in the future, it would be a compile error."
  },
  {
   "t": "2026-06-23T11:46:00Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "18b0efc5adfafca3755a2220a3d830d3ebe9d32b",
   "in_reply_to": 3451979776,
   "text": "Per https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4778806596, I convinced myself this isn't an issue."
  },
  {
   "t": "2026-06-23T12:06:35Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "ACK https://github.com/bitcoin/bitcoin/pull/35569/commits/c2dbec1ed279d73c76a28d23d02fcc3bdd433a1f\n\nAdding observer functions for data members moves towards an overall better design where the caller does not depend on the class layout. While not fully implemented in this PR, this is the first step towards a transaction type with regular semantics.\n\nMore concretely, direct access to `vin`, `vout`, `version`, and `nLockTime` effectively makes the class layout the public API. Rather, we should have serialisation and the transaction API be expressed in an implementation agnostic interface instead of tightly coupled to today's concrete implementation.\n\nThanks for taking the suggestions regarding removing the assignment operator, and removing the default move constructor. These changes ensure we preserve the same class invariants while refactoring."
  },
  {
   "t": "2026-06-23T12:43:04Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "@josibake, I don't want to add the names of one particular class into `ObserversBase`. That class is intended to be a common base and provides the logic for checks that inherit from it and parameterize it with names in their constructor.\n\nThe mutation check in the current implementation is not powerful; it has some false negatives. For `CTransaction`, that is not an issue, because its members cannot be mutated anyway. But if I fix it and add a checker for `CMutableTransaction`, I get changes in another 119 lines, including the one that @janb84 identified.\n\nI can integrate that into this PR, or keep this PR focused on `CTransaction` and do `CMutableTransaction` as a follow-up."
  },
  {
   "t": "2026-06-23T12:55:33Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nAbsolutely! \"tinkering\" was meant to imply this was not the proper way to do it, rather I do think it would be nice to detect the template case in a more clean way. However, once I ran the hacked code and realised there were no other such cases, it started to feel like a waste of time.\n\n[quoted text omitted]\nI think this is the right call. As you said, it keeps this PR focused, and it may be that `CMutableTransaction` is made unnecessary as this refactor progress, but that is not a topic for this PR."
  },
  {
   "t": "2026-06-25T12:43:34Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "a35a2b108acf355293443d5ad42cbf84152a3363",
   "in_reply_to": null,
   "text": "a35a2b108acf355293443d5ad42cbf84152a3363: I don't think nodiscard should be added here, because the boilerplate  adds basically no value. It isn't in the dev notes, but the pattern is documented in:\n\nhttps://github.com/bitcoin/bitcoin/blob/7b84e5106c38c146f2393b9a337e2d604fd64faf/src/kernel/bitcoinkernel.h#L32-L40"
  },
  {
   "t": "2026-06-26T09:46:17Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "a35a2b108acf355293443d5ad42cbf84152a3363",
   "in_reply_to": null,
   "text": "a35a2b108acf355293443d5ad42cbf84152a3363: You say that a span can not be returned here in the blog, but I wonder why that is. The blog says that some call sites have hard-coded vector::iterator types. However, those should be trivial to adjust as well.\n\nI think the real reason is that span serialization doesn't support non-Byte spans, and also doesn't support the length prefix.\n\na bit unrelated, but I wonder if it could make sense to allow all spans to be serialized (iff the element can be serialized), but require an explicit mode to be set: Either without serializing the size prefix, or without.\n\nIn any case, I think `LIFETIMEBOUND` could be added here?"
  },
  {
   "t": "2026-06-26T10:03:27Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/policy/truc_policy.h",
   "commit": "c2dbec1ed279d73c76a28d23d02fcc3bdd433a1f",
   "in_reply_to": null,
   "text": "nit in the last commit: Using the `std::` prefix may be minimally more correct, but there is no place in the current codebase that uses the prefix for such integral types. It may be better to drop it for consistency. Also, while it doesn't help type safety, I minimally prefer to have named types here. It is unlikely that the type is going to change again (27e70f1f5be1f536f2314cd2ea42b4f80d927fbd), but it can't hurt. Just a nit, though."
  },
  {
   "t": "2026-06-26T10:21:16Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "a35a2b108acf355293443d5ad42cbf84152a3363",
   "in_reply_to": 3480528778,
   "text": "edit: I read your other blog post and I see you want to replace this by `        encode_range(w, tx.vin, encode_txin);` in which case my alternative suggestion may be stale."
  },
  {
   "t": "2026-06-26T11:00:50Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "c2dbec1ed279d73c76a28d23d02fcc3bdd433a1f",
   "text": "left some nits, but feel free to ignore."
  },
  {
   "t": "2026-06-26T13:53:53Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "a35a2b108acf355293443d5ad42cbf84152a3363",
   "in_reply_to": 3474445560,
   "text": "I can remove it if there is consensus that `[[nodiscard]]` is not wanted here. I just got used to following clang-tidy's [modernize-use-nodiscard](https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-nodiscard.html), which adds it to all `const` member functions."
  },
  {
   "t": "2026-06-26T14:00:00Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "a35a2b108acf355293443d5ad42cbf84152a3363",
   "in_reply_to": 3480528778,
   "text": "[quoted text omitted]\n\nYes, they could be automatically refactored with clang-tidy's [modernize-use-auto](https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-auto.html) and further with [modernize-loop-convert](https://clang.llvm.org/extra/clang-tidy/checks/modernize/loop-convert.html).\nBut those cleanups are orthogonal. They could be done as a follow-up, or they could be done before this one. But I'd rather not squash them into this PR."
  },
  {
   "t": "2026-06-26T14:19:39Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "a35a2b108acf355293443d5ad42cbf84152a3363",
   "in_reply_to": 3474445560,
   "text": "I think the modernize-use-discard is harmful for promoting the exact opposite of a meaningful policy. I wonder if anyone has this check enabled in a useful way. Let's recall that in modern C++ (smart pointers and optional values), the isn't really a risk of resource leaks (like in the kernel C-header). So the remaining use cases are [legacy code](https://github.com/bitcoin/bitcoin/pull/34520), or error/status codes, but both of those use-cases are excluded in modernize-use-discard.\n\nIf we wanted to follow the clang-tidy rule, it should be enabled in the config, but again I don't think that is useful.\n\nInstead, it could make sense to copy-paste the existing kernel header policy to the dev notes, so that it is clear it applies to all C++ code in this repo?"
  },
  {
   "t": "2026-06-26T14:29:50Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "a35a2b108acf355293443d5ad42cbf84152a3363",
   "in_reply_to": 3474445560,
   "text": "I don't consider passing around smart pointers modern C++. Pointers (\"smart\" or not) are low level utilities that should be used exclusively for implementing composite types and never appear in function signatures, neither as arguments nor as return type.\n\nThe purpose of `modernize-use-nodiscard` is not to avoid resource leaks, but to indicate: \"This function has no observable side effect. Only call this function if you are interested in its returned value.\""
  },
  {
   "t": "2026-06-26T15:11:36Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "1472e6e52c480b9692a2701ba1cc1e3e0b9e4b3e"
  },
  {
   "t": "2026-06-26T15:14:14Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "I removed `[[nodiscard]]` and `std::` as per @maflcko's comments. I still think they make sense in both places, but maybe it is better to add them with a repository wide cleanup, by applying `modernize-use-nodiscard`.\n\n**Edit**: Also added `LIFETIMEBOUND`."
  },
  {
   "t": "2026-06-26T15:18:51Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "a2b339547f6c8acc34ec18d6b44abf0d728a9886"
  },
  {
   "t": "2026-06-26T16:06:20Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "926618a85c78d8c91d625098b744297c30091bd9",
   "in_reply_to": 3453169670,
   "text": "Just read this comment, and deleting (or better not providing it) is fine, because a  move ctor didn't exist before either. Though, in this project it  should be fine to provide one, as `bugprone-use-after-move` prevents unwanted access to the moved-from object."
  },
  {
   "t": "2026-06-27T10:55:09Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "I am still thinking about this change conceptually. As of this pull request, the changes are mostly a no-op cleanup. Yes, it is a bit nicer to use `private` over `const` here, but externally, the same copy and move ctors are provided, so I think this change is mostly a style change. It seems there are several goals:\n\n* https://purplekarrot.net/blog/bitcoin-tidy-transaction.html (step 4): \"Implement Deserialization\". I think this is equally a mostly style-wise cleanup and otherwise a no-op. I think it can probably be skipped as optional.\n* \"Step 7: One Indirection\": I think this is nice. Practically in most places `CTransactionRef` is used (but it is not really a reference), so hiding the shared pointer and allowing to pass `CTransaction` as-is with shared pointer semantics seems simpler and nicer.\n\nI am happy to review this pull as-is, and it seems fine to merge as-is, but I wonder if other reviewers think that step 7 is worthwhile, (and whether step 4 is worthwhile or can be skipped)?"
  },
  {
   "t": "2026-06-29T10:34:51Z",
   "kind": "comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "text": "I think this PR can be assessed independent from steps 4 and 7. The old design violated the C++ Core Guidelines [C.12](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c12-dont-make-data-members-const-or-references-in-a-copyable-or-movable-type)."
  },
  {
   "t": "2026-06-29T11:14:59Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nPerhaps we use the word style differently, but I tend to think of style as two different representations of the _same_ architectural principle.\n\nI'd argue that style is the wrong framing here since we are talking about an architectural refactor, specifically encapsulation. This PR makes the invariant the responsibility of the class, enforced through a private representation rather than a `const` keyword on a public field. This fundamentally changes which designs are possible, now and in the future. Identical semantics today, as you point out, but the set of possibilities open for discussion is now open.\n\nSaid differently, I don't think reviewers being interested in exactly 4 and 7 as they are proposed today is particularly relevant for merging this PR. Rather, being interested in discussing 4 and 7 at all and perhaps other follow ups is the strongest argument for this PR on its own: these follow up discussions are now possible."
  },
  {
   "t": "2026-06-29T11:15:34Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "reACK https://github.com/bitcoin/bitcoin/pull/35569/commits/a2b339547f6c8acc34ec18d6b44abf0d728a9886"
  },
  {
   "t": "2026-06-29T18:30:50Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "This still seems a waste of review resources to me. If you want to be able to mutate a transaction, use `CMutableTransaction`, don't add a +644-513 PR because you dislike the coding style.\n\n[quoted text omitted]\nThat guideline only says \"Don\u2019t make data members const or references in a copyable or movable type\" which is fine -- `CTransaction` doesn't have much need to be copied or moved (if different parts of the code want different handles on the same tx, that's what `CTransactionRef` is for), and copying/moving is only possible because of implicit constructors. Deleting those constructors catches a few mistakes: https://github.com/ajtowns/bitcoin/commits/202606-del-tx-copy-cons/"
  },
  {
   "t": "2026-06-30T11:12:05Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI disagree with this framing, and I don't think your objections apply here. This PR is not talking about mutating a transaction. The new API is const observer access, assignment remains deleted, and move is not provided.\n\nFurthermore, `CMutableTransaction` existing is not a reason for `CTransaction` to expose its layout. The question here is not whether mutation should exist somewhere. It is whether callers of the immutable transaction type should depend directly on `vin`, `vout`, `version`, and `nLockTime`. They should not. Those fields make up the invariant that transaction data, `hash`, `m_witness_hash`, and `m_has_witness` all describe the same object. That invariant belongs behind the `CTransaction` API boundary.\n\n`CTransactionRef` is also not a sufficient answer. `CTransactionRef` is `std::shared_ptr<const CTransaction>` exposed as a public vocabulary type. What I mean here is APIs that want \u201ca transaction\u201d therefore inherit pointer semantics: ownership, nullability, aliasing, `.get()`, `.reset()`, and `use_count()`. Those are not transaction semantics. If shared immutable storage is useful (it is), it should be an implementation detail behind a transaction API, not something every caller has to pull in.\n\nThis is why I [said](https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4831850845) \u201ccoding style\u201d is the wrong characterisation. Public fields vs private fields behind observers are not two styles of implementing the same abstraction. Public fields make the class layout the API. Private fields make `CTransaction` responsible for its own representation and invariants. I very much disagree that reviewing these types of architectural changes are a waste of reviewer time as they have implications on what is possible or not possible in the codebase today and into the future. They also matter for the safety, correctness, and maintainability of the code. These seem like topics reviewers would be interested in, to me.\n\nRegarding the copy/move branch you posted, this reinforces the C.12 point rather than refuting it. Public `const` data members create an irregular type surface where copy exists, assignment does not, and move behaviour is surprising. If making those operations explicit exposes call sites that need cleanup (which your branch does), then the current design already has accidental special member semantics. C.12\u2019s point is that those semantics should be intentional, not side effects of `const` fields. Deleting copy/move might be an interesting follow up, but it is not an alternative to this PR. It addresses one symptom while leaving layout as API (the root cause) untouched. This PR instead establishes the boundary: callers should not depend on the storage layout of a type whose fields participate in an invariant. Said differently, your branch seems to be treating a symptom, not addressing the root cause.\n\nI will reiterate: we do not need agreement on those future designs for this PR to be valuable today. Reviewers can still argue later for non copyable `CTransaction`, an internal shared immutable body, a different `CTransactionRef`, or a value handle over an immutable body. We could also agree on no further change. This PR only establishes the prerequisite boundary for those discussions: callers stop depending on concrete layout, while today\u2019s immutability and aliasing needs are preserved."
  },
  {
   "t": "2026-06-30T11:32:37Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI can give a few more guidelines that are violated in the current design:\n\n* [C.3: Represent the distinction between an interface and an implementation using a class](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rc-interface)\n* [C.4: Make a function a member only if it needs direct access to the representation of a class](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rc-member)\n* [C.9: Minimize exposure of members](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rc-private)\n* [C.11: Make concrete types regular](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rc-regular)\n* [C.81: Use =delete when you want to disable default behavior (**without wanting an alternative**)](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rc-delete)\n\nShould I continue and dig out more C++ Core Guidelines that are violated here? Or would you rather prefer examples from C++ literature? Conference talks? You name it."
  },
  {
   "t": "2026-06-30T13:11:22Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "Concept NACK. At this point my impression is that providing the kernel API is primarily acting as a supply-chain attack vector, encouraging multiple significant refactors into consensus critical code for extremely spurious reasons, and wasting limited development resources on low and negative impact activities.\n\n[quoted text omitted]\nPerhaps you should review the [introductory section](): \"We do not suffer the delusion that every one of these rules can be effectively applied to every code base.\", \"The rules are not perfect. A rule can do harm by prohibiting something that is useful in a given situation.\"\n\n[quoted text omitted]\nI could see the argument for declaring `CTransaction` as-is as a struct rather than a class per C.2 or C.8, but that's not a rule we consistently use in this codebase, and because we don't do that, the follow-on assumptions about how classes should act are also not justified:\n\n[quoted text omitted]\nYes, that one is a good idea, and it catches real implementation flaws, and I included it in the patchset I linked.\n\n[quoted text omitted]\n\"Should I continue with arguments to authority?\" No, you shouldn't. You should justify changes with the practical benefits they achieve.\n\nIn this case, separating an implementation and interface allows you to change the implementation without impacting the places where that interface is used, but that's not the case here: we shouldn't be changing the implementation because \"create a CTransaction, cache its hashes, treat it as immutable, and keep it around as a shared_ptr\" is already what we want. Instead, for 0% benefit, this PR is hitting 100% of the cost by changing every place that touches the interface. The only way you could get a worse tradeoff is by actively introducing bugs, as well.\n\n[quoted text omitted]\nThe only reason for making that change now is to prepare for making it mutable in future. Pretending otherwise just comes across as dishonest. I am not currently very convinced that the proposed serialization changes are a worthwhile improvement (eg, where are the benchmarks, if compile time improvements are the main argument?), but claiming that is the rationale while constantly arguing about style guidelines and private fields also comes across as fairly dishonest.\n\n[quoted text omitted]\nThe reason for `CTransaction` to expose its layout is that adding an abstraction layer over the top buys nothing but complexity and code churn. Dealing with transactions is pretty much the primary job of our codebase, so the implementation details are relevant to pretty much every part of the code, not something that should be abstracted away.\n\n[quoted text omitted]\nAPIs that want \"a transaction\" can accept `const CTransaction&` if they simply want to use it for the lifetime of that call, or a `CTransactionRef` if they want to be able to extend the lifetime of that transaction, at which point ownership/lifetime/etc are worth worrying about anyway. If they want to modify the transaction, they can either copy it or  accept some form of `CMutableTransaction` in the first place."
  },
  {
   "t": "2026-06-30T13:43:26Z",
   "kind": "comment",
   "who": "ryanofsky",
   "assoc": "MEMBER",
   "text": "re: https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4843836472\n\n[quoted text omitted]\nFor the record, josibake's description is accurate: as of the latest revision, assignment is deleted and the observers are const.\n\nOn the direction: step 6 from the blog post would have `CTransaction` hold `shared_ptr<const TransactionData>` internally, making the shared body immutable at the type level. That reads to me as increasing immutability guarantees rather than decreasing them. Am I reading the end goal right? If there's a specific harm you see in the steps between here and there, it would be useful to understand concretely.\n\nEDIT: Corrected reference to step 6 instead of step 7 in blog post."
  },
  {
   "t": "2026-06-30T13:57:35Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n100%, @ryanofsky. My goal is to make `CBlock`, `CTransaction` and a few more primitive types immutable. But a properly immutable type is a type that has no accessible modifier functions. It is not a type with deleted copy/assignment.\n\nMaking `CTransaction` non-copyable and then storing it as a pointer in `CBlock` and making it publicly accessible does not gain any immutability at all, as clients can freely reassign it to another transaction object or even to `nullptr`. I understand that some clients need nullability, but `CBlock` is not one of them and consensus code is not defined to operate on nullable transactions."
  },
  {
   "t": "2026-06-30T14:09:38Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWhere did you read that, @ajtowns? My [blog](https://purplekarrot.net/blog/serialization.html) mentions compile times, but then proceeds with:\n\n[quoted text omitted]"
  },
  {
   "t": "2026-06-30T14:16:59Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThe proposed direction is not toward mutable transaction data. It is toward a value handle over immutable transaction data, as explained in the blogs, by @purpleKarrot , and myself in this PR. This is a stronger immutability model than exposing shared pointer semantics as the transaction API."
  },
  {
   "t": "2026-07-02T17:45:01Z",
   "kind": "comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "text": "**Verified 858899e reproduces.**\n\nChecked out ede288d1 and ran the documented fixup with clang/clang-tidy 22.1.8 (per `ci/test/00_setup_env_native_tidy.sh`):\n\n```sh\nexport CC=\"$(which clang-22)\"\nexport CXX=\"$(which clang++-22)\"\ncmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DBUILD_GUI=ON -DBUILD_KERNEL_LIB=ON -DBUILD_UTIL_CHAINSTATE=ON\ncmake -B build/tidy -S contrib/devtools/bitcoin-tidy\ncmake --build build\ncmake --build build/tidy\nrun-clang-tidy-22 -p build -load='build/tidy/libbitcoin-tidy.so' -checks='-*,bitcoin-tx-observers' -fix\n```\n\nThe resulting `git diff` is identical to 858899e.\n\n---\n\n**Tests: suite passes on tip**\n\nRan the full unit test suite. The tip (`CTransaction: Make data members private`) depends on every external access having been correctly converted to observer calls in the prior commit."
  },
  {
   "t": "2026-07-02T20:46:14Z",
   "kind": "review",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "a2b339547f6c8acc34ec18d6b44abf0d728a9886",
   "text": "[quoted text omitted]\n\nI disagree with that. In my opinion, refactors of the very basic consensus code should have a concrete advantage and should not be done on general grounds such as C++ guidelines alone.\nSo I do think that this would need to be justified by possible follow-ups, or at least some non-generic examples such as preventable bugs or footguns (which @ajtowns alternative branch does).\n\nOn a language level, terms such as `vin`, `vout` etc. have long become part of the public protocol:\nFor example encapsulating `vin` through  `GetInputs()` internally within bitcoin core, just to expose them as `vin` again to the outside via RPC  (`getrawtransaction`) seems weird to me and it would probably be a nuisance for devs to remember the right term on the right level.\n\nIn summary, I think that `CTransaction` is not just a random C++ class, but at the heart of the bitcoin protocol itself, so I don't think arguing only with generic C++ guidelines re: encapsulation is sufficient here.\n\n[quoted text omitted]\nThe kernel project is mentioned neither in the PR description nor the blog post.\nIf this helps the kernel project to design their API better (or if there is any other relation to the kernel work) it would be good to explain this in more detail."
  },
  {
   "t": "2026-07-02T23:08:21Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": null,
   "text": "What's the reason for dropping the consts from these?\n```suggestion\n    const bool m_has_witness;\n    const Txid hash;\n    const Wtxid m_witness_hash;\n```"
  },
  {
   "t": "2026-07-02T23:14:05Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "contrib/devtools/bitcoin-tidy/observers-base.cpp",
   "commit": "6d6c33787f0f7b4065abcf4d294230f340070ec2",
   "in_reply_to": null,
   "text": "I agree that this check is useful during development, but I'm not sure it should remain in the tree after the migration.\n\nAfter the final commit, the compiler enforces the `CTransaction` boundary, so this check is only scaffolding unless we plan to keep using it for other classes. If it is only scaffolding, could the PR end with a commit that removes it, or could the helper live outside the repo?\n\nIf it is meant to stay as reusable `bitcoin-tidy` infrastructure, could we align it with the existing check style first? The new files currently use different conventions from `nontrivial-threadlocal` (no MIT header, `#pragma once`, global namespace, 2-space indentation). Or is that also something we *should* change because other other guidelines request it?"
  },
  {
   "t": "2026-07-02T23:16:24Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": null,
   "text": "What's the purpose of introducing trailing-return syntax here? What role does `auto` serve, apart from aligning the names? Seems aggressive...\n\n```suggestion\n    uint32_t GetVersion() const { return version; }\n    const std::vector<CTxIn>& GetInputs() const LIFETIMEBOUND { return vin; }\n    const std::vector<CTxOut>& GetOutputs() const LIFETIMEBOUND { return vout; }\n    uint32_t GetLockTime() const { return nLockTime; }\n```\n\n---\n\nMore broadly, as @mzumsande also mentioned, I am not convinced that introducing new names for vin, vout, version, and nLockTime is an improvement. Could we keep the protocol names instead?\n```cpp\nuint32_t version() const { return version; }\nconst std::vector<CTxIn>& vin() const LIFETIMEBOUND { return vin; }\nconst std::vector<CTxOut>& vout() const LIFETIMEBOUND { return vout; }\nuint32_t nLockTime() const { return nLockTime; }\n```\nWith indexed helpers, the diff becomes even closer (and safer) to the current code:\n```cpp\nconst CTxIn& vin(size_t index) const LIFETIMEBOUND { return m_vin.at(index); }\nconst CTxOut& vout(size_t index) const LIFETIMEBOUND { return m_vout.at(index); }\n```\n\nFor example:\n\n```diff\n- if (tx.vin.empty())\n+ if (tx.vin().empty())\n\n- if (tx.vout.empty())\n+ if (tx.vout().empty())\n\n- for (const auto& txout : tx.vout)\n+ for (const auto& txout : tx.vout())\n\n- for (const auto& txin : tx.vin) {\n+ for (const auto& txin : tx.vin()) {\n\n- if (tx.vin[0].scriptSig.size() < 2 || tx.vin[0].scriptSig.size() > 100)\n+ if (tx.vin(0).scriptSig.size() < 2 || tx.vin(0).scriptSig.size() > 100)\n\n- for (const auto& txin : tx.vin)\n+ for (const auto& txin : tx.vin())\n```\n\nThat would make the mechanical diff much easier to review manually.\nThe renaming question can be debated separately, but as-is the change reads like a broad spelling rewrite with non-negligible review cost and little immediate benefit."
  },
  {
   "t": "2026-07-02T23:33:26Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/consensus/tx_verify.cpp",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": null,
   "text": "It's awkward to keep these references now that the field is hidden:\n\n```patch\n- // Even if tx.nLockTime() isn't satisfied by nBlockHeight/nBlockTime, a\n+ // Even if the transaction's nLockTime isn't satisfied by nBlockHeight/nBlockTime, a\n\n- // Note these tests were originally written with tx.version=1\n+ // Note these tests were originally written with transaction version 1\n\n- // We want to make sure tx.vout is not used now that we are passing outputs as a vector of recipients.\n+ // We want to make sure the transaction outputs are not used now that we are passing outputs as a vector of recipients.\n\n- // Clear tx.vout since it is not meant to be used now that we are passing outputs directly.\n+ // Clear the transaction outputs since they are not meant to be used now that we are passing outputs directly.\n\n- // Clear tx.vout since it is not meant to be used now that we are passing outputs directly.\n+ // Clear the transaction outputs since they are not meant to be used now that we are passing outputs directly.\n\n- // Clear tx.vout since it is not meant to be used now that we are passing outputs directly.\n+ // Clear the transaction outputs since they are not meant to be used now that we are passing outputs directly.\n\n- // txouts needs to be in the order of tx.vin\n+ // txouts needs to be in the order of the transaction inputs\n\n- // We want to make sure tx.vout is not used now that we are passing outputs as a vector of recipients.\n+ // We want to make sure the transaction outputs are not used now that we are passing outputs as a vector of recipients.\n```"
  },
  {
   "t": "2026-07-03T00:18:29Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThese extra accessors feel awkward, especially on `CMutableTransaction`.\n\n[quoted text omitted]\nThis seems like a code smell worth addressing before adding more API surface.\nIf the issue is that some generic read-only code is instantiated with both `CTransaction` and `CMutableTransaction`, could we make that dependency explicit instead of adding duplicate read APIs to the mutable builder type?\n\nOr could the generic read-only code use an immutable transaction view instead?\nFor example, serialization/read-only helpers could depend on a cheap non-owning transaction view constructible from both types, or mutable transactions could be moved into an immutable serializable type at the boundary where that is needed. That would let `CTransaction` own its private representation while `CMutableTransaction` remains the straightforward mutable construction type.\n\nAnother option would be to move a mutable transaction into an immutable serializable type at the boundaries where serialization is needed."
  },
  {
   "t": "2026-07-03T00:35:19Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/policy/policy.h",
   "commit": "ef3a175126164d05c82a0b41d166a523fa4c9459",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nI would be for these changes in a separate PR."
  },
  {
   "t": "2026-07-03T01:03:06Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "a2b339547f6c8acc34ec18d6b44abf0d728a9886",
   "text": "Approach NACK for now.\n\nI don't see a concrete benefit here that justifies rewriting so many transaction call sites in consensus, policy, mempool, wallet, tests, fuzzers, and kernel-facing code.\n\nTechnically, the current patch seems mostly behavior-preserving: callers could not mutate `CTransaction` before because the public fields were `const`, and callers still cannot mutate it after because the new observers are const. Assignment remains deleted and move is not provided. So the practical safety delta appears small, while the review cost is high.\n\nI also share the concern raised above about \u201carguments to authority.\u201d Guidelines and literature can be useful for framing, but they should not substitute for showing why this particular change improves *Bitcoin Core specifically*.\n\nThe PR also has knock-on effects that make the abstraction feel premature: `CMutableTransaction` gets duplicate const observers just so generic code keeps compiling.\n\nIf the goal is a later shared immutable transaction body or a new serialization boundary, I think that design should be motivated directly first, with concrete examples of why it is worth doing. Without that, this feels like a broad mechanical refactor of very central code for speculative follow-up work."
  },
  {
   "t": "2026-07-03T04:37:59Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516569133,
   "text": "[quoted text omitted]\n\nReadability. Did you see how the names of the function names are nicely aligned? Is this not something that directly jumps to the eye?\n\n[quoted text omitted]\nI have explained the naming choice in my blog referenced in the PR description. It is not possible in C++ because it creates a naming conflict. You cannot overload a member function with a data member.\n\n[quoted text omitted]\nLet's not create more technical debt. We currently have lots of index based loops that could be rewritten as range based loops or algorithms. Introducing \"helpers\" complicates such modernization.  When only the first element is needed, `.front()` should be preferred."
  },
  {
   "t": "2026-07-03T05:11:02Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516569133,
   "text": "[quoted text omitted]\n\nSure, I even mentioned it on the same line: \"What role does auto serve, apart from aligning the names?\"\n\n[quoted text omitted]\nAre you planning on changing it in the whole codebase? Or will this be the only place where we use \"readable\" code?\n\n[quoted text omitted]\nNot sure what you mean, I simply didn't migrate `CMutableTransaction` and renamed the fields to start with `m_` prefix locally."
  },
  {
   "t": "2026-07-03T05:25:12Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516569133,
   "text": "[quoted text omitted]\n\nSure, the naming conflict can be solved by changing the data members, but then the three-step refactoring approach is not possible:\n\n1. Introduce new API without any other changes.\n2. Migrate from old API to new API fully automated with no manual intervention.\n3. Retire old API.\n\nSince you said you disagree with the \"Approach\", can you explain another approach and also explain what problems you see with the approach that I follow?"
  },
  {
   "t": "2026-07-03T06:10:23Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516569133,
   "text": "The approach that I'm objecting to is aiming to do things \"by the book\" without understanding the project specifics. I fell into the same trap at the beginning, and I also received a lot of pushback. This is a huge refactor at the most critical part of the code, and it's not obvious what we're getting in return (besides appeals to authority, which most of us are allergic to). Especially since the refactor revealed incidental loose coupling (`CMutableTransaction` and `CTransaction` having to have the same field names) which should be hardened instead of papered over by adding yet another getter to the already public fields. It's also trying to force new names instead of considering what the domain already has. When I suggest alternatives, the reply is just a patronizing \"that's impossible in C++\". This approach seems too aggressive and dismissive to me, hence my nack."
  },
  {
   "t": "2026-07-03T08:24:59Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYour response seems to be arguing against a claim I did not make. My argument is not \u201cmerge this because the C++ Core Guidelines say so.\u201d The guidelines are excellent supporting evidence, but not the justification. The justification I am arguing for is that `CTransaction` currently exposes the layout as the API. By making fields participating in the invariant public, we are tightly coupling all callers to the representation. This is a fragile design. Rather, the representation of the invariant should be owned entirely by `CTransaction` behind an API, i.e. encapsulation.\n\nThe question is not whether encapsulation is idiomatic in the abstract, or performative to some set of guidelines. The question is whether `CTransaction`\u2019s concrete layout defining its public API is _better_ than `CTransaction` exposing transaction behaviour while owning its representation and invariants internally. I have not seen an argument for why the current public layout design is better. Most objections seem to defend the status quo by pointing to review cost or familiarity, but those are **not** arguments that public storage is the better abstraction.\n\n@ajtowns 's branch, as I already pointed out, illustrates my point. It found places where the current design allows accidental full transaction copies when the intent was to keep or pass a transaction reference. Deleting copy/move catches some of those mistakes, which is good, but it addresses one symptom. The broader issue remains: callers depend directly on the storage layout of a type whose fields participate in an invariant. Encapsulation gives us a boundary where those semantics can be made intentional, whether the followup is deleting copy/move, using an internal immutable body (as explained in the blogs and the design I strongly prefer!), changing `CTransactionRef`, or something else. The C++ core guidelines are relevant here because they describe the class of failure mode ajs branch attempts to fix. This is not to say \"oh we must have strict adherence to the guidelines,\" rather its the C++ core guidelines pointing out: \"you guys aren't the first project to make this mistake.\"\n\nThe naming concern you raise is orthogonal, imo. Whether the observers should be called `GetInputs()` / `GetOutputs()` or use familiar vocabulary such as `vin()` / `vout()` is a separate API naming discussion (and likely a mechanical rename/scripted diff). It is not an argument for keeping the storage layout public. Likewise, `vin` and `vout` being protocol vocabulary does not mean the in-memory C++ representation must expose public fields. I do not agree with the the framing of `CTransaction` as being the heart of the bitcoin protocol. Its not. Its a C++ class in this project that represents protocol data. The protocol and serialisation format must remain precise and stable (and implementation agnostic!), but that does not require class layout to be the public API. This is mentioned explicitly in the serialisation blog.\n\nAs I argued before: this refactor stands own its own because it establishes a clear API boundary not tightly coupled to representation. This enables the discussion and implementation of future design, e.g. the _stronger immutability_ model detailed by @purpleKarrot in his blogs. The status quo keeps API defined by class layout, which is fragile and forces us into suboptimal design space."
  },
  {
   "t": "2026-07-06T03:10:56Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp",
   "commit": "6d6c33787f0f7b4065abcf4d294230f340070ec2",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nwill this be executed by tidy on every run now?"
  },
  {
   "t": "2026-07-07T02:17:40Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "a2b339547f6c8acc34ec18d6b44abf0d728a9886",
   "text": "To be clear, I can agree with the concept of moving `CTransaction` away from public storage given good motivation and easy review. My main concern is the approach: I could agree with it if the path were broken into smaller manually reviewable steps with non-general, concrete Bitcoin Core reasoning for each step: what each step enables, what reviewers should check, and why it is worth doing even if the later design changes.\nGeneral C++ best-practice arguments are not enough for me here.\n\nIt would also help to have the rest of the intended series prepared as draft PRs or demonstration branches, so reviewers can evaluate the path end-to-end instead of reviewing a preparatory refactor against a series of \"to be continued\" blog posts.\n\nFor comparison, I tried a smaller shape here over the weekend: [l0rinc/bitcoin#212 commits](https://github.com/l0rinc/bitcoin/pull/212/commits).\nIt differs from this PR in three main ways:\n1. It first routes generic mutable and immutable transaction reads through explicit helpers, so `CMutableTransaction` can stay the mutable builder instead of getting mirror observers just to preserve shared public field spelling.\n2. It uses a reusable, parameterized, tested `bitcoin-tidy` field-observer helper and runs it one field at a time (`nLockTime`, `version`, `vout`, `vin`), so each scripted diff is a smaller, CI-passing review unit.\n3. It keeps the protocol names as observers (`tx.vout()`, `tx.vin()`, etc.) instead of renaming the API to `GetOutputs()` / `GetInputs()`, and updates the related code comments.\n\nThat shape makes the call-site diff mostly adding parentheses after the generic-read split, keeps mutable writes and exceptional serialization paths out of the immutable-transaction migration, and should reduce the need for repository-specific macro skips such as `READWRITE` or `VARINT` in the tidy check.\nIf compile-time improvements, a future serialization boundary, kernel API shape, or stronger immutability motivate this refactor, it would be helpful to state that in the PR with concrete examples or evidence.\nLinked posts can provide background, but the GitHub PR itself should carry the reviewer-relevant motivation and tradeoffs."
  },
  {
   "t": "2026-07-08T11:51:42Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n\"The refactoring is a prerequisite for further refactoring that will untangle the type from serialization logic (...) which will result in ... faster compilation.\""
  },
  {
   "t": "2026-07-21T17:16:22Z",
   "kind": "comment",
   "who": "jonatack",
   "assoc": "MEMBER",
   "text": "Needs rebase."
  },
  {
   "t": "2026-07-27T14:38:33Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "6d6c33787f0f7b4065abcf4d294230f340070ec2"
  },
  {
   "t": "2026-07-30T07:25:10Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Maybe turn into draft while CI is red?"
  },
  {
   "t": "2026-08-03T14:58:01Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "7280a562ebebf56858caeac9d3d28736437c47b8"
  },
  {
   "t": "2026-08-03T15:13:48Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "10485b67784de75809173efc753d926793cbfe25"
  },
  {
   "t": "2026-08-03T16:13:53Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "The `clang-tidy` check is now using annotations rather than hardcoding `CTransaction` and the names of the data members.\n\n@theuni, should `bitcoin-tidy` live in a separate repository?"
  },
  {
   "t": "2026-08-06T08:04:43Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "ef3a175126164d05c82a0b41d166a523fa4c9459"
  },
  {
   "t": "2026-08-06T09:09:05Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "@ajtowns, @mzumsande, @l0rinc, and others:\n\nI want to assure you, that this refactoring is not motivated on \"general grounds such as C++ guidelines\" or \"appeals to authority\". It was also not done \"without understanding the project specifics\" or \"without considering why certain decisions were taken\".\n\nIt is a byproduct of a very deep analysis of the consensus validation code with the goal of providing a stateless validation component: #35904\n\nI do understand the motivations that led to the data members being `const`, which then led to the split between `CTransaction` and `CMutableTransaction`, which then led to the requirement for certain function templates taking a generic transaction type, which then led to the problem that it is no longer obvious whether the template instantiations that are tested are actually the ones that are used in production. It is an intervention spiral: A hack always demands further hacks.\n\nI also do understand the project's requirements on stability and performance and I have identified places where those requirements are not fulfilled, like the errors that are thrown during deserialization, for which I have provided a proof-of-concept solution that also does not rely on overload resolution and is therefore easier to reason about than the current deserialization code.\n\nAm I not allowed to identify such problems just because the original authors provided more proof of work? How is that not \"appeals to authority\"?"
  },
  {
   "t": "2026-08-06T16:41:56Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "This is an amazing use of `bitcoin-tidy`. Nice work getting the attribute/annotation machinery working. It's a thing of beauty :)\n\nAt least for now, I think it makes sense to keep it part of this repo. Allowing the plugin to be updated in the same PR as the changes that use it makes life substantially easier. I realize it could be helpful (and more secure) to separate the concerns, but I think it makes sense to defer that move until there's a concrete reason to do it."
  },
  {
   "t": "2026-08-11T12:03:46Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "contrib/devtools/bitcoin-tidy/observers-base.cpp",
   "commit": "6d6c33787f0f7b4065abcf4d294230f340070ec2",
   "in_reply_to": 3516562601,
   "text": "I have rewritten the check in a way that it no longer hardcodes any concrete symbol names. This makes the check generally useful for other places and should stay.\n\nI have no strong opinion on the code style of bitcoin-tidy. Ideally, it would follow LLVM's style just in case we want to upstream any of those."
  },
  {
   "t": "2026-08-11T12:08:20Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516569133,
   "text": "The motivation for this change is mentioned in #35904. Please stop making those unfounded claims. This is a tiny, straightforward change, as most if it is automated and reproducible."
  },
  {
   "t": "2026-08-11T12:13:24Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/consensus/tx_verify.cpp",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516615730,
   "text": "I agree. Lesson to learn: Avoid referring to concrete symbol names in technical writing. But this is a much larger issue that should be approached independently. See also https://github.com/bitcoin/bips/pull/2195#discussion_r3747735099."
  },
  {
   "t": "2026-08-11T12:14:58Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/policy/policy.h",
   "commit": "ef3a175126164d05c82a0b41d166a523fa4c9459",
   "in_reply_to": 3516781523,
   "text": "I have moved them to a separate commit."
  },
  {
   "t": "2026-08-11T12:18:44Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516741637,
   "text": "The code smell is `CMutableTransaction` as a whole. There is no real use-case for a mutable transaction type. As explained in #35904, there are only a limited amount of places where transactions are mutated, and those actually benefit from being rewritten anyway."
  },
  {
   "t": "2026-08-11T18:02:33Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516741637,
   "text": "[quoted text omitted]\n\nYes, so let's clean that up first - please see my branch where I did just that."
  },
  {
   "t": "2026-08-12T07:38:44Z",
   "kind": "review_comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516741637,
   "text": "You did not remove `CMutableTransaction` in your branch. Instead, you added yet another layer of indirection on top, proving my theory of the *intervention spiral*. https://github.com/bitcoin/bitcoin/issues/35904 explains what refactoring steps are needed to make `CMutableTransaction` obsolete."
  },
  {
   "t": "2026-08-12T08:48:59Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nIs this the vocabulary that we want Bitcoin to maintain for the next hundred years across implementations? In that case we should specify that in [BIP453](https://github.com/bitcoin/bips/pull/2195). But if we assume that coming generations will find terms like `inputs` and `outputs` easier to reason about, we should specify those instead and then slowly converge code bases and RPC protocols towards that BIP. A function like `GetInputs()` can be interpreted as applying a *coding style* on the standardized term `inputs`."
  },
  {
   "t": "2026-08-12T12:17:41Z",
   "kind": "comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nSure, why not - it's just words, I have no intentions of telling people what terms to use or not to use.\n\n[quoted text omitted]\nFirst, it _is_ already specified in the linked BIP453 draft, as one of multiple synonyms, see \"transaction output list\"/\"transaction input list\".\nMore importantly, BIP453, as I understand it, is descriptive and not normative in nature. If many code bases would start using a different term for something, we should amend the BIP by adding this term as a synonym instead of telling the code bases to change."
  },
  {
   "t": "2026-08-12T13:17:19Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWell, in your original comment you had a very strong opinion that `vin` and `vout` have to stay, because they \"have long become part of the public protocol\".  That sounds very normative."
  },
  {
   "t": "2026-08-12T14:38:04Z",
   "kind": "comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI'm not sure if you are serious, and I will leave this unproductive discussion after this, but normative/descriptive are categories for universal language-defining documents such as BIPs, and I only brought them up in response to you talking about \"future generations\" using terms. (\"we should specify those instead and then slowly converge code bases and RPC protocols towards that BIP\").\n\nMe having an opinion to a proposed change in a single local project that I contribute to has nothing to do with that.\nBesides, I did not even utter a \"very strong opinion that vin and vout have to stay\" as you allege, but pointed out the potential confusion of encapsulating things on an intermediate level under a different name, that are then exposed under the internal name with the RPC interface: I wrote \"For example encapsulating vin through GetInputs() internally within bitcoin core, just to expose them as vin again to the outside via RPC (getrawtransaction) seems weird to me\".\n\nI did not opine on whether to resolve this confusion by keeping the terms, or by removing `vin` also from the rpc.\nAnd I certainly don't care what terms other projects or future generations use. So please stop putting words in my mouth that I didn't say."
  },
  {
   "t": "2026-08-12T17:39:36Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "@purpleKarrot, you're receiving valuable feedback, but instead of learning from it, you act offended and try to force your subjective opinion even harder.\nThe responsibility falls on you to argue for the change, but completely ignoring the established culture and trying to force your own opinion won't scale in this environment.\nThere are many things we need to fix here (especially now that AI scanning reveals hundreds of potential attack angles), and these opinionated refactors are not the best use of our time.\nI will unsubscribe from this thread. I'm not here to continue arguing."
  },
  {
   "t": "2026-08-12T19:05:57Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "In the \"valuable feedback\", the change is descibed as:\n\n* \"not a good idea at all\"\n* \"a waste of review resources\"\n* \"acting as a supply-chain attack vector\"\n* \"wasting limited development resources\"\n* \"low and negative impact\"\n* \"arguments to authority\" (3x)\n* \"0% benefit, 100% of the cost\"\n* \"only actively introducing bugs would be worse\"\n* \"comes across as dishonest\" (2x)\n* \"buys nothing but complexity and code churn\"\n* \"done on general grounds such as C++ guidelines alone\" (2x)\n* \"seems weird to me\"\n* \"no concrete benefit\"\n* \"small practical safety delta\"\n* \"premature\"\n* \"without understanding the project specifics\"\n* \ud83d\udc4e\n\nIt also continued in a similar style in private conversation.\n\nNot one single comment was articulating that in this particular case, `public` `const`data members should be preferred.\nThe only complains were about\n\n* how the change was approached,\n* how the change was articulated,\n* that this change was proposed in the first place, and foremost\n* that I dared to propose this change without going through some form of trial first."
  },
  {
   "t": "2026-08-13T08:31:53Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-08-13T09:38:54Z",
   "kind": "comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "text": "The larger motivation is still not mentioned in this PR description and I think that's part of the issue here causing disagreement.\n\nMy impression is that the author's view is that this refactoring by itself serves the long term good, and that PRs should stay very \"low-level\" building blocks which may be referenced by larger projects but for some reason shouldn't spell out the larger goal. Is the expectation that people reviewing should either be aware of the larger goal anyway, and otherwise not review?\n\nIt's not immediately obvious why the Kernel API would be concerned with internals of Bitcoin Core. Kernel as it stands today is a bigger higher level wrapper of Core features than the prior libconsensus approach. **But I've heard rumors of a surviving subdomain of Kernel which aspires to expose a stateless/pure block/transaction validation API, with part of the goal being that *Bitcoin Core should use that API*. That would in turn justify \"reaching in\" to refactor these Core types.** The validation library is mentioned in the linked serialization article, but I think spelling it out in the PR description together with the end goal of making Bitcoin Core use it is not too much to ask.\n\nI'm not experienced with the benefits and pitfalls of public `interface` with private `shared_ptr<implementation>` member vs passing around `shared_ptr<type>`, nor `const` variants. The former promotes hidden indirection/pointer chasing while the latter has explicit pointer chasing but also risks accidental copying (https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4835740603). I think many other seasoned devs here (both proponents and opponents of the PR) are also not deeply familiar with the subculture of \"regular types\" and their trade-offs.\n\nEspecially without knowing the overarching desired end state of Bitcoin Core using the public validation library, I can really understand judging this refactoring as overrated off-hand.\n\n(In the past I've been annoyed at `CTransaction` on master and thought a better name would be `ImmutableTransaction`)."
  },
  {
   "t": "2026-08-13T13:55:56Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThe PR is older than https://github.com/bitcoin/bitcoin/issues/35904 and I haven't updated the description yet to mention it. It is also not strictly required for a stateless validation library. The issue description makes a clear separation between the necessary refactoring steps and additional \"interface segregation\" steps. This PR falls into the second category.\n\nBut I doubt that the missing link in the description is the root cause of the disagreement here.\n\n[quoted text omitted]\nThis is correct.\n\n[quoted text omitted]\nI did not make it a secret that this serves https://github.com/bitcoin/bitcoin/issues/35904. But I also don't want to frame it as a requirement of that issue. Encapsulation has benefits by itself.\n\n[quoted text omitted]\nIt has got little to do with the Kernel API and it is not a rumor. It has its own issue. Discussion about validation should take place there. This PR is about providing encapsulation for `CTransaction`.\n\n[quoted text omitted]\nThat is a different stance than \"you don't understand the project's specifics\". For those willing to get familiar and experienced, I am happy to provide explanations or point to articles, books, videos. But those that dismiss any best practice of the larger C++ community as \"preachy\", or \"appeal to authority\", I cannot help.\n\n[quoted text omitted]\nThat should not be the case. As I stated above, and as @josibake has pointed out repeatedly above, this PR provides benefits by itself.\n\n[quoted text omitted]\nThe idea of having an immutable type associated with a \"builder\" is alien to C++. It is a necessary evil in languages built on reference semantics, where this pattern is very common. You can compare how strings work differently in C++ vs C#. The fundamental idea is that mutability and sharing should be mutually exclusive to prevent conflicts. Where everything is shared by default (C#), you want to restrict mutation. When things are copied by default (C++), mutation is less of an issue.\n\nFor `CTransaction` however (and also for `CBlock`, `CTxIn`, `CTxOut`, and `COutPoint`), mutation is a rare use case.\nThose types should be made immutable, with no associated builder (no `CMutableTransaction`), and code that \"builds\" those types in procedural code should be rewritten in a declarative style (this mostly affects test code).\n\nThe following code builds a transaction using the `CMutableTransaction` builder:\n\nhttps://github.com/bitcoin/bitcoin/blob/11090c8bb359f894ef7d97b65aff52fe8191aec1/src/test/miner_tests.cpp#L162-L168\n\nThe same logic in declarative style does not need a builder:\n\n```cpp\n    const auto tx = CTransaction{\n        CTransaction::CURRENT_VERSION,\n        std::vector{\n            CTxIn{\n                COutPoint{txFirst[0]->GetHash(), 0},\n                CScript() << OP_1,\n                CTxIn::SEQUENCE_FINAL,\n            },\n        },\n        std::vector{\n            CTxOut{\n                5000000000LL - 1000,\n                CScript{},\n            },\n        },\n        0,\n    };\n```\n\n The best way to make a type immutable in C++ is to not provide any mutating member function. This does not include `operator=`, however. Removing `operator=` does not make a type less mutable, it makes it less usable. For example, it can no longer be stored in a vector. One workaround (note: intervention spiral!), is to wrap it in a `std::shared_ptr`, which is what `CTransactionRef` is doing. But this has two nasty consequences: It introduces sharing, so now you really need to prevent mutation. The much bigger issue is that it introduces emptyness. Each `CTransactionRef` can hold a transaction, it can be changed to a different transaction, and it can hold not a transaction at all. Making the `std::shared_ptr` an implementation detail of `CTransaction` allows providing a never-empty guarantee, which makes this type easier and safer to use.\n\nThose benefits are completely orthogonal to https://github.com/bitcoin/bitcoin/issues/35904. It should be possible to judge this PR even without knowing the desired shape of the validation library."
  },
  {
   "t": "2026-08-13T22:18:23Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nAn object with a functioning `operator=` is mutable."
  },
  {
   "t": "2026-08-14T05:34:26Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nOperations are defined on the *type*, not on the *object*. A *type* that does not provide any mutating member functions is immutable. An *immutable* type can still be *regular* (regularity requires `operator=`).\n\nWhether an object can be modified through a *variable*, depends on the declaration of that variable (controlled with `const` and `mutable`).\n\nThis is consistent with other languages: In C# or JavaScript, it is possible to reassign to a variable that holds an object of an immutable type, unless reassignment is explicitly prevented through that variable. Further, this is the current behaviour of `CTransactionRef`."
  },
  {
   "t": "2026-08-14T11:35:04Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "reACK https://github.com/bitcoin/bitcoin/commit/ef3a175126164d05c82a0b41d166a523fa4c9459\n\nI find the new approach with clang-tidy annotations to be much easier to follow and, as you mention, gets rid of hardcoding the class names. Nicely done!"
  },
  {
   "t": "2026-08-14T12:53:27Z",
   "kind": "comment",
   "who": "ryanofsky",
   "assoc": "MEMBER",
   "text": "Concept ACK. I don't see practical downsides worth blocking over, and the techniques and direction seem valuable.\n\nI've also been thinking that `CBlock` might be an interesting place to apply the same ideas. It's mutable right now, so unlike `CTransaction` the benefits of locking down the interface would be more visible, especially in mining code. If this PR is stuck, maybe `CBlock` could be a more useful place to apply the same techniques?"
  },
  {
   "t": "2026-08-14T13:06:40Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "@ryanofsky `CBlock`, along with a few other primitive types, are mentioned in https://github.com/bitcoin/bitcoin/issues/35904 to be made immutable. Not sure if there is a required / desirable order to do them in, I'll let @purpleKarrot speak to that.\n\nEDIT: Although, I suspect its better to have `CTransaction` be a regular, immutable type first since a block is a collection of transactions."
  },
  {
   "t": "2026-08-14T20:34:57Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "13b62f714a08e4a87f226a1ec232d9de4dbbe505"
  },
  {
   "t": "2026-08-14T21:03:19Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n`CTransaction` is the easiest start, because all member accesses are read accesses. `COutPoint` is also fairly easy, because most member accesses are either read accesses or assignments. Most assignments of one member are directly followed by an assignment of the other member (the order is not always the same), which can be rewritten with another bitcoin-tidy check (WIP: https://github.com/purpleKarrot/bitcoin/commits/outpoint-encapsulation).\n\n`CTxIn` and `CTxOut` are a bit more tricky, because it is much harder to determine whether a member access is mutating or not. It may be obvious when looking at the code, but for clang-tidy, it requires much deeper analysis of the AST.\n\n`CBlock` involves even more work. If we order this from lowest to highest hanging fruits, it would be `CTransaction`, `COutPoint`, `CTxOut`, `CTxIn`, `CBlock`. But it would just as well be possible to parallelize some work. `Coin` may also be an interesting candidate to look at."
  },
  {
   "t": "2026-08-15T03:58:11Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "Note: The three step approach, where\n\n1. a new interface is added,\n2. the code base is migrated to the new interface,\n3. the old interface is removed,\n\nreally pays off when rebasing: Conflicting files can be reset completely and the bitcoin-tidy check can be rerun on them.\n\nWith the approach that does all three changes in a single clang-tidy check as suggested by @l0rinc, resetting individual files leaves them in an uncompilable state, as they use an interface that is no longer available."
  },
  {
   "t": "2026-08-15T04:51:28Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThe commits in https://github.com/l0rinc/bitcoin/pull/212 all compile just fine."
  },
  {
   "t": "2026-08-15T06:13:18Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nSure, each commit compiles as a whole. But how do you resolve merge conflicts? When resetting a subset of a commit, it does not compile and the clang-tidy is not able to run either. You either need to resolve manually, or you need to reset completely."
  },
  {
   "t": "2026-08-15T18:30:21Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n[quoted text omitted]\nBeing deliberately obtuse is not helpful. If you're going to insist on unnecessary precision: mutable types are types whose objects can be mutated after construction. In C++, objects can be mutated by non-member functions, so not providing mutating member functions is no guarantee of immutability; in particular `operator=` is often not a member function.\n\n[quoted text omitted]\n`CTransactionRef` is an alias for `shared_ptr`, it is not surprising that you a pointer can point at different objects over its lifetime. Other languages put different priorities on the relationship between references and the objects they reference; eg python makes numbers immutable, with every change to a number resulting in the reference pointing at a different object."
  },
  {
   "t": "2026-08-16T06:13:05Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n**Edited**\n\nI redact my comment. I still want to highlight this quote, but leave it uncommented."
  },
  {
   "t": "2026-08-17T17:43:22Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI don't think anyones being obtuse, I think you're not following the discussion. Furthermore, what you claim is \"often\" isn't possible in C++. You cannot have a non-member `operator=`, as a language design choice.\n\nThis PR encapsulates `CTransaction` by making the data members private and and providing access through const readers. Making the data members non-const here in no way weakens the immutability of `CTransaction`. If you disagree, an example specific to this PR would be great.\n\nIt was also stated in the original blog, reiterated by me, and corroborated by @ryanofsky that the desired end goal stated by the author is a _stronger immutability_ guarantee than what we have today. To reiterate again:\n\n```c++\nclass CTransaction {\npublic:\n    CTransaction(const CTransaction&) = default;\n    CTransaction(CTransaction&&) noexcept = default;\n    CTransaction& operator=(const CTransaction&) = default;\n    CTransaction& operator=(CTransaction&&) noexcept = default;\n\n    // the observers added in this PR\n   ...\n\nprivate:\n    std::shared_ptr<const TransactionData> m_data;\n};\n```\n\nThis gives a `CTransaction` _type_ with regular value semantics, which serves as a handle to an immutable transaction body. If you believe this introduces mutability or is worse than the current design, please provide an example specific to what I reiterated above.\n\n---\n\nAs I was testing this PR, inspired by my comment on the `bugprone-use-after-move` behaviour, I noticed the flag isn't smart enough to catch aliases. So its possible to create something like:\n\n```cpp\nCMutableTransaction mtx;\nmtx.vin.resize(1);\nmtx.vin[0].scriptSig = CScript() << OP_1;\n\nCTxIn& in = mtx.vin[0]; // alias!\n\nconst CTransactionRef tx = MakeTransactionRef(std::move(mtx));\nconst Txid og_hash = tx->GetHash();\n\nin.scriptSig << OP_2; // mutate!\n\nassert(&tx->vin[0] == &in);\nassert(tx->vin[0].scriptSig == (CScript() << OP_1 << OP_2));   // not the same!\nassert(tx->GetHash() == og_hash); // but the same hash!\n```\n\nSo its possible to write perfectly valid code that does not exploit UB etc, that compiles and violates the invariant of the class by mutating `CTransaction` after construction. I've only spent a day or two looking into this so I don't want to overstate the claim, but I do feel increasingly strongly that moving towards regular types with value semantics and more explicit ownership will help us fix these bugs. By more explicit, I mean the constructor needs to own its storage exclusively. This is the thing that matters for closing footguns like the one mentioned above, afaict. Why does it matter that `CTxIn` and `CTxOut` are regular types? Cheap copies!"
  },
  {
   "t": "2026-08-17T17:44:33Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "reACK https://github.com/bitcoin/bitcoin/commit/13b62f714a08e4a87f226a1ec232d9de4dbbe505\n\nLast push was a rebase."
  },
  {
   "t": "2026-08-19T20:25:24Z",
   "kind": "comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "text": "Without wading into the meta-discussion here, or picking a specific approach to support, I'd like to bring up a concrete potential advantage of better `CTransaction` encapsulation (not as a suggestion for inclusion in this PR, but perhaps as motivation for it).\n\nOne of the ideas I had when I introduced the `CTransaction` / `CMutableTransaction` split, was that `CTransaction` could use more efficient memory allocation. For example, all dynamically-allocated memory within a transaction could be placed in a single allocator arena owned by the `CTransaction`, avoiding the overhead of individual vectors + allocation overheads for all inputs, outputs, scriptsigs, scriptpubkeys, witnesses, and witness stack elements, also reducing memory fragmentation and improving locality.\n\nI never explored that idea further at the time, but #36015 makes this suddenly a lot more relevant again."
  },
  {
   "t": "2026-08-20T06:47:54Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI think this is a good point, but I don't think it is fixed by this pull. IIUC this is only fixed by the next pull that re-writes the deserialize approach. If we wanted to fix it today, it could be done by passing the mutable tx as `const&` (at the cost of a copy). An alternative to fix it today would be to implement a simple/stricter version of use-after-move that detects use after moved fields (partial moves) or partial uses after full moves.\n\nAlso, if we wanted to implement an arena, it could be done today, without changing code outside of `src/primitives`, with something like:\n\nEncapsulate only the heap\n\n```cpp\n#include <iostream>\n#include <memory>\n#include <span>\n#include <vector>\n#include <cstdint>\n\nnamespace detail {\nstruct Body {\n    std::vector<int> m_vin{};\n    std::vector<int> m_vout{};\n};\n} // namespace detail\n\nstruct MutTx {\n    uint32_t version{1};\n    std::vector<int> vin{};\n    std::vector<int> vout{};\n};\n\nstruct Tx {\nprivate:\n    const std::shared_ptr<const detail::Body> m_body;\n\npublic:\n    const uint32_t version;\n\n    // Initialized from not-null m_body. (Note: Span-serialize is incompatible with vector-serialize, so this would need to be fixed somehow)\n    const std::span<const int> vin{m_body->m_vin};\n    const std::span<const int> vout{m_body->m_vout};\n\n    // Deep-copy to decouple heap buffers and guarantee true immutability, alternatively implement a stronger use-after-move static analysis.\n    explicit Tx(const MutTx& tx)\n        : m_body(std::make_shared<const detail::Body>(\n              detail::Body{tx.vin, tx.vout})),\n          version{tx.version} {}\n\n    // Default copy/move constructors work out of the box\n    Tx(const Tx&) = default;\n    Tx(Tx&&) noexcept = default;\n\n    // Wasn't present before, keep deleted\n    Tx& operator=(const Tx&) = delete;\n    Tx& operator=(Tx&&) = delete;\n};\n```\n\n(No strong opinion, just wanted to drop this off here)"
  },
  {
   "t": "2026-08-20T08:39:22Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nCorrect. My claim wasn't that this is fixed by this pull, rather the direction this pull moves us in should make it easier for us to fix this bug. I kept riffing on that idea here https://github.com/bitcoin/bitcoin/issues/35904#issuecomment-5331901823 and came up with something that composes well with the deserialisation approach proposed by @purpleKarrot (still working out some rough edges) and doesn't sacrifice performance (via copies) where it matters, e.g., block validation and reading transactions off the wire."
  },
  {
   "t": "2026-08-20T09:54:19Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI don't want to understate the benefits of the other follow ups proposed by the author, but I am strongly in favour of what you mention here, and it is my primary interest in better encapsulation for our primitive types. This idea has been gnawing at me since Research Week a few years back where, if you recall, myself and a few others proposed an idea of completely getting rid of transactions and thinking only in terms of blocks and block chunks.\n\nObviously, we still need a transaction API for this codebase, but encapsulating the concrete layout allows us to have a single transaction type that works with an arena that is allocated for an _entire block_ and also for a single transaction off the wire (perhaps even a cluster, but I haven't thought about this deeply yet):\n\nTransactions as views over a block backed arena\n\n```cpp\n// the arena owns the bytes once; everything else is an offset into them. nothing\n// inside a transaction is separately allocated, so there is no caller supplied container\n// to allow for the nasty modify-through-reference-after-move!\nstruct tx_meta {\n    std::uint32_t byte_off, byte_len, wit_off, wit_count;\n};\n\nstruct wit_meta {\n    std::uint32_t off, len;\n};\n\n// read off the wire / disk / etc once, and then only need one allocation per\n// column. this example is simplified but in total its something like 6 allocations\nstruct arena {\n    std::vector<std::byte> bytes;\n    std::vector<tx_meta> tx;\n    std::vector<wit_meta> wit;\n};\n\nclass transaction;\nauto parse_transaction(std::span<std::byte const> wire) -> transaction;\n\n// shared handle and an index. thats the whole type and a copy is a refcount bump\nclass transaction {\n  public:\n    transaction(std::shared_ptr<arena const> a, std::uint32_t i)\n        : _arena{std::move(a)}, _index{i} {}\n\n    // all observers resolve through the same meta, no\n    // \"which backing am I\" branch\n    auto witness_count() const -> std::uint32_t { return meta().wit_count; }\n    auto witness_item(std::uint32_t k) const -> std::span<std::byte const> {\n        auto const& w = _arena->wit[meta().wit_off + k];\n        return std::span{_arena->bytes}.subspan(w.off, w.len);\n    }\n    auto serialized() const -> std::span<std::byte const> {\n        return std::span{_arena->bytes}.subspan(meta().byte_off, meta().byte_len);\n    }\n\n    // rehoming out of a block arena is just parsing your own bytes\n    auto compact() const -> transaction { return parse_transaction(serialized()); }\n\n  private:\n    auto meta() const -> tx_meta const& { return _arena->tx[_index]; }\n    std::shared_ptr<arena const> _arena;\n    std::uint32_t _index{};\n};\n\nauto build(std::span<std::byte const> wire, std::uint32_t n_tx) -> std::shared_ptr<arena const> {\n    auto const shape = count_shape(wire, n_tx);\n    auto a = std::make_shared<arena>();\n    a->bytes.assign(wire.begin(), wire.end());\n    a->tx.resize(shape.txs);\n    a->wit.resize(shape.wit_items);\n    fill(*a, wire, n_tx); // indexed writes only\n    return a;\n}\n\n// only difference between the two representations is n_tx!\nauto parse_block(std::span<std::byte const> wire) -> std::vector<transaction> {\n    auto const n_tx = std::to_integer<std::uint32_t>(wire[0]);\n    auto const a = build(wire.subspan(1), n_tx);\n    std::vector<transaction> out;\n    out.reserve(n_tx);\n    for (std::uint32_t i = 0; i < n_tx; ++i) {\n        out.emplace_back(a, i);\n    }\n    return out;\n}\n\nauto parse_transaction(std::span<std::byte const> wire) -> transaction {\n    return transaction{build(wire, 1), 0};\n}\n```\n\nThis is a code golf'd version of a proof of concept I've been working on to benchmark, and to illustrate the idea. Needs more refinement, but the early benchmark numbers I've been getting from toy code is ~15x improvement on block parsing. I also strongly suspect block validation itself can be improved by >10% because of how much easier bytes can be slurped into hashes, signatures validated, arrays can be traversed etc etc, and as a long term goal having a block representation in memory that is set up for batch validation. Said differently, having proper encapsulation allows us to maintain a stable API for testing and use, while having the freedom to adapt the concrete representation of the data over time to adapt to the needs of the workflow.\n\nIts also worth point out how it reduces opportunities for error: ask the OS 250k times for allocations, you have 250k opportunites for error. Ask 6 times, you have 6 opportunities for error."
  },
  {
   "t": "2026-08-20T12:07:28Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/primitives/transaction.h",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "in_reply_to": 3516547054,
   "text": "Yeah, seems fine to leave all of them const? The benefit is that initialization is enforced by the compiler and assignment-ctor is deleted anyway?"
  },
  {
   "t": "2026-08-21T03:50:37Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "2a700a6723b9f54c0cfac64b594c5ec175bf833d"
  },
  {
   "t": "2026-08-23T05:47:16Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYeah, I had `operator=` confused with `operator+` etc for no good reason. Surprisingly, when I'm annoyed I don't think as clearly as I might at other times; glad to see your response to that is to try annoy me further, rather than, say, trying to reconstruct whatever part of my claim might have been valid, and finding a way to find some actual common ground.\n\nIn any event, even though that example was wrong, the point it was trying to support (\"not providing mutating member functions is no guarantee of immutability\") is not:\n\n```c++\n// A type that does not provide any mutating member functions\nclass GodThisIsBoring\n{\nprivate:\n    int i;\npublic:\n    GodThisIsBoring(int j) : i{j} { }\n    friend const GodThisIsBoring& operator+(GodThisIsBoring& a, GodThisIsBoring& b);\n    int get() const { return i; }\n};\n\nconst GodThisIsBoring& operator+(GodThisIsBoring& a, GodThisIsBoring& b)\n{\n    a.i += b.i;\n    return a;\n}\n\nvoid foo() {\n    GodThisIsBoring b1{5}, b2{6};\n    std::cout << b1.get() << \" \" << b2.get() << std::endl;\n    b1 + b2;\n    std::cout << b1.get() << \" \" << b2.get() << std::endl;\n}\n```\n\nThe \"edited\" above elided this comment, which I assume remains your opinion, and seems to me to warrant a response:\n\n[quoted text omitted]\nEncapsulation alone simply buys code churn and inefficiency; hence the 93-files-touched and the \"you need to look at the blog posts and followup PRs to see why this is cool\" advocacy. Except for sipa's recent suggestion, I don't think any of the followups actually have significant benefit from encapsulation. The original, now long distant, justification for this PR seemed to be making the object non-const so that some (de)serialization changes could be made, . I'm very skeptical that those changes are necessary of even very useful for those changes, and I think that reducing the const-ness of CTransaction is unnecessarily dangerous.\n\nMy impression is you've been assuming that I'm \"sabotaging and trolling\" since [my first comments on this PR](https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4762580192). While I'm sure you're predisposed to disbelieve me, I'm not attempting to troll or sabotage; at CoreDev Oct 2025, we identified \"PRs don't get a response; it would be better to have early negative feedback than nothing\" as a potential improvement, so I've been trying to do that since then, at least in areas I care a little about. It has certainly made it clear to me why \"no response\" is preferable from a reviewer's perspective.\n\nIf your reaction to criticism is both to reject it and ridicule the critic, rather than try to make absolutely sure there is no substance to it, I don't think you have any business touching consensus critical code. Beyond that, that you've changed the PR to somewhat avoid the criticism while still denying its validity comes across as deceptive to me; my expectation is you're deferring the changes you actually want, hoping to game the review process rather than address the criticism directly.\n\njosi:\n\n[quoted text omitted]\nI gave such an example here https://github.com/bitcoin/bitcoin/pull/35569#issuecomment-4765667849\n\nThe current version of the PR doesn't provide any mutating functions on CTransaction, as can be seen by re-adding the \"const\" modifier to all its member variables and recompiling. See also https://github.com/bitcoin/bitcoin/pull/35569/changes#r3516547054 That comment hasn't been replied to in 50 days, despite being listed as a key property of the PR in the description (\"after which the data members are made private and non-const.\"). My assumption is that the functions that will mutate those members have been deferred to a future PR.\n\n[quoted text omitted]\nThe type that we have as a shared pointer to transaction data is called `CTransactionRef`, not `CTransaction`.\n\n[quoted text omitted]\nYes, and you can achieve that result much more simply without any assistance from the constructor with `const_cast` even with the best attempts at making the type immutable:\n\n```c++\nstruct T\n{\n    const int i;\n};\nusing TRef = std::shared_ptr<T>;\n\nvoid bar() {\n    TRef myt = std::make_shared<T>(3);\n    std::cout << myt->i << std::endl;\n    *const_cast<int*>(&myt->i) = 99;\n    std::cout << myt->i << std::endl;\n}\n```\n\nThe only answer C++ has for that is things like \"don't use const_cast\" and \"don't create confusing aliases\" and \"don't write buggy code\". The point of defensive coding, like having CTransaction objects be immutable is to make code that does weird things either fail to compile or at least look strange (`*const_cast<...>(...)`) so it at least takes deliberate effort to introduce bugs, not, sadly, to make them entirely impossible."
  },
  {
   "t": "2026-08-23T06:19:01Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis is my objection as well, treating everyone else like idiots doesn't seem like a winning strategy.\n\n[quoted text omitted]\n+1\n\n@purpleKarrot, what is your goal here - to make Bitcoin and Core better in any way you can, or to improve your blog? To learn from others, or to force your opinion and act like a CTO? You haven't contributed enough for me to tell whether you're here because you care or because Bitcoin would look good on your CV. Nothing personal, but Bitcoin is about PoW - do the work before the announcements.\n\nWe need more experts in this project, but so far you haven't demonstrated that: you're not learning, you're not reacting, you're not actually building. Try reviewing more PRs, be helpful, be less entitled - before you start rearchitecting everything you personally disagree with. I understand the drive, I have the same impulse, others have helped me align with the project. We don't need heroes here, we need team players."
  },
  {
   "t": "2026-08-23T09:39:45Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "```cpp\nfriend const GodThisIsBoring& operator+(GodThisIsBoring& a, GodThisIsBoring& b);\n```\n\nThat is a mutating function provided by the class. `friend` functions have the same access rights as member functions. Yes, you have proven that it is possible to provide a mutating function that is not a member function, but that is not an argument against encapsulation. It still stands that an object of a type that is designed for immutability by not providing mutating functions cannot have its value mutated from outside (except with `const_cast` which is also not an argument against encapsulation).\n\nZero-overhead encapsulation is the whole reason why C++ was invented in the first place. It allows changing the implementation of a class without requiring changes in the clients and has no runtime cost. Sure, fixing a class that leaks implementation details requires changing all clients. But that is a one-time change. Once encapsulation is established, then changing the implementation in a way described by sipa becomes possible without affecting the clients.\n\nI still haven't seen a single technical argument against encapsulation. You two are trying to attack me on a non-technical level for reasons that I don't follow."
  },
  {
   "t": "2026-08-23T16:27:52Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nLet me invert that: it's your job to sell your idea, not our job to attack it. The change might make sense conceptually, and I even provided a full end-to-end alternative (which you also ignored), but you arrogantly dismiss most review comments because you think you always know better. Please read our replies - most of our concerns are about you not listening to feedback and acting entitled and offended. This change affects all of us, it's not just your decision to make.\n\nShow us the full vision (in code, not blog posts) so we can consider it ourselves.\nAnd when you get pushback, work with the people instead of trying to shame them into obedience."
  },
  {
   "t": "2026-08-23T18:52:33Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Reminder for everybody here to keep the comments on topic. If you wish to offer meta-commentary, take it to the meta repository https://github.com/bitcoin-core/meta (and maybe take a breather and read through the guidelines there).\n\n[quoted text omitted]\nI think the changes here introduce some interesting concepts. Interrogating them for their perceived and actual capabilities is part of the review process, especially if they revolve around concepts that have not been widely perceived by contributors to Bitcoin Core as problems to be solved with priority. I'm happy that this is getting attention and review in the first place and is not facing stonewalled silence."
  },
  {
   "t": "2026-08-24T06:39:05Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "I'm sorry this discussion has gotten away from the technical merit. I commit to getting back on track and hope others will too.\n\nI have also raised two issues in https://github.com/bitcoin-core/meta."
  },
  {
   "t": "2026-08-24T10:59:38Z",
   "kind": "force_push",
   "who": "purpleKarrot",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06"
  },
  {
   "t": "2026-08-24T16:09:31Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis is the \"we have a value type at home\" argument, except whats at home is a generic CRef, i.e. `std::shared_ptr<const CTransaction>`. Its nullable, needs `.get()` to open and has never once enforced \"this is a transaction\" as an invariant. And, I'm not just complaining about style or ergonomics: `CBlock::vtx` is `std::vector<CTransactionRef>`, so a block can represent a null transaction slot. This is a state the protocol has no meaning for at all, but the type allows it anyway. Sounds footgunny!\n\nThe reason its nullable in the first place is that deserialisation fills in a declared, possibly empty handle. Your own example needs `foo` to exist as an empty `shared_ptr` before the read succeeds. Make deserialisation an explicit construct-or-fail step (as demonstrated in those pesky blogs) and theres no moment left where the type needs to represent \"empty.\" Thats what lets the proposed version of `CTransaction` enforce \"always valid\" the way it already enforces its other invariants, and why `CTransactionRef` call sites would become `CTransaction` and be able to drop an unnecessary null check."
  },
  {
   "t": "2026-08-24T16:12:38Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "reACK https://github.com/bitcoin/bitcoin/commit/402500baad826efff3dbe52c75694d93121e1b06"
  },
  {
   "t": "2026-08-24T20:24:11Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "I went through the discussion and collected the questions that appeared to require a decision or clarification. The current resolutions are:\n\n* **Why use trailing-return syntax for the accessors?**\n  The motivation is readability: using `auto ... -> Type` aligns the function names, making the API easier to scan. This is a stylistic choice for this API; it is not intended to imply that all existing code should be rewritten to use trailing-return syntax.\n\n* **Why not name the accessors after the protocol fields (`version()`, `vin()`, `vout()`, `nLockTime()`)?**\n  C++ does not allow a member function and a data member to have the same name. Renaming the data members  would prevent the desired 3-step migration. The `Get*` names allow the new API to be introduced without changing the representation. Most importantly, the naming scheme is consistent with the Symbol naming conventions documented in the developer notes.\n\n* **Why add the same accessors to `CMutableTransaction`?**\n  The accessors are needed because generic read-only code is currently instantiated with both transaction types. Providing the same read-only interface keeps that code generic without introducing an abstraction or additional conversion at serialization boundaries and it will simplify making the code concrete once it is only ever instantiated with a single transaction type.\n\n* **Should the read-only code instead use an immutable transaction view?**\n  No. This would add yet another transaction type, resulting in a total of four: `CTransaction`, `CMutableTransaction`, `CTransactionRef`, and `CImmutableTransactionView`. Instead, the goal is to reduce the number of transaction types to one: `CTransaction`. Once `CTransaction` has regular value semantics, `CTransactionRef` will be obsolete. Once all procedural transaction building code is written in a declarative style, `CMutableTransaction` will be obsolete. Generic code that is parameterized on the transaction type will then become concrete.\n\n* **Should indexed `vin(index)` / `vout(index)` helpers be added?**\n  No. They would preserve the existing index-based access patterns rather than encouraging migration toward ranges and standard algorithms. Where only the first element is needed, `.front()` is preferred.\n\n* **Should `LIFETIMEBOUND` be added to accessors returning references?**\n  Yes. This has been done for `GetInputs()` and `GetOutputs()`.\n\n* **Why remove `const` from the data members if they are not going to be modified?**\n  To align with common practice. In C++20, it is discouraged to have `const` data members, because they make the type's semantics hard to understand. [WG21 P3812](https://wg21.link/p3812) discusses this limitation and may change this practice in a future version of the language.\n  This does not imply any plan to introduce mutation of individual transaction fields.\n\n* **Why disable assignment operations?**\n  `CTransaction` is currently passed around as `CTransactionRef` (aka `std::shared_ptr<const CTransaction>`). This prevents mutation through that particular handle, but it does **not** guarantee that there is no mutable alias to the same `CTransaction` object elsewhere. Such an alias could modify the object behind the `const` shared pointer, causing spooky action at a distance. The longer-term goal is to make `CTransaction` a proper value type with immutable underlying state and then use it to replace all occurrences of `CTransactionRef`, at which point this concern goes away.\n\n* **Should the new `bitcoin-tidy` check remain after the migration?**\n  Yes. The check has been generalized so that it does not hard-code the particular symbols involved in this migration, making it useful beyond this one conversion.\n\n* **What coding style should the new `bitcoin-tidy` code follow?**\n  No strong preference. Following LLVM's style is has an advantage should the code eventually be upstreamed to clang-tidy.\n\n* **Show us the full vision (in code, not blog posts)**\n  The reference implementation for [`std::bitcoin`](https://github.com/purpleKarrot/std-bitcoin) has it all. [Here](https://github.com/purpleKarrot/std-bitcoin/blob/master/module/bitcoin.vocabulary.cpp#L125-L187) is how a transaction may look like as an immutable value type with a remote body and a never-empty guarantee. [Here](https://github.com/purpleKarrot/std-bitcoin/blob/043d24ad4adfe0d98506d4d117a9a16fd0ea5fb7/src/serdes_decode.cpp#L220-L243) is how deserialization is completely orthogonal, does not require access to the private data members and does not require transactions to be held by a pointer.\n\nOverall, the remaining questions are therefore either **resolved by the current implementation** or concern **larger architectural cleanups that are intentionally outside the scope of this PR**."
  },
  {
   "t": "2026-09-09T08:07:11Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "contrib/devtools/bitcoin-tidy/use-observers.cpp",
   "commit": "f5a5b041984477110ab041f13cf9af0e6d3c78cb",
   "in_reply_to": null,
   "text": "nit: missing the MIT header - same for the header file."
  },
  {
   "t": "2026-09-09T08:10:54Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "contrib/devtools/bitcoin-tidy/use-observers.cpp",
   "commit": "f5a5b041984477110ab041f13cf9af0e6d3c78cb",
   "in_reply_to": null,
   "text": "Might be valuable to document the check's boundaries to reduce surprises like `CalculateOutputValue` later on."
  },
  {
   "t": "2026-09-09T08:32:26Z",
   "kind": "review_comment",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "path": "contrib/devtools/bitcoin-tidy/use-observers.cpp",
   "commit": "f5a5b041984477110ab041f13cf9af0e6d3c78cb",
   "in_reply_to": null,
   "text": "Does this fire in the current tree?\n\nLeft over from annotating something else, or anticipating a future use of the check?"
  },
  {
   "t": "2026-09-09T09:31:18Z",
   "kind": "review",
   "who": "alexanderwiederin",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "402500baad826efff3dbe52c75694d93121e1b06",
   "text": "ACK 402500baad826efff3dbe52c75694d93121e1b06\n\nReproduced the fixup commit. Built 79319eb with clang 22.1.8 (`-DBUILD_GUI=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DBUILD_KERNEL_LIB=ON -DBUILD_UTIL_CHAINSTATE=ON`) and ran the check per the commit message.\n\nResult: 86 files, 485 insertions, 485 deletions, and git diff 6b99660 empty.\n\nAlso compared object files across the fixup commit: 53 of the 86 modified files compile to byte-identical object files, the rest differ only in register allocation and assertion-string lengths.\n\nUnit and Functional tests green"
  }
 ],
 "labels_log": [
  {
   "t": "2026-06-19T17:07:39Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-21T17:40:42Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-22T13:06:41Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-22T14:46:55Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-22T15:06:53Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-22T16:44:21Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-26T15:19:45Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-26T16:25:26Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-09T02:42:36Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-09T13:41:44Z",
   "action": "labeled",
   "label": "Refactoring",
   "who": "willcl-ark"
  },
  {
   "t": "2026-07-09T13:41:44Z",
   "action": "labeled",
   "label": "Validation",
   "who": "willcl-ark"
  },
  {
   "t": "2026-07-27T15:04:57Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-27T16:14:03Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-03T16:22:08Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-04T22:55:48Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-06T09:19:59Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-14T17:51:37Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-14T22:29:28Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-20T21:35:35Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-21T03:59:53Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-24T09:21:24Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-24T11:31:19Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-06-20T03:55:12Z",
   "kind": "ready_for_review",
   "who": "purpleKarrot"
  },
  {
   "t": "2026-06-22T12:41:35Z",
   "kind": "renamed",
   "who": "purpleKarrot",
   "from": "Making CTransaction a Regular Type",
   "to": "Encapsulation for CTransaction"
  },
  {
   "t": "2026-08-03T13:49:01Z",
   "kind": "convert_to_draft",
   "who": "purpleKarrot"
  },
  {
   "t": "2026-08-03T16:11:18Z",
   "kind": "ready_for_review",
   "who": "purpleKarrot"
  }
 ],
 "text_chars": 96298,
 "text_tokens_estimate": 24074,
 "changed_paths": [
  "contrib/devtools/bitcoin-tidy/CMakeLists.txt",
  "contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp",
  "contrib/devtools/bitcoin-tidy/use-observers.cpp",
  "contrib/devtools/bitcoin-tidy/use-observers.h",
  "src/attributes.h",
  "src/bench/blockencodings.cpp",
  "src/bench/coin_selection.cpp",
  "src/bench/mempool_ephemeral_spends.cpp",
  "src/bench/mempool_stress.cpp",
  "src/bench/txorphanage.cpp",
  "src/blockfilter.cpp",
  "src/coins.cpp",
  "src/common/bloom.cpp",
  "src/consensus/tx_check.cpp",
  "src/consensus/tx_verify.cpp",
  "src/consensus/validation.h",
  "src/core_io.cpp",
  "src/core_memusage.h",
  "src/index/coinstatsindex.cpp",
  "src/index/txospenderindex.cpp",
  "src/kernel/bitcoinkernel.cpp",
  "src/node/miner.cpp",
  "src/node/mini_miner.cpp",
  "src/node/psbt.cpp",
  "src/node/transaction.cpp",
  "src/node/txdownloadman_impl.cpp",
  "src/node/txorphanage.cpp",
  "src/policy/ephemeral_policy.cpp",
  "src/policy/packages.cpp",
  "src/policy/policy.cpp",
  "src/policy/policy.h",
  "src/policy/truc_policy.cpp",
  "src/policy/truc_policy.h",
  "src/primitives/transaction.cpp",
  "src/primitives/transaction.h",
  "src/psbt.cpp",
  "src/psbt.h",
  "src/qt/transactiondesc.cpp",
  "src/qt/transactionrecord.cpp",
  "src/qt/walletmodeltransaction.cpp",
  "src/rpc/blockchain.cpp",
  "src/rpc/mempool.cpp",
  "src/rpc/mining.cpp",
  "src/rpc/rawtransaction.cpp",
  "src/script/interpreter.cpp",
  "src/signet.cpp",
  "src/test/coins_tests.cpp",
  "src/test/coinsviewoverlay_tests.cpp",
  "src/test/fuzz/cmpctblock.cpp",
  "src/test/fuzz/coins_view.cpp",
  "src/test/fuzz/mini_miner.cpp",
  "src/test/fuzz/package_eval.cpp",
  "src/test/fuzz/script_flags.cpp",
  "src/test/fuzz/script_interpreter.cpp",
  "src/test/fuzz/script_sign.cpp",
  "src/test/fuzz/tx_pool.cpp",
  "src/test/fuzz/txdownloadman.cpp",
  "src/test/fuzz/txorphan.cpp",
  "src/test/fuzz/util.cpp",
  "src/test/fuzz/utxo_snapshot.cpp",
  "src/test/fuzz/utxo_total_supply.cpp",
  "src/test/miner_tests.cpp",
  "src/test/miniminer_tests.cpp",
  "src/test/script_assets_tests.cpp",
  "src/test/script_tests.cpp",
  "src/test/sighash_tests.cpp",
  "src/test/sigopcount_tests.cpp",
  "src/test/transaction_tests.cpp",
  "src/test/txospenderindex_tests.cpp",
  "src/test/txvalidation_tests.cpp",
  "src/test/txvalidationcache_tests.cpp",
  "src/test/util/setup_common.cpp",
  "src/test/util/transaction_utils.cpp",
  "src/test/util/txmempool.cpp",
  "src/test/validation_block_tests.cpp",
  "src/test/validation_tests.cpp",
  "src/txmempool.cpp",
  "src/util/rbf.cpp",
  "src/validation.cpp",
  "src/wallet/feebumper.cpp",
  "src/wallet/interfaces.cpp",
  "src/wallet/receive.cpp",
  "src/wallet/rpc/coins.cpp",
  "src/wallet/rpc/spend.cpp",
  "src/wallet/rpc/transactions.cpp",
  "src/wallet/scriptpubkeyman.cpp",
  "src/wallet/spend.cpp",
  "src/wallet/test/coinselector_tests.cpp",
  "src/wallet/test/group_outputs_tests.cpp",
  "src/wallet/test/spend_tests.cpp",
  "src/wallet/test/wallet_tests.cpp",
  "src/wallet/transaction.h",
  "src/wallet/wallet.cpp"
 ],
 "files": [
  {
   "path": "contrib/devtools/bitcoin-tidy/CMakeLists.txt",
   "add": 7,
   "del": 1
  },
  {
   "path": "contrib/devtools/bitcoin-tidy/bitcoin-tidy.cpp",
   "add": 2,
   "del": 0
  },
  {
   "path": "contrib/devtools/bitcoin-tidy/use-observers.cpp",
   "add": 55,
   "del": 0
  },
  {
   "path": "contrib/devtools/bitcoin-tidy/use-observers.h",
   "add": 13,
   "del": 0
  },
  {
   "path": "src/attributes.h",
   "add": 9,
   "del": 0
  },
  {
   "path": "src/bench/blockencodings.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/bench/coin_selection.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/bench/mempool_ephemeral_spends.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/bench/mempool_stress.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/bench/txorphanage.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/blockfilter.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/coins.cpp",
   "add": 5,
   "del": 5
  },
  {
   "path": "src/common/bloom.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/consensus/tx_check.cpp",
   "add": 6,
   "del": 6
  },
  {
   "path": "src/consensus/tx_verify.cpp",
   "add": 17,
   "del": 17
  },
  {
   "path": "src/consensus/validation.h",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/core_io.cpp",
   "add": 11,
   "del": 11
  },
  {
   "path": "src/core_memusage.h",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/index/coinstatsindex.cpp",
   "add": 6,
   "del": 6
  },
  {
   "path": "src/index/txospenderindex.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/kernel/bitcoinkernel.cpp",
   "add": 11,
   "del": 11
  },
  {
   "path": "src/node/miner.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/node/mini_miner.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/node/psbt.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/node/transaction.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/node/txdownloadman_impl.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/node/txorphanage.cpp",
   "add": 7,
   "del": 7
  },
  {
   "path": "src/policy/ephemeral_policy.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/policy/packages.cpp",
   "add": 6,
   "del": 6
  },
  {
   "path": "src/policy/policy.cpp",
   "add": 18,
   "del": 18
  },
  {
   "path": "src/policy/policy.h",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/policy/truc_policy.cpp",
   "add": 12,
   "del": 12
  },
  {
   "path": "src/policy/truc_policy.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/primitives/transaction.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/primitives/transaction.h",
   "add": 39,
   "del": 28
  },
  {
   "path": "src/psbt.cpp",
   "add": 6,
   "del": 6
  },
  {
   "path": "src/psbt.h",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/qt/transactiondesc.cpp",
   "add": 7,
   "del": 7
  },
  {
   "path": "src/qt/transactionrecord.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/qt/walletmodeltransaction.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/rpc/blockchain.cpp",
   "add": 18,
   "del": 18
  },
  {
   "path": "src/rpc/mempool.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/rpc/mining.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/rpc/rawtransaction.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/script/interpreter.cpp",
   "add": 34,
   "del": 34
  },
  {
   "path": "src/signet.cpp",
   "add": 5,
   "del": 5
  },
  {
   "path": "src/test/coins_tests.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/coinsviewoverlay_tests.cpp",
   "add": 9,
   "del": 9
  },
  {
   "path": "src/test/fuzz/cmpctblock.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/fuzz/coins_view.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/fuzz/mini_miner.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/fuzz/package_eval.cpp",
   "add": 12,
   "del": 12
  },
  {
   "path": "src/test/fuzz/script_flags.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/test/fuzz/script_interpreter.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/fuzz/script_sign.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/fuzz/tx_pool.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/fuzz/txdownloadman.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/fuzz/txorphan.cpp",
   "add": 12,
   "del": 12
  },
  {
   "path": "src/test/fuzz/util.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/fuzz/utxo_snapshot.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/fuzz/utxo_total_supply.cpp",
   "add": 6,
   "del": 6
  },
  {
   "path": "src/test/miner_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/miniminer_tests.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/script_assets_tests.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/script_tests.cpp",
   "add": 17,
   "del": 17
  },
  {
   "path": "src/test/sighash_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/sigopcount_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/transaction_tests.cpp",
   "add": 11,
   "del": 11
  },
  {
   "path": "src/test/txospenderindex_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/txvalidation_tests.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/txvalidationcache_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/util/setup_common.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/util/transaction_utils.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/util/txmempool.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/test/validation_block_tests.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/validation_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/txmempool.cpp",
   "add": 18,
   "del": 18
  },
  {
   "path": "src/util/rbf.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/validation.cpp",
   "add": 38,
   "del": 38
  },
  {
   "path": "src/wallet/feebumper.cpp",
   "add": 8,
   "del": 8
  },
  {
   "path": "src/wallet/interfaces.cpp",
   "add": 8,
   "del": 8
  },
  {
   "path": "src/wallet/receive.cpp",
   "add": 15,
   "del": 15
  },
  {
   "path": "src/wallet/rpc/coins.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/rpc/spend.cpp",
   "add": 5,
   "del": 5
  },
  {
   "path": "src/wallet/rpc/transactions.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/scriptpubkeyman.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/spend.cpp",
   "add": 19,
   "del": 19
  },
  {
   "path": "src/wallet/test/coinselector_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/test/group_outputs_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/test/spend_tests.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/test/wallet_tests.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/wallet/transaction.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/wallet.cpp",
   "add": 33,
   "del": 33
  }
 ],
 "test_lines": 330,
 "git": {
  "head": "402500baad826efff3dbe52c75694d93121e1b06",
  "head_matches_backup": true,
  "base": "994c17d6c0a1453a1d7cc44ee2bbc49afa2d1155",
  "commits": [
   {
    "sha": "f5a5b04198",
    "subject": "bitcoin-tidy: Add use-observers check",
    "files": 4,
    "add": 77,
    "del": 1
   },
   {
    "sha": "0d6480b1f2",
    "subject": "attributes: Add USE_OBSERVER annotation helper",
    "files": 1,
    "add": 9,
    "del": 0
   },
   {
    "sha": "ff4aed823a",
    "subject": "CTransaction: Add observer member functions",
    "files": 1,
    "add": 10,
    "del": 0
   },
   {
    "sha": "79319eb9e0",
    "subject": "CTransaction: Annotate data members with their corresponding observer",
    "files": 1,
    "add": 4,
    "del": 4
   },
   {
    "sha": "6b99660bee",
    "subject": "bitcoin-tidy: Apply use-observers fixup",
    "files": 86,
    "add": 485,
    "del": 485
   },
   {
    "sha": "0c83541147",
    "subject": "policy: Use explicit version type",
    "files": 4,
    "add": 6,
    "del": 6
   },
   {
    "sha": "402500baad",
    "subject": "CTransaction: Make data members private",
    "files": 1,
    "add": 23,
    "del": 22
   }
  ],
  "patch_truncated": true
 },
 "input_hash": "303fa7f33eb49974",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}