ct.cpp raw
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 #include <wallet/ct.h>
6 #include <wallet/wallet.h>
7 #include <psbt.h>
8 #include <wallet/receive.h>
9 #include <wallet/spend.h>
10
11 #include <consensus/amount.h>
12 #include <crypto/sha256.h>
13 #include <secp256k1.h>
14 #include <consensus/ct.h>
15 #include <crypto/bignum.h>
16 #include <random.h>
17 #include <script/script.h>
18 #include <serialize.h>
19 #include <tinyformat.h>
20 #include <wallet/db.h>
21 #include <wallet/types.h>
22
23 #include <cstring>
24 #include <algorithm>
25 #include <limits>
26
27 namespace wallet {
28
29 CScript GetConfidentialScript(const BPCommitment& commitment)
30 {
31 return CScript() << OP_4 << commitment;
32 }
33
34 bool CreateConfidentialOutput(CAmount amount_attosats, FastRandomContext& rng,
35 BPCommitment& commitment, CTReceipt& receipt)
36 {
37 // __int128 max is 2^127 - 1, inside the 2^128 range bound; negative is invalid.
38 if (amount_attosats < 0) return false;
39
40 auto blinding = rng.randbytes(BP_SCALAR_SIZE);
41 auto seed = rng.randbytes(BP_SCALAR_SIZE);
42 receipt.blinding.assign(blinding.begin(), blinding.end());
43 receipt.seed.assign(seed.begin(), seed.end());
44
45 if (!ProveBulletproof(amount_attosats, receipt.blinding, receipt.seed,
46 commitment, receipt.proof)) {
47 return false;
48 }
49 receipt.SetAmount(amount_attosats);
50 return true;
51 }
52
53 static BigNum CurveOrderNum()
54 {
55 static const uint8_t n_bytes[32] = {
56 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
57 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
58 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,
59 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41,
60 };
61 return BigNum(std::vector<uint8_t>(n_bytes, n_bytes + 32), true);
62 }
63
64 static BigNum ScalarFromBE(const BPScalar& be)
65 {
66 if (be.size() != BP_SCALAR_SIZE) return BigNum(0u);
67 return BigNum(std::vector<uint8_t>(be.begin(), be.end()), true) % CurveOrderNum();
68 }
69
70 static bool ScalarToBE(const BigNum& x, BPScalar& out)
71 {
72 auto le = x.to_bytes(BP_SCALAR_SIZE);
73 out.assign(BP_SCALAR_SIZE, 0);
74 for (size_t i = 0; i < BP_SCALAR_SIZE; i++) out[i] = le[BP_SCALAR_SIZE - 1 - i];
75 return true;
76 }
77
78 bool ComputeCTExcess(const std::vector<BPScalar>& in_blindings,
79 const std::vector<BPScalar>& out_blindings, BPScalar& excess)
80 {
81 BigNum e(0u), n = CurveOrderNum();
82 for (const auto& b : in_blindings) {
83 if (b.size() != BP_SCALAR_SIZE) return false;
84 e = (e + ScalarFromBE(b)) % n;
85 }
86 for (const auto& b : out_blindings) {
87 if (b.size() != BP_SCALAR_SIZE) return false;
88 e = (e + n - ScalarFromBE(b)) % n;
89 }
90 if (e.is_zero()) return false; // kernel key would be the identity
91 return ScalarToBE(e, excess);
92 }
93
94 bool BuildCTKernelOutput(const BPScalar& excess, CAmount fee, const CTransaction& tx,
95 int kernel_index,
96 const std::vector<uint8_t>& E,
97 const std::vector<uint8_t>& enc_amount,
98 const std::vector<uint8_t>& enc_blind,
99 CScript& kernel_script,
100 const std::vector<CAmount>& transparent_input_values)
101 {
102 if (excess.size() != BP_SCALAR_SIZE || fee < 0) return false;
103 if (!E.empty() && (E.size() != CT_STEALTH_EPHEM_SIZE ||
104 enc_amount.size() != CT_STEALTH_ENC_SIZE ||
105 enc_blind.size() != CT_STEALTH_ENC_SIZE))
106 return false;
107
108 CTKernelData kernel;
109 kernel.fee = fee;
110 kernel.has_stealth = !E.empty();
111 kernel.E = E;
112 kernel.enc_amount = enc_amount;
113 kernel.enc_blind = enc_blind;
114
115 const uint256 msg = ComputeCTKernelMessage(tx, kernel_index, kernel, transparent_input_values);
116 std::vector<uint8_t> sig;
117 if (!CreateCTKernelSig(excess, {msg.begin(), msg.end()}, sig)) return false;
118 kernel.sig = std::move(sig);
119
120 std::vector<uint8_t> magic = {CT_KERNEL_MAGIC_BYTE0, CT_KERNEL_MAGIC_BYTE1};
121 std::vector<uint8_t> fb(CT_FEE_SIZE);
122 for (size_t i = 0; i < CT_FEE_SIZE; i++) {
123 fb[i] = uint8_t(static_cast<uint64_t>(static_cast<__uint128_t>(fee) >> (8 * i)));
124 }
125 kernel_script = CScript() << OP_RETURN << magic << fb << kernel.sig;
126 if (kernel.has_stealth) {
127 kernel_script << std::vector<uint8_t>{CT_STEALTH_MARKER};
128 kernel_script << E << enc_amount << enc_blind;
129 }
130 return true;
131 }
132
133 util::Result<CTCreationResult> CreateConfidentialTransaction(
134 const std::vector<std::pair<COutPoint, CTReceipt>>& ct_inputs,
135 const std::vector<CAmount>& output_amounts, CAmount fee, FastRandomContext& rng)
136 {
137 if (ct_inputs.empty() || output_amounts.empty()) {
138 return util::Error{Untranslated("confidential transactions need inputs and outputs")};
139 }
140 if (fee < 0) return util::Error{Untranslated("negative fee")};
141
142 CTCreationResult out;
143 out.tx.version = 2;
144
145 // Inputs: CT outpoints, witness carries the stored range proof.
146 std::vector<BPScalar> in_blindings;
147 for (const auto& [outpoint, receipt] : ct_inputs) {
148 CTxIn txin;
149 txin.prevout = outpoint;
150 txin.scriptWitness.stack.push_back(SerializeBulletproof(receipt.proof));
151 out.tx.vin.push_back(std::move(txin));
152 in_blindings.push_back(receipt.blinding);
153 }
154
155 // Outputs: fresh commitments + proofs at attosat scale.
156 std::vector<BPScalar> out_blindings;
157 for (const CAmount amount : output_amounts) {
158 BPCommitment commitment;
159 CTReceipt receipt;
160 if (!CreateConfidentialOutput(amount, rng, commitment, receipt)) {
161 return util::Error{Untranslated("failed to create confidential output")};
162 }
163 receipt.vout_index = static_cast<uint32_t>(out.tx.vout.size());
164 out.tx.vout.emplace_back(CTxOut(0, GetConfidentialScript(commitment)));
165 out_blindings.push_back(receipt.blinding);
166 out.new_receipts.push_back(std::move(receipt));
167 }
168
169 // Kernel: excess signature over the 128-bit fee.
170 const int kernel_index = static_cast<int>(out.tx.vout.size());
171 out.kernel_index = kernel_index;
172 BPScalar excess;
173 if (!ComputeCTExcess(in_blindings, out_blindings, excess)) {
174 return util::Error{Untranslated("excess is zero - re-blind an output and retry")};
175 }
176 CScript kernel_script;
177 if (!BuildCTKernelOutput(excess, fee, CTransaction(out.tx), kernel_index,
178 /*E=*/{}, /*enc_amount=*/{}, /*enc_blind=*/{},
179 kernel_script)) {
180 return util::Error{Untranslated("failed to build kernel output")};
181 }
182 out.tx.vout.emplace_back(CTxOut(0, kernel_script));
183 return out;
184 }
185
186 // ---------------------------------------------------------------------------
187 // Stealth CT: ECDH-derived blinding for non-interactive confidential payments.
188 // ---------------------------------------------------------------------------
189
190 namespace {
191
192 secp256k1_context* CTStealthContext()
193 {
194 static secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
195 return ctx;
196 }
197
198 const std::vector<uint8_t>& BasePointGCompressed()
199 {
200 static const std::vector<uint8_t> g = {
201 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0,
202 0x62, 0x95, 0xCE, 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D,
203 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98,
204 };
205 return g;
206 }
207
208 bool ParsePoint(const std::vector<uint8_t>& in, secp256k1_pubkey& out)
209 {
210 return secp256k1_ec_pubkey_parse(CTStealthContext(), &out, in.data(), in.size()) == 1;
211 }
212
213 bool SerializePoint(const secp256k1_pubkey& pub, std::vector<uint8_t>& out)
214 {
215 out.resize(CT_STEALTH_EPHEM_SIZE);
216 size_t len = CT_STEALTH_EPHEM_SIZE;
217 if (!secp256k1_ec_pubkey_serialize(CTStealthContext(), out.data(), &len, &pub,
218 SECP256K1_EC_COMPRESSED)) return false;
219 out.resize(len);
220 return true;
221 }
222
223 // 32-byte tagged hash.
224 std::vector<uint8_t> StealthTaggedHash(const char* label, const std::vector<uint8_t>& data)
225 {
226 CSHA256 sha;
227 sha.Write(reinterpret_cast<const uint8_t*>(label), std::strlen(label));
228 sha.Write(data.data(), data.size());
229 std::vector<uint8_t> out(CSHA256::OUTPUT_SIZE);
230 sha.Finalize(out.data());
231 return out;
232 }
233
234 // scalar*point via the secp256k1 tweak API (the 32-byte tweak is reduced
235 // mod n internally, matching the BigNum SMod used for the scalar side).
236 bool TweakMul(const std::vector<uint8_t>& point, const std::vector<uint8_t>& scalar32,
237 std::vector<uint8_t>& out)
238 {
239 secp256k1_pubkey pub;
240 if (!ParsePoint(point, pub)) return false;
241 if (!secp256k1_ec_pubkey_tweak_mul(CTStealthContext(), &pub, scalar32.data())) return false;
242 return SerializePoint(pub, out);
243 }
244
245 bool PointAdd(const std::vector<uint8_t>& a, const std::vector<uint8_t>& b,
246 std::vector<uint8_t>& out)
247 {
248 secp256k1_pubkey pa, pb;
249 if (!ParsePoint(a, pa) || !ParsePoint(b, pb)) return false;
250 const secp256k1_pubkey* ptrs[2] = {&pa, &pb};
251 secp256k1_pubkey sum;
252 if (!secp256k1_ec_pubkey_combine(CTStealthContext(), &sum, ptrs, 2)) return false;
253 return SerializePoint(sum, out);
254 }
255
256 // Fresh valid ephemeral scalar (re-hash until < n).
257 bool RandomScalar32(FastRandomContext& rng, std::vector<uint8_t>& out)
258 {
259 for (;;) {
260 auto b = rng.randbytes(BP_SCALAR_SIZE);
261 std::vector<uint8_t> candidate(b.begin(), b.end());
262 if (secp256k1_ec_seckey_verify(CTStealthContext(), candidate.data())) {
263 out = std::move(candidate);
264 return true;
265 }
266 }
267 }
268
269 // v (attosats, __int128) as 16 little-endian bytes.
270 std::vector<uint8_t> AmountLE16(CAmount v)
271 {
272 std::vector<uint8_t> out(16);
273 for (size_t i = 0; i < 16; i++) out[i] = uint8_t(static_cast<uint64_t>(static_cast<__uint128_t>(v) >> (8 * i)));
274 return out;
275 }
276
277 CAmount AmountFromLE16(const std::vector<uint8_t>& b)
278 {
279 CAmount v = 0;
280 for (size_t i = 0; i < 16 && i < b.size(); i++) {
281 v |= static_cast<CAmount>(static_cast<__uint128_t>(b[i]) << (8 * i));
282 }
283 return v;
284 }
285
286 // scalar mod n arithmetic via BigNum (matches the bulletproofs helpers).
287 BigNum StealthOrder()
288 {
289 static const uint8_t n_bytes[32] = {
290 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
291 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
292 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,
293 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41,
294 };
295 return BigNum(std::vector<uint8_t>(n_bytes, n_bytes + 32), true);
296 }
297
298 bool ScalarAddBE(const std::vector<uint8_t>& a, const std::vector<uint8_t>& b,
299 BPScalar& out)
300 {
301 if (a.size() != BP_SCALAR_SIZE || b.size() != BP_SCALAR_SIZE) return false;
302 BigNum n = StealthOrder();
303 BigNum x = BigNum(std::vector<uint8_t>(a.begin(), a.end()), true) % n;
304 BigNum y = BigNum(std::vector<uint8_t>(b.begin(), b.end()), true) % n;
305 BigNum s = (x + y) % n;
306 if (s.is_zero()) return false;
307 auto le = s.to_bytes(BP_SCALAR_SIZE);
308 out.assign(BP_SCALAR_SIZE, 0);
309 for (size_t i = 0; i < BP_SCALAR_SIZE; i++) out[i] = le[BP_SCALAR_SIZE - 1 - i];
310 return true;
311 }
312
313 // CKey secret as a 32-byte BE scalar vector.
314 std::vector<uint8_t> SecretBytes(const CKey& key)
315 {
316 std::vector<uint8_t> out(key.size());
317 for (size_t i = 0; i < key.size(); i++) out[i] = UCharCast(key.begin())[i];
318 return out;
319 }
320
321 } // namespace
322
323 StealthCTAddress CreateStealthCTAddress(const CKey& view_secret, const CKey& spend_secret)
324 {
325 StealthCTAddress addr;
326 addr.view = view_secret.GetPubKey();
327 addr.spend = spend_secret.GetPubKey();
328 return addr;
329 }
330
331 bool CreateStealthOutput(CAmount amount_attosats, const StealthCTAddress& address,
332 FastRandomContext& rng, StealthPayment& out)
333 {
334 if (amount_attosats < 0) return false;
335 if (!address.view.IsFullyValid() || !address.spend.IsFullyValid()) return false;
336
337 // Sender-chosen blinding: the sender signs the excess that covers this
338 // output itself, so no receiver interaction and no kernel offset points
339 // (arbitrary offset points in the balance equation would allow minting
340 // arbitrary value via b*H terms).
341 std::vector<uint8_t> blind_bytes;
342 if (!RandomScalar32(rng, blind_bytes)) return false;
343 out.blinding = blind_bytes;
344
345 // Ephemeral t; E = t*G.
346 std::vector<uint8_t> t;
347 if (!RandomScalar32(rng, t)) return false;
348 std::vector<uint8_t> E;
349 if (!TweakMul(BasePointGCompressed(), t, E)) return false;
350
351 // shared_v = t*V_view (view key can scan).
352 const std::vector<uint8_t> view_bytes(address.view.begin(), address.view.end());
353 std::vector<uint8_t> shared_v;
354 if (!TweakMul(view_bytes, t, shared_v)) return false;
355 // shared_s = t*V_spend (only the spend key recovers the blinding).
356 const std::vector<uint8_t> spend_bytes(address.spend.begin(), address.spend.end());
357 std::vector<uint8_t> shared_s;
358 if (!TweakMul(spend_bytes, t, shared_s)) return false;
359
360 // C = r*G + v*H.
361 if (!CommitAmount(amount_attosats, out.blinding, out.commitment)) return false;
362
363 // enc_amount = v XOR H("CTStealthAmount" || shared_v), 16-byte LE padded.
364 {
365 const auto amount_hash = StealthTaggedHash("Limenka/CTStealthAmount", shared_v);
366 std::vector<uint8_t> padded(32, 0);
367 const auto le16 = AmountLE16(amount_attosats);
368 for (size_t i = 0; i < 16; i++) padded[i] = le16[i];
369 out.enc_amount.resize(32);
370 for (size_t i = 0; i < 32; i++) out.enc_amount[i] = padded[i] ^ amount_hash[i];
371 }
372 // enc_blind = r XOR H("CTStealthBlind" || shared_s).
373 {
374 const auto blind_hash = StealthTaggedHash("Limenka/CTStealthBlind", shared_s);
375 out.enc_blind.resize(32);
376 for (size_t i = 0; i < 32; i++) out.enc_blind[i] = blind_bytes[i] ^ blind_hash[i];
377 }
378
379 out.E = std::move(E);
380 out.amount = amount_attosats;
381 return true;
382 }
383
384 std::optional<std::pair<CAmount, BPScalar>> RecoverStealthOutput(
385 const CKey& view_secret, const CKey& spend_secret,
386 const std::vector<uint8_t>& E, const std::vector<uint8_t>& enc_amount,
387 const std::vector<uint8_t>& enc_blind, const BPCommitment& commitment)
388 {
389 if (!view_secret.IsValid() || !spend_secret.IsValid()) return std::nullopt;
390 if (E.size() != CT_STEALTH_EPHEM_SIZE || enc_amount.size() != CT_STEALTH_ENC_SIZE ||
391 enc_blind.size() != CT_STEALTH_ENC_SIZE || commitment.size() != BP_POINT_SIZE)
392 return std::nullopt;
393
394 // shared_v = E*v_scan: decrypt the amount.
395 std::vector<uint8_t> shared_v;
396 if (!TweakMul(E, SecretBytes(view_secret), shared_v)) return std::nullopt;
397 const auto amount_hash = StealthTaggedHash("Limenka/CTStealthAmount", shared_v);
398 std::vector<uint8_t> vle(16);
399 for (size_t i = 0; i < 16; i++) vle[i] = enc_amount[i] ^ amount_hash[i];
400 const CAmount v = AmountFromLE16(vle);
401
402 // shared_s = E*v_spend: decrypt the blinding.
403 std::vector<uint8_t> shared_s;
404 if (!TweakMul(E, SecretBytes(spend_secret), shared_s)) return std::nullopt;
405 const auto blind_hash = StealthTaggedHash("Limenka/CTStealthBlind", shared_s);
406 BPScalar blind(BP_SCALAR_SIZE, 0);
407 for (size_t i = 0; i < 32; i++) blind[i] = enc_blind[i] ^ blind_hash[i];
408
409 // C must equal r*G + v*H.
410 BPCommitment check;
411 if (!CommitAmount(v, blind, check)) return std::nullopt;
412 if (check != commitment) return std::nullopt;
413
414 return std::make_pair(v, blind);
415 }
416
417 util::Result<CTCreationResult> CreateStealthTransaction(
418 const std::vector<std::pair<COutPoint, CTReceipt>>& ct_inputs,
419 const StealthCTAddress& address, CAmount amount, CAmount fee, FastRandomContext& rng,
420 int change_outputs)
421 {
422 if (ct_inputs.empty()) {
423 return util::Error{Untranslated("confidential transactions need inputs")};
424 }
425 if (amount < 0 || fee < 0) return util::Error{Untranslated("negative amount or fee")};
426
427 CTCreationResult out;
428 out.tx.version = 2;
429
430 std::vector<BPScalar> in_blindings;
431 for (const auto& [outpoint, receipt] : ct_inputs) {
432 CTxIn txin;
433 txin.prevout = outpoint;
434 txin.scriptWitness.stack.push_back(SerializeBulletproof(receipt.proof));
435 out.tx.vin.push_back(std::move(txin));
436 in_blindings.push_back(receipt.blinding);
437 }
438
439 StealthPayment payment;
440 if (!CreateStealthOutput(amount, address, rng, payment)) {
441 return util::Error{Untranslated("failed to derive stealth output")};
442 }
443 out.tx.vout.emplace_back(CTxOut(0, GetConfidentialScript(payment.commitment)));
444
445 // Value balance: inputs must cover amount + fee; the surplus becomes
446 // own confidential change outputs (fresh blinding), split into
447 // change_outputs random widely-varying pieces for privacy.
448 CAmount total_in{0};
449 for (const auto& [outpoint, receipt] : ct_inputs) total_in += receipt.Amount();
450 const CAmount change = total_in - amount - fee;
451 std::vector<BPScalar> out_blindings;
452 if (change < 0) {
453 return util::Error{Untranslated("insufficient input value for amount + fee")};
454 }
455 if (change > 0) {
456 for (const CAmount change_part : SplitAttosats(change, change_outputs, rng)) {
457 if (change_part <= 0) continue;
458 BPCommitment change_commitment;
459 CTReceipt change_receipt;
460 if (!CreateConfidentialOutput(change_part, rng, change_commitment, change_receipt)) {
461 return util::Error{Untranslated("failed to create change output")};
462 }
463 change_receipt.vout_index = static_cast<uint32_t>(out.tx.vout.size());
464 out.tx.vout.emplace_back(CTxOut(0, GetConfidentialScript(change_commitment)));
465 out_blindings.push_back(change_receipt.blinding);
466 out.new_receipts.push_back(std::move(change_receipt));
467 }
468 }
469
470 // The sender chose the stealth blinding itself, so it folds into the
471 // excess directly (no kernel offset points).
472 out_blindings.push_back(payment.blinding);
473 out.stealth_payment = payment;
474 const int kernel_index = static_cast<int>(out.tx.vout.size());
475 out.kernel_index = kernel_index;
476 BPScalar excess;
477 if (!ComputeCTExcess(in_blindings, out_blindings, excess)) {
478 return util::Error{Untranslated("excess is zero - re-blind an output and retry")};
479 }
480 CScript kernel_script;
481 if (!BuildCTKernelOutput(excess, fee, CTransaction(out.tx), kernel_index,
482 payment.E, payment.enc_amount, payment.enc_blind,
483 kernel_script)) {
484 return util::Error{Untranslated("failed to build kernel output")};
485 }
486 out.tx.vout.emplace_back(CTxOut(0, kernel_script));
487 return out;
488 }
489
490 util::Result<MintCreationResult> CreateMintTransaction(
491 const std::vector<std::pair<COutPoint, CAmount>>& transparent_inputs,
492 const std::vector<CAmount>& output_amounts, CAmount fee, FastRandomContext& rng)
493 {
494 if (transparent_inputs.empty() || output_amounts.empty()) {
495 return util::Error{Untranslated("mint transactions need inputs and outputs")};
496 }
497 if (fee < 0) return util::Error{Untranslated("negative fee")};
498
499 MintCreationResult out;
500 out.tx.version = 2;
501
502 // Transparent inputs: satoshi values widen to attosats.
503 CAmount total_in{0};
504 std::vector<CAmount> in_values_attosats;
505 for (const auto& [outpoint, value_sats] : transparent_inputs) {
506 if (value_sats <= 0) return util::Error{Untranslated("non-positive transparent input")};
507 CTxIn txin;
508 txin.prevout = outpoint;
509 out.tx.vin.push_back(std::move(txin));
510 const CAmount attosats = value_sats * ATTOSATS_PER_SATOSHI;
511 in_values_attosats.push_back(attosats);
512 total_in += attosats;
513 }
514
515 // Confidential outputs.
516 CAmount total_out{0};
517 std::vector<BPScalar> out_blindings;
518 for (const CAmount amount : output_amounts) {
519 if (amount < 0) return util::Error{Untranslated("negative output amount")};
520 BPCommitment commitment;
521 CTReceipt receipt;
522 if (!CreateConfidentialOutput(amount, rng, commitment, receipt)) {
523 return util::Error{Untranslated("failed to create confidential output")};
524 }
525 receipt.vout_index = static_cast<uint32_t>(out.tx.vout.size());
526 out.tx.vout.emplace_back(CTxOut(0, GetConfidentialScript(commitment)));
527 out_blindings.push_back(receipt.blinding);
528 out.new_receipts.push_back(std::move(receipt));
529 total_out += amount;
530 }
531
532 if (total_in != total_out + fee) {
533 return util::Error{Untranslated("input value must equal outputs + fee (no implicit change)")};
534 }
535
536 // Kernel: the excess is the negation of the output blindings (the
537 // transparent inputs contribute no blinding).
538 const int kernel_index = static_cast<int>(out.tx.vout.size());
539 out.kernel_index = kernel_index;
540 BPScalar excess;
541 if (!ComputeCTExcess(/*in_blindings=*/{}, out_blindings, excess)) {
542 return util::Error{Untranslated("excess is zero - re-blind an output and retry")};
543 }
544 CScript kernel_script;
545 if (!BuildCTKernelOutput(excess, fee, CTransaction(out.tx), kernel_index,
546 /*E=*/{}, /*enc_amount=*/{}, /*enc_blind=*/{},
547 kernel_script, in_values_attosats)) {
548 return util::Error{Untranslated("failed to build kernel output")};
549 }
550 out.tx.vout.emplace_back(CTxOut(0, kernel_script));
551 return out;
552 }
553
554 namespace {
555 // Decimal conversion for __int128 (strprintf's __int128 formatter does not
556 // honor width/padding).
557 std::string Uint128ToString(CAmount n)
558 {
559 if (n == 0) return "0";
560 std::string s;
561 while (n > 0) {
562 s.push_back('0' + static_cast<char>(n % 10));
563 n /= 10;
564 }
565 std::reverse(s.begin(), s.end());
566 return s;
567 }
568 } // namespace
569
570 std::string AttosatsToString(CAmount v)
571 {
572 bool neg = v < 0;
573 if (neg) v = -v;
574 const CAmount whole = v / LAMBDA_SCALE;
575 const CAmount frac = v % LAMBDA_SCALE;
576 std::string s = Uint128ToString(whole);
577 if (frac > 0) {
578 std::string f = Uint128ToString(frac);
579 f.insert(0, 26 - f.size(), '0');
580 while (!f.empty() && f.back() == '0') f.pop_back();
581 s += "." + f;
582 }
583 return neg ? "-" + s : s;
584 }
585
586 bool ParseAttosatsString(const std::string& s, CAmount& out)
587 {
588 if (s.empty()) return false;
589 bool neg = false;
590 size_t pos = 0;
591 if (s[0] == '-') { neg = true; pos = 1; }
592
593 CAmount result = 0;
594 size_t frac_digits = 0;
595 bool in_frac = false;
596 // max ends in 7 (2^127-1); the check must run BEFORE the multiply+add,
597 // and reject the last digit when it exceeds that final value.
598 const CAmount limit = std::numeric_limits<CAmount>::max() / 10;
599 const int last_digit = int(std::numeric_limits<CAmount>::max() % 10);
600 auto step = [&](int digit) {
601 if (result > limit) return false;
602 if (result == limit && digit > last_digit) return false;
603 result = result * 10 + digit;
604 return true;
605 };
606 for (; pos < s.size(); ++pos) {
607 const char c = s[pos];
608 if (c == '.') {
609 if (in_frac) return false;
610 in_frac = true;
611 continue;
612 }
613 if (c < '0' || c > '9') return false;
614 if (!step(c - '0')) return false;
615 if (in_frac) ++frac_digits;
616 }
617 if (frac_digits > 26) return false;
618 for (size_t i = frac_digits; i < 26; ++i) {
619 if (!step(0)) return false;
620 }
621 out = neg ? -result : result;
622 return true;
623 }
624
625 bool ListUnspentCTOutputs(const CWallet& wallet, const std::vector<COutPoint>& outpoints,
626 std::vector<std::pair<COutPoint, CTReceipt>>& out)
627 {
628 out.clear();
629 LOCK(wallet.cs_wallet);
630 WalletBatch batch{wallet.GetDatabase()};
631 std::map<uint256, std::vector<CTReceipt>> all;
632 if (!batch.ListCTReceipts(all)) return false;
633 for (const auto& [txid, receipts] : all) {
634 for (const auto& receipt : receipts) {
635 const COutPoint op(Txid::FromUint256(txid), receipt.vout_index);
636 if (wallet.IsSpent(op)) continue;
637 if (!outpoints.empty() &&
638 std::find(outpoints.begin(), outpoints.end(), op) == outpoints.end()) {
639 continue;
640 }
641 out.emplace_back(op, receipt);
642 }
643 }
644 return true;
645 }
646
647 static util::Result<std::vector<std::pair<COutPoint, CTReceipt>>> SelectCTInputs(CWallet& wallet, CAmount total_needed)
648 {
649 std::vector<std::pair<COutPoint, CTReceipt>> all;
650 if (!ListUnspentCTOutputs(wallet, /*outpoints=*/{}, all)) {
651 return util::Error{Untranslated("failed to enumerate CT receipts")};
652 }
653 std::vector<std::pair<COutPoint, CTReceipt>> inputs;
654 CAmount total_in{0};
655 for (const auto& [op, rec] : all) {
656 if (total_in >= total_needed) break;
657 inputs.emplace_back(op, rec);
658 total_in += rec.Amount();
659 }
660 if (total_in < total_needed) {
661 return util::Error{Untranslated("insufficient confidential funds")};
662 }
663 return inputs;
664 }
665
666 CAmount CWallet::GetPreciseBalanceAttosats(int min_depth) const
667 {
668 // GetBalance locks cs_wallet internally; do not hold it here.
669 const Balance bal = GetBalance(*this, min_depth, /*avoid_reuse=*/true);
670 CAmount total = bal.m_mine_trusted * ATTOSATS_PER_SATOSHI;
671
672 // Collect the wallet's confidential receipts.
673 std::map<uint256, std::vector<CTReceipt>> receipts;
674 {
675 LOCK(cs_wallet);
676 WalletBatch batch(GetDatabase());
677 batch.ListCTReceipts(receipts);
678 }
679
680 // Confidential outputs: the receipts carry the attosat amounts.
681 {
682 LOCK(cs_wallet);
683 for (const auto& [txid, recs] : receipts) {
684 for (const auto& receipt : recs) {
685 const COutPoint op(Txid::FromUint256(txid), receipt.vout_index);
686 if (IsSpent(op)) continue;
687 total += receipt.Amount();
688 }
689 }
690 }
691 return total;
692 }
693
694 util::Result<uint256> CWallet::SendStealthPayment(const CTxDestination& dest, CAmount amount, CAmount fee, int change_outputs)
695 {
696 if (!std::holds_alternative<WitnessV4StealthAddress>(dest)) {
697 return util::Error{Untranslated("not an lm2 stealth address")};
698 }
699 const auto& stealth_dest = std::get<WitnessV4StealthAddress>(dest);
700 const StealthCTAddress address{stealth_dest.view, stealth_dest.spend};
701 if (amount <= 0 || fee < 0 || change_outputs < 1) return util::Error{Untranslated("invalid amount or fee")};
702
703 // Greedy selection across unspent CT receipts (oldest txid first).
704 const auto sel = SelectCTInputs(*this, amount + fee);
705 if (!sel) return util::Error{util::ErrorString(sel)};
706 const std::vector<std::pair<COutPoint, CTReceipt>>& inputs = *sel;
707
708 FastRandomContext rng_fast;
709 const auto res = CreateStealthTransaction(inputs, address, amount, fee, rng_fast, change_outputs);
710 if (!res) return util::Error{util::ErrorString(res)};
711
712 CTransactionRef tx = MakeTransactionRef(res->tx);
713 {
714 LOCK(cs_wallet);
715 WalletBatch batch(GetDatabase());
716 if (!res->new_receipts.empty() &&
717 !batch.WriteCTReceipts(tx->GetHash(), res->new_receipts)) {
718 return util::Error{Untranslated("failed to persist CT change receipt")};
719 }
720 }
721 CommitTransaction(tx, /*mapValue=*/{}, /*orderForm=*/{});
722 return uint256(tx->GetHash());
723 }
724
725 util::Result<uint256> CWallet::MintConfidential(CAmount amount, CAmount fee, int output_count)
726 {
727 if (amount <= 0 || fee < 0 || output_count < 1) return util::Error{Untranslated("invalid amount or fee")};
728 const CAmount total_needed = amount + fee;
729
730 // Select transparent coins to cover amount + fee in whole satoshis.
731 std::vector<COutput> selected;
732 {
733 LOCK(cs_wallet);
734 auto avail = AvailableCoinsListUnspent(*this);
735 CAmount sat_selected{0};
736 const CAmount sat_needed = (total_needed + ATTOSATS_PER_SATOSHI - 1) / ATTOSATS_PER_SATOSHI;
737 for (const auto& [type, group] : avail.coins) {
738 if (sat_selected >= sat_needed) break;
739 for (const auto& coin : group) {
740 if (sat_selected >= sat_needed) break;
741 selected.push_back(coin);
742 sat_selected += coin.txout.nValue;
743 }
744 }
745 if (sat_selected < sat_needed) {
746 return util::Error{Untranslated("insufficient transparent funds")};
747 }
748 }
749
750 std::vector<std::pair<COutPoint, CAmount>> transparent_inputs;
751 std::map<COutPoint, Coin> coins;
752 CAmount in_attosats{0};
753 for (const auto& coin : selected) {
754 transparent_inputs.emplace_back(coin.outpoint, coin.txout.nValue);
755 coins.emplace(coin.outpoint, Coin(coin.txout, /*nHeight=*/0, /*fCoinBase=*/false));
756 in_attosats += coin.txout.nValue * ATTOSATS_PER_SATOSHI;
757 }
758 const CAmount change = in_attosats - amount - fee;
759 if (change < 0) return util::Error{Untranslated("selection did not cover amount + fee")};
760 // All minted value (amount + change) becomes confidential outputs,
761 // split into output_count random widely-varying pieces.
762 FastRandomContext rng_fast;
763 std::vector<CAmount> output_amounts = SplitAttosats(amount + change, output_count, rng_fast);
764
765 const auto res = CreateMintTransaction(transparent_inputs, output_amounts, fee, rng_fast);
766 if (!res) return util::Error{util::ErrorString(res)};
767
768 CMutableTransaction mtx{res->tx};
769 {
770 LOCK(cs_wallet);
771 std::map<int, bilingual_str> input_errors;
772 std::optional<CAmount> inputs_sum;
773 if (!SignTransaction(mtx, coins, SIGHASH_ALL, input_errors, &inputs_sum)) {
774 return util::Error{Untranslated("failed to sign mint inputs")};
775 }
776 }
777
778 CTransactionRef tx = MakeTransactionRef(mtx);
779 {
780 LOCK(cs_wallet);
781 WalletBatch batch(GetDatabase());
782 if (!batch.WriteCTReceipts(tx->GetHash(), res->new_receipts)) {
783 return util::Error{Untranslated("failed to persist CT receipts")};
784 }
785 }
786 CommitTransaction(tx, /*mapValue=*/{}, /*orderForm=*/{});
787 return uint256(tx->GetHash());
788 }
789
790 static CAmount RandRange128(FastRandomContext& rng, CAmount range)
791 {
792 // Uniform draw in [0, range) for 128-bit ranges (rng.randrange only
793 // takes uint64 - casting a wider range truncates and corrupts the
794 // distribution).
795 const __uint128_t r = static_cast<__uint128_t>(range);
796 if (r <= std::numeric_limits<uint64_t>::max()) {
797 return CAmount(rng.randrange(uint64_t(r)));
798 }
799 const __uint128_t limit = ((__uint128_t{0} - 1) / r) * r; // largest multiple of r
800 for (;;) {
801 const __uint128_t v = (__uint128_t(rng.rand64()) << 64) | rng.rand64();
802 if (v < limit) return CAmount(v % r);
803 }
804 }
805
806 std::vector<CAmount> SplitAttosats(CAmount total, int n, FastRandomContext& rng)
807 {
808 if (n < 1) n = 1;
809 if (n > 16) n = 16;
810 std::vector<CAmount> parts;
811 parts.reserve(n);
812 if (total <= 0 || n == 1) {
813 parts.push_back(total);
814 return parts;
815 }
816 CAmount remaining = total;
817 for (int i = 0; i < n - 1; ++i) {
818 // Each part between 10% and 90% of the remainder, keeping enough
819 // for at least 1 unit in every remaining part.
820 const CAmount min_part = std::max<CAmount>(remaining / 10, 1);
821 const CAmount reserve = (CAmount)(n - i - 1);
822 const CAmount max_part = std::max<CAmount>(min_part, remaining - reserve);
823 CAmount part = min_part;
824 if (max_part > min_part) {
825 part = min_part + RandRange128(rng, max_part - min_part);
826 }
827 parts.push_back(part);
828 remaining -= part;
829 }
830 parts.push_back(remaining);
831 // Shuffle for additional unlinkability.
832 for (int i = parts.size() - 1; i > 0; --i) {
833 const int j = rng.randrange(i + 1);
834 std::swap(parts[i], parts[j]);
835 }
836 return parts;
837 }
838
839 static std::vector<uint8_t> CTPSBTFullKey()
840 {
841 // Wire key = <type 0xFC> <compact-size-prefixed identifier> <compact-size
842 // subtype> <raw keydata>. The deserializer reads the first byte as the
843 // type, then the identifier and subtype, and treats the remainder as
844 // keydata.
845 DataStream ss;
846 ss << PSBT_GLOBAL_PROPRIETARY;
847 ss << std::vector<uint8_t>{'l', 'i', 'm', 'e', 'n', 'k', 'a'};
848 ss << CompactSizeWriter(0u);
849 ss << uint8_t{'c'} << uint8_t{'t'};
850 std::vector<uint8_t> key;
851 key.assign(reinterpret_cast<const uint8_t*>(ss.data()),
852 reinterpret_cast<const uint8_t*>(ss.data()) + ss.size());
853 return key;
854 }
855
856 bool SetCTPSBTData(PartiallySignedTransaction& psbt, const CTPSBTData& data)
857 {
858 PSBTProprietary prop;
859 prop.identifier = {'l', 'i', 'm', 'e', 'n', 'k', 'a'};
860 prop.subtype = 0;
861 prop.key = CTPSBTFullKey();
862 DataStream ss;
863 ss << data;
864 prop.value.assign(reinterpret_cast<const uint8_t*>(ss.data()),
865 reinterpret_cast<const uint8_t*>(ss.data()) + ss.size());
866 for (auto it = psbt.m_proprietary.begin(); it != psbt.m_proprietary.end();) {
867 if (it->identifier == prop.identifier && it->subtype == prop.subtype &&
868 it->key == prop.key) {
869 it = psbt.m_proprietary.erase(it);
870 } else {
871 ++it;
872 }
873 }
874 psbt.m_proprietary.insert(std::move(prop));
875 return true;
876 }
877
878 bool GetCTPSBTData(const PartiallySignedTransaction& psbt, CTPSBTData& data)
879 {
880 for (const auto& prop : psbt.m_proprietary) {
881 if (prop.identifier == std::vector<uint8_t>{'l', 'i', 'm', 'e', 'n', 'k', 'a'} &&
882 prop.subtype == 0) {
883 DataStream ss(prop.value);
884 try {
885 ss >> data;
886 return true;
887 } catch (const std::ios_base::failure&) {
888 return false;
889 }
890 }
891 }
892 return false;
893 }
894
895 bool BuildCTKernelScript(const CTKernelData& kernel, CScript& script)
896 {
897 std::vector<uint8_t> magic = {CT_KERNEL_MAGIC_BYTE0, CT_KERNEL_MAGIC_BYTE1};
898 std::vector<uint8_t> fb(CT_FEE_SIZE);
899 for (size_t i = 0; i < CT_FEE_SIZE; i++) {
900 fb[i] = uint8_t(static_cast<uint64_t>(static_cast<__uint128_t>(kernel.fee) >> (8 * i)));
901 }
902 script = CScript() << OP_RETURN << magic << fb << kernel.sig;
903 if (kernel.has_stealth) {
904 script << std::vector<uint8_t>{CT_STEALTH_MARKER};
905 script << kernel.E << kernel.enc_amount << kernel.enc_blind;
906 }
907 return true;
908 }
909
910 util::Result<PartiallySignedTransaction> CWallet::CreateCTMintPSBT(CAmount amount, CAmount fee, int output_count)
911 {
912 if (amount <= 0 || fee < 0 || output_count < 1) return util::Error{Untranslated("invalid amount or fee")};
913 const CAmount total_needed = amount + fee;
914
915 // Select transparent coins (same as MintConfidential).
916 std::vector<COutput> selected;
917 {
918 LOCK(cs_wallet);
919 auto avail = AvailableCoinsListUnspent(*this);
920 CAmount sat_selected{0};
921 const CAmount sat_needed = (total_needed + ATTOSATS_PER_SATOSHI - 1) / ATTOSATS_PER_SATOSHI;
922 for (const auto& [type, group] : avail.coins) {
923 if (sat_selected >= sat_needed) break;
924 for (const auto& coin : group) {
925 if (sat_selected >= sat_needed) break;
926 selected.push_back(coin);
927 sat_selected += coin.txout.nValue;
928 }
929 }
930 if (sat_selected < sat_needed) return util::Error{Untranslated("insufficient transparent funds")};
931 }
932
933 std::vector<std::pair<COutPoint, CAmount>> transparent_inputs;
934 std::map<COutPoint, Coin> coins;
935 CAmount in_attosats{0};
936 for (const auto& coin : selected) {
937 transparent_inputs.emplace_back(coin.outpoint, coin.txout.nValue);
938 coins.emplace(coin.outpoint, Coin(coin.txout, 0, false));
939 in_attosats += coin.txout.nValue * ATTOSATS_PER_SATOSHI;
940 }
941 const CAmount change = in_attosats - amount - fee;
942 if (change < 0) return util::Error{Untranslated("selection did not cover amount + fee")};
943
944 FastRandomContext rng_fast;
945 const std::vector<CAmount> output_amounts = SplitAttosats(amount + change, output_count, rng_fast);
946 const auto res = CreateMintTransaction(transparent_inputs, output_amounts, fee, rng_fast);
947 if (!res) return util::Error{util::ErrorString(res)};
948
949 // Build the PSBT with the transparent UTXOs and the CT kernel payload
950 // (signature empty - filled at finalize).
951 PartiallySignedTransaction psbt{res->tx};
952 for (size_t i = 0; i < res->tx.vin.size(); ++i) {
953 const auto it = coins.find(res->tx.vin[i].prevout);
954 if (it != coins.end()) {
955 psbt.inputs[i].witness_utxo = it->second.out;
956 }
957 }
958
959 const auto parsed = ParseCTKernelOutput(res->tx.vout[res->kernel_index]);
960 if (!parsed) return util::Error{Untranslated("failed to parse kernel output")};
961 CTPSBTData data;
962 data.kernel = *parsed;
963 data.kernel.sig.clear();
964 data.out_receipts = res->new_receipts;
965
966 CScript empty_kernel;
967 if (!BuildCTKernelScript(data.kernel, empty_kernel)) {
968 return util::Error{Untranslated("failed to assemble empty kernel")};
969 }
970 psbt.tx->vout[res->kernel_index].scriptPubKey = empty_kernel;
971 if (!SetCTPSBTData(psbt, data)) return util::Error{Untranslated("failed to store CT PSBT data")};
972 return psbt;
973 }
974
975 util::Result<PartiallySignedTransaction> CWallet::CreateCTStealthPSBT(const CTxDestination& dest, CAmount amount, CAmount fee, int change_outputs)
976 {
977 if (!std::holds_alternative<WitnessV4StealthAddress>(dest)) {
978 return util::Error{Untranslated("not an lm2 stealth address")};
979 }
980 const auto& stealth_dest = std::get<WitnessV4StealthAddress>(dest);
981 const StealthCTAddress address{stealth_dest.view, stealth_dest.spend};
982 if (amount <= 0 || fee < 0 || change_outputs < 1) return util::Error{Untranslated("invalid amount or fee")};
983
984 const auto sel = SelectCTInputs(*this, amount + fee);
985 if (!sel) return util::Error{util::ErrorString(sel)};
986 const std::vector<std::pair<COutPoint, CTReceipt>>& inputs = *sel;
987
988 FastRandomContext rng_fast;
989 const auto res = CreateStealthTransaction(inputs, address, amount, fee, rng_fast, change_outputs);
990 if (!res) return util::Error{util::ErrorString(res)};
991
992 // Parse the built kernel (has stealth fields + the receiver's offset),
993 // then empty its signature for later finalization.
994 const auto parsed = ParseCTKernelOutput(res->tx.vout[res->kernel_index]);
995 if (!parsed) return util::Error{Untranslated("failed to parse kernel output")};
996 CTPSBTData data;
997 data.kernel = *parsed;
998 data.kernel.sig.clear();
999 data.in_receipts.clear();
1000 data.in_receipts.reserve(inputs.size());
1001 for (const auto& [op, rec] : inputs) data.in_receipts.push_back(rec);
1002 data.out_receipts = res->new_receipts;
1003 if (res->stealth_payment) {
1004 data.out_blindings_extra = {res->stealth_payment->blinding};
1005 }
1006
1007 PartiallySignedTransaction psbt{res->tx};
1008 // Strip the CT input proofs and the kernel signature from the PSBT tx:
1009 // finalization owns both. Carry each CT input's commitment script so
1010 // finalize can re-attach the proofs by prevout.
1011 for (size_t i = 0; i < res->tx.vin.size(); ++i) {
1012 const CTReceipt& rec = data.in_receipts[i];
1013 BPCommitment commitment;
1014 if (!CommitAmount(rec.Amount(), rec.blinding, commitment)) {
1015 return util::Error{Untranslated("failed to reconstruct CT input commitment")};
1016 }
1017 psbt.tx->vin[i].scriptWitness.stack.clear();
1018 psbt.inputs[i].witness_utxo = CTxOut(0, GetConfidentialScript(commitment));
1019 }
1020
1021 CScript empty_kernel;
1022 if (!BuildCTKernelScript(data.kernel, empty_kernel)) {
1023 return util::Error{Untranslated("failed to assemble empty kernel")};
1024 }
1025 psbt.tx->vout[res->kernel_index].scriptPubKey = empty_kernel;
1026 if (!SetCTPSBTData(psbt, data)) return util::Error{Untranslated("failed to store CT PSBT data")};
1027 return psbt;
1028 }
1029
1030 bool CWallet::FinalizeCTPSBT(PartiallySignedTransaction& psbt, CMutableTransaction& out)
1031 {
1032 CTPSBTData data;
1033 if (!GetCTPSBTData(psbt, data)) return false;
1034
1035 CMutableTransaction tx = *psbt.tx;
1036
1037 std::map<COutPoint, Coin> coins;
1038 for (size_t i = 0; i < tx.vin.size(); ++i) {
1039 CTxOut utxo;
1040 if (psbt.GetInputUTXO(utxo, i)) coins.emplace(tx.vin[i].prevout, Coin(utxo, 0, false));
1041 }
1042
1043 const int kidx = GetCTKernelOutputIndex(CTransaction(tx));
1044 if (kidx == NO_CT_KERNEL_OUTPUT) return false;
1045 std::vector<BPScalar> in_blindings, out_blindings;
1046 for (const auto& r : data.in_receipts) in_blindings.push_back(r.blinding);
1047 for (const auto& r : data.out_receipts) out_blindings.push_back(r.blinding);
1048 for (const auto& b : data.out_blindings_extra) out_blindings.push_back(b);
1049 BPScalar excess;
1050 if (!ComputeCTExcess(in_blindings, out_blindings, excess)) return false;
1051
1052 // The kernel message binds the transparent mint input values (attosats),
1053 // matching the consensus CheckCTTransaction derivation exactly.
1054 std::vector<CAmount> transparent_in_values;
1055 for (size_t i = 0; i < tx.vin.size(); ++i) {
1056 const auto it = coins.find(tx.vin[i].prevout);
1057 if (it == coins.end()) continue;
1058 int witver; std::vector<uint8_t> witprog;
1059 const bool is_ct = it->second.out.scriptPubKey.IsWitnessProgram(witver, witprog) &&
1060 witver == 4 && witprog.size() == WITNESS_V4_BPCT_SIZE;
1061 if (!is_ct) transparent_in_values.push_back(it->second.out.nValue * ATTOSATS_PER_SATOSHI);
1062 }
1063
1064 // Finalize the kernel FIRST: the transparent input signatures commit to
1065 // all outputs (SIGHASH_ALL), including the kernel, so the kernel script
1066 // must be final before they are signed.
1067 const uint256 msg = ComputeCTKernelMessage(CTransaction(tx), kidx, data.kernel, transparent_in_values);
1068 std::vector<uint8_t> sig;
1069 if (!CreateCTKernelSig(excess, {msg.begin(), msg.end()}, sig)) return false;
1070 data.kernel.sig = std::move(sig);
1071 CScript ks;
1072 if (!BuildCTKernelScript(data.kernel, ks)) return false;
1073 tx.vout[kidx].scriptPubKey = std::move(ks);
1074
1075 // Attach the CT input proofs (in order of CT inputs).
1076 size_t ct_in = 0;
1077 for (size_t i = 0; i < tx.vin.size() && ct_in < data.in_receipts.size(); ++i) {
1078 const auto it = coins.find(tx.vin[i].prevout);
1079 if (it == coins.end()) continue;
1080 int witver; std::vector<uint8_t> witprog;
1081 if (it->second.out.scriptPubKey.IsWitnessProgram(witver, witprog) &&
1082 witver == 4 && witprog.size() == WITNESS_V4_BPCT_SIZE) {
1083 tx.vin[i].scriptWitness.stack.clear();
1084 tx.vin[i].scriptWitness.stack.push_back(SerializeBulletproof(data.in_receipts[ct_in].proof));
1085 ++ct_in;
1086 }
1087 }
1088
1089 // Transparent inputs last: signable from the PSBT UTXOs against the
1090 // finalized kernel. CT inputs have no script signature (the kernel
1091 // excess authorizes them), so only the transparent subset is signed.
1092 std::map<COutPoint, Coin> transparent_coins;
1093 for (const auto& [op, coin] : coins) {
1094 int witver; std::vector<uint8_t> witprog;
1095 const bool is_ct = coin.out.scriptPubKey.IsWitnessProgram(witver, witprog) &&
1096 witver == 4 && witprog.size() == WITNESS_V4_BPCT_SIZE;
1097 if (!is_ct) transparent_coins.emplace(op, coin);
1098 }
1099 if (!transparent_coins.empty()) {
1100 LOCK(cs_wallet);
1101 std::map<int, bilingual_str> input_errors;
1102 std::optional<CAmount> inputs_sum;
1103 if (!SignTransaction(tx, transparent_coins, SIGHASH_ALL, input_errors, &inputs_sum)) {
1104 return false;
1105 }
1106 }
1107
1108 out = std::move(tx);
1109 return true;
1110 }
1111
1112 } // namespace wallet
1113