Given that has_witness is only used for templating, what do you think about this approach? I think it’s a bit more straightforward, and as a probably not-very-useful benefit, it also allows for adding more txid types when necessary.
 0diff --git a/src/util/transaction_identifier.h b/src/util/transaction_identifier.h
 1index b5c0f25093..a845c5c0f9 100644
 2--- a/src/util/transaction_identifier.h
 3+++ b/src/util/transaction_identifier.h
 4@@ -14,7 +14,7 @@
 5  * transaction_identifier to uint256 (e.g. by making the base class private).
 6  * This should be easy to do once most of the code base has converted to the
 7  * Txid and Wtxid types to avoid churn. */
 8-template <bool has_witness>
 9+template <typename Derived>
10 class transaction_identifier : public uint256
11 {
12 public:
13@@ -24,8 +24,7 @@ public:
14     transaction_identifier(const Other& other)
15         : uint256{other}
16     {
17-        static_assert(std::is_same_v<Other, transaction_identifier<has_witness>>,
18-                      "Forbidden copy type");
19+        static_assert(std::is_same_v<Other, Derived>, "Forbidden copy type");
20     }
21 
22     // Allow comparison with the same transaction_identifier type
23@@ -39,7 +38,7 @@ public:
24     constexpr int Compare(const Other& other) const
25     {
26         if constexpr (std::is_same_v<Other, uint256> || // TODO forbid uint256 comparisons
27-                      std::is_same_v<Other, transaction_identifier<has_witness>>) {
28+                      std::is_same_v<Other, Derived>) {
29             return reinterpret_cast<const uint256&>(*this).Compare(other);
30         } else {
31             static_assert(ALWAYS_FALSE<Other>, "Forbidden comparison type");
32@@ -49,12 +48,12 @@ public:
33     uint256 Uint256() const { return reinterpret_cast<const uint256&>(*this); }
34 
35     template <typename Other>
36-    static transaction_identifier FromUint256(const Other& other)
37+    static Derived FromUint256(const Other& other)
38     {
39         // TODO this does not need to be a template function after we disallow
40-        // `uint256 a = transaction_identifier<has_witness>{};`
41+        // `uint256 a = Derived{};`
42         if constexpr (std::is_same_v<Other, uint256>) {
43-            return reinterpret_cast<const transaction_identifier&>(other);
44+            return reinterpret_cast<const Derived&>(other);
45         } else {
46             static_assert(ALWAYS_FALSE<Other>, "FromUint256 only allows uint256");
47         }
48@@ -62,8 +61,8 @@ public:
49 };
50 
51 /** Txid commits to all transaction fields except the witness. */
52-using Txid = transaction_identifier<false>;
53+class Txid : public transaction_identifier<Txid> {};
54 /** Wtxid commits to all transaction fields including the witness. */
55-using Wtxid = transaction_identifier<true>;
56+class Wtxid : public transaction_identifier<Wtxid> {};
57 
58 #endif // BITCOIN_UTIL_TRANSACTION_IDENTIFIER_H