diff --git a/2pc-compose.cfg b/2pc-compose.cfg index de681116b..63328a60e 100644 --- a/2pc-compose.cfg +++ b/2pc-compose.cfg @@ -1,4 +1,6 @@ 2pc=1 +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" sentinel_count=1 sentinel0_endpoint="sentinel0:5555" sentinel0_loglevel="WARN" diff --git a/2pc.cfg.sample b/2pc.cfg.sample index 4ac01bc76..1f22b4835 100644 --- a/2pc.cfg.sample +++ b/2pc.cfg.sample @@ -1,4 +1,6 @@ 2pc=1 +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" sentinel_count=1 sentinel0_endpoint="127.0.0.1:5557" sentinel0_loglevel="WARN" diff --git a/README.md b/README.md index 479369c91..9002646ad 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,12 @@ Additionally, you can start the atomizer architecture by passing `--file docker- ## Setup test wallets and test them The following commands are all performed from within the second container we started in the previous step. + +* First, make a demo wallet for minting coins + ```terminal + # ./build/src/uhs/client/client-cli wallet0.dat demowallet + ``` + In each of the below commands, you should pass `atomizer-compose.cfg` instead of `2pc-compose.cfg` if you started the atomizer architecture. * Mint new coins (e.g., 10 new UTXOs each with a value of 5 atomic units of currency) diff --git a/atomizer-compose.cfg b/atomizer-compose.cfg index 74ef9543c..e613babd7 100644 --- a/atomizer-compose.cfg +++ b/atomizer-compose.cfg @@ -1,3 +1,5 @@ +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" atomizer_count=1 atomizer0_endpoint="atomizer0:5555" atomizer0_raft_endpoint="atomizer0:6666" diff --git a/multimachine.cfg.sample b/multimachine.cfg.sample index 1c788e52a..8ff7e83b7 100644 --- a/multimachine.cfg.sample +++ b/multimachine.cfg.sample @@ -1,3 +1,5 @@ +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" atomizer_count=3 atomizer0_endpoint="192.168.1.2:5555" atomizer0_raft_endpoint="192.168.1.2:6666" diff --git a/scripts/test.sh b/scripts/test.sh index 430cf27a6..ca0f08492 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -28,6 +28,7 @@ run_test_suite () { } echo "Running unit tests..." +cp tests/unit/*.cfg $BUILD_DIR run_test_suite "tests/unit/run_unit_tests" "unit_tests_coverage" echo "Running integration tests..." diff --git a/src/uhs/atomizer/sentinel/controller.cpp b/src/uhs/atomizer/sentinel/controller.cpp index 9dc21cf26..007151ce7 100644 --- a/src/uhs/atomizer/sentinel/controller.cpp +++ b/src/uhs/atomizer/sentinel/controller.cpp @@ -86,7 +86,7 @@ namespace cbdc::sentinel { auto controller::execute_transaction(transaction::full_tx tx) -> std::optional { - const auto res = transaction::validation::check_tx(tx); + const auto res = transaction::validation::check_tx(tx, m_opts); tx_status status{tx_status::pending}; if(res.has_value()) { status = tx_status::static_invalid; @@ -118,7 +118,7 @@ namespace cbdc::sentinel { auto controller::validate_transaction(transaction::full_tx tx) -> std::optional { - const auto res = transaction::validation::check_tx(tx); + const auto res = transaction::validation::check_tx(tx, m_opts); if(res.has_value()) { return std::nullopt; } @@ -167,7 +167,17 @@ namespace cbdc::sentinel { return; } - send_compact_tx(ctx); + // If the tx has no inputs, it's a mint. Send it directly to one of the + // shards + if(ctx.m_inputs.empty()) { + auto ctx_pkt + = std::make_shared(cbdc::make_buffer(ctx)); + if(!m_shard_network.send_to_one(ctx_pkt)) { + m_logger->error("Failed to send mint tx to shard"); + } + } else { + send_compact_tx(ctx); + } } void controller::send_compact_tx(const transaction::compact_tx& ctx) { diff --git a/src/uhs/atomizer/shard/shard.cpp b/src/uhs/atomizer/shard/shard.cpp index c328acfa0..6a322668a 100644 --- a/src/uhs/atomizer/shard/shard.cpp +++ b/src/uhs/atomizer/shard/shard.cpp @@ -120,10 +120,13 @@ namespace cbdc::shard { cbdc::watchtower::tx_error_sync{}}; } + atomizer::tx_notify_request msg; + + // If the tx has no inputs, it's a mint. if(tx.m_inputs.empty()) { - return cbdc::watchtower::tx_error{ - tx.m_id, - cbdc::watchtower::tx_error_inputs_dne{{}}}; + msg.m_tx = std::move(tx); + msg.m_block_height = best_block_height(); + return msg; } auto read_options = m_read_options; @@ -158,7 +161,6 @@ namespace cbdc::shard { cbdc::watchtower::tx_error_inputs_dne{dne_inputs}}; } - atomizer::tx_notify_request msg; msg.m_attestations = std::move(attestations); msg.m_tx = std::move(tx); msg.m_block_height = snp_height; diff --git a/src/uhs/client/client-cli.cpp b/src/uhs/client/client-cli.cpp index 0801b79e0..73bcd5483 100644 --- a/src/uhs/client/client-cli.cpp +++ b/src/uhs/client/client-cli.cpp @@ -10,9 +10,11 @@ #include "crypto/sha256.h" #include "twophase_client.hpp" #include "uhs/transaction/messages.hpp" +#include "uhs/transaction/wallet.hpp" #include "util/common/config.hpp" #include "util/serialization/util.hpp" +#include #include #include @@ -31,10 +33,50 @@ auto mint_command(cbdc::client& client, const std::vector& args) const auto n_outputs = std::stoull(args[5]); const auto output_val = std::stoul(args[6]); - const auto mint_tx + const auto [tx, resp] = client.mint(n_outputs, static_cast(output_val)); - std::cout << cbdc::to_string(cbdc::transaction::tx_id(mint_tx)) + if(!tx.has_value()) { + std::cout << "Could not generate valid mint tx." << std::endl; + return false; + } + + std::cout << "tx_id:" << std::endl + << cbdc::to_string(cbdc::transaction::tx_id(tx.value())) << std::endl; + + if(resp.has_value()) { + std::cout << "Sentinel responded: " + << cbdc::sentinel::to_string(resp.value().m_tx_status) + << std::endl; + if(resp.value().m_tx_error.has_value()) { + std::cout << "Validation error: " + << cbdc::transaction::validation::to_string( + resp.value().m_tx_error.value()) + << std::endl; + } + } + + return true; +} + +/// Generate a demo wallet for use with demo. It creates a minter public key +/// to match the value pre-configued in the provided 'cfg' files. +/// +/// Example use: client-cli 'wallet file name' demowallet +auto generate_demo_minter_wallet(const std::vector& args) + -> bool { + const auto wallet_file = args[1]; + if(std::filesystem::exists(wallet_file)) { + std::cout << " " << wallet_file << " already exists" << std::endl; + return false; + } + cbdc::transaction::wallet wallet{}; + const auto pk = wallet.generate_test_minter_key(); + const auto hexed = cbdc::to_string(pk); + wallet.save(wallet_file); + + std::cout << " Created demo wallet. Saved to: " << wallet_file << "\n" + << " Minter public key is: " << hexed << std::endl; return true; } @@ -204,9 +246,45 @@ auto confirmtx_command(cbdc::client& client, return true; } +auto dispatch_command(const std::string& command, + cbdc::client& client, + const std::vector& args) -> bool { + if(command == "mint") { + return mint_command(client, args); + } else if(command == "send") { + return send_command(client, args); + } else if(command == "fan") { + return fan_command(client, args); + } else if(command == "sync") { + client.sync(); + } else if(command == "newaddress") { + newaddress_command(client); + } else if(command == "info") { + const auto balance = client.balance(); + const auto n_txos = client.utxo_count(); + std::cout << "Balance: " << cbdc::client::print_amount(balance) + << ", UTXOs: " << n_txos + << ", pending TXs: " << client.pending_tx_count() + << std::endl; + } else if(command == "importinput") { + return importinput_command(client, args); + } else if(command == "confirmtx") { + return confirmtx_command(client, args); + } else { + std::cerr << "Unknown command" << std::endl; + } + return true; +} + // LCOV_EXCL_START auto main(int argc, char** argv) -> int { auto args = cbdc::config::get_args(argc, argv); + + // Create a demo wallet + if(args.size() == 3 && args[2] == "demowallet") { + return generate_demo_minter_wallet(args) ? 0 : -1; + } + static constexpr auto min_arg_count = 5; if(args.size() < min_arg_count) { std::cerr << "Usage: " << args[0] @@ -215,7 +293,12 @@ auto main(int argc, char** argv) -> int { return 0; } - auto cfg_or_err = cbdc::config::load_options(args[1]); + const auto config_file = args[1]; + const auto client_file = args[2]; + const auto wallet_file = args[3]; + const auto command = args[4]; + + auto cfg_or_err = cbdc::config::load_options(config_file); if(std::holds_alternative(cfg_or_err)) { std::cerr << "Error loading config file: " << std::get(cfg_or_err) << std::endl; @@ -226,9 +309,6 @@ auto main(int argc, char** argv) -> int { SHA256AutoDetect(); - const auto wallet_file = args[3]; - const auto client_file = args[2]; - auto logger = std::make_shared( cbdc::config::defaults::log_level); @@ -249,40 +329,8 @@ auto main(int argc, char** argv) -> int { return -1; } - const auto command = std::string(args[4]); - if(command == "mint") { - if(!mint_command(*client, args)) { - return -1; - } - } else if(command == "send") { - if(!send_command(*client, args)) { - return -1; - } - } else if(command == "fan") { - if(!fan_command(*client, args)) { - return -1; - } - } else if(command == "sync") { - client->sync(); - } else if(command == "newaddress") { - newaddress_command(*client); - } else if(command == "info") { - const auto balance = client->balance(); - const auto n_txos = client->utxo_count(); - std::cout << "Balance: " << cbdc::client::print_amount(balance) - << ", UTXOs: " << n_txos - << ", pending TXs: " << client->pending_tx_count() - << std::endl; - } else if(command == "importinput") { - if(!importinput_command(*client, args)) { - return -1; - } - } else if(command == "confirmtx") { - if(!confirmtx_command(*client, args)) { - return -1; - } - } else { - std::cerr << "Unknown command" << std::endl; + if(!dispatch_command(command, *client, args)) { + return -1; } // TODO: check that the send queue has drained before closing diff --git a/src/uhs/client/client.cpp b/src/uhs/client/client.cpp index 84af37014..366e937d0 100644 --- a/src/uhs/client/client.cpp +++ b/src/uhs/client/client.cpp @@ -54,19 +54,29 @@ namespace cbdc { return ss.str(); } + auto client::mint_for_testing(size_t n_outputs, uint32_t output_val) + -> std::pair, + std::optional> { + m_wallet.generate_test_minter_key(); + return mint(n_outputs, output_val); + } + auto client::mint(size_t n_outputs, uint32_t output_val) - -> transaction::full_tx { + -> std::pair, + std::optional> { + static constexpr auto null_return + = std::make_pair(std::nullopt, std::nullopt); + auto mint_tx = m_wallet.mint_new_coins(n_outputs, output_val); - import_transaction(mint_tx); - // TODO: make a formal way of minting. For now bypass the sentinels. - if(!send_mint_tx(mint_tx)) { - m_logger->error("Failed to send mint tx"); + auto res = send_transaction(mint_tx); + if(!res.has_value()) { + return null_return; } save(); - return mint_tx; + return std::make_pair(mint_tx, res.value()); } void client::sign_transaction(transaction::full_tx& tx) { diff --git a/src/uhs/client/client.hpp b/src/uhs/client/client.hpp index 6b505ee7e..79aa1bc07 100644 --- a/src/uhs/client/client.hpp +++ b/src/uhs/client/client.hpp @@ -49,6 +49,19 @@ namespace cbdc { /// \return USD formatted value. static auto print_amount(uint64_t val) -> std::string; + /// \brief Mint coins for testing. + /// + /// Provides a pre-calculated keypair to be used for configuration + /// files for testing and demo environments. Use the hexed public key: + /// 1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244 as + /// the value for minter0 in a configuration file. + /// \param n_outputs number of new spendable outputs to create. + /// \param output_val value of the amount to associate with each output in the base unit of the currency. + /// \return the transaction and sentinel response. + auto mint_for_testing(size_t n_outputs, uint32_t output_val) + -> std::pair, + std::optional>; + /// \brief Creates the specified number spendable outputs each with the /// specified value. /// @@ -57,9 +70,10 @@ namespace cbdc { /// transaction to the system via \ref send_mint_tx. /// \param n_outputs number of new spendable outputs to create. /// \param output_val value of the amount to associate with each output in the base unit of the currency. - /// \return the completed transaction. + /// \return the transaction and sentinel response. auto mint(size_t n_outputs, uint32_t output_val) - -> transaction::full_tx; + -> std::pair, + std::optional>; /// \brief Send a specified amount from this client's wallet to a /// target address. @@ -237,6 +251,8 @@ namespace cbdc { /// \brief Sends the given minting transaction to a service that will /// accept and process it. /// + /// TODO: Remove. No longer needed + /// /// Called by \ref mint to send the resulting transaction. Subclasses /// should define custom transmission logic here. /// \param mint_tx invalid transaction that mints new coins. diff --git a/src/uhs/transaction/validation.cpp b/src/uhs/transaction/validation.cpp index ce6aa18c6..3337edd89 100644 --- a/src/uhs/transaction/validation.cpp +++ b/src/uhs/transaction/validation.cpp @@ -33,8 +33,13 @@ namespace cbdc::transaction::validation { return std::tie(m_code, m_idx) == std::tie(rhs.m_code, rhs.m_idx); } - auto check_tx(const cbdc::transaction::full_tx& tx) + auto check_tx(const cbdc::transaction::full_tx& tx, + const cbdc::config::options& options) -> std::optional { + if(tx.m_inputs.empty()) { + return check_mint_tx(tx, options); + } + const auto structure_err = check_tx_structure(tx); if(structure_err) { return structure_err; @@ -72,6 +77,37 @@ namespace cbdc::transaction::validation { return std::nullopt; } + auto check_mint_tx(const cbdc::transaction::full_tx& tx, + const cbdc::config::options& options) + -> std::optional { + // ensure there are outputs + const auto output_count_err = check_output_count(tx); + if(output_count_err) { + return output_count_err; + } + // ensure the number of outputs match the number of witnesses + if(tx.m_outputs.size() != tx.m_witness.size()) { + return tx_error(tx_error_code::mint_output_witness_mismatch); + } + // ensure each output has a value >= 1 + for(size_t idx = 0; idx < tx.m_outputs.size(); idx++) { + const auto& out = tx.m_outputs[idx]; + const auto output_err = check_output_value(out); + if(output_err) { + return tx_error{output_error{output_err.value(), idx}}; + } + } + + for(size_t idx = 0; idx < tx.m_witness.size(); idx++) { + const auto witness_err = check_mint_witness(tx, idx, options); + if(witness_err) { + return tx_error{witness_error{witness_err.value(), idx}}; + } + } + + return std::nullopt; + } + auto check_tx_structure(const cbdc::transaction::full_tx& tx) -> std::optional { const auto input_count_err = check_input_count(tx); @@ -419,4 +455,113 @@ namespace cbdc::transaction::validation { && tx.verify(secp_context.get(), att); }); } + + auto check_mint_witness(const cbdc::transaction::full_tx& tx, + size_t idx, + const cbdc::config::options& options) + -> std::optional { + const auto& witness_program = tx.m_witness[idx]; + if(witness_program.empty()) { + return witness_error_code::missing_witness_program_type; + } + + const auto witness_program_type + = static_cast( + witness_program[0]); + switch(witness_program_type) { + case witness_program_type::p2pk: + return check_mint_p2pk_witness(tx, idx, options); + default: + return witness_error_code::unknown_witness_program_type; + } + } + + auto check_mint_p2pk_witness(const cbdc::transaction::full_tx& tx, + size_t idx, + const cbdc::config::options& options) + -> std::optional { + const auto witness_len_err = check_p2pk_witness_len(tx, idx); + if(witness_len_err) { + return witness_len_err; + } + + const auto witness_commitment_err + = check_mint_p2pk_witness_commitment(tx, idx); + if(witness_commitment_err) { + return witness_commitment_err; + } + + const auto witness_sig_err + = check_mint_p2pk_witness_signature(tx, idx, options); + if(witness_sig_err) { + return witness_sig_err; + } + + return std::nullopt; + } + + auto + check_mint_p2pk_witness_commitment(const cbdc::transaction::full_tx& tx, + size_t idx) + -> std::optional { + const auto& wit = tx.m_witness[idx]; + const auto witness_program_hash + = hash_data(wit.data(), p2pk_witness_prog_len); + + // Differs from normal tx that checks the inputs + // Here we make sure the 'payee' in the output is the minter + const auto& witness_program_commitment + = tx.m_outputs[idx].m_witness_program_commitment; + + if(witness_program_hash != witness_program_commitment) { + return witness_error_code::program_mismatch; + } + + return std::nullopt; + } + + auto + check_mint_p2pk_witness_signature(const cbdc::transaction::full_tx& tx, + size_t idx, + const cbdc::config::options& options) + -> std::optional { + const auto& wit = tx.m_witness[idx]; + secp256k1_xonly_pubkey pubkey{}; + + // TODO: use C++20 std::span to avoid pointer arithmetic in validation + // code + pubkey_t pubkey_arr{}; + std::memcpy(pubkey_arr.data(), + &wit[sizeof(witness_program_type)], + sizeof(pubkey_arr)); + + // check if the signer is an authorized minter identified in the + // configuration + if(options.m_minter_pubkeys.count(pubkey_arr) == 0) { + return witness_error_code::invalid_minter_key; + } + + if(secp256k1_xonly_pubkey_parse(secp_context.get(), + &pubkey, + pubkey_arr.data()) + != 1) { + return witness_error_code::invalid_public_key; + } + + const auto sighash = cbdc::transaction::tx_id(tx); + + std::array sig_arr{}; + std::memcpy(sig_arr.data(), + &wit[p2pk_witness_prog_len], + sizeof(sig_arr)); + if(secp256k1_schnorrsig_verify(secp_context.get(), + sig_arr.data(), + sighash.data(), + &pubkey) + != 1) { + return witness_error_code::invalid_signature; + } + + return std::nullopt; + } } diff --git a/src/uhs/transaction/validation.hpp b/src/uhs/transaction/validation.hpp index 03acf20e2..febdce710 100644 --- a/src/uhs/transaction/validation.hpp +++ b/src/uhs/transaction/validation.hpp @@ -67,6 +67,7 @@ namespace cbdc::transaction::validation { program_mismatch, ///< The witness's specified program doesn't match its commitment invalid_public_key, ///< The witness's public key is invalid + invalid_minter_key, ///< The witness is not a valid minter invalid_signature ///< The witness's signature is invalid }; @@ -80,6 +81,8 @@ namespace cbdc::transaction::validation { ///< The total values of inputs and outputs do not match value_overflow, ///< The total value of inputs/outputs overflows a 64-bit integer + mint_output_witness_mismatch ///< number of outputs don't match + ///< witnesses }; /// An error that may occur when sentinels validate witness commitments @@ -118,8 +121,14 @@ namespace cbdc::transaction::validation { /// \note This function returns immediately on the first-found error. /// /// \param tx transaction to validate + /// \param options configuration options /// \return null if transaction is valid, otherwise error information - auto check_tx(const transaction::full_tx& tx) -> std::optional; + auto check_tx(const transaction::full_tx& tx, + const cbdc::config::options& options) + -> std::optional; + auto check_mint_tx(const cbdc::transaction::full_tx& tx, + const cbdc::config::options& options) + -> std::optional; auto check_tx_structure(const transaction::full_tx& tx) -> std::optional; auto check_input_structure(const transaction::input& inp) -> std::optional< @@ -164,6 +173,25 @@ namespace cbdc::transaction::validation { const transaction::compact_tx& tx, const std::unordered_set& pubkeys, size_t threshold) -> bool; + + // TODO: Document below + auto check_mint_witness(const cbdc::transaction::full_tx& tx, + size_t idx, + const cbdc::config::options& options) + -> std::optional; + auto check_mint_p2pk_witness(const cbdc::transaction::full_tx& tx, + size_t idx, + const cbdc::config::options& options) + -> std::optional; + auto + check_mint_p2pk_witness_commitment(const cbdc::transaction::full_tx& tx, + size_t idx) + -> std::optional; + auto + check_mint_p2pk_witness_signature(const cbdc::transaction::full_tx& tx, + size_t idx, + const cbdc::config::options& options) + -> std::optional; } #endif // OPENCBDC_TX_SRC_TRANSACTION_VALIDATION_H_ diff --git a/src/uhs/transaction/wallet.cpp b/src/uhs/transaction/wallet.cpp index 059bd686f..ae2882e25 100644 --- a/src/uhs/transaction/wallet.cpp +++ b/src/uhs/transaction/wallet.cpp @@ -25,22 +25,55 @@ namespace cbdc { auto transaction::wallet::mint_new_coins(const size_t n_outputs, const uint32_t output_val) -> transaction::full_tx { - transaction::full_tx ret; + transaction::full_tx tx; + + const auto pubkey = generate_minter_key(); for(size_t i = 0; i < n_outputs; i++) { transaction::output out; - - const auto pubkey = generate_key(); - out.m_witness_program_commitment = transaction::validation::get_p2pk_witness_commitment(pubkey); - out.m_value = output_val; + tx.m_outputs.push_back(out); + } - ret.m_outputs.push_back(out); + const auto sighash = transaction::tx_id(tx); + tx.m_witness.resize(n_outputs); + const privkey_t seckey = m_keys.at(pubkey); + + // Sign each output + for(size_t i = 0; i < n_outputs; i++) { + auto& sig = tx.m_witness[i]; + sig.resize(transaction::validation::p2pk_witness_len); + sig[0] = std::byte( + transaction::validation::witness_program_type::p2pk); + std::memcpy( + &sig[sizeof(transaction::validation::witness_program_type)], + pubkey.data(), + pubkey.size()); + + secp256k1_keypair keypair{}; + [[maybe_unused]] const auto ret + = secp256k1_keypair_create(m_secp.get(), + &keypair, + seckey.data()); + assert(ret == 1); + + std::array sig_arr{}; + [[maybe_unused]] const auto sign_ret + = secp256k1_schnorrsig_sign(m_secp.get(), + sig_arr.data(), + sighash.data(), + &keypair, + nullptr, + nullptr); + std::memcpy(&sig[transaction::validation::p2pk_witness_prog_len], + sig_arr.data(), + sizeof(sig_arr)); + assert(sign_ret == 1); } - return ret; + return tx; } auto transaction::wallet::send_to(const uint32_t amount, @@ -121,6 +154,35 @@ namespace cbdc { return ret; } + auto transaction::wallet::generate_minter_key() -> pubkey_t { + if(m_pubkeys.empty()) { + // new wallet. create the minter key + return generate_key(); + } + // first key is always the minter key + return m_pubkeys[0]; + } + + auto transaction::wallet::generate_test_minter_key() -> pubkey_t { + const privkey_t seckey + = cbdc::privkey_t{'t', 'e', 's', 't', 'i', 'n', 'g'}; + pubkey_t ret = pubkey_from_privkey(seckey, m_secp.get()); + { + std::unique_lock lg(m_keys_mut); + m_pubkeys.push_back(ret); + m_keys.insert({ret, seckey}); + m_witness_programs.insert( + {transaction::validation::get_p2pk_witness_commitment(ret), + ret}); + } + + return ret; + } + + auto transaction::wallet::minter_pubkey_as_hex() -> std::string { + return to_string(generate_minter_key()); + } + auto transaction::wallet::generate_key() -> pubkey_t { // Unique lock on m_keys, m_keygen and m_keygen_dist { @@ -129,7 +191,7 @@ namespace cbdc { std::shared_lock lg(m_keys_mut); if(m_keys.size() > max_keys) { std::uniform_int_distribution keyshuffle_dist( - 0, + 1, m_keys.size() - 1); const auto index = keyshuffle_dist(m_shuffle); return m_pubkeys[index]; diff --git a/src/uhs/transaction/wallet.hpp b/src/uhs/transaction/wallet.hpp index 4414f5848..045eed5f0 100644 --- a/src/uhs/transaction/wallet.hpp +++ b/src/uhs/transaction/wallet.hpp @@ -117,6 +117,26 @@ namespace cbdc::transaction { const pubkey_t& payee) -> std::vector; + /// Generates the key pair that can be used to mint new coins + /// \return the public key. + auto generate_minter_key() -> pubkey_t; + + /// Return the minter's public key as a hex string that can be + /// used in the configuration file. + /// \return hex string + auto minter_pubkey_as_hex() -> std::string; + + /// Generate a fixed public key for a minter. This is + /// so pre-configured (testing/demo) files can be used with a known + /// minter's public key. + /// + /// This should only be used for testing and experimentation. + /// + /// Hexed version of the public key: + /// 1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244 + /// \return a deterministic public key + auto generate_test_minter_key() -> pubkey_t; + /// Generates a new public key at which this wallet can receive /// payments via \ref send_to. /// \return a new public key. diff --git a/src/uhs/twophase/sentinel_2pc/controller.cpp b/src/uhs/twophase/sentinel_2pc/controller.cpp index e4728e6e1..b68f872d9 100644 --- a/src/uhs/twophase/sentinel_2pc/controller.cpp +++ b/src/uhs/twophase/sentinel_2pc/controller.cpp @@ -75,7 +75,8 @@ namespace cbdc::sentinel_2pc { auto controller::execute_transaction( transaction::full_tx tx, execute_result_callback_type result_callback) -> bool { - const auto validation_err = transaction::validation::check_tx(tx); + const auto validation_err + = transaction::validation::check_tx(tx, m_opts); if(validation_err.has_value()) { auto tx_id = transaction::tx_id(tx); m_logger->debug( @@ -120,7 +121,8 @@ namespace cbdc::sentinel_2pc { auto controller::validate_transaction( transaction::full_tx tx, validate_result_callback_type result_callback) -> bool { - const auto validation_err = transaction::validation::check_tx(tx); + const auto validation_err + = transaction::validation::check_tx(tx, m_opts); if(validation_err.has_value()) { result_callback(std::nullopt); return true; diff --git a/src/util/common/config.cpp b/src/util/common/config.cpp index 6fc5ac616..47ecfdb81 100644 --- a/src/util/common/config.cpp +++ b/src/util/common/config.cpp @@ -241,6 +241,12 @@ namespace cbdc::config { return ss.str(); } + auto get_minter_key(size_t minter_id) -> std::string { + std::stringstream ss; + ss << minter_prefix << minter_id; + return ss.str(); + } + auto read_shard_endpoints(options& opts, const parser& cfg) -> std::optional { const auto shard_count = cfg.get_ulong(shard_count_key).value_or(0); @@ -590,6 +596,23 @@ namespace cbdc::config { } } + auto read_minter_options(options& opts, const parser& cfg) + -> std::optional { + const auto minter_count = cfg.get_ulong(minter_count_key).value_or(0); + + for(size_t i{0}; i < minter_count; i++) { + const auto minter_k = get_minter_key(i); + const auto v = cfg.get_string(minter_k); + if(!v.has_value()) { + return "Missing minter setting: " + std::to_string(i) + " (" + + minter_k + ")"; + } + const auto pubkey = cbdc::hash_from_hex(v.value()); + opts.m_minter_pubkeys.insert(pubkey); + } + return std::nullopt; + } + void read_loadgen_options(options& opts, const parser& cfg) { opts.m_input_count = cfg.get_ulong(input_count_key).value_or(opts.m_input_count); @@ -655,6 +678,11 @@ namespace cbdc::config { return err.value(); } + err = read_minter_options(opts, cfg); + if(err.has_value()) { + return err.value(); + } + read_raft_options(opts, cfg); read_loadgen_options(opts, cfg); diff --git a/src/util/common/config.hpp b/src/util/common/config.hpp index 1ae3f72ea..80ecf96a7 100644 --- a/src/util/common/config.hpp +++ b/src/util/common/config.hpp @@ -118,6 +118,9 @@ namespace cbdc::config { static constexpr auto coordinator_max_threads = "coordinator_max_threads"; static constexpr auto initial_mint_count_key = "initial_mint_count"; static constexpr auto initial_mint_value_key = "initial_mint_value"; + static constexpr auto minter_count_key = "minter_count"; + static constexpr auto minter_prefix = "minter"; + static constexpr auto loadgen_count_key = "loadgen_count"; static constexpr auto shard_completed_txs_cache_size = "shard_completed_txs_cache_size"; @@ -239,6 +242,8 @@ namespace cbdc::config { size_t m_initial_mint_count{defaults::initial_mint_count}; /// Value for all outputs in the initial mint transaction. size_t m_initial_mint_value{defaults::initial_mint_value}; + /// Set of public keys belonging to authorized minters + std::unordered_set m_minter_pubkeys; /// Number of blocks to store in watchtower block caches. /// (0=unlimited). Defaults to 1 hour of blocks. diff --git a/tests/integration/atomizer_end_to_end_test.cpp b/tests/integration/atomizer_end_to_end_test.cpp index 46388912d..ecb0b5408 100644 --- a/tests/integration/atomizer_end_to_end_test.cpp +++ b/tests/integration/atomizer_end_to_end_test.cpp @@ -62,7 +62,7 @@ class atomizer_end_to_end_test : public ::testing::Test { std::this_thread::sleep_for(m_block_wait_interval); - m_sender->mint(10, 10); + m_sender->mint_for_testing(10, 10); std::this_thread::sleep_for(m_block_wait_interval); m_sender->sync(); diff --git a/tests/integration/integration_tests.cfg b/tests/integration/integration_tests.cfg index 16c26c5c4..5cce85b74 100644 --- a/tests/integration/integration_tests.cfg +++ b/tests/integration/integration_tests.cfg @@ -1,3 +1,5 @@ +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" archiver_count=1 archiver0_endpoint="127.0.0.1:5558" archiver0_db="archiver0_db" diff --git a/tests/integration/integration_tests_2pc.cfg b/tests/integration/integration_tests_2pc.cfg index 6087c9d47..5a68c330a 100644 --- a/tests/integration/integration_tests_2pc.cfg +++ b/tests/integration/integration_tests_2pc.cfg @@ -1,4 +1,6 @@ 2pc=1 +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" sentinel_count=1 sentinel0_endpoint="127.0.0.1:5557" sentinel0_loglevel="DEBUG" diff --git a/tests/integration/replicated_atomizer.cfg b/tests/integration/replicated_atomizer.cfg index c3a1a83ac..374eb3769 100644 --- a/tests/integration/replicated_atomizer.cfg +++ b/tests/integration/replicated_atomizer.cfg @@ -1,3 +1,5 @@ +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" archiver_count=1 archiver0_endpoint="127.0.0.1:5558" archiver0_db="archiver0_db" diff --git a/tests/integration/replicated_shard.cfg b/tests/integration/replicated_shard.cfg index 7354bec1f..94b9d78df 100644 --- a/tests/integration/replicated_shard.cfg +++ b/tests/integration/replicated_shard.cfg @@ -1,3 +1,5 @@ +minter_count=1 +minter0="1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e24c3674d67a0f142af244" archiver_count=1 archiver0_endpoint="127.0.0.1:5558" archiver0_db="archiver0_db" diff --git a/tests/integration/sentinel_2pc_integration_test.cpp b/tests/integration/sentinel_2pc_integration_test.cpp index 0c44be28d..dedb61623 100644 --- a/tests/integration/sentinel_2pc_integration_test.cpp +++ b/tests/integration/sentinel_2pc_integration_test.cpp @@ -51,6 +51,7 @@ TEST_F(sentinel_2pc_integration_test, valid_signed_tx) { cbdc::transaction::wallet wallet; cbdc::transaction::full_tx m_valid_tx{}; + wallet.generate_test_minter_key(); auto mint_tx1 = wallet.mint_new_coins(2, 100); wallet.confirm_transaction(mint_tx1); diff --git a/tests/integration/sentinel_integration_test.cpp b/tests/integration/sentinel_integration_test.cpp index 425556966..bccb86019 100644 --- a/tests/integration/sentinel_integration_test.cpp +++ b/tests/integration/sentinel_integration_test.cpp @@ -49,6 +49,7 @@ TEST_F(sentinel_integration_test, valid_signed_tx) { cbdc::transaction::wallet wallet; cbdc::transaction::full_tx m_valid_tx{}; + wallet.generate_test_minter_key(); auto mint_tx1 = wallet.mint_new_coins(2, 100); wallet.confirm_transaction(mint_tx1); diff --git a/tests/integration/two_phase_end_to_end_test.cpp b/tests/integration/two_phase_end_to_end_test.cpp index ae82015fd..4e82f4a99 100644 --- a/tests/integration/two_phase_end_to_end_test.cpp +++ b/tests/integration/two_phase_end_to_end_test.cpp @@ -49,7 +49,7 @@ class two_phase_end_to_end_test : public ::testing::Test { std::this_thread::sleep_for(m_wait_interval); - m_sender->mint(10, 10); + m_sender->mint_for_testing(10, 10); std::this_thread::sleep_for(m_wait_interval); m_sender->sync(); diff --git a/tests/unit/config_test.cpp b/tests/unit/config_test.cpp index 1c741d0fa..0be4f0922 100644 --- a/tests/unit/config_test.cpp +++ b/tests/unit/config_test.cpp @@ -3,6 +3,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include "util.hpp" #include "util/common/config.hpp" #include @@ -27,7 +28,14 @@ class config_validation_test : public ::testing::Test { m_twophase_opts.m_locking_shard_endpoints.emplace_back(); m_twophase_opts.m_coordinator_endpoints.emplace_back(); - m_example_config = "archiver0_endpoint=\"127.0.0.1:5558\"\n" + m_example_config = "minter_count=2\n" + "minter0=" + "\"d1fa877eb8ea6e66d207be5780c4261453313929fbec0f55" + "2aaeb055a3563c13\"\n" + "minter1=" + "\"ecc477729befbfdf71e0f86dafb2943f728fd8c183962012" + "c7edf55e2d599f5a\"\n" + "archiver0_endpoint=\"127.0.0.1:5558\"\n" "archiver0_db=\"ex_db\"\n" "window_size=40000\n" "shard0_loglevel=\"WARN\"\n" @@ -121,3 +129,26 @@ TEST_F(config_validation_test, parsing_validation) { auto nonexistent = ex.get_string("lorem ipsum"); EXPECT_FALSE(nonexistent.has_value()); } + +class ConfigWithFileTest : public ::testing::Test { + protected: + void SetUp() override { + cbdc::test::load_config(m_basic_cfg_path, m_opts); + } + static constexpr auto m_basic_cfg_path = "config_tests.cfg"; + cbdc::config::options m_opts{}; +}; + +TEST_F(ConfigWithFileTest, load_from_file) { + const cbdc::pubkey_t good_key = cbdc::hash_from_hex( + "ecc477729befbfdf71e0f86dafb2943f728fd8c183962012c7edf55e2d599f5a"); + + EXPECT_TRUE(m_opts.m_minter_pubkeys.size() == 2); + EXPECT_TRUE(m_opts.m_minter_pubkeys.count(good_key) == 1); + EXPECT_TRUE(m_opts.m_minter_pubkeys.count(cbdc::hash_from_hex("aaa")) + == 0); + + auto r1 = m_opts.m_minter_pubkeys.find(good_key); + EXPECT_TRUE(r1 != m_opts.m_minter_pubkeys.end()); + EXPECT_EQ(good_key, *r1); +} diff --git a/tests/unit/config_tests.cfg b/tests/unit/config_tests.cfg new file mode 100644 index 000000000..fe21bc1a5 --- /dev/null +++ b/tests/unit/config_tests.cfg @@ -0,0 +1,23 @@ +2pc=1 +minter_count=2 +minter0="d1fa877eb8ea6e66d207be5780c4261453313929fbec0f552aaeb055a3563c13" +minter1="ecc477729befbfdf71e0f86dafb2943f728fd8c183962012c7edf55e2d599f5a" +sentinel_count=1 +sentinel0_endpoint="127.0.0.1:5557" +sentinel0_loglevel="DEBUG" +sentinel0_private_key="0000000000000001000000000000000000000000000000000000000000000000" +sentinel0_public_key="eaa649f21f51bdbae7be4ae34ce6e5217a58fdce7f47f9aa7f3b58fa2120e2b3" +shard_count=1 +shard0_count=1 +shard0_start=0 +shard0_end=255 +shard0_loglevel="DEBUG" +shard0_0_endpoint="127.0.0.1:8987" +shard0_0_raft_endpoint="127.0.0.1:8988" +shard0_0_readonly_endpoint="127.0.0.1:8989" +coordinator_count=1 +coordinator0_count=1 +coordinator0_loglevel="DEBUG" +coordinator0_0_endpoint="127.0.0.1:8888" +coordinator0_0_raft_endpoint="127.0.0.1:8889" +coordinator_max_threads=1 diff --git a/tests/unit/shard_test.cpp b/tests/unit/shard_test.cpp index e52fbac92..f8c2f265f 100644 --- a/tests/unit/shard_test.cpp +++ b/tests/unit/shard_test.cpp @@ -81,14 +81,10 @@ TEST_F(shard_test, digest_tx_empty_inputs) { ctx.m_inputs = {}; ctx.m_uhs_outputs = {{'x'}, {'y'}}; + // Txs without inputs are valid mint txs auto res = m_shard.digest_transaction(ctx); - ASSERT_TRUE(std::holds_alternative(res)); - auto got = std::get(res); - - cbdc::watchtower::tx_error want{{'a'}, - cbdc::watchtower::tx_error_inputs_dne{{}}}; - - ASSERT_EQ(got, want); + ASSERT_TRUE( + std::holds_alternative(res)); } TEST_F(shard_test, digest_tx_inputs_dne) { diff --git a/tests/unit/validation_test.cpp b/tests/unit/validation_test.cpp index df5655726..2edfb0bd8 100644 --- a/tests/unit/validation_test.cpp +++ b/tests/unit/validation_test.cpp @@ -26,6 +26,7 @@ class WalletTxValidationTest : public ::testing::Test { = wallet1.send_to(200, wallet2.generate_key(), true).value(); } + cbdc::config::options m_opts{}; cbdc::transaction::full_tx m_valid_tx{}; cbdc::transaction::full_tx m_valid_tx_multi_inp{}; @@ -44,14 +45,14 @@ class WalletTxValidationTest : public ::testing::Test { }; TEST_F(WalletTxValidationTest, valid) { - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_FALSE(err.has_value()); } TEST_F(WalletTxValidationTest, no_inputs) { m_valid_tx.m_inputs.clear(); - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( std::holds_alternative( @@ -60,13 +61,16 @@ TEST_F(WalletTxValidationTest, no_inputs) { auto tx_err = std::get(err.value()); - ASSERT_EQ(tx_err, cbdc::transaction::validation::tx_error_code::no_inputs); + // We have this err because 'no inputs' runs the check_mint_tx + ASSERT_EQ(tx_err, + cbdc::transaction::validation::tx_error_code:: + mint_output_witness_mismatch); } TEST_F(WalletTxValidationTest, no_outputs) { m_valid_tx.m_outputs.clear(); - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( std::holds_alternative( @@ -82,7 +86,7 @@ TEST_F(WalletTxValidationTest, no_outputs) { TEST_F(WalletTxValidationTest, missing_witness) { m_valid_tx.m_witness.clear(); - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( std::holds_alternative( @@ -98,7 +102,7 @@ TEST_F(WalletTxValidationTest, missing_witness) { TEST_F(WalletTxValidationTest, zero_output) { m_valid_tx.m_outputs[0].m_value = 0; - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( @@ -118,7 +122,7 @@ TEST_F(WalletTxValidationTest, duplicate_input) { m_valid_tx.m_witness.emplace_back(m_valid_tx.m_witness[0]); m_valid_tx.m_outputs[0].m_value *= 2; - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( @@ -135,7 +139,7 @@ TEST_F(WalletTxValidationTest, duplicate_input) { TEST_F(WalletTxValidationTest, invalid_input_prevout) { m_valid_tx.m_inputs[0].m_prevout_data.m_value = 0; - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( @@ -152,7 +156,7 @@ TEST_F(WalletTxValidationTest, invalid_input_prevout) { TEST_F(WalletTxValidationTest, asymmetric_inout_set) { m_valid_tx.m_outputs[0].m_value--; - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( @@ -249,7 +253,7 @@ TEST_F(WalletTxValidationTest, witness_p2pk_invalid_signature) { TEST_F(WalletTxValidationTest, check_transaction_with_unknown_witness_program_type) { m_valid_tx.m_witness[0][0] = std::byte(0xFF); - auto err = cbdc::transaction::validation::check_tx(m_valid_tx); + auto err = cbdc::transaction::validation::check_tx(m_valid_tx, m_opts); ASSERT_TRUE(err.has_value()); ASSERT_TRUE( std::holds_alternative( @@ -336,3 +340,71 @@ TEST_F(WalletTxValidationTest, sign_verify_compact) { ASSERT_FALSE( cbdc::transaction::validation::check_attestations(ctx, m_pubkeys, 2)); } + +class MinterValidationTest : public ::testing::Test { + protected: + void SetUp() override { + const auto minter_pub = m_minter.generate_minter_key(); + m_opts.m_minter_pubkeys.insert(minter_pub); + } + + cbdc::transaction::wallet m_minter{}; + cbdc::transaction::wallet m_not_minter{}; + cbdc::config::options m_opts{}; +}; + +// To Test: +// * valid mint +// * invalid mint (wrong signer) +// * missing_output +// * missing output value +// * wrong number of witnesses +// * wrong output committment +TEST_F(MinterValidationTest, valid_mint) { + cbdc::transaction::full_tx tx = m_minter.mint_new_coins(5, 10); + auto err = cbdc::transaction::validation::check_tx(tx, m_opts); + ASSERT_FALSE(err.has_value()); +} + +TEST_F(MinterValidationTest, invalid_mint) { + cbdc::transaction::full_tx tx = m_not_minter.mint_new_coins(1, 1000); + auto err = cbdc::transaction::validation::check_mint_p2pk_witness(tx, + 0, + m_opts); + ASSERT_TRUE(err.has_value()); + ASSERT_EQ( + err.value(), + cbdc::transaction::validation::witness_error_code::invalid_minter_key); +} + +TEST_F(MinterValidationTest, no_outputs) { + cbdc::transaction::full_tx tx = m_minter.mint_new_coins(5, 10); + tx.m_outputs.clear(); + + auto err = cbdc::transaction::validation::check_tx(tx, m_opts); + ASSERT_TRUE(err.has_value()); +} + +TEST_F(MinterValidationTest, no_output_value) { + cbdc::transaction::full_tx tx = m_minter.mint_new_coins(5, 10); + tx.m_outputs[0].m_value = 0; + + auto err = cbdc::transaction::validation::check_tx(tx, m_opts); + ASSERT_TRUE(err.has_value()); +} + +TEST_F(MinterValidationTest, missing_witness) { + cbdc::transaction::full_tx tx = m_minter.mint_new_coins(5, 10); + tx.m_witness.clear(); + + auto err = cbdc::transaction::validation::check_tx(tx, m_opts); + ASSERT_TRUE(err.has_value()); +} + +TEST_F(MinterValidationTest, bad_witness_committment) { + cbdc::transaction::full_tx tx = m_minter.mint_new_coins(5, 10); + tx.m_outputs[0].m_witness_program_commitment = cbdc::hash_t{0}; + + auto err = cbdc::transaction::validation::check_tx(tx, m_opts); + ASSERT_TRUE(err.has_value()); +} diff --git a/tests/unit/wallet_test.cpp b/tests/unit/wallet_test.cpp index a1ff2c7b6..e48aee7df 100644 --- a/tests/unit/wallet_test.cpp +++ b/tests/unit/wallet_test.cpp @@ -201,3 +201,17 @@ TEST_F(WalletTest, load_save) { ASSERT_EQ(m_wallet.balance(), new_wal.balance()); ASSERT_EQ(m_wallet.count(), new_wal.count()); } + +TEST_F(WalletTest, minter_key_generation) { + // always generates the same key + const auto k1 = m_wallet.generate_minter_key(); + const auto k2 = m_wallet.generate_minter_key(); + ASSERT_EQ(k1, k2); + const auto k3 = cbdc::hash_from_hex(m_wallet.minter_pubkey_as_hex()); + ASSERT_EQ(k3, k1); + + const auto tkey = m_wallet.generate_test_minter_key(); + ASSERT_EQ(tkey, + cbdc::hash_from_hex("1f05f6173c4f7bef58f7e912c4cb1389097a38f1a9e" + "24c3674d67a0f142af244")); +}