OpenCBDC Transaction Processor
Loading...
Searching...
No Matches
uhs/client/client.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 "client.hpp"
7
8#include "bech32/bech32.h"
9#include "bech32/util/strencodings.h"
10#include "crypto/sha256.h"
19
20#include <filesystem>
21#include <iomanip>
22#include <utility>
23
24namespace cbdc {
25
26 namespace address {
27 auto decode(const std::string& addr_str)
28 -> std::optional<cbdc::hash_t> {
29 // TODO: if/when bech32m is merged into Bitcoin Core, switch to
30 // that.
31 // see: https://github.com/bitcoin/bitcoin/pull/20861
32 const auto [hrp, enc_data] = bech32::Decode(addr_str);
33 if(hrp != cbdc::config::bech32_hrp) {
34 std::cout << "Invalid address encoding" << std::endl;
35 return std::nullopt;
36 }
37 auto data = std::vector<uint8_t>();
38 ConvertBits<bech32_bits_per_symbol, bits_per_byte, false>(
39 [&](uint8_t c) {
40 data.push_back(c);
41 },
42 enc_data.begin(),
43 enc_data.end());
44
45 auto pubkey = cbdc::hash_t();
46 if(data[0]
47 != static_cast<uint8_t>(
49 || data.size() != pubkey.size() + 1) {
50 std::cout << "Address is not a supported type" << std::endl;
51 return std::nullopt;
52 }
53
54 data.erase(data.begin());
55 std::copy_n(data.begin(), pubkey.size(), pubkey.begin());
56
57 return pubkey;
58 }
59 }
60
62 std::shared_ptr<logging::log> logger,
63 std::string wallet_file,
64 std::string client_file)
65 : m_opts(std::move(opts)),
66 m_logger(std::move(logger)),
67 m_sentinel_client(m_opts.m_sentinel_endpoints, m_logger),
68 m_client_file(std::move(client_file)),
69 m_wallet_file(std::move(wallet_file)) {}
70
71 auto client::init() -> bool {
72 if(std::filesystem::exists(m_wallet_file)) {
73 m_wallet.load(m_wallet_file);
74 } else {
75 m_logger->warn("Existing wallet file not found");
76 }
77
78 load_client_state();
79
80 if(!m_sentinel_client.init()) {
81 m_logger->error("Failed to initialize sentinel client.");
82 return false;
83 }
84
85 return init_derived();
86 }
87
88 auto client::print_amount(uint64_t val) -> std::string {
89 std::stringstream ss;
90 ss << config::currency_symbol << std::fixed << std::setprecision(2)
91 << static_cast<double>(val) / 100.0;
92 return ss.str();
93 }
94
95 auto client::mint(size_t n_outputs, uint32_t output_val)
97 auto mint_tx = m_wallet.mint_new_coins(n_outputs, output_val);
98 import_transaction(mint_tx);
99
100 // TODO: make a formal way of minting. For now bypass the sentinels.
101 if(!send_mint_tx(mint_tx)) {
102 m_logger->error("Failed to send mint tx");
103 }
104
105 return mint_tx;
106 }
107
109 m_wallet.sign(tx);
110 }
111
112 void client::register_pending_tx(const transaction::full_tx& tx) {
113 // Mark all inputs as pending spend
114 for(const auto& in : tx.m_inputs) {
115 m_pending_spend.insert({in.hash(), in});
116 }
117
118 save();
119 }
120
121 auto client::create_transaction(uint32_t value, const pubkey_t& payee)
122 -> std::optional<transaction::full_tx> {
123 auto tx = m_wallet.send_to(value, payee, true);
124 if(!tx.has_value()) {
125 return std::nullopt;
126 }
127
128 register_pending_tx(tx.value());
129
130 return tx;
131 }
132
133 auto client::send(uint32_t value, const pubkey_t& payee)
134 -> std::pair<std::optional<transaction::full_tx>,
135 std::optional<cbdc::sentinel::execute_response>> {
136 static constexpr auto null_return
137 = std::make_pair(std::nullopt, std::nullopt);
138
139 auto spend_tx = create_transaction(value, payee);
140 if(!spend_tx.has_value()) {
141 m_logger->error("Failed to generate wallet spend tx.");
142 return null_return;
143 }
144
145 auto res = send_transaction(spend_tx.value());
146 if(!res.has_value()) {
147 return null_return;
148 }
149
150 return std::make_pair(spend_tx.value(), res.value());
151 }
152
153 auto client::fan(uint32_t count, uint32_t value, const pubkey_t& payee)
154 -> std::pair<std::optional<transaction::full_tx>,
155 std::optional<cbdc::sentinel::execute_response>> {
156 static constexpr auto null_return
157 = std::make_pair(std::nullopt, std::nullopt);
158
159 auto tx = m_wallet.fan(count, value, payee, true);
160 if(!tx.has_value()) {
161 m_logger->error("Failed to generate wallet fan tx");
162 return null_return;
163 }
164
165 register_pending_tx(tx.value());
166
167 auto res = send_transaction(tx.value());
168 if(!res.has_value()) {
169 return null_return;
170 }
171
172 return std::make_pair(tx.value(), res.value());
173 }
174
176 -> std::optional<cbdc::sentinel::execute_response> {
177 import_transaction(tx);
178
179 auto res = m_sentinel_client.execute_transaction(tx);
180 if(!res.has_value()) {
181 m_logger->error("Failed to send transaction to sentinel.");
182 return std::nullopt;
183 }
184
185 m_logger->info("Sentinel responded:",
186 cbdc::sentinel::to_string(res.value().m_tx_status),
187 "for",
189
190 if(res.value().m_tx_status == sentinel::tx_status::confirmed) {
191 confirm_transaction(transaction::tx_id(tx));
192 }
193
194 return res;
195 }
196
198 const pubkey_t& payee)
199 -> std::vector<transaction::input> {
200 return transaction::wallet::export_send_inputs(send_tx, payee);
201 }
202
204 if(m_wallet.is_spendable(in)) {
205 m_pending_inputs.insert({in.m_prevout.m_tx_id, in});
206 save();
207 } else {
208 m_logger->warn("Ignoring non-spendable input");
209 }
210 }
211
213 auto addr = m_wallet.generate_key();
214 save();
215 return addr;
216 }
217
218 auto client::balance() -> uint64_t {
219 return m_wallet.balance();
220 }
221
222 auto client::utxo_count() -> size_t {
223 return m_wallet.count();
224 }
225
226 auto client::pending_tx_count() -> size_t {
227 return m_pending_txs.size();
228 }
229
231 return m_pending_inputs.size();
232 }
233
234 void client::import_transaction(const transaction::full_tx& tx) {
235 m_pending_txs.insert({transaction::tx_id(tx), tx});
236 save();
237 }
238
239 auto client::check_pending(const transaction::input& inp) -> bool {
240 // TODO: This can probably be done more efficiently, but we
241 // might have to keep a secondary map to index the pending tx set on
242 // input hash?
243 for(auto& it : m_pending_txs) {
244 for(auto& in : it.second.m_inputs) {
245 if(in == inp) {
246 return true;
247 }
248 }
249 }
250 return false;
251 }
252
253 auto client::abandon_transaction(const hash_t& tx_id) -> bool {
254 bool success{false};
255 const auto it = m_pending_txs.find(tx_id);
256 if(it != m_pending_txs.end()) {
257 auto tx = it->second;
258 m_pending_txs.erase(it);
259
260 // Add the used inputs back to the wallet if they are still
261 // pending and not used in any other pending transaction.
262 for(auto& i : tx.m_inputs) {
263 const auto ps_it = m_pending_spend.find(i.hash());
264 if(ps_it != m_pending_spend.end()) {
265 auto pending = check_pending(i);
266 if(!pending) {
267 m_pending_spend.erase(ps_it);
268 m_wallet.confirm_inputs({i});
269 }
270 }
271 }
272 success = true;
273 }
274
275 save();
276
277 return success;
278 }
279
280 auto client::confirm_transaction(const hash_t& tx_id) -> bool {
281 // TODO: Should abandon and confirm be combined somehow?
282 // for instance: finish_transaction(hash_t tx_id, bool succeeded)
283 bool success{false};
284 const auto it = m_pending_txs.find(tx_id);
285 if(it != m_pending_txs.end()) {
286 m_wallet.confirm_transaction(it->second);
287 for(auto& i : it->second.m_inputs) {
288 const auto ps_it = m_pending_spend.find(i.hash());
289 if(ps_it != m_pending_spend.end()) {
290 m_pending_spend.erase(ps_it);
291 }
292 }
293 m_pending_txs.erase(it);
294 success = true;
295 }
296
297 const auto pi_it = m_pending_inputs.find(tx_id);
298 if(pi_it != m_pending_inputs.end()) {
299 m_wallet.confirm_inputs({pi_it->second});
300 m_pending_inputs.erase(pi_it);
301 success = true;
302 }
303
304 save();
305
306 return success;
307 }
308
309 void client::load_client_state() {
310 std::ifstream client_file(m_client_file,
311 std::ios::binary | std::ios::in);
312 if(client_file.good()) {
313 auto deser = cbdc::istream_serializer(client_file);
314 if(!(deser >> m_pending_txs >> m_pending_inputs
315 >> m_pending_spend)) {
316 m_logger->fatal("Error deserializing client file");
317 }
318 } else {
319 m_logger->warn("Existing client file not found");
320 }
321 }
322
323 void client::save_client_state() {
324 std::ofstream client_file(m_client_file,
325 std::ios::binary | std::ios::trunc
326 | std::ios::out);
327 if(!client_file.good()) {
328 m_logger->fatal("Failed to open client file for saving");
329 }
330 auto ser = cbdc::ostream_serializer(client_file);
331 if(!(ser << m_pending_txs << m_pending_inputs << m_pending_spend)) {
332 m_logger->fatal("Failed to write client data");
333 }
334 }
335
336 void client::save() {
337 save_client_state();
338 m_wallet.save(m_wallet_file);
339 }
340
342 -> std::unordered_map<hash_t, transaction::full_tx, hashing::null> {
343 return m_pending_txs;
344 }
345
347 -> std::unordered_map<hash_t, transaction::input, hashing::null> {
348 return m_pending_inputs;
349 }
350}
auto pending_inputs() const -> std::unordered_map< hash_t, transaction::input, hashing::null >
Returns the set of imported inputs from senders.
auto fan(uint32_t count, uint32_t value, const pubkey_t &payee) -> std::pair< std::optional< transaction::full_tx >, std::optional< cbdc::sentinel::execute_response > >
Send a specified number of fixed-value outputs from this client's wallet to a target address.
auto init() -> bool
Initializes the client.
auto pending_tx_count() -> size_t
Returns the number of unconfirmed transactions.
auto confirm_transaction(const hash_t &tx_id) -> bool
Confirms the transaction with the given ID.
client(cbdc::config::options opts, std::shared_ptr< logging::log > logger, std::string wallet_file, std::string client_file)
Constructor.
auto balance() -> uint64_t
Returns the balance in this client's wallet.
auto new_address() -> pubkey_t
Generates a new wallet address that other clients can use to send money to this client using send.
auto abandon_transaction(const hash_t &tx_id) -> bool
Abandons a transaction currently awaiting confirmation.
void sign_transaction(transaction::full_tx &tx)
Signs the given transaction for as far as client's wallet contains the transaction's keys.
auto pending_txs() const -> std::unordered_map< hash_t, transaction::full_tx, hashing::null >
Returns the set of transactions pending confirmation.
void import_send_input(const transaction::input &in)
Imports transaction data from a sender.
auto send_transaction(const transaction::full_tx &tx) -> std::optional< cbdc::sentinel::execute_response >
Send the given transaction to the sentinel.
auto create_transaction(uint32_t value, const pubkey_t &payee) -> std::optional< transaction::full_tx >
Create a new transaction.
static auto export_send_inputs(const transaction::full_tx &send_tx, const pubkey_t &payee) -> std::vector< transaction::input >
Extracts the transaction data that recipients need from senders to confirm pending transfers.
auto utxo_count() -> size_t
Returns the number of UTXOs in this client's wallet.
@ public_key
Pay-to-Public-Key (P2PK) address data.
auto check_pending(const transaction::input &inp) -> bool
Checks the client's pending transaction set for the specified transaction.
auto pending_input_count() -> size_t
Returns the number of pending received inputs.
auto send(uint32_t value, const pubkey_t &payee) -> std::pair< std::optional< transaction::full_tx >, std::optional< cbdc::sentinel::execute_response > >
Send a specified amount from this client's wallet to a target address.
auto mint(size_t n_outputs, uint32_t output_val) -> transaction::full_tx
Creates the specified number spendable outputs each with the specified value.
static auto print_amount(uint64_t val) -> std::string
Format a value given in currency base units as USD.
Implementation of serializer for reading from a std::istream.
Implementation of serializer for writing to a std::ostream.
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
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
Tools for reading options from a configuration file and building application-specific parameter sets ...
auto decode(const std::string &addr_str) -> std::optional< cbdc::hash_t >
auto to_string(tx_status status) -> std::string
Return a human-readable string describing a tx_status.
@ confirmed
Executed to completion.
auto tx_id(const full_tx &tx) noexcept -> hash_t
Calculates the unique hash of a full transaction.
std::array< unsigned char, cbdc::hash_size > hash_t
SHA256 hash container.
auto to_string(const hash_t &val) -> std::string
Converts a hash to a hexadecimal string.
std::array< unsigned char, pubkey_len > pubkey_t
A public key of a public/private keypair.
Definition keys.hpp:25
Project-wide configuration options.
Definition config.hpp:132
A complete transaction.
std::vector< input > m_inputs
The set of inputs for the transaction.
An input for a new transaction.
out_point m_prevout
The unique identifier of the output.
hash_t m_tx_id
The hash of the transaction which created the out_point.