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