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