1 // Copyright (c) 2025 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #ifndef LIMENKA_CONSENSUS_CT_H
6 #define LIMENKA_CONSENSUS_CT_H
7
8 #include <hash.h>
9 #include <primitives/transaction.h>
10 #include <span.h>
11 #include <uint256.h>
12
13 #include <cstddef>
14 #include <cstdint>
15 #include <optional>
16 #include <string>
17
18 // Confidential transaction (P2BPCT) kernel output magic: OP_RETURN <"BK">.
19 static constexpr uint8_t CT_KERNEL_MAGIC_BYTE0 = 0x42; // 'B'
20 static constexpr uint8_t CT_KERNEL_MAGIC_BYTE1 = 0x4B; // 'K'
21
22 // Marker for "no kernel output found in this transaction".
23 static constexpr int NO_CT_KERNEL_OUTPUT = -1;
24
25 // Kernel signature size (BIP340 Schnorr) and fee field size.
26 // The fee is a 128-bit value in the same units as the committed amounts.
27 // Stealth fields: a 1-byte marker, a 33-byte ephemeral point E, and two
28 // 32-byte encrypted fields (amount to the view key, blinding to the spend
29 // key).
30 static constexpr size_t CT_KERNEL_SIG_SIZE = 64;
31 static constexpr size_t CT_FEE_SIZE = 16;
32 static constexpr size_t CT_STEALTH_EPHEM_SIZE = 33;
33 static constexpr uint8_t CT_STEALTH_MARKER = 0x53; // 'S'
34 static constexpr size_t CT_STEALTH_ENC_SIZE = 32;
35
36 // Kernel output format:
37 // OP_RETURN <"BK"> <fee:16 little-endian> <sig:64>
38 // [<"S"> <E:33> <enc_amount:32> <enc_blind:32>]
39 //
40 // Nothing else may follow: the kernel format is closed. The balance
41 // equation is a plain Pedersen point sum - there are NO offset terms.
42 // (A previous design carried "blinding offsets" as arbitrary points;
43 // that allowed an attacker to include b*H points for chosen b and mint
44 // arbitrary value, since consensus cannot distinguish a G-only point
45 // from an H-containing one. Offsets are removed entirely.)
46 //
47 // Stealth payments (receiver publishes a static (view, spend) address):
48 // The sender chooses the blinding r itself (so it can sign the kernel
49 // excess that covers the output - no receiver interaction, no offset
50 // points), then encrypts the amount to the view key and the blinding
51 // to the spend key:
52 // E = t*G (ephemeral, shared secret basis)
53 // enc_amount= v XOR H("CTStealthAmount" || t*V_view)
54 // enc_blind = r XOR H("CTStealthBlind" || t*V_spend)
55 // The view key alone can scan (decrypt v); only the spend key can
56 // recover the blinding. The commitment C = r*G + v*H is an ordinary
57 // confidential output bound by the balance equation.
58 struct CTKernelData {
59 CAmount fee{0};
60 std::vector<uint8_t> sig; // 64 bytes
61 bool has_stealth{false};
62 std::vector<uint8_t> E; // 33 bytes (empty if !has_stealth)
63 std::vector<uint8_t> enc_amount; // 32 bytes (empty if !has_stealth)
64 std::vector<uint8_t> enc_blind; // 32 bytes (empty if !has_stealth)
65 };
66
67 /** True if the script is the CT kernel output prefix: OP_RETURN <"BK"> ... */
68 inline bool IsCTKernelScript(const CScript& spk)
69 {
70 if (spk.size() < 4) return false;
71 if (spk[0] != OP_RETURN) return false;
72 if (spk[1] != 0x02) return false; // push 2 bytes
73 return spk[2] == CT_KERNEL_MAGIC_BYTE0 && spk[3] == CT_KERNEL_MAGIC_BYTE1;
74 }
75
76 /** Find the index of the confidential transaction kernel output.
77 * Returns NO_CT_KERNEL_OUTPUT if not found (takes the last match).
78 */
79 inline int GetCTKernelOutputIndex(const CTransaction& tx)
80 {
81 int pos = NO_CT_KERNEL_OUTPUT;
82 for (size_t o = 0; o < tx.vout.size(); ++o) {
83 const CScript& spk = tx.vout[o].scriptPubKey;
84 CScript::const_iterator pc = spk.begin();
85 opcodetype opcode;
86 std::vector<uint8_t> data;
87 if (!spk.GetOp(pc, opcode, data) || opcode != OP_RETURN) continue;
88 if (!spk.GetOp(pc, opcode, data)) continue;
89 if (data.size() == 2 && data[0] == CT_KERNEL_MAGIC_BYTE0 &&
90 data[1] == CT_KERNEL_MAGIC_BYTE1) {
91 pos = static_cast<int>(o);
92 }
93 }
94 return pos;
95 }
96
97 /** Parse the fee and kernel signature from a kernel output. */
98 inline std::optional<CTKernelData> ParseCTKernelOutput(const CTxOut& vout)
99 {
100 const CScript& spk = vout.scriptPubKey;
101 CScript::const_iterator pc = spk.begin();
102 opcodetype opcode;
103 std::vector<uint8_t> magic;
104 std::vector<uint8_t> fee;
105 std::vector<uint8_t> sig;
106 if (!spk.GetOp(pc, opcode, magic) || opcode != OP_RETURN) return std::nullopt;
107 if (!spk.GetOp(pc, opcode, magic)) return std::nullopt;
108 if (magic.size() != 2 || magic[0] != CT_KERNEL_MAGIC_BYTE0 ||
109 magic[1] != CT_KERNEL_MAGIC_BYTE1) return std::nullopt;
110 if (!spk.GetOp(pc, opcode, fee) || fee.size() != CT_FEE_SIZE) return std::nullopt;
111 if (!spk.GetOp(pc, opcode, sig) || sig.size() != CT_KERNEL_SIG_SIZE) return std::nullopt;
112
113 // Optional stealth fields: <"S"> <E:33> <enc_amount:32> <enc_blind:32>.
114 bool has_stealth = false;
115 std::vector<uint8_t> E, enc_amount, enc_blind;
116 {
117 std::vector<uint8_t> marker;
118 if (spk.GetOp(pc, opcode, marker) && marker.size() == 1 &&
119 marker[0] == CT_STEALTH_MARKER) {
120 if (!spk.GetOp(pc, opcode, E) || E.size() != CT_STEALTH_EPHEM_SIZE)
121 return std::nullopt;
122 if (!spk.GetOp(pc, opcode, enc_amount) || enc_amount.size() != CT_STEALTH_ENC_SIZE)
123 return std::nullopt;
124 if (!spk.GetOp(pc, opcode, enc_blind) || enc_blind.size() != CT_STEALTH_ENC_SIZE)
125 return std::nullopt;
126 has_stealth = true;
127 } else if (!marker.empty()) {
128 // The kernel format is closed: no fields past the stealth block.
129 return std::nullopt;
130 }
131 }
132
133 // The kernel format is closed: no trailing fields.
134 {
135 std::vector<uint8_t> extra;
136 if (spk.GetOp(pc, opcode, extra)) return std::nullopt;
137 }
138
139 CAmount fee_val = 0;
140 for (size_t i = 0; i < CT_FEE_SIZE; i++) {
141 // Shift through unsigned so byte 15 cannot shift into the sign bit
142 // (signed left shift into the sign bit is UB).
143 fee_val |= static_cast<CAmount>(static_cast<__uint128_t>(fee[i]) << (8 * i));
144 }
145
146 CTKernelData out;
147 out.fee = fee_val;
148 out.sig = std::move(sig);
149 out.has_stealth = has_stealth;
150 out.E = std::move(E);
151 out.enc_amount = std::move(enc_amount);
152 out.enc_blind = std::move(enc_blind);
153 return out;
154 }
155
156 /** Compute the 32-byte kernel message the excess signature signs.
157 * Binds the fee, input prevouts, output scriptPubKeys (excluding the
158 * kernel output, whose scriptPubKey contains the signature itself), and
159 * the stealth fields (so they cannot be substituted).
160 */
161 inline uint256 ComputeCTKernelMessage(const CTransaction& tx, int kernel_index,
162 const CTKernelData& kernel,
163 const std::vector<CAmount>& transparent_input_values = {})
164 {
165 HashWriter hw{};
166 hw << std::string("ct-kernel");
167 // 16-byte little-endian fee (the __int128 stream serializer is 8-byte,
168 // so write the two halves explicitly).
169 hw << static_cast<uint64_t>(kernel.fee)
170 << static_cast<uint64_t>(static_cast<__uint128_t>(kernel.fee) >> 64);
171 for (const auto& txin : tx.vin) {
172 hw << txin.prevout;
173 }
174 // Mint inputs: bind their visible values (attosats) so the balance
175 // cannot be re-targeted after signing.
176 hw << static_cast<uint64_t>(transparent_input_values.size());
177 for (const CAmount v : transparent_input_values) {
178 hw << static_cast<uint64_t>(v)
179 << static_cast<uint64_t>(static_cast<__uint128_t>(v) >> 64);
180 }
181 for (size_t i = 0; i < tx.vout.size(); i++) {
182 if (static_cast<int>(i) == kernel_index) continue;
183 hw << tx.vout[i].scriptPubKey;
184 }
185 hw << static_cast<uint8_t>(kernel.has_stealth ? 1 : 0);
186 if (kernel.has_stealth) {
187 hw << kernel.E;
188 hw << kernel.enc_amount;
189 hw << kernel.enc_blind;
190 }
191 return hw.GetHash();
192 }
193
194 #endif // LIMENKA_CONSENSUS_CT_H
195