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