OpenCBDC Transaction Processor
Loading...
Searching...
No Matches
wallet.cpp
Go to the documentation of this file.
1// Copyright (c) 2021 MIT Digital Currency Initiative,
2// Federal Reserve Bank of Boston
3// Distributed under the MIT software license, see the accompanying
4// file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
6#include "wallet.hpp"
7
13
14#include <secp256k1_schnorrsig.h>
15
16namespace cbdc {
18 auto seed = std::chrono::high_resolution_clock::now()
19 .time_since_epoch()
20 .count();
21 seed %= std::numeric_limits<uint32_t>::max();
22 m_shuffle.seed(static_cast<uint32_t>(seed));
23 }
24
25 auto transaction::wallet::mint_new_coins(const size_t n_outputs,
26 const uint32_t output_val)
29
30 for(size_t i = 0; i < n_outputs; i++) {
32
33 const auto pubkey = generate_key();
34
37
38 out.m_value = output_val;
39
40 ret.m_outputs.push_back(out);
41 }
42
43 return ret;
44 }
45
46 auto transaction::wallet::send_to(const uint32_t amount,
47 const pubkey_t& payee,
48 bool sign_tx)
49 -> std::optional<transaction::full_tx> {
50 auto maybe_tx = accumulate_inputs(amount);
51 if(!maybe_tx.has_value()) {
52 return std::nullopt;
53 }
54
55 auto& ret = maybe_tx.value().first;
56 auto total_amount = maybe_tx.value().second;
57
58 transaction::output destination_out;
59 destination_out.m_value = amount;
60
61 destination_out.m_witness_program_commitment
63 ret.m_outputs.push_back(destination_out);
64
65 if(total_amount > amount) {
66 // Add the change output if we need to
67 transaction::output change_out;
68 change_out.m_value = total_amount - amount;
69 const auto pubkey = generate_key();
72 ret.m_outputs.push_back(change_out);
73 }
74
75 if(sign_tx) {
76 sign(ret);
77 }
78
79 return ret;
80 }
81
83 -> std::optional<transaction::full_tx> {
84 if(m_seed_from == m_seed_to) {
85 return std::nullopt;
86 }
88 tx.m_inputs.resize(1);
89 tx.m_outputs.resize(1);
90 tx.m_inputs[0].m_prevout.m_tx_id = {0};
91 tx.m_inputs[0].m_prevout_data.m_value = m_seed_value;
92 tx.m_inputs[0].m_prevout_data.m_witness_program_commitment = {0};
93 tx.m_outputs[0].m_witness_program_commitment
94 = m_seed_witness_commitment;
95 tx.m_outputs[0].m_value = m_seed_value;
96 tx.m_inputs[0].m_prevout.m_index = seed_idx;
97 return tx;
98 }
99
100 auto transaction::wallet::create_seeded_input(size_t seed_idx)
101 -> std::optional<transaction::input> {
102 if(auto tx = create_seeded_transaction(seed_idx)) {
103 const auto& tx_id = transaction::tx_id(tx.value());
104 return transaction::input_from_output(tx.value(), 0, tx_id);
105 }
106 return std::nullopt;
107 }
108
110 const transaction::full_tx& send_tx,
111 const pubkey_t& payee) -> std::vector<transaction::input> {
112 auto wit_comm
114 auto ret = std::vector<input>();
115 for(uint32_t i = 0; i < send_tx.m_outputs.size(); i++) {
116 if(send_tx.m_outputs[i].m_witness_program_commitment == wit_comm) {
117 ret.push_back(
118 transaction::input_from_output(send_tx, i).value());
119 }
120 }
121 return ret;
122 }
123
125 // Unique lock on m_keys, m_keygen and m_keygen_dist
126 {
127 // TODO: add config parameter where 0 = never reuse.
128 static constexpr size_t max_keys = 10000;
129 std::shared_lock<std::shared_mutex> lg(m_keys_mut);
130 if(m_keys.size() > max_keys) {
131 std::uniform_int_distribution<size_t> keyshuffle_dist(
132 0,
133 m_keys.size() - 1);
134 const auto index = keyshuffle_dist(m_shuffle);
135 return m_pubkeys[index];
136 }
137 }
138
139 std::uniform_int_distribution<unsigned char> keygen;
140
141 privkey_t seckey;
142 for(auto&& b : seckey) {
143 b = keygen(*m_random_source);
144 }
145 pubkey_t ret = pubkey_from_privkey(seckey, m_secp.get());
146 {
147 std::unique_lock<std::shared_mutex> lg(m_keys_mut);
148 m_pubkeys.push_back(ret);
149 m_keys.insert({ret, seckey});
150 m_witness_programs.insert(
152 ret});
153 }
154
155 return ret;
156 }
157
159 // TODO: other sighash types besides SIGHASH_ALL?
160 const auto sighash = transaction::tx_id(tx);
161 tx.m_witness.resize(tx.m_inputs.size());
162
163 for(size_t i = 0; i < tx.m_inputs.size(); i++) {
164 const auto& wit_commit
165 = tx.m_inputs[i].m_prevout_data.m_witness_program_commitment;
166
167 privkey_t seckey{};
168 pubkey_t pubkey{};
169 bool key_ours = false;
170 {
171 std::shared_lock<std::shared_mutex> sl(m_keys_mut);
172 const auto wit_prog = m_witness_programs.find(wit_commit);
173 key_ours = wit_prog != m_witness_programs.end();
174 if(key_ours) {
175 pubkey = wit_prog->second;
176 seckey = m_keys.at(pubkey);
177 }
178 }
179
180 if(key_ours) {
181 auto& sig = tx.m_witness[i];
182 sig.resize(transaction::validation::p2pk_witness_len);
183 sig[0] = std::byte(
185 std::memcpy(
186 &sig[sizeof(
188 pubkey.data(),
189 pubkey.size());
190
191 secp256k1_keypair keypair{};
192 [[maybe_unused]] const auto ret
193 = secp256k1_keypair_create(m_secp.get(),
194 &keypair,
195 seckey.data());
196 assert(ret == 1);
197
198 std::array<unsigned char, sig_len> sig_arr{};
199 [[maybe_unused]] const auto sign_ret
200 = secp256k1_schnorrsig_sign(m_secp.get(),
201 sig_arr.data(),
202 sighash.data(),
203 &keypair,
204 nullptr,
205 nullptr);
206 std::memcpy(
207 &sig[transaction::validation::p2pk_witness_prog_len],
208 sig_arr.data(),
209 sizeof(sig_arr));
210 assert(sign_ret == 1);
211 }
212 }
213 }
214
215 void transaction::wallet::update_balance(
216 const std::vector<transaction::input>& credits,
217 const std::vector<transaction::input>& debits) {
218 std::unique_lock<std::shared_mutex> lu(m_utxos_mut);
219 for(const auto& inp : credits) {
220 const auto added = m_utxos_set.insert(inp);
221 if(added.second) {
222 m_balance += inp.m_prevout_data.m_value;
223 m_spend_queue.push_back(inp);
224 }
225 }
226
227 for(const auto& inp : debits) {
228 const auto erased = m_utxos_set.erase(inp) > 0;
229 if(erased) {
230 m_balance -= inp.m_prevout_data.m_value;
231 }
232 }
233 assert(m_spend_queue.size() == m_utxos_set.size());
234 }
235
237 uint32_t value,
238 size_t begin_seed,
239 size_t end_seed) -> bool {
240 if(end_seed <= begin_seed) {
241 return false;
242 }
243
244 pubkey_t pubkey = pubkey_from_privkey(privkey, m_secp.get());
245 auto witness_commitment
247 {
248 std::unique_lock<std::shared_mutex> lg(m_keys_mut);
249 if(!m_keys.empty()) {
250 return false;
251 }
252 m_pubkeys.push_back(pubkey);
253 m_keys.insert({pubkey, privkey});
254 m_witness_programs.insert({witness_commitment, pubkey});
255 }
256 seed_readonly(witness_commitment, value, begin_seed, end_seed);
257 return true;
258 }
259
260 void transaction::wallet::seed_readonly(const hash_t& witness_commitment,
261 uint32_t value,
262 size_t begin_seed,
263 size_t end_seed) {
264 m_seed_from = begin_seed;
265 m_seed_to = end_seed;
266 m_seed_value = value;
267 m_seed_witness_commitment = witness_commitment;
268 }
269
270 void
272 const auto tx_id = transaction::tx_id(tx);
273 std::vector<transaction::input> new_utxos;
274 {
275 std::shared_lock<std::shared_mutex> sl(m_keys_mut);
276 for(uint32_t i = 0; i < tx.m_outputs.size(); i++) {
277 const auto& out = tx.m_outputs[i];
278 if(m_witness_programs.find(out.m_witness_program_commitment)
279 != m_witness_programs.end()) {
280 new_utxos.push_back(
281 transaction::input_from_output(tx, i, tx_id).value());
282 }
283 }
284 }
285 update_balance(new_utxos, tx.m_inputs);
286 }
287
288 auto transaction::wallet::balance() const -> uint64_t {
289 std::shared_lock<std::shared_mutex> lg(m_utxos_mut);
290 // TODO: handle overflow
291 auto balance = m_balance;
292 if(m_seed_from != m_seed_to) {
293 balance += (m_seed_to - m_seed_from) * m_seed_value;
294 }
295 return balance;
296 }
297
298 auto transaction::wallet::count() const -> size_t {
299 std::shared_lock<std::shared_mutex> lg(m_utxos_mut);
300 auto size = m_utxos_set.size();
301 if(m_seed_from != m_seed_to) {
302 size += (m_seed_to - m_seed_from);
303 }
304 return size;
305 }
306
307 void transaction::wallet::save(const std::string& wallet_file) const {
308 std::ofstream wal_file(wallet_file,
309 std::ios::binary | std::ios::trunc
310 | std::ios::out);
311 if(!wal_file.good()) {
312 // TODO: add a logger to wallet or give the save/load function
313 // return
315 std::exit(EXIT_FAILURE);
316 }
317 auto ser = ostream_serializer(wal_file);
318 {
319 std::shared_lock<std::shared_mutex> lk(m_keys_mut);
320 ser << m_keys;
321 }
322
323 {
324 std::shared_lock<std::shared_mutex> lu(m_utxos_mut);
325 ser << m_utxos_set;
326 }
327 }
328
329 void transaction::wallet::load(const std::string& wallet_file) {
330 std::ifstream wal_file(wallet_file, std::ios::binary | std::ios::in);
331 if(wal_file.good()) {
332 auto deser = istream_serializer(wal_file);
333 {
334 std::unique_lock<std::shared_mutex> lk(m_keys_mut);
335
336 m_keys.clear();
337 m_pubkeys.clear();
338 m_witness_programs.clear();
339
340 deser >> m_keys;
341 for(const auto& k : m_keys) {
342 m_pubkeys.push_back(k.first);
343 m_witness_programs.insert(
345 k.first),
346 k.first});
347 }
348 }
349
350 {
351 std::unique_lock<std::shared_mutex> lu(m_utxos_mut);
352
353 m_utxos_set.clear();
354 m_spend_queue.clear();
355 m_balance = 0;
356
357 deser >> m_utxos_set;
358 for(const auto& utxo : m_utxos_set) {
359 m_balance += utxo.m_prevout_data.m_value;
360 m_spend_queue.push_back(utxo);
361 }
362 }
363 }
364 }
365
366 auto transaction::wallet::send_to(size_t input_count,
367 size_t output_count,
368 const pubkey_t& payee,
369 bool sign_tx)
370 -> std::optional<transaction::full_tx> {
371 assert(input_count > 0);
372 assert(output_count > 0);
373
374 // TODO: handle overflow with large output values.
375 uint64_t total_amount = 0;
377 uint64_t output_val{};
378
379 {
380 std::unique_lock<std::shared_mutex> ul(m_utxos_mut);
381 if((m_utxos_set.size() + m_seed_to - m_seed_from) < input_count) {
382 return std::nullopt;
383 }
384
385 ret.m_inputs.reserve(input_count);
386
387 size_t seeded_inputs = 0;
388 while(m_seed_from != m_seed_to
389 && ret.m_inputs.size() < input_count) {
390 auto seed_utxo = create_seeded_input(m_seed_from);
391 if(!seed_utxo) {
392 break;
393 }
394 ret.m_inputs.push_back(seed_utxo.value());
395 ret.m_witness.emplace_back(sig_len, std::byte(0));
396 total_amount += m_seed_value;
397 m_seed_from++;
398 seeded_inputs++;
399 }
400
401 for(auto utxo = m_spend_queue.begin();
402 (utxo != m_spend_queue.end())
403 && (ret.m_inputs.size() < input_count);
404 utxo++) {
405 ret.m_inputs.push_back(*utxo);
406 total_amount += utxo->m_prevout_data.m_value;
407 }
408
409 output_val = total_amount / output_count;
410 if(output_val == 0 && output_count > 1) {
411 // Caller asked for more outputs than we can make with the
412 // amount of coins we have.
413 m_seed_from -= seeded_inputs;
414 return std::nullopt;
415 }
416
417 for(size_t i = seeded_inputs; i < ret.m_inputs.size(); i++) {
418 auto& inp = ret.m_inputs[i];
419 m_balance -= inp.m_prevout_data.m_value;
420 m_utxos_set.erase(inp);
421 m_spend_queue.pop_front();
422 }
423 }
424
425 auto wit_comm
427 ret.m_outputs.reserve(output_count);
428 for(size_t i{0}; i < output_count; i++) {
429 transaction::output send_out;
430 if(i == output_count - 1) {
431 send_out.m_value = total_amount;
432 } else {
433 send_out.m_value = output_val;
434 }
435 total_amount -= send_out.m_value;
436 send_out.m_witness_program_commitment = wit_comm;
437 ret.m_outputs.push_back(send_out);
438 }
439
440 assert(total_amount == 0);
441
442 if(sign_tx) {
443 sign(ret);
444 }
445
446 return ret;
447 }
448
450 const std::vector<transaction::input>& credits) {
451 update_balance(credits, {});
452 }
453
454 auto transaction::wallet::fan(size_t output_count,
455 uint32_t value,
456 const pubkey_t& payee,
457 bool sign_tx)
458 -> std::optional<transaction::full_tx> {
459 const uint64_t amount = output_count * value;
460 auto maybe_tx = accumulate_inputs(amount);
461 if(!maybe_tx.has_value()) {
462 return std::nullopt;
463 }
464
465 auto& ret = maybe_tx.value().first;
466 auto total_amount = maybe_tx.value().second;
467
468 if(total_amount > amount) {
469 // Add the change output if we need to
470 transaction::output change_out;
471 change_out.m_value = total_amount - amount;
472 const auto pubkey = generate_key();
475 ret.m_outputs.push_back(change_out);
476 }
477
478 transaction::output destination_out;
479 destination_out.m_value = value;
480
481 destination_out.m_witness_program_commitment
483 for(size_t i{0}; i < output_count; i++) {
484 ret.m_outputs.push_back(destination_out);
485 }
486
487 if(sign_tx) {
488 sign(ret);
489 }
490
491 return ret;
492 }
493
494 auto transaction::wallet::accumulate_inputs(uint64_t amount)
495 -> std::optional<std::pair<full_tx, uint64_t>> {
496 uint64_t total_amount = 0;
497 auto ret = full_tx();
498 {
499 std::unique_lock<std::shared_mutex> ul(m_utxos_mut);
500 size_t seeded_inputs = 0;
501 while(m_seed_from != m_seed_to && total_amount < amount) {
502 auto seed_utxo = create_seeded_input(m_seed_from);
503 if(!seed_utxo) {
504 break;
505 }
506 ret.m_inputs.push_back(seed_utxo.value());
507 ret.m_witness.emplace_back(sig_len, std::byte(0));
508 total_amount += m_seed_value;
509 m_seed_from++;
510 seeded_inputs++;
511 }
512
513 auto utxo = m_spend_queue.begin();
514 while((total_amount < amount) && (utxo != m_spend_queue.end())) {
515 ret.m_inputs.push_back(*utxo);
516 ret.m_witness.emplace_back(sig_len, std::byte(0));
517 total_amount += utxo->m_prevout_data.m_value;
518 std::advance(utxo, 1);
519 }
520
521 if(total_amount < amount) {
522 m_seed_from -= seeded_inputs;
523 return std::nullopt;
524 }
525
526 for(size_t i = seeded_inputs; i < ret.m_inputs.size(); i++) {
527 const auto del_utxo = m_spend_queue.begin();
528 m_balance -= del_utxo->m_prevout_data.m_value;
529 m_utxos_set.erase(*del_utxo);
530 m_spend_queue.pop_front();
531 }
532 }
533 return {{ret, total_amount}};
534 }
535
537 -> bool {
538 const auto& in_key = in.m_prevout_data.m_witness_program_commitment;
539 bool key_ours = false;
540 {
541 std::shared_lock<std::shared_mutex> sl(m_keys_mut);
542 const auto wit_prog = m_witness_programs.find(in_key);
543 key_ours = wit_prog != m_witness_programs.end();
544 }
545 return key_ours;
546 }
547}
Implementation of serializer for reading from a std::istream.
Implementation of serializer for writing to a std::ostream.
void confirm_inputs(const std::vector< input > &credits)
Given a set of credit inputs, add the UTXOs and update the wallet's balance.
Definition wallet.cpp:449
void seed_readonly(const hash_t &witness_commitment, uint32_t value, size_t begin_seed, size_t end_seed)
Marks the wallet as having read-only pre-seeded outputs to spend.
Definition wallet.cpp:260
auto generate_key() -> pubkey_t
Generates a new public key at which this wallet can receive payments via send_to.
Definition wallet.cpp:124
void load(const std::string &wallet_file)
Overwrites the current state of the wallet with data loaded from a file saved via the Wallet::save fu...
Definition wallet.cpp:329
void confirm_transaction(const full_tx &tx)
Confirms a transaction.
Definition wallet.cpp:271
auto mint_new_coins(size_t n_outputs, uint32_t output_val) -> full_tx
Mints new spendable outputs.
Definition wallet.cpp:25
auto count() const -> size_t
Returns the number of UTXOs stored in this wallet.
Definition wallet.cpp:298
static auto export_send_inputs(const full_tx &send_tx, const pubkey_t &payee) -> std::vector< input >
Extracts the transaction data that recipients need from senders to confirm pending transfers.
Definition wallet.cpp:109
auto send_to(uint32_t amount, const pubkey_t &payee, bool sign_tx) -> std::optional< full_tx >
Generates a new send transaction with a set value.
Definition wallet.cpp:46
void sign(full_tx &tx) const
Signs each of the transaction's inputs using Schnorr signatures.
Definition wallet.cpp:158
void save(const std::string &wallet_file) const
Save the state of the wallet to a binary data file.
Definition wallet.cpp:307
auto is_spendable(const input &in) const -> bool
Checks if the input is spendable by the current wallet.
Definition wallet.cpp:536
auto create_seeded_transaction(size_t seed_idx) -> std::optional< full_tx >
Creates a new transaction from seeded outputs.
Definition wallet.cpp:82
auto fan(size_t output_count, uint32_t value, const pubkey_t &payee, bool sign_tx) -> std::optional< transaction::full_tx >
Generates a transaction sending multiple outputs of a set value.
Definition wallet.cpp:454
auto seed(const privkey_t &privkey, uint32_t value, size_t begin_seed, size_t end_seed) -> bool
Marks the wallet as having pre-seeded outputs to spend.
Definition wallet.cpp:236
wallet()
Constructor.
Definition wallet.cpp:17
auto balance() const -> uint64_t
Returns the total balance of the wallet, e.g.
Definition wallet.cpp:288
auto get_p2pk_witness_commitment(const pubkey_t &payee) -> hash_t
witness_program_type
Specifies how validators should interpret the witness program.
auto tx_id(const full_tx &tx) noexcept -> hash_t
Calculates the unique hash of a full transaction.
auto input_from_output(const full_tx &tx, size_t i, const hash_t &txid) -> std::optional< input >
Converts the output at the specified index to an input.
std::array< unsigned char, cbdc::hash_size > hash_t
SHA256 hash container.
std::array< unsigned char, pubkey_len > privkey_t
A private key of a public/private keypair.
Definition keys.hpp:23
auto pubkey_from_privkey(const privkey_t &privkey, secp256k1_context *ctx) -> pubkey_t
Generates a public key from the specified private key.
Definition keys.cpp:12
std::array< unsigned char, pubkey_len > pubkey_t
A public key of a public/private keypair.
Definition keys.hpp:25
A complete transaction.
std::vector< input > m_inputs
The set of inputs for the transaction.
std::vector< output > m_outputs
The set of new outputs created by the transaction.
std::vector< witness_t > m_witness
The set of witnesses.
An input for a new transaction.
An output of a transaction.
uint64_t m_value
The integral value of the output, in atomic units of currency.
hash_t m_witness_program_commitment
Hash of the witness program.