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 <consensus/amount.h>
   6  #include <core_io.h>
   7  #include <psbt.h>
   8  #include <key_io.h>
   9  #include <rpc/util.h>
  10  #include <util/moneystr.h>
  11  #include <wallet/ct.h>
  12  #include <wallet/spend.h>
  13  #include <wallet/rpc/util.h>
  14  #include <wallet/wallet.h>
  15  #include <wallet/walletdb.h>
  16  
  17  #include <limits>
  18  #include <tinyformat.h>
  19  
  20  #include <univalue.h>
  21  
  22  namespace wallet {
  23  
  24  static CAmount EstimateCTFee(const CWallet& wallet, int vin_count, int vout_count)
  25  {
  26      // 10 (base) + transparent in (68) + CT outputs (35) + kernel (86) +
  27      // CT input proof witnesses (754/4 rounded up).
  28      const int vsize = 10 + vin_count * (68 + 189) + vout_count * 35 + 86;
  29      CFeeRate feerate = wallet.chain().mempoolMinFee();
  30      if (feerate == CFeeRate(0)) feerate = CFeeRate(1000); // fallback 1 sat/vB
  31      const CAmount fee_sats = feerate.GetFee(vsize);
  32      return std::max(fee_sats, CAmount{1}) * ATTOSATS_PER_SATOSHI;
  33  }
  34  
  35  static CAmount AmountFromAttosatValue(const UniValue& value)
  36  {
  37      // STR only: numeric JSON travels through UniValue's double storage and
  38      // silently loses precision beyond ~15 significant digits (26-decimal λ
  39      // amounts need all 128 bits).
  40      if (!value.isStr()) {
  41          throw JSONRPCError(RPC_TYPE_ERROR, "Amount must be a string (full 128-bit precision)");
  42      }
  43      CAmount amount_attosats;
  44      if (!ParseAttosatsString(value.getValStr(), amount_attosats)) {
  45          throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
  46      }
  47      if (amount_attosats <= 0) {
  48          throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive");
  49      }
  50      return amount_attosats;
  51  }
  52  
  53  RPCHelpMan getnewstealthaddress()
  54  {
  55      return RPCHelpMan{"getnewstealthaddress",
  56                  "\nReturns the wallet's stealth CT address (lm2).  Payments to it are\n"
  57                  "confidential: amounts and the receiver identity stay hidden on-chain.\n"
  58                  "The address is deterministic for the wallet (derived from its master\n"
  59                  "key), so it never needs to be regenerated.\n",
  60                  {},
  61                  RPCResult{
  62                      RPCResult::Type::OBJ, "", "",
  63                      {
  64                          {RPCResult::Type::STR, "address", "The lm2 stealth address"},
  65                          {RPCResult::Type::STR, "view_pubkey", "The view (scan) public key"},
  66                          {RPCResult::Type::STR, "spend_pubkey", "The spend public key"},
  67                      }},
  68                  RPCExamples{
  69                      HelpExampleCli("getnewstealthaddress", "") +
  70                      HelpExampleRpc("getnewstealthaddress", "")},
  71                  [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
  72                  {
  73                      std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
  74                      if (!wallet) return UniValue::VNULL;
  75  
  76                      // Pre-activation P2BPCT (v4) outputs are anyone-can-spend:
  77                      // refuse to hand out lm2 addresses until the fork gate.
  78                      if (Params().GetChainType() == ChainType::FORK) {
  79                          const int64_t mtp = wallet->chain().getTipMtp();
  80                          if (mtp + Params().GetConsensus().nForkActivationBias < Params().GetConsensus().nForkActivationMTP) {
  81                              throw JSONRPCError(RPC_WALLET_ERROR, "lm2 stealth addresses cannot be created before fork activation: their outputs would be spendable by anyone");
  82                          }
  83                      }
  84  
  85                      LOCK(wallet->cs_wallet);
  86                      CKey view, spend;
  87                      if (!wallet->GetStealthKeys(view, spend)) {
  88                          throw JSONRPCError(RPC_WALLET_ERROR, "This wallet has no stealth keys");
  89                      }
  90                      const auto dest = wallet->GetStealthDestination();
  91                      UniValue result(UniValue::VOBJ);
  92                      result.pushKV("address", EncodeDestination(dest));
  93                      result.pushKV("view_pubkey", HexStr(dest.view));
  94                      result.pushKV("spend_pubkey", HexStr(dest.spend));
  95                      return result;
  96                  }};
  97  }
  98  
  99  RPCHelpMan listctreceipts()
 100  {
 101      return RPCHelpMan{"listctreceipts",
 102                  "\nList the confidential (CT) outputs this wallet can spend: created,\n"
 103                  "changed, and stealth-recovered, with their hidden attosat amounts.\n",
 104                  {},
 105                  RPCResult{
 106                      RPCResult::Type::ARR, "", "",
 107                      {
 108                          {RPCResult::Type::OBJ, "", "",
 109                          {
 110                              {RPCResult::Type::STR, "txid", "The transaction that created the output"},
 111                              {RPCResult::Type::NUM, "vout", "The output index"},
 112                              {RPCResult::Type::STR, "amount", "The committed amount in λ (26 decimals)"},
 113                          }},
 114                      }},
 115                  RPCExamples{
 116                      HelpExampleCli("listctreceipts", "") +
 117                      HelpExampleRpc("listctreceipts", "")},
 118                  [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
 119                  {
 120                      std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
 121                      if (!wallet) return UniValue::VNULL;
 122  
 123                      LOCK(wallet->cs_wallet);
 124                      WalletBatch batch{wallet->GetDatabase()};
 125                      std::map<uint256, std::vector<CTReceipt>> all;
 126                      if (!batch.ListCTReceipts(all)) {
 127                          throw JSONRPCError(RPC_WALLET_ERROR, "Failed to enumerate CT receipts");
 128                      }
 129                      UniValue result(UniValue::VARR);
 130                      for (const auto& [txid, receipts] : all) {
 131                          for (const auto& receipt : receipts) {
 132                              UniValue entry(UniValue::VOBJ);
 133                              entry.pushKV("txid", txid.ToString());
 134                              entry.pushKV("vout", uint64_t(receipt.vout_index));
 135                              entry.pushKV("amount", AttosatsToString(receipt.Amount()));
 136                              result.push_back(std::move(entry));
 137                          }
 138                      }
 139                      return result;
 140                  }};
 141  }
 142  
 143  RPCHelpMan sendtostealth()
 144  {
 145      return RPCHelpMan{"sendtostealth",
 146                  "\nSend a confidential payment to an lm2 stealth address.  Selects\n"
 147                  "spendable CT outputs, pays the given amount in λ, and returns the\n"
 148                  "transaction id.  Unspent surplus becomes confidential change.\n",
 149                  {
 150                      {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The lm2 stealth address to pay"},
 151                      {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "The amount in λ (26 decimal places)"},
 152                      {"fee", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The explicit fee in λ (default: feerate estimate)"},
 153                      {"change_outputs", RPCArg::Type::NUM, RPCArg::Default{1}, "Number of confidential change outputs to split the surplus into (1-16, random widely-varying sizes)"},
 154                  },
 155                  RPCResult{
 156                      RPCResult::Type::OBJ, "", "",
 157                      {
 158                          {RPCResult::Type::STR_HEX, "txid", "The transaction id"},
 159                      }},
 160                  RPCExamples{
 161                      HelpExampleCli("sendtostealth", "\"lm2...\" 0.1") +
 162                      HelpExampleRpc("sendtostealth", "\"lm2...\", 0.1")},
 163                  [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
 164                  {
 165                      std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
 166                      if (!wallet) return UniValue::VNULL;
 167  
 168                      const CTxDestination dest = DecodeDestination(request.params[0].get_str());
 169                      if (!std::holds_alternative<WitnessV4StealthAddress>(dest)) {
 170                          throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid lm2 stealth address");
 171                      }
 172                      const CAmount amount = AmountFromAttosatValue(request.params[1]);
 173                      CAmount fee = EstimateCTFee(*wallet, /*vin=*/1, /*vout=*/1);
 174                      if (!request.params[2].isNull()) {
 175                          fee = AmountFromAttosatValue(request.params[2]);
 176                      }
 177  
 178                      int change_outputs = request.params[3].isNull() ? 1 : request.params[3].getInt<int>();
 179                      const auto txid = wallet->SendStealthPayment(dest, amount, fee, change_outputs);
 180                      if (!txid) {
 181                          throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(txid).original);
 182                      }
 183  
 184                      UniValue result(UniValue::VOBJ);
 185                      result.pushKV("txid", txid->ToString());
 186                      return result;
 187                  }};
 188  }
 189  
 190  RPCHelpMan mintct()
 191  {
 192      return RPCHelpMan{"mintct",
 193                  "\nMint transparent value into confidential (CT) outputs.  Spends\n"
 194                  "transparent wallet coins and creates a confidential output of the\n"
 195                  "given amount in λ.  The surplus becomes a second confidential\n"
 196                  "output (change).\n",
 197                  {
 198                      {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "The amount to mint, in λ (26 decimal places)"},
 199                      {"fee", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The explicit fee in λ (default: feerate estimate)"},
 200                      {"outputs", RPCArg::Type::NUM, RPCArg::Default{2}, "Number of confidential outputs to split the minted value into (1-16, random widely-varying sizes; all outputs are confidential)"},
 201                  },
 202                  RPCResult{
 203                      RPCResult::Type::OBJ, "", "",
 204                      {
 205                          {RPCResult::Type::STR_HEX, "txid", "The transaction id"},
 206                      }},
 207                  RPCExamples{
 208                      HelpExampleCli("mintct", "0.5") +
 209                      HelpExampleRpc("mintct", "0.5")},
 210                  [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
 211                  {
 212                      std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
 213                      if (!wallet) return UniValue::VNULL;
 214  
 215                      const CAmount amount = AmountFromAttosatValue(request.params[0]);
 216                      CAmount fee = EstimateCTFee(*wallet, /*vin=*/1, /*vout=*/1);
 217                      if (!request.params[1].isNull()) {
 218                          fee = AmountFromAttosatValue(request.params[1]);
 219                      }
 220  
 221                      int output_count = request.params[2].isNull() ? 2 : request.params[2].getInt<int>();
 222                      const auto txid = wallet->MintConfidential(amount, fee, output_count);
 223                      if (!txid) {
 224                          throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(txid).original);
 225                      }
 226  
 227                      UniValue result(UniValue::VOBJ);
 228                      result.pushKV("txid", txid->ToString());
 229                      return result;
 230                  }};
 231  }
 232  
 233  RPCHelpMan createctpsbt()
 234  {
 235      return RPCHelpMan{"createctpsbt",
 236                  "\nBuild an unsigned confidential-transaction PSBT for a mint.\n"
 237                  "The transparent inputs are signable by any signer (including a\n"
 238                  "hardware wallet); the kernel signature and CT proofs are filled\n"
 239                  "by finalizectpsbt.  The PSBT carries blindings, so treat it as\n"
 240                  "sensitive material.\n",
 241                  {
 242                      {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "The amount to mint, in λ (26 decimal places)"},
 243                      {"fee", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The explicit fee in λ (default: feerate estimate)"},
 244                      {"outputs", RPCArg::Type::NUM, RPCArg::Default{2}, "Number of confidential outputs to split the minted value into (1-16)"},
 245                  },
 246                  RPCResult{
 247                      RPCResult::Type::STR, "psbt", "The base64-encoded unsigned PSBT"},
 248                  RPCExamples{
 249                      HelpExampleCli("createctpsbt", "\"10\"") +
 250                      HelpExampleRpc("createctpsbt", "\"10\"")},
 251                  [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
 252                  {
 253                      std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
 254                      if (!wallet) return UniValue::VNULL;
 255  
 256                      const CAmount amount = AmountFromAttosatValue(request.params[0]);
 257                      CAmount fee = EstimateCTFee(*wallet, /*vin=*/1, /*vout=*/1);
 258                      if (!request.params[1].isNull()) {
 259                          fee = AmountFromAttosatValue(request.params[1]);
 260                      }
 261                      const int output_count = request.params[2].isNull() ? 2 : request.params[2].getInt<int>();
 262  
 263                      const auto psbt = wallet->CreateCTMintPSBT(amount, fee, output_count);
 264                      if (!psbt) {
 265                          throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(psbt).original);
 266                      }
 267                      DataStream ss;
 268                      ss << *psbt;
 269                      return EncodeBase64(ss);
 270                  }};
 271  }
 272  
 273  RPCHelpMan finalizectpsbt()
 274  {
 275      return RPCHelpMan{"finalizectpsbt",
 276                  "\nFinalize a confidential-transaction PSBT: signs the transparent\n"
 277                  "inputs, attaches the CT proofs, and signs the kernel.  Returns the\n"
 278                  "fully-signed transaction as hex.\n",
 279                  {
 280                      {"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "The base64-encoded CT PSBT"},
 281                  },
 282                  RPCResult{
 283                      RPCResult::Type::STR_HEX, "hex", "The hex-encoded finalized transaction"},
 284                  RPCExamples{
 285                      HelpExampleCli("finalizectpsbt", "\"psbt\"") +
 286                      HelpExampleRpc("finalizectpsbt", "\"psbt\"")},
 287                  [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
 288                  {
 289                      std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
 290                      if (!wallet) return UniValue::VNULL;
 291  
 292                      PartiallySignedTransaction psbt;
 293                      std::string error;
 294                      if (!DecodeBase64PSBT(psbt, request.params[0].get_str(), error)) {
 295                          throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed: %s", error));
 296                      }
 297  
 298                      CMutableTransaction mtx;
 299                      if (!wallet->FinalizeCTPSBT(psbt, mtx)) {
 300                          throw JSONRPCError(RPC_WALLET_ERROR, "Failed to finalize CT PSBT (missing CT payload, zero excess, or signing failure)");
 301                      }
 302  
 303                      DataStream ss;
 304                      ss << TX_WITH_WITNESS(CTransaction(mtx));
 305                      return HexStr(ss);
 306                  }};
 307  }
 308  
 309  RPCHelpMan createstealthpsbt()
 310  {
 311      return RPCHelpMan{"createstealthpsbt",
 312                  "\nBuild an unsigned confidential-transaction PSBT that spends the\n"
 313                  "wallet's own CT outputs to an lm2 stealth address, with\n"
 314                  "confidential change.  The kernel (which carries the encrypted\n"
 315                  "amount and blinding fields) is signed at finalization.  The PSBT\n"
 316                  "carries blindings, so treat it as sensitive material.\n",
 317                  {
 318                      {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The lm2 stealth address to pay"},
 319                      {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "The amount in λ (26 decimal places)"},
 320                      {"fee", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The explicit fee in λ (default: feerate estimate)"},
 321                      {"change_outputs", RPCArg::Type::NUM, RPCArg::Default{1}, "Number of confidential change outputs (1-16)"},
 322                  },
 323                  RPCResult{
 324                      RPCResult::Type::STR, "psbt", "The base64-encoded unsigned PSBT"},
 325                  RPCExamples{
 326                      HelpExampleCli("createstealthpsbt", "\"lm2...\" 0.1") +
 327                      HelpExampleRpc("createstealthpsbt", "\"lm2...\", 0.1")},
 328                  [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
 329                  {
 330                      std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
 331                      if (!wallet) return UniValue::VNULL;
 332  
 333                      const CTxDestination dest = DecodeDestination(request.params[0].get_str());
 334                      if (!std::holds_alternative<WitnessV4StealthAddress>(dest)) {
 335                          throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid lm2 stealth address");
 336                      }
 337                      const CAmount amount = AmountFromAttosatValue(request.params[1]);
 338                      CAmount fee = EstimateCTFee(*wallet, /*vin=*/1, /*vout=*/1);
 339                      if (!request.params[2].isNull()) {
 340                          fee = AmountFromAttosatValue(request.params[2]);
 341                      }
 342                      const int change_outputs = request.params[3].isNull() ? 1 : request.params[3].getInt<int>();
 343  
 344                      const auto psbt = wallet->CreateCTStealthPSBT(dest, amount, fee, change_outputs);
 345                      if (!psbt) {
 346                          throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(psbt).original);
 347                      }
 348                      DataStream ss;
 349                      ss << *psbt;
 350                      return EncodeBase64(ss);
 351                  }};
 352  }
 353  
 354  } // namespace wallet
 355