diff --git a/bftengine/include/bftengine/Replica.hpp b/bftengine/include/bftengine/Replica.hpp index 82e3ce2fec..b02bd0579b 100644 --- a/bftengine/include/bftengine/Replica.hpp +++ b/bftengine/include/bftengine/Replica.hpp @@ -28,7 +28,7 @@ #include "PersistentStorage.hpp" #include "IRequestHandler.hpp" #include "InternalBFTClient.hpp" - +#include "Timers.hpp" namespace concord::cron { class TicksGenerator; } @@ -38,7 +38,10 @@ class ISecretsManagerImpl; } namespace bftEngine { - +namespace impl { +class MsgsCommunicator; +class MsgHandlersRegistrator; +} // namespace impl // Possible values for 'flags' parameter enum MsgFlag : uint64_t { EMPTY_FLAGS = 0x0, @@ -48,6 +51,7 @@ enum MsgFlag : uint64_t { KEY_EXCHANGE_FLAG = 0x8, // TODO [TK] use reconfig_flag TICK_FLAG = 0x10, RECONFIG_FLAG = 0x20, + INTERNAL_FLAG = 0x40, PUBLISH_ON_CHAIN_OBJECT_FLAG = 0x80, CLIENTS_PUB_KEYS_FLAG = 0x100, DB_CHECKPOINT_FLAG = 0x200 @@ -135,6 +139,10 @@ class IReplica { // Returns the internal persistent storage object. virtual std::shared_ptr persistentStorage() const = 0; + + virtual std::shared_ptr getMsgsCommunicator() const { return nullptr; } + virtual std::shared_ptr getMsgHandlersRegistrator() const { return nullptr; } + virtual concordUtil::Timers *getTimers() { return nullptr; } }; } // namespace bftEngine diff --git a/bftengine/src/bftengine/BFTEngine.cpp b/bftengine/src/bftengine/BFTEngine.cpp index e86cda82c5..f9d4b198e3 100644 --- a/bftengine/src/bftengine/BFTEngine.cpp +++ b/bftengine/src/bftengine/BFTEngine.cpp @@ -69,6 +69,12 @@ class ReplicaInternal : public IReplica { std::shared_ptr internalClient() const override { return internal_client_; } + std::shared_ptr getMsgsCommunicator() const override { return replica_->getMsgsCommunicator(); } + std::shared_ptr getMsgHandlersRegistrator() const override { + return replica_->getMsgHandlersRegistrator(); + } + concordUtil::Timers *getTimers() override { return replica_->getTimers(); } + private: std::unique_ptr replica_; std::condition_variable debugWait_; diff --git a/bftengine/src/bftengine/ReplicaBase.hpp b/bftengine/src/bftengine/ReplicaBase.hpp index 74899f19b1..eb87aaa317 100644 --- a/bftengine/src/bftengine/ReplicaBase.hpp +++ b/bftengine/src/bftengine/ReplicaBase.hpp @@ -52,7 +52,7 @@ class ReplicaBase { std::shared_ptr getMsgsCommunicator() const { return msgsCommunicator_; } std::shared_ptr getMsgHandlersRegistrator() const { return msgHandlers_; } - + concordUtil::Timers* getTimers() { return &timers_; } void SetAggregator(std::shared_ptr aggregator) { if (aggregator) { aggregator_ = aggregator; diff --git a/bftengine/src/bftengine/ReplicaImp.cpp b/bftengine/src/bftengine/ReplicaImp.cpp index b899f2fc10..69cd113052 100644 --- a/bftengine/src/bftengine/ReplicaImp.cpp +++ b/bftengine/src/bftengine/ReplicaImp.cpp @@ -438,10 +438,10 @@ void ReplicaImp::onMessage(ClientRequestMsg *m) { // check message validity const bool invalidClient = - !isValidClient(clientId) && - !((repsInfo->isIdOfReplica(clientId) || repsInfo->isIdOfPeerRoReplica(clientId)) && (flags & RECONFIG_FLAG)); + !isValidClient(clientId) && !((repsInfo->isIdOfReplica(clientId) || repsInfo->isIdOfPeerRoReplica(clientId)) && + (flags & RECONFIG_FLAG || flags & INTERNAL_FLAG)); const bool sentFromReplicaToNonPrimary = - !(flags & RECONFIG_FLAG) && repsInfo->isIdOfReplica(senderId) && !isCurrentPrimary(); + !(flags & RECONFIG_FLAG || flags & INTERNAL_FLAG) && repsInfo->isIdOfReplica(senderId) && !isCurrentPrimary(); if (invalidClient) { ++numInvalidClients; @@ -5487,7 +5487,9 @@ void ReplicaImp::executeRequestsInPrePrepareMsg(concordUtils::SpanWrapper &paren reqIdx++; continue; } - const bool validClient = isValidClient(clientId) || ((req.flags() & RECONFIG_FLAG) && isIdOfReplica(clientId)); + const bool validClient = + isValidClient(clientId) || + ((req.flags() & RECONFIG_FLAG || req.flags() & INTERNAL_FLAG) && isIdOfReplica(clientId)); if (!validClient) { ++numInvalidClients; LOG_WARN(CNSUS, "The client is not valid" << KVLOG(clientId)); diff --git a/bftengine/src/bftengine/messages/ClientRequestMsg.cpp b/bftengine/src/bftengine/messages/ClientRequestMsg.cpp index b722248323..1b27f8164b 100644 --- a/bftengine/src/bftengine/messages/ClientRequestMsg.cpp +++ b/bftengine/src/bftengine/messages/ClientRequestMsg.cpp @@ -100,7 +100,8 @@ bool ClientRequestMsg::shouldValidateAsync() const { // manner, as that will lead to overhead. Similarly, key exchanges should happen rarely, and thus we should validate // as quick as possible, in sync. const auto* header = msgBody(); - if (((header->flags & RECONFIG_FLAG) != 0) || ((header->flags & KEY_EXCHANGE_FLAG) != 0)) { + if (((header->flags & RECONFIG_FLAG) != 0) || ((header->flags & KEY_EXCHANGE_FLAG) != 0) || + (header->flags & INTERNAL_FLAG) != 0) { return false; } return true; @@ -119,7 +120,8 @@ void ClientRequestMsg::validateImp(const ReplicasInfo& repInfo) const { } PrincipalId clientId = header->idOfClientProxy; - if ((header->flags & RECONFIG_FLAG) == 0) ConcordAssert(this->senderId() != repInfo.myId()); + if ((header->flags & RECONFIG_FLAG) == 0 && (header->flags & INTERNAL_FLAG) == 0) + ConcordAssert(this->senderId() != repInfo.myId()); /// to do - should it be just the header? auto minMsgSize = sizeof(ClientRequestMsgHeader) + header->cidLength + spanContextSize() + header->reqSignatureLength; @@ -135,9 +137,9 @@ void ClientRequestMsg::validateImp(const ReplicasInfo& repInfo) const { bool isIdOfExternalClient = repInfo.isIdOfExternalClient(clientId); bool doSigVerify = false; bool emptyReq = (header->requestLength == 0); - if ((header->flags & RECONFIG_FLAG) != 0 && + if (((header->flags & RECONFIG_FLAG) != 0 || (header->flags & INTERNAL_FLAG) != 0) && (repInfo.isIdOfReplica(clientId) || repInfo.isIdOfPeerRoReplica(clientId))) { - // Allow every reconfiguration message from replicas (it will be verified in the reconfiguration handler) + // Allow every reconfiguration/internal message from replicas (it will be verified in the reconfiguration handler) return; } if (!repInfo.isValidPrincipalId(clientId)) { diff --git a/kvbc/include/Replica.h b/kvbc/include/Replica.h index 3fe7d7055d..aec6846aa4 100644 --- a/kvbc/include/Replica.h +++ b/kvbc/include/Replica.h @@ -181,6 +181,11 @@ class Replica : public IReplica, } virtual ~Replica(); + std::shared_ptr getMsgsCommunicator() const { return m_replicaPtr->getMsgsCommunicator(); } + std::shared_ptr getMsgHandlersRegistrator() const { + return m_replicaPtr->getMsgHandlersRegistrator(); + } + concordUtil::Timers *getTimers() { return m_replicaPtr->getTimers(); } protected: RawBlock getBlockInternal(BlockId blockId) const; diff --git a/utt-replica/signature-processor/include/sigProcessor.hpp b/utt-replica/signature-processor/include/sigProcessor.hpp index 5d61f7d63d..d00eb57126 100644 --- a/utt-replica/signature-processor/include/sigProcessor.hpp +++ b/utt-replica/signature-processor/include/sigProcessor.hpp @@ -36,6 +36,26 @@ class SigProcessor { */ using GenerateAppClientRequestCb = std::function(uint64_t, const std::vector&)>; + class CompleteSignatureMsg { + std::map> sigs; + std::vector full_sig; + uint32_t num_replicas{0}; + + public: + CompleteSignatureMsg(uint32_t n, const std::map>&, const std::vector&); + explicit CompleteSignatureMsg() = default; + CompleteSignatureMsg(const std::vector& buffer); + bool validate() const; + const std::map>& getPartialSigs() const; + const std::vector& getFullSig() const; + /* + The serialized output is in the following format: + [num_replicas|full_sig.size()|full_sig|sigs.size()|s.size()|s] (for s in sigs) + */ + std::vector serialize() const; + CompleteSignatureMsg& deserialize(const std::vector&); + }; + static const GenerateAppClientRequestCb default_client_app_request_generator; private: @@ -136,13 +156,9 @@ class SigProcessor { /** * @brief Publishing the complete signature to the consensus * - * @param sig_id the signature id - * @param complete_signature the complete aggregated signature - * @param cb a callback that defines how the upper level generates the application request to the consensus + * @param job_entry The relevant job */ - void publishCompleteSignature(uint64_t sig_id, - const std::vector& complete_signature, - const GenerateAppClientRequestCb& cb); + void publishCompleteSignature(const SigJobEntry& job_entry); /** * @brief Handles the event of timeout for a specific job * diff --git a/utt-replica/signature-processor/src/sigProcessor.cpp b/utt-replica/signature-processor/src/sigProcessor.cpp index 7c8a77e2de..8ef23cc90a 100644 --- a/utt-replica/signature-processor/src/sigProcessor.cpp +++ b/utt-replica/signature-processor/src/sigProcessor.cpp @@ -17,6 +17,7 @@ #include "bftengine/ReplicaConfig.hpp" #include "bftengine/messages/MessageBase.hpp" #include "bftengine/messages/ClientRequestMsg.hpp" +#include "bftengine/Replica.hpp" #include namespace utt { @@ -141,8 +142,7 @@ void SigProcessor::processSignature(uint64_t sig_id, #endif // if we already have enough valid partial signatures, lets publish the complete signature and return if (entry->partial_sigs.size() == threshold_) { - auto complete_sig = libutt::api::Utils::aggregateSigShares(n_, entry->partial_sigs); - publishCompleteSignature(sig_id, complete_sig, entry->client_app_data_generator_cb_); + publishCompleteSignature(*entry); return; } } @@ -179,26 +179,26 @@ void SigProcessor::onReceivingNewPartialSig(uint64_t sig_id, // We don't care doing this part under the entry lock, because if we did reach the threshold, we won't use this // entry anymore if (entry->client_app_data_generator_cb_ != nullptr && entry->partial_sigs.size() == threshold_) { - auto sig = libutt::api::Utils::aggregateSigShares(n_, entry->partial_sigs); - publishCompleteSignature(sig_id, sig, entry->client_app_data_generator_cb_); + publishCompleteSignature(*entry); } } } -void SigProcessor::publishCompleteSignature(uint64_t sig_id, - const std::vector& sig, - const GenerateAppClientRequestCb& cb) { + +void SigProcessor::publishCompleteSignature(const SigJobEntry& job_entry) { + auto sig_id = job_entry.job_id; + auto fsig = libutt::api::Utils::aggregateSigShares(n_, job_entry.partial_sigs); + CompleteSignatureMsg msg(n_, job_entry.partial_sigs, fsig); auto requestSeqNum = std::chrono::duration_cast(getMonotonicTime().time_since_epoch()).count(); - std::vector appClientReq = cb(sig_id, sig); - std::unique_ptr cmsg = - std::make_unique(repId_, - 0x0, - requestSeqNum, - (uint32_t)appClientReq.size(), - (const char*)appClientReq.data(), - 60000, - "new-utt-sig-" + std::to_string(sig_id)); - msgs_communicator_->getIncomingMsgsStorage()->pushExternalMsg(std::move(cmsg)); + std::vector appClientReq = job_entry.client_app_data_generator_cb_(sig_id, msg.serialize()); + auto crm = std::make_unique(repId_, + bftEngine::MsgFlag::INTERNAL_FLAG, + requestSeqNum, + (uint32_t)appClientReq.size(), + (const char*)appClientReq.data(), + 60000, + "new-utt-sig-" + std::to_string(sig_id)); + msgs_communicator_->getIncomingMsgsStorage()->pushExternalMsg(std::move(crm)); } // Called by the validating thread void SigProcessor::onReceivingNewValidFullSig(uint64_t sig_id) { @@ -227,4 +227,73 @@ void SigProcessor::onJobTimeout(uint64_t job_id, const std::vector& sig msgs_communicator_->sendAsyncMessage((uint64_t)rid, msg.body(), msg.size()); } } + +SigProcessor::CompleteSignatureMsg::CompleteSignatureMsg(uint32_t n, + const std::map>& psigs, + const std::vector& fsig) + : sigs{psigs}, full_sig{fsig}, num_replicas{n} {} +bool SigProcessor::CompleteSignatureMsg::validate() const { + auto complete_sig = libutt::api::Utils::aggregateSigShares(num_replicas, sigs); + return full_sig == complete_sig; +} +SigProcessor::CompleteSignatureMsg::CompleteSignatureMsg(const std::vector& buffer) { deserialize(buffer); } +const std::vector& SigProcessor::CompleteSignatureMsg::getFullSig() const { return full_sig; } +const std::map>& SigProcessor::CompleteSignatureMsg::getPartialSigs() const { + return sigs; +} +std::vector SigProcessor::CompleteSignatureMsg::serialize() const { + uint64_t loc{0}; + uint64_t fsig_size = full_sig.size(); + uint64_t psigs_size = sigs.size(); + size_t msg_size = sizeof(num_replicas) + sizeof(fsig_size) + full_sig.size(); + msg_size += sizeof(psigs_size); + for (const auto& [k, v] : sigs) msg_size += (sizeof(k) + sizeof(uint64_t) + v.size()); + std::vector ret(msg_size); + std::memcpy(ret.data(), &num_replicas, sizeof(num_replicas)); + loc += sizeof(num_replicas); + std::memcpy(ret.data() + loc, &fsig_size, sizeof(fsig_size)); + loc += sizeof(fsig_size); + std::memcpy(ret.data() + loc, full_sig.data(), full_sig.size()); + loc += full_sig.size(); + std::memcpy(ret.data() + loc, &psigs_size, sizeof(psigs_size)); + loc += sizeof(psigs_size); + for (const auto& [k, v] : sigs) { + std::memcpy(ret.data() + loc, &k, sizeof(k)); + loc += sizeof(k); + uint64_t ssize = v.size(); + std::memcpy(ret.data() + loc, &ssize, sizeof(ssize)); + loc += sizeof(ssize); + std::memcpy(ret.data() + loc, v.data(), v.size()); + loc += v.size(); + } + return ret; +} + +SigProcessor::CompleteSignatureMsg& SigProcessor::CompleteSignatureMsg::deserialize( + const std::vector& buffer) { + size_t loc{0}; + std::memcpy(&num_replicas, buffer.data(), sizeof(num_replicas)); + loc += sizeof(num_replicas); + uint64_t fsig_size{0}; + std::memcpy(&fsig_size, buffer.data() + loc, sizeof(fsig_size)); + loc += sizeof(fsig_size); + full_sig.resize(fsig_size); + std::memcpy(full_sig.data(), buffer.data() + loc, fsig_size); + loc += fsig_size; + uint64_t psigs_nums{0}; + std::memcpy(&psigs_nums, buffer.data() + loc, sizeof(psigs_nums)); + loc += sizeof(psigs_nums); + for (size_t i = 0; i < psigs_nums; i++) { + uint32_t rep_id{0}; + std::memcpy(&rep_id, buffer.data() + loc, sizeof(rep_id)); + loc += sizeof(rep_id); + uint64_t s_size{0}; + std::memcpy(&s_size, buffer.data() + loc, sizeof(s_size)); + loc += sizeof(s_size); + sigs[rep_id] = std::vector(s_size); + std::memcpy(sigs[rep_id].data(), buffer.data() + loc, s_size); + loc += s_size; + } + return *this; +} } // namespace utt \ No newline at end of file diff --git a/utt-replica/signature-processor/tests/sigProcessorTests.cpp b/utt-replica/signature-processor/tests/sigProcessorTests.cpp index 1535d4fd73..0eba5aa626 100644 --- a/utt-replica/signature-processor/tests/sigProcessorTests.cpp +++ b/utt-replica/signature-processor/tests/sigProcessorTests.cpp @@ -130,6 +130,7 @@ class TestReceiver : public bft::communication::IReceiver { struct GpData { libutt::CommKey cck; libutt::CommKey rck; + bool budget_policy = true; }; std::tuple init(size_t n, size_t thresh) { @@ -304,9 +305,13 @@ class test_utt_instance : public ::testing::Test { msr->registerMsgHandler(MsgCode::ClientRequest, [&, sp](bftEngine::impl::MessageBase* message) { ClientRequestMsg* msg = (ClientRequestMsg*)message; uint64_t job_id{0}; - libutt::api::types::Signature fsig(msg->requestLength() - sizeof(uint64_t)); + + std::vector cs_buffer(msg->requestLength() - sizeof(uint64_t)); std::memcpy(&job_id, msg->requestBuf(), sizeof(uint64_t)); - std::memcpy(fsig.data(), msg->requestBuf() + sizeof(uint64_t), fsig.size()); + std::memcpy(cs_buffer.data(), msg->requestBuf() + sizeof(uint64_t), cs_buffer.size()); + utt::SigProcessor::CompleteSignatureMsg cs_msg(cs_buffer); + ASSERT_TRUE(cs_msg.validate()); + auto& fsig = cs_msg.getFullSig(); { std::unique_lock lk(sigs_lock); for (size_t j = 0; j < n; j++) { @@ -418,7 +423,6 @@ class utt_complete_system : public utt_system_include_budget { } std::unordered_map> coins; }; - TEST_F(test_utt_instance, test_clients_registration) { registerClients( job_id, diff --git a/utt/CMakeLists.txt b/utt/CMakeLists.txt index 70f7673691..cacbdb4cd0 100644 --- a/utt/CMakeLists.txt +++ b/utt/CMakeLists.txt @@ -41,6 +41,12 @@ option( OFF ) +option( + BUILD_PRIVACY_ADMIN_CLI + "Enable building of admin-cli" + OFF +) + # # Configure CCache if available # @@ -186,6 +192,9 @@ add_subdirectory(libxutils) if(BUILD_WALLET_CLI) add_subdirectory(wallet-cli) endif() +if(BUILD_PRIVACY_ADMIN_CLI) + add_subdirectory(admin-cli) +endif() # [TODO-UTT] This improved api for libutt could go into its own subproject set(newutt_src diff --git a/utt/admin-cli/CMakeLists.txt b/utt/admin-cli/CMakeLists.txt new file mode 100644 index 0000000000..fb8343e09f --- /dev/null +++ b/utt/admin-cli/CMakeLists.txt @@ -0,0 +1,14 @@ +add_subdirectory("proto") + +set(privacy-admin-cli-src + src/main.cpp + src/admin.cpp +) + +add_executable(privacy-admin-cli ${privacy-admin-cli-src}) + +target_include_directories(privacy-admin-cli PUBLIC include/ ../utt-client-api/include ../utt-common-api/include) + +target_link_libraries(privacy-admin-cli PUBLIC + privacy-admin-api-proto utt_client_api +) \ No newline at end of file diff --git a/utt/admin-cli/README.md b/utt/admin-cli/README.md new file mode 100644 index 0000000000..3057de0e58 --- /dev/null +++ b/utt/admin-cli/README.md @@ -0,0 +1,3 @@ +# UTT Admin Command-Line Application + +A command-line application for a UTT admin. Uses gRPC to talk to a admin service, for which we provide the interface in [proto/api/v1/api.proto](proto/api/v1/api.proto). The actual implementation of an admin service is not provided. \ No newline at end of file diff --git a/utt/admin-cli/include/admin.hpp b/utt/admin-cli/include/admin.hpp new file mode 100644 index 0000000000..ffc73cc60d --- /dev/null +++ b/utt/admin-cli/include/admin.hpp @@ -0,0 +1,42 @@ +// UTT Client API +// +// Copyright (c) 2020-2022 VMware, Inc. All Rights Reserved. +// +// This product is licensed to you under the Apache 2.0 license (the "License"). +// You may not use this product except in compliance with the Apache 2.0 +// License. +// +// This product may include a number of subcomponents with separate copyright +// notices and license terms. Your use of these subcomponents is subject to the +// terms and conditions of the sub-component's license, as noted in the LICENSE +// file. + +#pragma once + +#include +#include + +#include +#include "api.grpc.pb.h" // Generated from utt/admin/proto/api + +#include + +namespace AdminApi = vmware::concord::utt::admin::api::v1; + +class Admin { + public: + using Connection = std::unique_ptr; + using Channel = std::unique_ptr>; + + static Connection newConnection(); + + /// [TODO-UTT] Should be performed by an admin app + /// @brief Deploy a privacy application + /// @return The public configuration of the deployed application + static bool deployApp(Channel& chan, bool budget_policy); + + /// @brief Request the creation of a privacy budget. The amount of the budget is predetermined by the deployed app. + /// This operation could be performed entirely by an administrator, but we add it in the admin + /// for demo purposes. + static void createPrivacyBudget(Channel& chan); +}; \ No newline at end of file diff --git a/utt/admin-cli/proto/CMakeLists.txt b/utt/admin-cli/proto/CMakeLists.txt new file mode 100644 index 0000000000..2487f51e15 --- /dev/null +++ b/utt/admin-cli/proto/CMakeLists.txt @@ -0,0 +1,15 @@ +find_package(Protobuf REQUIRED) +find_package(GRPC REQUIRED) + +include_directories(${GRPC_INCLUDE_DIR}) + +protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${CMAKE_CURRENT_BINARY_DIR} + api/v1/api.proto +) +grpc_generate_cpp(GRPC_SRCS GRPC_HDRS ${CMAKE_CURRENT_BINARY_DIR} + api/v1/api.proto +) + +add_library(privacy-admin-api-proto STATIC ${PROTO_SRCS} ${GRPC_SRCS}) +target_link_libraries(privacy-admin-api-proto PRIVATE protobuf::libprotobuf gRPC::grpc++) +target_include_directories(privacy-admin-api-proto PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) \ No newline at end of file diff --git a/utt/admin-cli/proto/api/v1/api.proto b/utt/admin-cli/proto/api/v1/api.proto new file mode 100644 index 0000000000..c6d336ef46 --- /dev/null +++ b/utt/admin-cli/proto/api/v1/api.proto @@ -0,0 +1,47 @@ +// Copyright 2021 VMware, all rights reserved +// +// UTT Admin's api + +syntax = "proto3"; +// [TODO-UTT] Condense this package identifier into fewer tokens? +package vmware.concord.utt.admin.api.v1; + +// Privacy Admin Service Interface +service AdminService { + rpc adminChannel(stream AdminRequest) returns (stream AdminResponse); +} + +message DeployPrivacyAppRequest { + optional bytes config = 1; +} + +message DeployPrivacyAppResponse { + optional string err = 1; // Returns any error generated during deployment + optional string privacy_contract_addr = 2; // Address of the deployed privacy contract + optional string token_contract_addr = 3; // Address of the deployed token contract +} + +message CreatePrivacyBudgetRequest { + optional string user_id = 1; +} + +message CreatePrivacyBudgetResponse { + optional string err = 1; + optional bytes budget = 2; + optional bytes signature = 3; +} + +message AdminRequest { + oneof req { + DeployPrivacyAppRequest deploy = 1; + CreatePrivacyBudgetRequest create_budget = 2; + } +} + +message AdminResponse { + optional string err = 1; + oneof resp { + DeployPrivacyAppResponse deploy = 2; + CreatePrivacyBudgetResponse create_budget = 3; + } +} \ No newline at end of file diff --git a/utt/admin-cli/src/admin.cpp b/utt/admin-cli/src/admin.cpp new file mode 100644 index 0000000000..447eabf129 --- /dev/null +++ b/utt/admin-cli/src/admin.cpp @@ -0,0 +1,96 @@ +// UTT Client API +// +// Copyright (c) 2020-2022 VMware, Inc. All Rights Reserved. +// +// This product is licensed to you under the Apache 2.0 license (the "License"). +// You may not use this product except in compliance with the Apache 2.0 +// License. +// +// This product may include a number of subcomponents with separate copyright +// notices and license terms. Your use of these subcomponents is subject to the +// terms and conditions of the sub-component's license, as noted in the LICENSE +// file. + +#include "admin.hpp" + +#include + +using namespace vmware::concord::utt::admin::api::v1; + +Admin::Connection Admin::newConnection() { + std::string grpcServerAddr = "127.0.0.1:49000"; + + std::cout << "Connecting to gRPC server at " << grpcServerAddr << "... "; + + auto chan = grpc::CreateChannel(grpcServerAddr, grpc::InsecureChannelCredentials()); + + if (!chan) { + throw std::runtime_error("Failed to create gRPC channel."); + } + auto timeoutSec = std::chrono::seconds(5); + if (chan->WaitForConnected(std::chrono::system_clock::now() + timeoutSec)) { + std::cout << "Connected.\n"; + } else { + throw std::runtime_error("Failed to connect to gRPC server after " + std::to_string(timeoutSec.count()) + + " seconds."); + } + + return AdminService::NewStub(chan); +} + +bool Admin::deployApp(Channel& chan, bool budget_policy) { + std::cout << "Deploying a new privacy application...\n"; + + // Generate a privacy config for a N=4 replica system tolerating F=1 failures + utt::client::ConfigInputParams params; + params.validatorPublicKeys = std::vector{4, "placeholderPublicKey"}; // N = 3 * F + 1 + params.threshold = 2; // F + 1 + params.useBudget = budget_policy; + auto config = utt::client::generateConfig(params); + if (config.empty()) throw std::runtime_error("Failed to generate a privacy app configuration!"); + + AdminRequest req; + req.mutable_deploy()->set_config(config.data(), config.size()); + chan->Write(req); + + AdminResponse resp; + chan->Read(&resp); + if (!resp.has_deploy()) throw std::runtime_error("Expected deploy response from admin service!"); + const auto& deployResp = resp.deploy(); + + // Note that keeping the config around in memory is just a temp solution and should not happen in real system + if (deployResp.has_err()) throw std::runtime_error("Failed to deploy privacy app: " + resp.err()); + + std::cout << "\nSuccessfully deployed privacy application\n"; + std::cout << "---------------------------------------------------\n"; + std::cout << "Privacy contract: " << deployResp.privacy_contract_addr() << '\n'; + std::cout << "Token contract: " << deployResp.token_contract_addr() << '\n'; + + std::cout << "\nYou are now ready to configure wallets.\n"; + + return true; +} + +void Admin::createPrivacyBudget(Channel& chan) { + (void)chan; + // [TODO-UTT] Create budget is done locally, should be done by the system + // grpc::ClientContext ctx; + + // CreatePrivacyBudgetRequest req; + // req.set_user_id(userId_); + + // CreatePrivacyBudgetResponse resp; + // conn->createPrivacyBudget(&ctx, req, &resp); + + // if (resp.has_err()) { + // std::cout << "Failed to create privacy budget:" << resp.err() << '\n'; + // } else { + // utt::PrivacyBudget budget = std::vector(resp.budget().begin(), resp.budget().end()); + // utt::RegistrationSig sig = std::vector(resp.signature().begin(), resp.signature().end()); + + // std::cout << "Got budget " << budget.size() << " bytes.\n"; + // std::cout << "Got budget sig " << sig.size() << " bytes.\n"; + + // user_->updatePrivacyBudget(budget, sig); + // } +} \ No newline at end of file diff --git a/utt/admin-cli/src/main.cpp b/utt/admin-cli/src/main.cpp new file mode 100644 index 0000000000..4803fb7729 --- /dev/null +++ b/utt/admin-cli/src/main.cpp @@ -0,0 +1,133 @@ +// UTT Client API +// +// Copyright (c) 2020-2022 VMware, Inc. All Rights Reserved. +// +// This product is licensed to you under the Apache 2.0 license (the "License"). +// You may not use this product except in compliance with the Apache 2.0 +// License. +// +// This product may include a number of subcomponents with separate copyright +// notices and license terms. Your use of these subcomponents is subject to the +// terms and conditions of the sub-component's license, as noted in the LICENSE +// file. + +#include +#include +#include + +#include "admin.hpp" + +void printHelp() { + std::cout << "\nCommands:\n"; + std::cout << "deploy [budget-policy-enabled/budget-policy-disabled] -- generates a privacy config and deploys the " + "privacy and token contracts\n." + "budget-policy-disabled means that budget coin's presence won't be enforced in the system\n" + "if no input was given, the default is budget-policy-enabled" + // [TODO-UTT] Admin creates a user's budget by supplying a user-id and amount from the CLI + // std::cout + // << "create-budget -- requests creation of a privacy budget, the amount is decided by the + // system.\n"; + std::cout + << '\n'; +} + +struct CLIApp { + grpc::ClientContext ctx; + Admin::Connection conn; + Admin::Channel chan; + utt::Configuration config; + utt::client::TestUserPKInfrastructure pki; + bool deployed = false; + + CLIApp() { + conn = Admin::newConnection(); + if (!conn) throw std::runtime_error("Failed to create admin connection!"); + + chan = conn->adminChannel(&ctx); + if (!chan) throw std::runtime_error("Failed to create admin streaming channel!"); + } + + ~CLIApp() { + std::cout << "Closing admin streaming channel... "; + chan->WritesDone(); + auto status = chan->Finish(); + std::cout << " Done.\n"; + // std::cout << "gRPC error code: " << status.error_code() << '\n'; + // std::cout << "gRPC error msg: " << status.error_message() << '\n'; + // std::cout << "gRPC error details: " << status.error_details() << '\n'; + } + + void deploy(bool budget_policy) { + if (deployed) { + std::cout << "The privacy app is already deployed.\n"; + return; + } + + deployed = Admin::deployApp(chan, budget_policy); + } + + void createBudgetCmd(const std::vector& cmdTokens) { + (void)cmdTokens; + //[TODO-UTT] Create by sending a request to the system + // if (cmdTokens.size() != 2) { + // std::cout << "Usage: create-budget \n"; + // return; + // } + // auto admin = getAdmin(cmdTokens[1]); + // if (!admin) { + // std::cout << "No admin for '" << cmdTokens[1] << "'\n"; + // return; + // } + + // admin->createPrivacyBudgetLocal(config, 10000); + } +}; + +int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + try { + utt::client::Initialize(); + + CLIApp app; + + std::cout << "\nSample Privacy Admin CLI Application.\n"; + + while (true) { + std::cout << "\nEnter command (type 'h' for commands 'Ctr-D' to quit):\n > "; + std::vector cmdTokens; + std::string token; + std::stringstream ss(cmd); + while (std::getline(ss, token, ' ')) cmdTokens.emplace_back(token); + if (cmdTokens.empty()) continue; + + if (std::cin.eof()) { + std::cout << "Quitting...\n"; + break; + } + + if (cmd == "h") { + printHelp(); + } else if (cmdTokens[0] == "deploy") { + if (cmdTokens.size() == 1) { + app.deploy(true); + continue; + } + bool budget_policy = cmdTokens[1] == "budget-policy-enabled"; + app.deploy(budget_policy); + } else if (!app.deployed) { + std::cout << "You must first deploy the privacy application. Use the 'deploy' command.\n"; + } else if (cmdTokens[0] == "create-budget") { + app.createBudgetCmd(cmdTokens); + } else { + std::cout << "Unknown command '" << cmd << "'\n"; + } + } + } catch (const std::runtime_error& e) { + std::cout << "Error (exception): " << e.what() << '\n'; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/utt/include/UTTParams.hpp b/utt/include/UTTParams.hpp index c074739ae0..acec10c7ad 100644 --- a/utt/include/UTTParams.hpp +++ b/utt/include/UTTParams.hpp @@ -56,6 +56,9 @@ class UTTParams { UTTParams(UTTParams&&) = default; UTTParams& operator=(UTTParams&&) = default; + bool getBudgetPolicy() const; + bool budget_policy = true; + private: friend std::ostream& ::operator<<(std::ostream& out, const libutt::api::UTTParams& params); friend std::istream& ::operator>>(std::istream& in, libutt::api::UTTParams& params); diff --git a/utt/include/burn.hpp b/utt/include/burn.hpp index cecaf90219..f2a9177bbd 100644 --- a/utt/include/burn.hpp +++ b/utt/include/burn.hpp @@ -61,6 +61,11 @@ class Burn { */ const Coin& getCoin() const; + /** + * @brief Get the pid of the owner of the burned coin + */ + const std::string& getOwnerPid() const; + public: friend class libutt::api::CoinsSigner; friend class libutt::api::Client; diff --git a/utt/include/coin.hpp b/utt/include/coin.hpp index 0896fb5f85..dd85c0a5d5 100644 --- a/utt/include/coin.hpp +++ b/utt/include/coin.hpp @@ -81,7 +81,8 @@ class Coin { const types::CurvePoint& val, const types::CurvePoint& client_id_hash, Type t, - const types::CurvePoint& expiration_date); + const types::CurvePoint& expiration_date, + bool finalize = true); Coin(); Coin(const Coin& c); Coin& operator=(const Coin& c); @@ -102,6 +103,7 @@ class Coin { * @param prf The secret client's PRF key */ void createNullifier(const UTTParams& p, const types::CurvePoint& prf); + void finalize(const UTTParams& p, const types::CurvePoint& prf); /** * @brief Check if this coin has a signature associated with it @@ -168,6 +170,8 @@ class Coin { */ types::CurvePoint getExpDateAsCurvePoint() const; + uint64_t getExpDate() const; + private: friend class Client; friend class operations::Burn; diff --git a/utt/include/config.hpp b/utt/include/config.hpp index 9329ed09f0..d79eaabce1 100644 --- a/utt/include/config.hpp +++ b/utt/include/config.hpp @@ -62,7 +62,7 @@ class Configuration { /// @brief Constructs a UTT instance configuration /// @param n The number of validators for multiparty signature computation /// @param t The number of validator shares required to reconstruct a signature - Configuration(uint16_t n, uint16_t t); + Configuration(uint16_t n, uint16_t t, bool budget_policy); ~Configuration(); Configuration(Configuration&& o); diff --git a/utt/include/serialization.hpp b/utt/include/serialization.hpp index 41a0651a7a..16a35afc28 100644 --- a/utt/include/serialization.hpp +++ b/utt/include/serialization.hpp @@ -10,7 +10,7 @@ // notices and license terms. Your use of these subcomponents is subject to the // terms and conditions of the sub-component's license, as noted in the LICENSE // file. - +#pragma once #include #include #include diff --git a/utt/include/transaction.hpp b/utt/include/transaction.hpp index 290b820682..83654518cc 100644 --- a/utt/include/transaction.hpp +++ b/utt/include/transaction.hpp @@ -70,20 +70,6 @@ class Transaction { */ std::vector getNullifiers() const; - /** - * @brief Get the transaction's input coins - * - * @return const std::vector& - */ - const std::vector& getInputCoins() const; - - /** - * @brief Get the transaction's budget coin - * - * @return std::optional - */ - std::optional getBudgetCoin() const; - /** * @brief Get the number of output coins in the transaction * @@ -91,13 +77,14 @@ class Transaction { */ uint32_t getNumOfOutputCoins() const; + bool hasBudgetCoin() const; + uint64_t getBudgetExpirationDate() const; + private: friend class libutt::api::CoinsSigner; friend class libutt::api::Client; friend std::ostream& ::operator<<(std::ostream& out, const libutt::api::operations::Transaction& tx); friend std::istream& ::operator>>(std::istream& in, libutt::api::operations::Transaction& tx); std::shared_ptr tx_; - std::vector input_coins_; - std::optional budget_coin_; }; } // namespace libutt::api::operations \ No newline at end of file diff --git a/utt/libutt/bench/BenchTxn.cpp b/utt/libutt/bench/BenchTxn.cpp index 4bad5237c7..54ec924703 100644 --- a/utt/libutt/bench/BenchTxn.cpp +++ b/utt/libutt/bench/BenchTxn.cpp @@ -113,13 +113,13 @@ class BenchTxn { // Step 2: Measure TXN validation // tqv.startLap(); - if (!tx.quickPayValidate(p, bpk, rpk)) { + if (!tx.quickPayValidate(p, bpk, rpk, true)) { testAssertFail("TXN should have QuickPay-verified"); } tqv.endLap(); tv.startLap(); - if (!tx.validate(p, bpk, rpk)) { + if (!tx.validate(p, bpk, rpk, true)) { testAssertFail("TXN should have verified"); } tv.endLap(); diff --git a/utt/libutt/include/utt/BurnOp.h b/utt/libutt/include/utt/BurnOp.h index 7749cb7d03..c9e860bfb3 100644 --- a/utt/libutt/include/utt/BurnOp.h +++ b/utt/libutt/include/utt/BurnOp.h @@ -56,7 +56,7 @@ class BurnOp { size_t getValue() const; - std::string getOwnerPid() const; + const std::string& getOwnerPid() const; std::string getNullifier() const; diff --git a/utt/libutt/include/utt/Coin.h b/utt/libutt/include/utt/Coin.h index e85979e4d2..5a89a99bc2 100644 --- a/utt/libutt/include/utt/Coin.h +++ b/utt/libutt/include/utt/Coin.h @@ -76,7 +76,13 @@ class Coin { // WARNING: Used only for deserialization Coin() {} // Used to create the coin's partial commitment (without nullfier) - Coin(const CommKey& ck, const Fr& sn, const Fr& val, const Fr& type, const Fr& exp_date, const Fr& pidHash); + Coin(const CommKey& ck, + const Fr& sn, + const Fr& val, + const Fr& type, + const Fr& exp_date, + const Fr& pidHash, + bool create_commitments = true); Coin(const CommKey& ck, const Nullifier::Params& np, @@ -163,14 +169,13 @@ class Coin { */ static Comm augmentComm(const CommKey& coinCK, const Comm& ccmTxn, const Fr& type, const Fr& exp_date); - protected: + public: /** * Computes the partial coin commitment: g_1^pid g_2^sn g_3^val g^r (assumes r is pre-set) and the value commitment * g_3^val g^z, by picking a random z */ void commit(); - - public: + void deterministically_commit(); /** * Call this when you need a full coin commitment to sign using RandSigSK::sign or * to verify using RandSig::verify diff --git a/utt/libutt/include/utt/Tx.h b/utt/libutt/include/utt/Tx.h index 8358aa2650..3e489da06b 100644 --- a/utt/libutt/include/utt/Tx.h +++ b/utt/libutt/include/utt/Tx.h @@ -31,7 +31,8 @@ class Tx { public: bool isSplitOwnCoins; // true when splitting your own coins; in this case, no budget coins are given as input, to // save TXN creation & validation time - + bool budgetPolicy; + bool is_budgeted = false; Comm rcm; // commitment to sending user's registration RandSig regsig; // signature on the registration commitment 'rcm' @@ -54,8 +55,9 @@ class Tx { const std::vector& c, std::optional b, // optional budget coin const std::vector>& recip, - const RandSigPK& bpk, // only used for debugging - const RegAuthPK& rpk); // only to encrypt for the recipients + const RandSigPK& bpk, // only used for debugging + const RegAuthPK& rpk, + bool budget_policy = true); // only to encrypt for the recipients Tx(const Params& p, const Fr pidHash, @@ -68,7 +70,8 @@ class Tx { const std::vector>& recip, std::optional bpk, // only used for debugging const RandSigPK& rpk, - const IEncryptor& encryptor); // only to encrypt for the recipients + const IEncryptor& encryptor, + bool budget_policy = true); // only to encrypt for the recipients public: size_t getSize() const { @@ -92,9 +95,9 @@ class Tx { */ G1 deriveRandSigBase(size_t txoIdx) const; - bool quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) const; + bool quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk, bool budget_policy) const; - bool validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) const; + bool validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk, bool budget_policy) const; /** * Returns the nullifiers of all coins spent by this TXN, including the budget coin's. diff --git a/utt/libutt/src/BurnOp.cpp b/utt/libutt/src/BurnOp.cpp index a0dca3e266..1d440d0839 100644 --- a/utt/libutt/src/BurnOp.cpp +++ b/utt/libutt/src/BurnOp.cpp @@ -96,7 +96,8 @@ BurnOp::BurnOp(const Params& p, recip, std::move(bpk), rpk.vk, - libutt::IBEEncryptor(rpk.mpk)); + libutt::IBEEncryptor(rpk.mpk), + false); assertTrue(internalTx.outs.size() == 1); @@ -181,7 +182,7 @@ size_t BurnOp::getSize() const { bool BurnOp::validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) const { InternalDataOfBurnOp* d = (InternalDataOfBurnOp*)this->p; if (d == nullptr) return false; - if (!d->tx.validate(p, bpk, rpk)) return false; + if (!d->tx.validate(p, bpk, rpk, false)) return false; if (d->tx.outs.size() != 1) return false; Fr hashPid = AddrSK::pidHash(d->pid); @@ -214,7 +215,7 @@ size_t BurnOp::getValue() const { return v; } -std::string BurnOp::getOwnerPid() const { +const std::string& BurnOp::getOwnerPid() const { InternalDataOfBurnOp* d = (InternalDataOfBurnOp*)this->p; assertTrue(d != nullptr); return d->pid; diff --git a/utt/libutt/src/Coin.cpp b/utt/libutt/src/Coin.cpp index 4c8c13bb0f..3271df283c 100644 --- a/utt/libutt/src/Coin.cpp +++ b/utt/libutt/src/Coin.cpp @@ -76,9 +76,24 @@ std::istream& operator>>(std::istream& in, libutt::Coin& c) { } namespace libutt { -Coin::Coin(const CommKey& ck, const Fr& sn, const Fr& val, const Fr& type, const Fr& exp_date, const Fr& pidHash) - : ck(ck), pid_hash(pidHash), sn(sn), val(val), type(type), exp_date(exp_date), t(Fr::random_element()) { - Coin::commit(); +Coin::Coin(const CommKey& ck, + const Fr& sn, + const Fr& val, + const Fr& type, + const Fr& exp_date, + const Fr& pidHash, + bool create_commitments) + : ck(ck), + pid_hash(pidHash), + sn(sn), + val(val), + type(type), + exp_date(exp_date), + t(Fr::zero() /*When the coin is not created by the client, we want to have a deterministic value for t*/) { + if (create_commitments) + commit(); + else + deterministically_commit(); } Coin::Coin(const CommKey& ck, const Nullifier::Params& np, @@ -88,9 +103,9 @@ Coin::Coin(const CommKey& ck, const Fr& type, const Fr& exp_date, const Fr& pidHash) - : ck(ck), pid_hash(pidHash), sn(sn), val(val), type(type), exp_date(exp_date), t(Fr::random_element()) { - Coin::commit(); + : ck(ck), pid_hash(pidHash), sn(sn), val(val), type(type), exp_date(exp_date) { createNullifier(np, prf); + commit(); } Coin::Coin(const CommKey& ck, @@ -103,7 +118,10 @@ Coin::Coin(const CommKey& ck, : Coin(ck, np, ask.s, sn, val, type, exp_date, ask.getPidHash()) {} bool Coin::hasValidSig(const RandSigPK& pk) const { return sig.verify(augmentComm(), pk); } -void Coin::createNullifier(const Nullifier::Params& np, const Fr& prf) { null = Nullifier(np, prf, sn, t); } +void Coin::createNullifier(const Nullifier::Params& np, const Fr& prf) { + t = Fr::random_element(); + null = Nullifier(np, prf, sn, t); +} Comm Coin::augmentComm(const CommKey& ck, const Comm& ccmTxn, const Fr& type, const Fr& exp_date) { // WARNING: Hardcoding this for now, since the code below assumes it // TODO(Safety): better ways of computing "subcommitment keys" @@ -124,6 +142,28 @@ Comm Coin::augmentComm(const CommKey& ck, const Comm& ccmTxn, const Fr& type, co return ccmTxn + ccmExtra; } +void Coin::deterministically_commit() { + assertGreaterThanOrEqual(ck.numMessages(), 3); + assertTrue(ck.hasCorrectG2()); + + // we first commit *partially* to the coin, but not to its type and expiration date, which will be given in plaintext + bool withG2 = true; // since we always need G2 counterparts to verify sigs + r = Fr::zero(); + ccm_txn = Comm::create(ck, {pid_hash, sn, val, Fr::zero(), Fr::zero(), r}, withG2); + assertTrue(ccm_txn.hasCorrectG2(ck)); + + // NOTE: This is a bit hard-coded. Ideally, we might've done ck_val = Params::getValCK()? + // But that couples Coin with Params. Maybe this is okay after all. + + // set the vcm commitment key to (g_3, g) + CommKey ck_val; + ck_val.g.push_back(ck.g[2]); // g_3 + ck_val.g.push_back(ck.getGen1()); // g + + z = Fr::zero(); + vcm = Comm::create(ck_val, {val, z}, false); +} + void Coin::commit() { assertGreaterThanOrEqual(ck.numMessages(), 3); assertTrue(ck.hasCorrectG2()); diff --git a/utt/libutt/src/Kzg.cpp b/utt/libutt/src/Kzg.cpp index 7029d173a3..dbcc4b6666 100644 --- a/utt/libutt/src/Kzg.cpp +++ b/utt/libutt/src/Kzg.cpp @@ -109,7 +109,7 @@ Params::Params(size_t q) : q(q) { int prevPct = -1; size_t c = 0; - loginfo << "Generating random q-SDH params, with q = " << q << endl; + logdbg << "Generating random q-SDH params, with q = " << q << endl; for (size_t i = 0; i <= q; i++) { g1si[i] = si * g1; g2si[i] = si * g2; diff --git a/utt/libutt/src/Simulation.cpp b/utt/libutt/src/Simulation.cpp index 825550f931..2ad42fe9ce 100644 --- a/utt/libutt/src/Simulation.cpp +++ b/utt/libutt/src/Simulation.cpp @@ -51,7 +51,7 @@ Tx sendValidTxOnNetwork(const Context& ctx, const Tx& inTx) { testAssertEqual(inTx, outTx); testAssertEqual(oldHash, newHash); - if (!outTx.validate(ctx.p_, ctx.bpk_, ctx.rpk_)) { + if (!outTx.validate(ctx.p_, ctx.bpk_, ctx.rpk_, true)) { testAssertFail("TXN should have verified"); } diff --git a/utt/libutt/src/Tx.cpp b/utt/libutt/src/Tx.cpp index 3e0c9e4c12..8e6e9db981 100644 --- a/utt/libutt/src/Tx.cpp +++ b/utt/libutt/src/Tx.cpp @@ -18,6 +18,7 @@ #include // WARNING: Include this last (see header file for details; thanks, C++) std::ostream& operator<<(std::ostream& out, const libutt::Tx& tx) { + out << tx.budgetPolicy << endl; out << tx.isSplitOwnCoins << endl; out << tx.rcm; out << tx.regsig; @@ -31,6 +32,9 @@ std::ostream& operator<<(std::ostream& out, const libutt::Tx& tx) { } std::istream& operator>>(std::istream& in, libutt::Tx& tx) { + in >> tx.budgetPolicy; + libff::consume_OUTPUT_NEWLINE(in); + in >> tx.isSplitOwnCoins; libff::consume_OUTPUT_NEWLINE(in); in >> tx.rcm; @@ -41,6 +45,9 @@ std::istream& operator>>(std::istream& in, libutt::Tx& tx) { in >> tx.budget_pi; + for (auto& txin : tx.ins) { + tx.is_budgeted = tx.is_budgeted || (txin.coin_type == libutt::Coin::BudgetType()); + } return in; } @@ -52,9 +59,21 @@ Tx::Tx(const Params& p, std::optional b, // optional budget coin const std::vector>& recip, const RandSigPK& bpk, // only used for debugging - const RegAuthPK& rpk) // only to encrypt for the recipients - : Tx{p, ask.pid_hash, ask.pid, ask.rcm, ask.rs, ask.s, c, std::move(b), recip, bpk, rpk.vk, IBEEncryptor(rpk.mpk)} { -} + const RegAuthPK& rpk, + bool budget_policy) // only to encrypt for the recipients + : Tx{p, + ask.pid_hash, + ask.pid, + ask.rcm, + ask.rs, + ask.s, + c, + std::move(b), + recip, + bpk, + rpk.vk, + IBEEncryptor(rpk.mpk), + budget_policy} {} Tx::Tx(const Params& p, const Fr pidHash, @@ -67,7 +86,8 @@ Tx::Tx(const Params& p, const std::vector>& recip, std::optional bpk, // only used for debugging const RandSigPK& rpk, - const IEncryptor& encryptor) { + const IEncryptor& encryptor, + bool budget_policy) { #ifndef NDEBUG (void)bpk; #endif @@ -78,8 +98,8 @@ Tx::Tx(const Params& p, Fr pid_hash_sender = pidHash; // the (single) sender's PID hash - isSplitOwnCoins = true; // true when this TXN simply splits the sender's coins - bool isBudgeted = b.has_value(); // true when this TXN is budgeted + isSplitOwnCoins = true; // true when this TXN simply splits the sender's coins + is_budgeted = b.has_value(); // true when this TXN is budgeted size_t totalIn = Coin::totalValue(coins); // total in value size_t totalOut = 0; // total out value @@ -119,14 +139,23 @@ Tx::Tx(const Params& p, forMeOutputs.insert(j); } } + budgetPolicy = isSplitOwnCoins ? false : budget_policy; + if (!budgetPolicy && is_budgeted) { + logerror << "budget policy is disabled, but a budget coin was given" << endl; + throw std::runtime_error("budget policy is false, but a budget coin was given"); + } + if (budgetPolicy && !is_budgeted) { + logerror << "budget policy is enabled, but the budget is missing" << endl; + throw std::runtime_error("budget policy is enabled, but the budget coin is missing"); + } // are you spending more than you have? if (totalIn != totalOut) { logerror << "Total-in is " << totalIn << " but total-out is " << totalOut << endl; throw std::runtime_error("Input and output normal coins must have same total"); } - if (isBudgeted) { + if (is_budgeted) { // if splitting your own coins, you don't need budgets if (isSplitOwnCoins) { throw std::runtime_error("You need not provide a budget coin when splitting your own coins"); @@ -189,7 +218,7 @@ Tx::Tx(const Params& p, // logtrace << "z_sum: " << z_sum << endl; // create input for budget coin too, if any - if (isBudgeted) { + if (is_budgeted) { ins.emplace_back(*b); } @@ -234,7 +263,7 @@ Tx::Tx(const Params& p, * - compute range proof for vcm_1 * - compute ctxt */ - bool icmPok = !(isBudgeted && pid_recip == pid); + bool icmPok = !(is_budgeted && pid_recip == pid); bool hasRangeProof = true; outs.emplace_back(p.getValCK(), p.getRangeProofParams(), @@ -256,7 +285,7 @@ Tx::Tx(const Params& p, /** * Step 4: Take care of budget proof, if budgeted TXN. */ - if (isBudgeted) { + if (is_budgeted) { // For the budget preservation, we have to check that: // budget_in - \sum_{j \in output coins for another} val_out(j) = budget_out Fr z_budget_recip = b->z; // the randomness of the input budget coin's 'vcm_1' @@ -315,7 +344,7 @@ Tx::Tx(const Params& p, ins[i].pi = SplitProof(p, pidHash, prf, coins.at(i), a, rcm, outsHash); } - if (isBudgeted) { + if (is_budgeted) { // logdbg << "Split proof for budget coin" << endl; ins.back().pi = SplitProof(p, pidHash, prf, *b, a, rcm, outsHash); } @@ -324,7 +353,7 @@ Tx::Tx(const Params& p, assertEqual(outs.size(), recip.size() + (b.has_value() ? 1 : 0)); } -bool Tx::quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) const { +bool Tx::quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk, bool budget_policy) const { /** * TODO(Perf): Do we even need to check coinsig? * TODO(Perf): Do we even need to check regsig? @@ -334,7 +363,6 @@ bool Tx::quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK /** * Step 1: Sanity check */ - bool isBudgeted = !isSplitOwnCoins; bool foundBudgetOutCoin = false, foundBudgetInCoin = false; for (auto& txout : outs) { @@ -347,17 +375,38 @@ bool Tx::quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK // TODO: check coin expiration here } - if (isBudgeted != foundBudgetOutCoin) { + // whether a budget policy is true or not, a split coins transaction should not have an input budget coin + if (isSplitOwnCoins && foundBudgetInCoin) { + logerror << "spliting your own coins doesn't require a budget" << endl; + return false; + } + + if (is_budgeted != foundBudgetOutCoin) { logerror << "TX claimed to be budgeted but did not have an output budget coin" << endl; return false; } - if (isBudgeted != foundBudgetOutCoin) { + if (is_budgeted != foundBudgetOutCoin) { logerror << "TX claimed to be budgeted but did not have an output budget coin" << endl; return false; } - assertTrue(!isBudgeted || budget_pi.has_value()); + if (budget_policy && !isSplitOwnCoins && !is_budgeted) { + logerror << "budget policy is enforced and budget is needed, but the transaction doesn't have a budget coin" + << endl; + return false; + } + if (!budget_policy && !isSplitOwnCoins && is_budgeted) { + logerror << "budget policy is disabled and budget is not needed, but the transaction does have a budget coin" + << endl; + return false; + } + + if ((budget_policy && !budgetPolicy) || (!budget_policy && budgetPolicy)) { + logerror << "mismatch between transaction's budget policy and the configuration budget policy" << endl; + } + + assertTrue(!is_budgeted || budget_pi.has_value()); /** * Step 2: Check registration authority's sig on registration commitment @@ -374,7 +423,7 @@ bool Tx::quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK for (size_t i = 0; i < ins.size(); i++) { // check this is a normal coin (except for the last one if budgeted, which is allowed not to be normal) if (ins[i].coin_type != Coin::NormalType()) { - if (!isBudgeted || i != ins.size() - 1) { + if (!is_budgeted || i != ins.size() - 1) { logerror << "Expected input #" << i << " to be a normal coin" << endl; return false; } @@ -401,17 +450,14 @@ bool Tx::quickPayValidate(const Params& p, const RandSigPK& bpk, const RegAuthPK return true; } -bool Tx::validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) const { - if (!quickPayValidate(p, bpk, rpk)) return false; - - bool isBudgeted = !isSplitOwnCoins; - +bool Tx::validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk, bool budget_policy) const { + if (!quickPayValidate(p, bpk, rpk, budget_policy)) return false; /** * Step 4: Check value preservation of normal coins. * - check the sum of in and out 'normal' coin value commitments is the same */ - size_t numNormalIn = ins.size() - (isBudgeted ? 1 : 0); - size_t numNormalOut = outs.size() - (isBudgeted ? 1 : 0); + size_t numNormalIn = ins.size() - (is_budgeted ? 1 : 0); + size_t numNormalOut = outs.size() - (is_budgeted ? 1 : 0); G1 incomms = G1::zero(), outcomms = G1::zero(); for (size_t i = 0; i < numNormalIn; i++) { @@ -434,7 +480,7 @@ bool Tx::validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) c for (size_t j = 0; j < outs.size(); j++) { // check this is a normal coin (except for the last one if budget, which is allowed not to be normal) if (outs[j].coin_type != Coin::NormalType()) { - if (!isBudgeted || j != outs.size() - 1) { + if (!is_budgeted || j != outs.size() - 1) { logerror << "Expected output #" << j << " to be a normal coin" << endl; return false; } @@ -447,7 +493,7 @@ bool Tx::validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) c // check recipient's identity commitment is indeed well-formed: e.g. it is not g_1^pid g_2^v g^t for v != 0 logtrace << "Checking output #" << j << "'s ZKPoK" << endl; - if (isBudgeted && budget_pi->forMeTxos.count(j) == 1) { + if (is_budgeted && budget_pi->forMeTxos.count(j) == 1) { // for budgeted TXNs, the budget proof already proves knowledge of sender-owned outputs, so this is unnecessary assertFalse(outs[j].icm_pok.has_value()); } else { @@ -465,7 +511,7 @@ bool Tx::validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) c } // check range proofs on out comms (in comms are good by invariant) - // if(isBudgeted && j == outs.size() - 1) { + // if(is_budgeted && j == outs.size() - 1) { // assertFalse(outs[j].range_pi.has_value()); //} else { assertTrue(outs[j].range_pi.has_value()); @@ -481,7 +527,7 @@ bool Tx::validate(const Params& p, const RandSigPK& bpk, const RegAuthPK& rpk) c * - check pid of input budget coin matches pid of output budget coin and of normal change coins * - value preservation of budget */ - if (isBudgeted) { + if (is_budgeted) { const auto& bout = outs.back(); if (bout.coin_type != Coin::BudgetType()) { logerror << "Expected last output coin to be a budget coin" << endl; diff --git a/utt/libutt/src/api/UTTParams.cpp b/utt/libutt/src/api/UTTParams.cpp index bb39048e07..4adb40cf30 100644 --- a/utt/libutt/src/api/UTTParams.cpp +++ b/utt/libutt/src/api/UTTParams.cpp @@ -6,11 +6,14 @@ #include std::ostream& operator<<(std::ostream& out, const libutt::api::UTTParams& params) { + out << params.getBudgetPolicy() << endl; out << params.getParams(); return out; } std::istream& operator>>(std::istream& in, libutt::api::UTTParams& params) { params.params.reset(new libutt::Params()); + in >> params.budget_policy; + libff::consume_OUTPUT_NEWLINE(in); in >> *(params.params); return in; } @@ -24,6 +27,7 @@ using Fr = typename libff::default_ec_pp::Fp_type; struct GpInitData { libutt::CommKey cck; libutt::CommKey rck; + bool budget_policy; }; void UTTParams::initLibs(const UTTParams::BaseLibsInitData& init_data) { // Apparently, libff logs some extra info when computing pairings @@ -61,16 +65,18 @@ UTTParams UTTParams::create(void* initData) { GpInitData* init_data = (GpInitData*)initData; *(gp.params) = libutt::Params::random(init_data->cck); gp.params->ck_reg = init_data->rck; + gp.budget_policy = init_data->budget_policy; } return gp; } const libutt::Params& UTTParams::getParams() const { return *params; } - +bool UTTParams::getBudgetPolicy() const { return budget_policy; } UTTParams::UTTParams(const UTTParams& other) { *this = other; } UTTParams& UTTParams::operator=(const UTTParams& other) { if (this == &other) return *this; params.reset(new libutt::Params()); *params = *(other.params); + budget_policy = other.budget_policy; return *this; } } // namespace libutt::api \ No newline at end of file diff --git a/utt/libutt/src/api/budget.cpp b/utt/libutt/src/api/budget.cpp index d601a48636..76e6ebbdf0 100644 --- a/utt/libutt/src/api/budget.cpp +++ b/utt/libutt/src/api/budget.cpp @@ -38,8 +38,8 @@ Budget::Budget(const UTTParams& d, fr_val.set_ulong(val); Fr fr_expdate; fr_expdate.set_ulong(exp_date); - coin_ = - libutt::api::Coin(d, snHash, fr_val.to_words(), pidHash, libutt::api::Coin::Type::Budget, fr_expdate.to_words()); + coin_ = libutt::api::Coin( + d, snHash, fr_val.to_words(), pidHash, libutt::api::Coin::Type::Budget, fr_expdate.to_words(), false); } libutt::api::Coin& Budget::getCoin() { return coin_; } const libutt::api::Coin& Budget::getCoin() const { return coin_; } diff --git a/utt/libutt/src/api/burn.cpp b/utt/libutt/src/api/burn.cpp index 6d5b5e7220..23068118f6 100644 --- a/utt/libutt/src/api/burn.cpp +++ b/utt/libutt/src/api/burn.cpp @@ -45,4 +45,5 @@ Burn& Burn::operator=(const Burn& other) { } std::string Burn::getNullifier() const { return burn_->getNullifier(); } const Coin& Burn::getCoin() const { return c_; } +const std::string& Burn::getOwnerPid() const { return burn_->getOwnerPid(); } } // namespace libutt::api::operations \ No newline at end of file diff --git a/utt/libutt/src/api/client.cpp b/utt/libutt/src/api/client.cpp index bb7967f8f9..8f6fb06c82 100644 --- a/utt/libutt/src/api/client.cpp +++ b/utt/libutt/src/api/client.cpp @@ -77,6 +77,8 @@ void Client::setRCMSig(const UTTParams& d, const types::CurvePoint& s2, const ty rcm_ = Commitment(d, Commitment::Type::REGISTRATION, m, true); rcm_sig_ = sig; ask_->rs = libutt::deserialize(sig); + if (!ask_->rs.verify(*rcm_.comm_, rpk_->vk)) + throw std::runtime_error("setRCMSig - failed to verify rcm against signature!"); } std::pair Client::getRcm() const { diff --git a/utt/libutt/src/api/coin.cpp b/utt/libutt/src/api/coin.cpp index 5f5d4d7f22..2472af7dc1 100644 --- a/utt/libutt/src/api/coin.cpp +++ b/utt/libutt/src/api/coin.cpp @@ -49,7 +49,8 @@ Coin::Coin(const UTTParams& d, const types::CurvePoint& val, const types::CurvePoint& pidhash, Type t, - const types::CurvePoint& exp_date) { + const types::CurvePoint& exp_date, + bool finalize) { Fr fr_sn; fr_sn.from_words(sn); Fr fr_val; @@ -60,7 +61,7 @@ Coin::Coin(const UTTParams& d, Fr pid_hash; pid_hash.from_words(pidhash); - coin_.reset(new libutt::Coin(d.getParams().ck_coin, fr_sn, fr_val, fr_type, fr_exp_date, pid_hash)); + coin_.reset(new libutt::Coin(d.getParams().ck_coin, fr_sn, fr_val, fr_type, fr_exp_date, pid_hash, finalize)); type_ = t; } Coin::Coin(const Coin& c) { @@ -94,6 +95,13 @@ void Coin::rerandomize(std::optional base_randomness) { if (base_randomness.has_value()) u_delta.from_words(*base_randomness); coin_->sig.rerandomize(coin_->r, u_delta); } +void Coin::finalize(const UTTParams& p, const types::CurvePoint& prf) { + Fr fr_prf; + fr_prf.from_words(prf); + coin_->createNullifier(p.getParams().null, fr_prf); + coin_->commit(); +} + void Coin::createNullifier(const UTTParams& d, const types::CurvePoint& prf) { Fr fr_prf; fr_prf.from_words(prf); @@ -102,4 +110,5 @@ void Coin::createNullifier(const UTTParams& d, const types::CurvePoint& prf) { types::CurvePoint Coin::getPidHash() const { return coin_->pid_hash.to_words(); } types::CurvePoint Coin::getSN() const { return coin_->sn.to_words(); } types::CurvePoint Coin::getExpDateAsCurvePoint() const { return coin_->exp_date.to_words(); } +uint64_t Coin::getExpDate() const { return coin_->exp_date.as_ulong(); } } // namespace libutt::api \ No newline at end of file diff --git a/utt/libutt/src/api/coinsSigner.cpp b/utt/libutt/src/api/coinsSigner.cpp index 08059e98ea..88a70fef11 100644 --- a/utt/libutt/src/api/coinsSigner.cpp +++ b/utt/libutt/src/api/coinsSigner.cpp @@ -83,7 +83,7 @@ bool CoinsSigner::validate(const UTTParams& p, const operation } template <> bool CoinsSigner::validate(const UTTParams& p, const operations::Transaction& tx) const { - return tx.tx_->validate(p.getParams(), *(bvk_), *(rvk_)); + return tx.tx_->validate(p.getParams(), *(bvk_), *(rvk_), p.getBudgetPolicy()); } template <> diff --git a/utt/libutt/src/api/config.cpp b/utt/libutt/src/api/config.cpp index b056b7e0db..9aa915f458 100644 --- a/utt/libutt/src/api/config.cpp +++ b/utt/libutt/src/api/config.cpp @@ -65,7 +65,7 @@ struct Configuration::Impl { Configuration::Configuration() : pImpl_{new Impl{}} {} -Configuration::Configuration(uint16_t n, uint16_t t) : Configuration() { +Configuration::Configuration(uint16_t n, uint16_t t, bool budget_policy) : Configuration() { if (n == 0 || t == 0 || t > n) throw std::runtime_error("Invalid number of validators and/or threshold"); pImpl_->n_ = n; @@ -80,12 +80,13 @@ Configuration::Configuration(uint16_t n, uint16_t t) : Configuration() { // Pass in the commitment keys to UTTParams // This struct matches the expected data structure by UTTParams::create since the method hides the actual type by // using a void* - struct CommitmentKeys { + struct GPInitData { libutt::CommKey cck; libutt::CommKey rck; + bool budget_policy; }; - CommitmentKeys commitmentKeys{dkg.getCK(), rsk.ck_reg}; - pImpl_->publicConfig_.pImpl_->params_ = libutt::api::UTTParams::create((void*)(&commitmentKeys)); + GPInitData GPInitData{dkg.getCK(), rsk.ck_reg, budget_policy}; + pImpl_->publicConfig_.pImpl_->params_ = libutt::api::UTTParams::create((void*)(&GPInitData)); // For some reason we need to go back and set the IBE parameters although we might not be using IBE. rsk.setIBEParams(pImpl_->publicConfig_.pImpl_->params_.getParams().ibe); diff --git a/utt/libutt/src/api/transaction.cpp b/utt/libutt/src/api/transaction.cpp index 4b02df1624..659ae8daeb 100644 --- a/utt/libutt/src/api/transaction.cpp +++ b/utt/libutt/src/api/transaction.cpp @@ -9,15 +9,10 @@ std::ostream& operator<<(std::ostream& out, const libutt::api::operations::Transaction& tx) { out << *(tx.tx_) << std::endl; - libutt::serializeVector(out, tx.input_coins_); - out << tx.budget_coin_; return out; } std::istream& operator>>(std::istream& in, libutt::api::operations::Transaction& tx) { in >> *(tx.tx_); - libff::consume_OUTPUT_NEWLINE(in); - libutt::deserializeVector(in, tx.input_coins_); - in >> tx.budget_coin_; return in; } namespace libutt::api::operations { @@ -27,8 +22,6 @@ Transaction::Transaction(const UTTParams& d, const std::optional& bc, const std::vector>& recipients, const IEncryptor& encryptor) { - input_coins_ = coins; - budget_coin_ = bc; Fr fr_pidhash; fr_pidhash.from_words(cid.getPidHash()); Fr prf; @@ -64,7 +57,8 @@ Transaction::Transaction(const UTTParams& d, fr_recipients, std::nullopt, rpk.vk, - encryptor)); + encryptor, + d.getBudgetPolicy())); } Transaction::Transaction() { tx_.reset(new libutt::Tx()); } Transaction::Transaction(const Transaction& other) { @@ -74,13 +68,15 @@ Transaction::Transaction(const Transaction& other) { Transaction& Transaction::operator=(const Transaction& other) { if (this == &other) return *this; *tx_ = *(other.tx_); - input_coins_ = other.input_coins_; - budget_coin_ = other.budget_coin_; return *this; } std::vector Transaction::getNullifiers() const { return tx_->getNullifiers(); } -const std::vector& Transaction::getInputCoins() const { return input_coins_; } -std::optional Transaction::getBudgetCoin() const { return budget_coin_; } uint32_t Transaction::getNumOfOutputCoins() const { return (uint32_t)tx_->outs.size(); } + +bool Transaction::hasBudgetCoin() const { return tx_->ins.back().coin_type == libutt::Coin::BudgetType(); } +uint64_t Transaction::getBudgetExpirationDate() const { + if (!hasBudgetCoin()) return 0; + return tx_->ins.back().exp_date.as_ulong(); +} } // namespace libutt::api::operations \ No newline at end of file diff --git a/utt/libutt/test/TestTxn.cpp b/utt/libutt/test/TestTxn.cpp index bb516ed702..6de8fd7c6d 100644 --- a/utt/libutt/test/TestTxn.cpp +++ b/utt/libutt/test/TestTxn.cpp @@ -127,11 +127,11 @@ void testBudgeted2to2Txn(size_t thresh, size_t n, size_t numCycles, bool isBudge if (isQuickPay) { // check transaction validates - if (!tx.quickPayValidate(p, bpk, rpk)) { + if (!tx.quickPayValidate(p, bpk, rpk, true)) { testAssertFail("TXN should have QuickPay-verified"); } } else { - if (!tx.validate(p, bpk, rpk)) { + if (!tx.validate(p, bpk, rpk, true)) { testAssertFail("TXN should have verified"); } } diff --git a/utt/tests/CMakeLists.txt b/utt/tests/CMakeLists.txt index 07714ab002..3d08199552 100644 --- a/utt/tests/CMakeLists.txt +++ b/utt/tests/CMakeLists.txt @@ -1,11 +1,6 @@ +find_package(GTest REQUIRED) set(newutt_test_sources - TestDistributedRegistration.cpp - TestMint.cpp - TestBurn.cpp - TestBudget.cpp - TestTransaction.cpp - TestTransactionWithRsaEncryption.cpp - TestSerialization.cpp + TestUttNewApi.cpp ) foreach(appSrc ${newutt_test_sources}) @@ -13,7 +8,7 @@ foreach(appSrc ${newutt_test_sources}) set(appDir ../bin/test) add_executable(${appName} ${appSrc}) - target_link_libraries(${appName} PRIVATE utt_api) + target_link_libraries(${appName} PRIVATE utt_api logging GTest::Main GTest::GTest) add_test(NAME ${appName} COMMAND ${appName}) set_target_properties(${appName} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${appDir}) diff --git a/utt/tests/TestBudget.cpp b/utt/tests/TestBudget.cpp deleted file mode 100644 index f51dae7ff1..0000000000 --- a/utt/tests/TestBudget.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "testUtils.hpp" - -#include "UTTParams.hpp" -#include "testUtils.hpp" -#include "budget.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace libutt; -using namespace libutt::api; -using namespace libutt::api::operations; -using namespace std::chrono; -int main(int argc, char* argv[]) { - std::srand(0); - (void)argc; - (void)argv; - size_t thresh = 3; - size_t n = 4; - size_t c = 10; - auto [d, dkg, rc] = testing::init(n, thresh); - auto registrators = testing::GenerateRegistrators(n, rc); - auto banks = testing::GenerateCommitters(n, dkg, rc.toPK()); - auto clients = testing::GenerateClients(c, dkg.getPK(), rc.toPK(), rc); - for (auto& c : clients) { - testing::registerClient(d, c, registrators, thresh); - } - - for (auto& c : clients) { - std::vector> rsigs; - std::vector shares_signers; - auto budget = Budget(d, c, 1000, 123456789); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(budget).front()); - shares_signers.push_back(banks[i]->getId()); - } - for (auto& b : banks) { - for (size_t i = 0; i < rsigs.size(); i++) { - auto& sig = rsigs[i]; - auto& signer = shares_signers[i]; - assertTrue(b->validatePartialSignature(signer, sig, 0, budget)); - } - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map> sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(budget, d, {blinded_sig}).front(); - assertTrue(c.validate(coin)); - } - - // Now, do the same for a budget created by the replicas. - for (auto& c : clients) { - std::vector> rsigs; - auto snHash = Fr::random_element() - .to_words(); // Assume each replica can compute the same sn using some common execution state - auto budget = Budget(d, snHash, c.getPidHash(), 1000, 123456789); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(budget).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map> sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(budget, d, {blinded_sig}).front(); - coin.createNullifier(d, c.getPRFSecretKey()); - assertTrue(!coin.getNullifier().empty()); - assertTrue(c.validate(coin)); - } - return 0; -} \ No newline at end of file diff --git a/utt/tests/TestBurn.cpp b/utt/tests/TestBurn.cpp deleted file mode 100644 index af9831d948..0000000000 --- a/utt/tests/TestBurn.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "testUtils.hpp" - -#include "UTTParams.hpp" -#include "testUtils.hpp" -#include "mint.hpp" -#include "burn.hpp" -#include -#include -#include -#include -#include -#include -#include - -using namespace libutt; -using namespace libutt::api; -using namespace libutt::api::operations; -int main(int argc, char* argv[]) { - std::srand(0); - (void)argc; - (void)argv; - size_t thresh = 3; - size_t n = 4; - size_t c = 10; - auto [d, dkg, rc] = testing::init(n, thresh); - auto registrators = testing::GenerateRegistrators(n, rc); - auto banks = testing::GenerateCommitters(n, dkg, rc.toPK()); - auto clients = testing::GenerateClients(c, dkg.getPK(), rc.toPK(), rc); - for (auto& c : clients) { - testing::registerClient(d, c, registrators, thresh); - } - - for (auto& c : clients) { - std::vector> rsigs; - std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); - auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(mint).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map> sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(mint, d, {blinded_sig}).front(); - assertTrue(c.validate(coin)); - Burn b_op{d, c, coin}; - for (auto& b : banks) { - assertTrue(b->validate(d, b_op)); - } - } - return 0; -} \ No newline at end of file diff --git a/utt/tests/TestDistributedRegistration.cpp b/utt/tests/TestDistributedRegistration.cpp deleted file mode 100644 index f98c45bdd4..0000000000 --- a/utt/tests/TestDistributedRegistration.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "UTTParams.hpp" -#include "testUtils.hpp" - -#include -#include -#include -#include - -using namespace libutt; -using namespace libutt::api; - -int main(int argc, char* argv[]) { - (void)argc; - (void)argv; - size_t thresh = 3; - size_t n = 4; - size_t c = 10; - auto [d, dkg, rc] = testing::init(n, thresh); - auto registrators = testing::GenerateRegistrators(n, rc); - auto banks = testing::GenerateCommitters(n, dkg, rc.toPK()); - auto clients = testing::GenerateClients(c, dkg.getSK().toPK(), rc.toPK(), rc); - - for (auto& c : clients) { - testing::registerClient(d, c, registrators, thresh); - auto rcm_data = c.rerandomizeRcm(d); - for (auto& r : registrators) { - assertTrue(r->validateRCM(rcm_data.first, rcm_data.second)); - } - } - - return 0; -} \ No newline at end of file diff --git a/utt/tests/TestMint.cpp b/utt/tests/TestMint.cpp deleted file mode 100644 index 2e17eaf074..0000000000 --- a/utt/tests/TestMint.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "testUtils.hpp" - -#include "UTTParams.hpp" -#include "testUtils.hpp" -#include "mint.hpp" -#include -#include -#include -#include -#include -#include - -using namespace libutt; -using namespace libutt::api; -using namespace libutt::api::operations; -int main(int argc, char* argv[]) { - std::srand(0); - (void)argc; - (void)argv; - size_t thresh = 3; - size_t n = 4; - size_t c = 10; - auto [d, dkg, rc] = testing::init(n, thresh); - auto registrators = testing::GenerateRegistrators(n, rc); - auto banks = testing::GenerateCommitters(n, dkg, rc.toPK()); - auto clients = testing::GenerateClients(c, dkg.getPK(), rc.toPK(), rc); - for (auto& c : clients) { - testing::registerClient(d, c, registrators, thresh); - } - - for (auto& c : clients) { - std::vector> rsigs; - std::vector shares_signers; - std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); - auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(mint).front()); - shares_signers.push_back(banks[i]->getId()); - } - for (auto& b : banks) { - for (size_t i = 0; i < rsigs.size(); i++) { - auto& sig = rsigs[i]; - auto& signer = shares_signers[i]; - assertTrue(b->validatePartialSignature(signer, sig, 0, mint)); - } - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map> sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(mint, d, {blinded_sig}).front(); - assertTrue(c.validate(coin)); - } - return 0; -} \ No newline at end of file diff --git a/utt/tests/TestSerialization.cpp b/utt/tests/TestSerialization.cpp deleted file mode 100644 index 568f9ba73e..0000000000 --- a/utt/tests/TestSerialization.cpp +++ /dev/null @@ -1,239 +0,0 @@ -#include "testUtils.hpp" -#include "coin.hpp" -#include "budget.hpp" -#include "burn.hpp" -#include "mint.hpp" -#include "commitment.hpp" -#include "transaction.hpp" -#include "config.hpp" -#include -#include -#include -#include -#include "serialization.hpp" -using namespace libutt; -using namespace libutt::api; -using namespace libutt::api::operations; - -int main(int argc, char* argv[]) { - (void)argc; - (void)argv; - - size_t thresh = 3; - size_t n = 4; - size_t c = 2; - auto [d, dkg, rc] = testing::init(n, thresh); - auto registrators = testing::GenerateRegistrators(n, rc); - auto banks = testing::GenerateCommitters(n, dkg, rc.toPK()); - auto clients = testing::GenerateClients(c, dkg.getPK(), rc.toPK(), rc); - for (auto& c : clients) { - testing::registerClient(d, c, registrators, thresh); - } - - // Test UTTParams de/serialization - std::vector serialized_params = libutt::api::serialize(d); - auto deserialized_params = libutt::api::deserialize(serialized_params); - assertTrue(deserialized_params == d); - - // Test Configuration de/serialization - { - auto config = libutt::api::Configuration((uint16_t)n, (uint16_t)thresh); - assertTrue(config.isValid()); - auto serialized_config = libutt::api::serialize(config); - auto deserialized_config = libutt::api::deserialize(serialized_config); - assertTrue(deserialized_config.isValid()); - assertTrue(deserialized_config == config); - - const auto& publicConfig = config.getPublicConfig(); - auto serialized_public_config = libutt::api::serialize(publicConfig); - auto deserialized_public_config = libutt::api::deserialize(serialized_public_config); - assertTrue(deserialized_public_config == publicConfig); - } - - Commitment rcm = clients[0].getRcm().first; - - // Test Commitment de/serialization - std::vector serialized_rcm = libutt::api::serialize(rcm); - auto deserialized_rcm = libutt::api::deserialize(serialized_rcm); - assertTrue(rcm == deserialized_rcm); - Fr fr_val; - fr_val.set_ulong(100); - - /* - We define a *complete coin* as a UTT coin that contains a nullifier. - We define an *incomplete coin* as a UTT coin that does not contain a nullifier. - */ - // Test a complete normal coin de/serialization - libutt::api::Coin c1(d, - Fr::random_element().to_words(), - Fr::random_element().to_words(), - fr_val.to_words(), - Fr::random_element().to_words(), - libutt::api::Coin::Type::Normal, - Fr::random_element().to_words()); - std::vector c1_serialized = libutt::api::serialize(c1); - libutt::api::Coin c1_deserialized = libutt::api::deserialize(c1_serialized); - assertTrue(c1.getNullifier() == c1_deserialized.getNullifier()); - assertTrue(c1.hasSig() == c1_deserialized.hasSig()); - assertTrue(c1.getType() == c1_deserialized.getType()); - assertTrue(c1.getVal() == c1_deserialized.getVal()); - assertTrue(c1.getSN() == c1_deserialized.getSN()); - assertTrue(c1.getExpDateAsCurvePoint() == c1_deserialized.getExpDateAsCurvePoint()); - - // Test an incomplete normal coin de/serialization - libutt::api::Coin c2(d, - Fr::random_element().to_words(), - fr_val.to_words(), - Fr::random_element().to_words(), - libutt::api::Coin::Type::Normal, - Fr::random_element().to_words()); - std::vector c2_serialized = libutt::api::serialize(c2); - libutt::api::Coin c2_deserialized = libutt::api::deserialize(c2_serialized); - assertTrue(c2.getNullifier() == c2_deserialized.getNullifier()); - assertTrue(c2.hasSig() == c2_deserialized.hasSig()); - assertTrue(c2.getType() == c2_deserialized.getType()); - assertTrue(c2.getVal() == c2_deserialized.getVal()); - assertTrue(c2.getSN() == c2_deserialized.getSN()); - assertTrue(c2.getExpDateAsCurvePoint() == c2_deserialized.getExpDateAsCurvePoint()); - - // Test a complete budget coin de/serialization - libutt::api::Coin c3(d, - Fr::random_element().to_words(), - fr_val.to_words(), - Fr::random_element().to_words(), - libutt::api::Coin::Type::Budget, - Fr::random_element().to_words()); - std::vector c3_serialized = libutt::api::serialize(c3); - libutt::api::Coin c3_deserialized = libutt::api::deserialize(c3_serialized); - assertTrue(c3.getNullifier() == c3_deserialized.getNullifier()); - assertTrue(c3.hasSig() == c3_deserialized.hasSig()); - assertTrue(c3.getType() == c3_deserialized.getType()); - assertTrue(c3.getVal() == c3_deserialized.getVal()); - assertTrue(c3.getSN() == c3_deserialized.getSN()); - assertTrue(c3.getExpDateAsCurvePoint() == c3_deserialized.getExpDateAsCurvePoint()); - - // Test an incomplete budget coin de/serialization - libutt::api::Coin c4(d, - Fr::random_element().to_words(), - Fr::random_element().to_words(), - fr_val.to_words(), - Fr::random_element().to_words(), - libutt::api::Coin::Type::Budget, - Fr::random_element().to_words()); - std::vector c4_serialized = libutt::api::serialize(c4); - libutt::api::Coin c4_deserialized = libutt::api::deserialize(c4_serialized); - assertTrue(c4.getNullifier() == c4_deserialized.getNullifier()); - assertTrue(c4.hasSig() == c4_deserialized.hasSig()); - assertTrue(c4.getType() == c4_deserialized.getType()); - assertTrue(c4.getVal() == c4_deserialized.getVal()); - assertTrue(c4.getSN() == c4_deserialized.getSN()); - assertTrue(c4.getExpDateAsCurvePoint() == c4_deserialized.getExpDateAsCurvePoint()); - - // Test Budget request de/serialization - libutt::api::operations::Budget b1( - d, Fr::random_element().to_words(), Fr::random_element().to_words(), 100, 987654321); - std::vector b1_serialized = libutt::api::serialize(b1); - libutt::api::operations::Budget b1_deserialized = - libutt::api::deserialize(b1_serialized); - assertTrue(b1.getHashHex() == b1_deserialized.getHashHex()); - const auto b1_coin = b1.getCoin(); - const auto c1_deserialized_coin = b1_deserialized.getCoin(); - assertTrue(b1_coin.getNullifier() == c1_deserialized_coin.getNullifier()); - assertTrue(b1_coin.hasSig() == c1_deserialized_coin.hasSig()); - assertTrue(b1_coin.getType() == c1_deserialized_coin.getType()); - assertTrue(b1_coin.getVal() == c1_deserialized_coin.getVal()); - assertTrue(b1_coin.getSN() == c1_deserialized_coin.getSN()); - assertTrue(b1_coin.getExpDateAsCurvePoint() == c1_deserialized_coin.getExpDateAsCurvePoint()); - - for (auto& c : clients) { - std::vector> rsigs; - std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); - auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); - - // Test Mint request de/serialization - std::vector mint_serialized = libutt::api::serialize(mint); - auto mint_deserialized = libutt::api::deserialize(mint_serialized); - assertTrue(mint.getHash() == mint_deserialized.getHash()); - assertTrue(mint.getVal() == mint_deserialized.getVal()); - assertTrue(mint.getRecipentID() == mint_deserialized.getRecipentID()); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(mint).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map> sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(mint, d, {blinded_sig}).front(); - libutt::api::operations::Burn b_op{d, c, coin}; - - // Test Burn request de/serialization - std::vector burn_serialized = libutt::api::serialize(b_op); - auto burn_deserialized = libutt::api::deserialize(burn_serialized); - assertTrue(b_op.getNullifier() == burn_deserialized.getNullifier()); - const auto& burn_coin = b_op.getCoin(); - const auto& burn_deserialized_coin = burn_deserialized.getCoin(); - assertTrue(burn_coin.getNullifier() == burn_deserialized_coin.getNullifier()); - assertTrue(burn_coin.hasSig() == burn_deserialized_coin.hasSig()); - assertTrue(burn_coin.getType() == burn_deserialized_coin.getType()); - assertTrue(burn_coin.getVal() == burn_deserialized_coin.getVal()); - assertTrue(burn_coin.getSN() == burn_deserialized_coin.getSN()); - assertTrue(burn_coin.getExpDateAsCurvePoint() == burn_deserialized_coin.getExpDateAsCurvePoint()); - } - const auto& client1 = clients[0]; - const auto& client2 = clients[1]; - std::vector rsigs; - std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); - auto mint = Mint(simulatonOfUniqueTxHash, 100, client1.getPid()); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(mint).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = client1.claimCoins(mint, d, {blinded_sig}).front(); - - rsigs.clear(); - auto budget = Budget(d, client1, 1000, 123456789); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(budget).front()); - } - sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - sigs.clear(); - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto bcoin = client1.claimCoins(budget, d, {blinded_sig}).front(); - auto encryptor = std::make_shared(rc.toPK().mpk); - Transaction tx(d, client1, {coin}, {bcoin}, {{client1.getPid(), 50}, {client2.getPid(), 50}}, *(encryptor)); - - // Test Transaction de/serialization - std::vector serialized_tx = libutt::api::serialize(tx); - auto deserialized_tx = libutt::api::deserialize(serialized_tx); - for (size_t i = 0; i < tx.getInputCoins().size(); i++) { - const auto& orig_coin = tx.getInputCoins().at(i); - const auto& des_coin = deserialized_tx.getInputCoins().at(i); - assertTrue(orig_coin.getNullifier() == des_coin.getNullifier()); - assertTrue(orig_coin.hasSig() == des_coin.hasSig()); - assertTrue(orig_coin.getType() == des_coin.getType()); - assertTrue(orig_coin.getVal() == des_coin.getVal()); - assertTrue(orig_coin.getSN() == des_coin.getSN()); - assertTrue(orig_coin.getExpDateAsCurvePoint() == des_coin.getExpDateAsCurvePoint()); - } - assertTrue(tx.getNullifiers() == deserialized_tx.getNullifiers()); - auto orig_bcoin = tx.getBudgetCoin(); - auto deserialize_bcoin = deserialized_tx.getBudgetCoin(); - assertTrue(orig_bcoin->getNullifier() == deserialize_bcoin->getNullifier()); - assertTrue(orig_bcoin->hasSig() == deserialize_bcoin->hasSig()); - assertTrue(orig_bcoin->getType() == deserialize_bcoin->getType()); - assertTrue(orig_bcoin->getVal() == deserialize_bcoin->getVal()); - assertTrue(orig_bcoin->getSN() == deserialize_bcoin->getSN()); - assertTrue(orig_bcoin->getExpDateAsCurvePoint() == deserialize_bcoin->getExpDateAsCurvePoint()); - - return 0; -} \ No newline at end of file diff --git a/utt/tests/TestTransaction.cpp b/utt/tests/TestTransaction.cpp deleted file mode 100644 index 527e474dc8..0000000000 --- a/utt/tests/TestTransaction.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "testUtils.hpp" - -#include "UTTParams.hpp" -#include "testUtils.hpp" -#include "mint.hpp" -#include "burn.hpp" -#include "transaction.hpp" -#include "budget.hpp" -#include "coin.hpp" -#include "types.hpp" -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -using namespace libutt; -using namespace libutt::api; -using namespace libutt::api::operations; -using namespace std::chrono; - -int main(int argc, char* argv[]) { - std::srand(0); - (void)argc; - (void)argv; - size_t thresh = 3; - size_t n = 4; - size_t c = 10; - auto [d, dkg, rc] = testing::init(n, thresh); - auto registrators = testing::GenerateRegistrators(n, rc); - auto banks = testing::GenerateCommitters(n, dkg, rc.toPK()); - auto clients = testing::GenerateClients(c, dkg.getPK(), rc.toPK(), rc); - for (auto& c : clients) { - testing::registerClient(d, c, registrators, thresh); - } - std::unordered_map> coins; - std::unordered_map bcoins; - std::unordered_map> encryptors_; - for (size_t i = 0; i < clients.size(); i++) { - encryptors_[i] = std::make_shared(rc.toPK().mpk); - } - for (auto& c : clients) { - std::vector rsigs; - auto budget = Budget(d, c, 1000, 123456789); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(budget).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(budget, d, {blinded_sig}).front(); - assertTrue(c.validate(coin)); - bcoins.emplace(c.getPid(), coin); - } - - for (auto& c : clients) { - std::vector rsigs; - std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); - auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(mint).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(mint, d, {blinded_sig}).front(); - assertTrue(c.validate(coin)); - coins[c.getPid()].emplace_back(std::move(coin)); - } - - // Now, each client transfers a 50$ to its predecessor; - for (size_t i = 0; i < clients.size(); i++) { - auto& issuer = clients[i]; - auto& receiver = clients[(i + 1) % clients.size()]; - Transaction tx(d, - issuer, - {coins[issuer.getPid()].front()}, - {bcoins[issuer.getPid()]}, - {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, - *(encryptors_.at((i + 1) % clients.size()))); - coins[issuer.getPid()].erase(coins[issuer.getPid()].begin()); - bcoins.erase(issuer.getPid()); - std::map> shares; - std::vector shares_signers; - for (size_t i = 0; i < banks.size(); i++) { - shares[i] = banks[i]->sign(tx); - shares_signers.push_back(banks[i]->getId()); - } - size_t num_coins = shares[0].size(); - for (size_t i = 0; i < num_coins; i++) { - std::vector share_sigs(n); - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)n); - for (auto s : sbs) { - share_sigs[s] = shares[s][i]; - } - for (auto& b : banks) { - for (size_t k = 0; k < share_sigs.size(); k++) { - auto& sig = share_sigs[k]; - auto& signer = shares_signers[k]; - assertTrue(b->validatePartialSignature(signer, sig, i, tx)); - } - } - } - - std::vector sigs; - for (size_t i = 0; i < num_coins; i++) { - std::map share_sigs; - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - for (auto s : sbs) { - share_sigs[s] = shares[s][i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {share_sigs}); - sigs.emplace_back(std::move(blinded_sig)); - } - auto issuer_coins = issuer.claimCoins(tx, d, sigs); - for (auto& coin : issuer_coins) { - assertTrue(issuer.validate(coin)); - if (coin.getType() == api::Coin::Type::Normal) { - coins[issuer.getPid()].emplace_back(std::move(coin)); - } else { - assertTrue(bcoins.find(issuer.getPid()) == bcoins.end()); - bcoins.emplace(issuer.getPid(), coin); - } - } - auto receiver_coins = receiver.claimCoins(tx, d, sigs); - for (auto& coin : receiver_coins) { - assertTrue(receiver.validate(coin)); - if (coin.getType() == api::Coin::Type::Normal) { - coins[receiver.getPid()].emplace_back(std::move(coin)); - } else { - assertTrue(false); - } - } - } - - // At the end of these phaese, the budget of each client is 950 and it has two coins of 50 each - for (const auto& c : clients) { - assertTrue(coins[c.getPid()].size() == 2); - for (const auto& coin : coins[c.getPid()]) assertTrue(coin.getVal() == 50U); - assertTrue(bcoins[c.getPid()].getVal() == 950U); - } - return 0; -} \ No newline at end of file diff --git a/utt/tests/TestTransactionWithRsaEncryption.cpp b/utt/tests/TestTransactionWithRsaEncryption.cpp deleted file mode 100644 index 9cc1e1e89f..0000000000 --- a/utt/tests/TestTransactionWithRsaEncryption.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include "testUtils.hpp" - -#include "UTTParams.hpp" -#include "testUtils.hpp" -#include "mint.hpp" -#include "burn.hpp" -#include "transaction.hpp" -#include "budget.hpp" -#include "coin.hpp" -#include "types.hpp" -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -using namespace libutt; -using namespace libutt::api; -using namespace libutt::api::operations; -using namespace std::chrono; -std::string privatek1 = - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIICXQIBAAKBgQDAyUwjK+m4EXyFKEha2NfQUY8eIqurRujNMqI1z7B3bF/8Bdg0\n" - "wffIYJUjiXo13hB6yOq+gD62BGAPRjmgReBniT3d2rLU0Z72zQ64Gof66jCGQt0W\n" - "0sfwDUv0XsfXXW9p1EGBYwIkgW6UGiABnkZcIUW4dzP2URoRCN/VIypkRwIDAQAB\n" - "AoGAa3VIvSoTAoisscQ8YHcSBIoRjiihK71AsnAQvpHfuRFthxry4qVjqgs71i0h\n" - "M7lt0iL/xePSEL7rlFf+cvnAFL4/j1R04ImBjRzWGnaNE8I7nNGGzJo9rL5I1oi3\n" - "zN2yUucTSGm7qR0MCNVy26zNmCuS/FdBPsfdZ017OTsHtPECQQDlWXAJG6nHyw2o\n" - "2cLYHzlyrrYgnWJkgFSKzr7VFNlHxfQSWXJ4zuDwhqkm3d176bVm4eHhDDv6f413\n" - "iQGraKvTAkEA1zAzpxfI7LAqd3sObWYstQb03IXE7yddMgbhoMDCT3gXhNaHKfjT\n" - "Z/GIk49jh8kyitN2FeYXXi9TiwrXStfhPQJBAMNea6ymjvstwoYKcgsOli5WG7ku\n" - "uEkqdFoGAdObvfeA7gfPgE7e1AiwfVkpd+l9TVTFqFe/xzv8+fJQmEZ+lJcCQQDN\n" - "5I7nh7h1zzEy1Qk+345TP262OT/u26kuHqtv1j+VLgDC10jIfg443D+jgITo/Tdg\n" - "4WeRGHCva3TyCtNoBxq5AkA9KZpKof4ripad4oIuCJpR/ZhQATgQwR9f+FlAxgP0\n" - "ABmBPCoxy4uGMtSBMqiiGpImbDuivYkhlBl7D8u8vn26\n" - "-----END RSA PRIVATE KEY-----\n"; -std::string publick1 = - "-----BEGIN PUBLIC KEY-----\n" - "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAyUwjK+m4EXyFKEha2NfQUY8e\n" - "IqurRujNMqI1z7B3bF/8Bdg0wffIYJUjiXo13hB6yOq+gD62BGAPRjmgReBniT3d\n" - "2rLU0Z72zQ64Gof66jCGQt0W0sfwDUv0XsfXXW9p1EGBYwIkgW6UGiABnkZcIUW4\n" - "dzP2URoRCN/VIypkRwIDAQAB\n" - "-----END PUBLIC KEY-----\n"; - -std::string privatek2 = - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIICXAIBAAKBgQDMw+qeJXQ/ZxrqSqjRXoN2wFYNwhljh7RTBJuIzaqK2pDL+zaK\n" - "aREzgufG/7r2uk8njWW7EJbriLcmj1oJA913LPl4YWUEORKl7Cb6wLoPO/E5gAt1\n" - "JJ0dhDeCRU+E7vtNQ4pLyy2dYS2GHO1Tm88yuFegS3gkINPYgzNggGJ2cQIDAQAB\n" - "AoGAScyCjqTpFMDQTojB91OdBfukCClghSKvtwv+EnwtbwX/EcVkjtX3QR1485vP\n" - "goT7akHn3FfKTPFlMRyRUpZ2Bov1whQ1ztuboIonFQ7ohbDTLE3QzUv4L3e8cEbY\n" - "51MSe8tEUVRxKu53nD3asWxAi/CEyqWvRCzf4s3Q6Xw3h5ECQQD8mBT6ervLr1Qk\n" - "oKmaAuPTDyZDaSjipD0/d1p1vG8Wb8go14tq89Ts+UIWzH5aGlidxTK9j/luQqlR\n" - "YVVGNkC3AkEAz4a8jtg2++fvWT0PDz6OBfw/iHJQzSmynlzKQzpRac02UBCPo4an\n" - "o7wl9uEnucXuVpCSo0JdSf+x4r9dwmCKFwJBAPWlGNG2xicBbPzp2cZTBSheVUG9\n" - "ZOtz+bRc5/YTuJzDPI6rf4QVeH60sNbnLAGIGaHlAsFi4Jmf7nWcCIftfuUCQEbx\n" - "hJxAhetvyn7zRKatd9fL99wpWD4Ktyk0B2EcGqDUqnCMeM4qRjzPIRtYtT/oziWB\n" - "nt943HNjmeguC1tbrVkCQBMd+kbpcoFHKKrC577FM24maWRTfXJeu2/o6pxUFIUY\n" - "kzkDZ2k+FfvXWaY+N5q5bJCayor8W1QeruHzewrQmgY=\n" - "-----END RSA PRIVATE KEY-----\n"; -std::string publick2 = - "-----BEGIN PUBLIC KEY-----\n" - "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMw+qeJXQ/ZxrqSqjRXoN2wFYN\n" - "whljh7RTBJuIzaqK2pDL+zaKaREzgufG/7r2uk8njWW7EJbriLcmj1oJA913LPl4\n" - "YWUEORKl7Cb6wLoPO/E5gAt1JJ0dhDeCRU+E7vtNQ4pLyy2dYS2GHO1Tm88yuFeg\n" - "S3gkINPYgzNggGJ2cQIDAQAB\n" - "-----END PUBLIC KEY-----\n"; - -std::string privatek3 = - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIICXAIBAAKBgQCnmdqW7f/rg8CiUzLugxc0jPMqzphhtl40IqINAk3pasCQsA3T\n" - "eHSa1fcKocFMY9bfQRvqiKpnK74d0V0fujFUaPIMlKMLWXEVefwe1fOYrDXOoz7N\n" - "3Go7x9u/LXwCA6HehXtIavtTPQs1yHldb/bDocEhjfGvU3TLXkAuGWsnmQIDAQAB\n" - "AoGBAJ6fRHyYICB8f7Kh35BRTYMU64fWI+5GtX3OUWTSi36g5EOL/GnqlSF94+OS\n" - "F+n+i/ycGJmuYuhmQ/bgkaxXghsDYb7fsdMJ8DEqUJAKbxeOosn8fxwmJkNAJ07J\n" - "+oAg/xkJ+ukyYnPf0P3UTuTZl0EFEpwu/vnX09QJGtuXgmQhAkEA0c0Co9MdP62r\n" - "/ybXXeqgaol2YVGzFr/bMz3hhlviV9IOGPRZmeQ8v+f1/lSsqZ8wSP5P8dkBo4UB\n" - "NSLaHAUL/QJBAMyB72EyHZUEFy3o241myqamfVtN+Dzo6qdPn/PfF/BLjwsEApCO\n" - "oUObmDDo/yiSSb00XSnn23bGYH1VJJDNJs0CQE1aG+YQ+VC4FJkfVfpvfjOpePcK\n" - "q0/w7r2mzBbAm+QrMz1qIfsGVoue12itCXgElEXlVc5iZyNF75sKvYXlKnUCQHHC\n" - "tc5zelEyfVJkff0ieQhLBOCNdtErH50Chg+6wi5BWcje6i5PqRVasEZE1etTtQEy\n" - "58Av4b0ojPQrMLP76uECQEz0c1RPDwMvznwT3BJxl8t4tixPML0nyBMD8ttnZswG\n" - "K/1CYV1uMNbchmuVQb2Kd2JyE1gQF8s3ShsbteMc5og=\n" - "-----END RSA PRIVATE KEY-----\n"; - -std::string publick3 = - "-----BEGIN PUBLIC KEY-----\n" - "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnmdqW7f/rg8CiUzLugxc0jPMq\n" - "zphhtl40IqINAk3pasCQsA3TeHSa1fcKocFMY9bfQRvqiKpnK74d0V0fujFUaPIM\n" - "lKMLWXEVefwe1fOYrDXOoz7N3Go7x9u/LXwCA6HehXtIavtTPQs1yHldb/bDocEh\n" - "jfGvU3TLXkAuGWsnmQIDAQAB\n" - "-----END PUBLIC KEY-----\n"; - -std::vector pr_keys{privatek1, privatek2, privatek3}; -std::vector pkeys{publick1, publick2, publick3}; -int main(int argc, char* argv[]) { - std::srand(0); - (void)argc; - (void)argv; - size_t thresh = 3; - size_t n = 4; - size_t c = 3; - auto [d, dkg, rc] = testing::init(n, thresh); - auto registrators = testing::GenerateRegistrators(n, rc); - auto banks = testing::GenerateCommitters(n, dkg, rc.toPK()); - auto clients = testing::GenerateClients(c, dkg.getPK(), rc.toPK(), pr_keys); - for (auto& c : clients) { - testing::registerClient(d, c, registrators, thresh); - } - std::unordered_map> coins; - std::unordered_map bcoins; - for (auto& c : clients) { - std::vector rsigs; - auto budget = Budget(d, c, 1000, 123456789); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(budget).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(budget, d, {blinded_sig}).front(); - assertTrue(c.validate(coin)); - bcoins.emplace(c.getPid(), coin); - } - - for (auto& c : clients) { - std::vector rsigs; - std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); - auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); - for (size_t i = 0; i < banks.size(); i++) { - rsigs.push_back(banks[i]->sign(mint).front()); - } - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - std::map sigs; - for (auto i : sbs) { - sigs[i] = rsigs[i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); - auto coin = c.claimCoins(mint, d, {blinded_sig}).front(); - assertTrue(c.validate(coin)); - coins[c.getPid()].emplace_back(std::move(coin)); - } - - // Now, each client transfers a 50$ to its predecessor; - for (size_t i = 0; i < clients.size(); i++) { - auto& issuer = clients[i]; - auto& receiver = clients[(i + 1) % clients.size()]; - std::map tx_pub_keys; - tx_pub_keys[issuer.getPid()] = pkeys[i]; - tx_pub_keys[receiver.getPid()] = pkeys[(i + 1) % clients.size()]; - libutt::RSAEncryptor tx_encryptor(tx_pub_keys); - Transaction tx(d, - issuer, - {coins[issuer.getPid()].front()}, - {bcoins[issuer.getPid()]}, - {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, - tx_encryptor); - coins[issuer.getPid()].erase(coins[issuer.getPid()].begin()); - bcoins.erase(issuer.getPid()); - std::unordered_map> shares; - for (size_t i = 0; i < banks.size(); i++) { - shares[i] = banks[i]->sign(tx); - } - size_t num_coins = shares[0].size(); - std::vector sigs; - for (size_t i = 0; i < num_coins; i++) { - std::map share_sigs; - auto sbs = testing::getSubset((uint32_t)n, (uint32_t)thresh); - for (auto s : sbs) { - share_sigs[s] = shares[s][i]; - } - auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {share_sigs}); - sigs.emplace_back(std::move(blinded_sig)); - } - auto issuer_coins = issuer.claimCoins(tx, d, sigs); - for (auto& coin : issuer_coins) { - assertTrue(issuer.validate(coin)); - if (coin.getType() == api::Coin::Type::Normal) { - coins[issuer.getPid()].emplace_back(std::move(coin)); - } else { - assertTrue(bcoins.find(issuer.getPid()) == bcoins.end()); - bcoins.emplace(issuer.getPid(), coin); - } - } - auto receiver_coins = receiver.claimCoins(tx, d, sigs); - for (auto& coin : receiver_coins) { - assertTrue(receiver.validate(coin)); - if (coin.getType() == api::Coin::Type::Normal) { - coins[receiver.getPid()].emplace_back(std::move(coin)); - } else { - assertTrue(false); - } - } - } - - // At the end of these phaese, the budget of each client is 950 and it has two coins of 50 each - for (const auto& c : clients) { - assertTrue(coins[c.getPid()].size() == 2); - for (const auto& coin : coins[c.getPid()]) assertTrue(coin.getVal() == 50U); - assertTrue(bcoins[c.getPid()].getVal() == 950U); - } - return 0; -} \ No newline at end of file diff --git a/utt/tests/TestUttNewApi.cpp b/utt/tests/TestUttNewApi.cpp new file mode 100644 index 0000000000..0e1590c334 --- /dev/null +++ b/utt/tests/TestUttNewApi.cpp @@ -0,0 +1,677 @@ +#include "UTTParams.hpp" +#include "testUtils.hpp" +#include "mint.hpp" +#include "budget.hpp" +#include "burn.hpp" +#include "commitment.hpp" +#include "transaction.hpp" +#include "config.hpp" +#include "serialization.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace libutt; +using namespace libutt::api; +using namespace libutt::api::operations; + +namespace { +class ibe_based_test_system : public libutt::api::testing::test_utt_instance { + protected: + void SetUp() override { libutt::api::testing::test_utt_instance::setUp(true, true, true); } +}; + +class ibe_based_test_system_without_registration : public libutt::api::testing::test_utt_instance { + protected: + void SetUp() override { libutt::api::testing::test_utt_instance::setUp(true, false, true); } +}; + +class rsa_based_test_system : public libutt::api::testing::test_utt_instance { + protected: + void SetUp() override { libutt::api::testing::test_utt_instance::setUp(false, true, true); } +}; + +class rsa_based_test_system_without_registration : public libutt::api::testing::test_utt_instance { + protected: + void SetUp() override { libutt::api::testing::test_utt_instance::setUp(false, false, true); } +}; + +class test_system_minted : public libutt::api::testing::test_utt_instance { + public: + void setUp(bool ibe, bool budget_policy) { + libutt::api::testing::test_utt_instance::setUp(ibe, true, budget_policy); + for (auto& c : clients) { + std::vector> rsigs; + std::string simulatonOfUniqueTxHash = + std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); + auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); + for (size_t i = 0; i < banks.size(); i++) { + rsigs.push_back(banks[i]->sign(mint).front()); + } + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + std::map> sigs; + for (auto i : sbs) { + sigs[i] = rsigs[i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); + auto coin = c.claimCoins(mint, d, {blinded_sig}).front(); + ASSERT_TRUE(c.validate(coin)); + coins[c.getPid()] = {coin}; + } + if (budget_policy) { + for (auto& c : clients) { + std::vector> rsigs; + std::vector shares_signers; + auto budget = Budget(d, c, 1000, 123456789); + for (size_t i = 0; i < banks.size(); i++) { + rsigs.push_back(banks[i]->sign(budget).front()); + shares_signers.push_back(banks[i]->getId()); + } + for (auto& b : banks) { + for (size_t i = 0; i < rsigs.size(); i++) { + auto& sig = rsigs[i]; + auto& signer = shares_signers[i]; + ASSERT_TRUE(b->validatePartialSignature(signer, sig, 0, budget)); + } + } + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + std::map> sigs; + for (auto i : sbs) { + sigs[i] = rsigs[i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); + auto coin = c.claimCoins(budget, d, {blinded_sig}).front(); + ASSERT_TRUE(c.validate(coin)); + bcoins[c.getPid()] = {coin}; + } + } + } + std::map> coins; + std::map> bcoins; +}; + +class ibe_based_test_system_minted : public test_system_minted { + protected: + void SetUp() override { test_system_minted::setUp(true, true); } +}; + +class ibe_based_test_system_minted_budget_policy_disabled : public test_system_minted { + protected: + void SetUp() override { test_system_minted::setUp(true, false); } +}; + +class ibe_based_test_system_minted_impl : public ibe_based_test_system_minted { + public: + ibe_based_test_system_minted_impl() { ibe_based_test_system_minted::SetUp(); } + void TestBody() override {} +}; + +class rsa_based_test_system_minted : public test_system_minted { + protected: + void SetUp() override { test_system_minted::setUp(false, true); } +}; + +TEST_F(ibe_based_test_system_without_registration, test_distributed_registration) { + for (auto& c : clients) { + registerClient(c); + auto rcm_data = c.rerandomizeRcm(d); + for (auto& r : registrators) { + ASSERT_TRUE(r->validateRCM(rcm_data.first, rcm_data.second)); + } + } +} + +TEST_F(rsa_based_test_system_without_registration, test_distributed_registration) { + for (auto& c : clients) { + registerClient(c); + auto rcm_data = c.rerandomizeRcm(d); + for (auto& r : registrators) { + ASSERT_TRUE(r->validateRCM(rcm_data.first, rcm_data.second)); + } + } +} + +TEST_F(ibe_based_test_system, test_mint) { + for (auto& c : clients) { + std::vector> rsigs; + std::vector shares_signers; + std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); + auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); + for (size_t i = 0; i < banks.size(); i++) { + rsigs.push_back(banks[i]->sign(mint).front()); + shares_signers.push_back(banks[i]->getId()); + } + for (auto& b : banks) { + for (size_t i = 0; i < rsigs.size(); i++) { + auto& sig = rsigs[i]; + auto& signer = shares_signers[i]; + ASSERT_TRUE(b->validatePartialSignature(signer, sig, 0, mint)); + } + } + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + std::map> sigs; + for (auto i : sbs) { + sigs[i] = rsigs[i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); + auto coin = c.claimCoins(mint, d, {blinded_sig}).front(); + ASSERT_TRUE(c.validate(coin)); + } +} + +TEST_F(ibe_based_test_system, test_budget_creation_by_client) { + for (auto& c : clients) { + std::vector> rsigs; + std::vector shares_signers; + auto budget = Budget(d, c, 1000, 123456789); + for (size_t i = 0; i < banks.size(); i++) { + rsigs.push_back(banks[i]->sign(budget).front()); + shares_signers.push_back(banks[i]->getId()); + } + for (auto& b : banks) { + for (size_t i = 0; i < rsigs.size(); i++) { + auto& sig = rsigs[i]; + auto& signer = shares_signers[i]; + ASSERT_TRUE(b->validatePartialSignature(signer, sig, 0, budget)); + } + } + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + std::map> sigs; + for (auto i : sbs) { + sigs[i] = rsigs[i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); + auto coin = c.claimCoins(budget, d, {blinded_sig}).front(); + ASSERT_TRUE(c.validate(coin)); + } +} + +TEST_F(ibe_based_test_system, test_budget_creation_by_replicas) { + for (auto& c : clients) { + std::vector> rsigs; + auto snHash = Fr::random_element() + .to_words(); // Assume each replica can compute the same sn using some common execution state + for (size_t i = 0; i < banks.size(); i++) { + // Each replica creates its own version of the budget coin and sign it. We expect to have deterministic coin here + auto budget = Budget(d, snHash, c.getPidHash(), 1000, 123456789); + rsigs.push_back(banks[i]->sign(budget).front()); + } + auto budget = Budget(d, snHash, c.getPidHash(), 1000, 123456789); + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + std::map> sigs; + for (auto i : sbs) { + sigs[i] = rsigs[i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {sigs}); + auto coin = c.claimCoins(budget, d, {blinded_sig}).front(); + coin.createNullifier(d, c.getPRFSecretKey()); + ASSERT_TRUE(!coin.getNullifier().empty()); + ASSERT_TRUE(c.validate(coin)); + } +} + +TEST_F(ibe_based_test_system_minted, test_valid_burn) { + for (auto& c : clients) { + Burn b_op{d, c, coins[c.getPid()].front()}; + for (auto& b : banks) { + ASSERT_TRUE(b->validate(d, b_op)); + } + } +} + +TEST_F(ibe_based_test_system_minted, test_invalid_burn) { + auto other_utt_sys = ibe_based_test_system_minted_impl(); + for (auto& c : clients) { + Burn b_op{d, c, coins[c.getPid()].front()}; + for (auto& b : other_utt_sys.banks) { + ASSERT_FALSE(b->validate(other_utt_sys.d, b_op)); + } + } +} + +TEST_F(ibe_based_test_system, test_serialization_configuration) { + auto config = libutt::api::Configuration((uint16_t)n, (uint16_t)thresh, true); + ASSERT_TRUE(config.isValid()); + auto serialized_config = libutt::api::serialize(config); + auto deserialized_config = libutt::api::deserialize(serialized_config); + ASSERT_TRUE(deserialized_config.isValid()); + ASSERT_TRUE(deserialized_config == config); + + const auto& publicConfig = config.getPublicConfig(); + auto serialized_public_config = libutt::api::serialize(publicConfig); + auto deserialized_public_config = libutt::api::deserialize(serialized_public_config); + ASSERT_TRUE(deserialized_public_config == publicConfig); +} + +TEST_F(ibe_based_test_system, test_serialization_global_params) { + std::vector serialized_params = libutt::api::serialize(d); + auto deserialized_params = libutt::api::deserialize(serialized_params); + ASSERT_TRUE(deserialized_params == d); +} + +TEST_F(ibe_based_test_system, test_serialization_commitment) { + Commitment rcm = clients[0].getRcm().first; + std::vector serialized_rcm = libutt::api::serialize(rcm); + auto deserialized_rcm = libutt::api::deserialize(serialized_rcm); + ASSERT_TRUE(rcm == deserialized_rcm); +} + +TEST_F(ibe_based_test_system, test_serialization_complete_coin) { + Fr fr_val; + fr_val.set_ulong(100); + libutt::api::Coin c(d, + Fr::random_element().to_words(), + Fr::random_element().to_words(), + fr_val.to_words(), + Fr::random_element().to_words(), + libutt::api::Coin::Type::Normal, + Fr::random_element().to_words()); + std::vector c_serialized = libutt::api::serialize(c); + libutt::api::Coin c_deserialized = libutt::api::deserialize(c_serialized); + ASSERT_TRUE(c.getNullifier() == c_deserialized.getNullifier()); + ASSERT_TRUE(c.hasSig() == c_deserialized.hasSig()); + ASSERT_TRUE(c.getType() == c_deserialized.getType()); + ASSERT_TRUE(c.getVal() == c_deserialized.getVal()); + ASSERT_TRUE(c.getSN() == c_deserialized.getSN()); + ASSERT_TRUE(c.getExpDateAsCurvePoint() == c_deserialized.getExpDateAsCurvePoint()); +} + +TEST_F(ibe_based_test_system, test_serialization_incomplete_coin) { + Fr fr_val; + fr_val.set_ulong(100); + libutt::api::Coin c(d, + Fr::random_element().to_words(), + fr_val.to_words(), + Fr::random_element().to_words(), + libutt::api::Coin::Type::Normal, + Fr::random_element().to_words()); + std::vector c_serialized = libutt::api::serialize(c); + libutt::api::Coin c_deserialized = libutt::api::deserialize(c_serialized); + ASSERT_TRUE(c.getNullifier() == c_deserialized.getNullifier()); + ASSERT_TRUE(c.hasSig() == c_deserialized.hasSig()); + ASSERT_TRUE(c.getType() == c_deserialized.getType()); + ASSERT_TRUE(c.getVal() == c_deserialized.getVal()); + ASSERT_TRUE(c.getSN() == c_deserialized.getSN()); + ASSERT_TRUE(c.getExpDateAsCurvePoint() == c_deserialized.getExpDateAsCurvePoint()); +} +TEST_F(ibe_based_test_system, test_serialization_complete_budget_coin) { + Fr fr_val; + fr_val.set_ulong(100); + libutt::api::Coin c(d, + Fr::random_element().to_words(), + fr_val.to_words(), + Fr::random_element().to_words(), + libutt::api::Coin::Type::Budget, + Fr::random_element().to_words()); + std::vector c_serialized = libutt::api::serialize(c); + libutt::api::Coin c_deserialized = libutt::api::deserialize(c_serialized); + ASSERT_TRUE(c.getNullifier() == c_deserialized.getNullifier()); + ASSERT_TRUE(c.hasSig() == c_deserialized.hasSig()); + ASSERT_TRUE(c.getType() == c_deserialized.getType()); + ASSERT_TRUE(c.getVal() == c_deserialized.getVal()); + ASSERT_TRUE(c.getSN() == c_deserialized.getSN()); + ASSERT_TRUE(c.getExpDateAsCurvePoint() == c_deserialized.getExpDateAsCurvePoint()); +} + +TEST_F(ibe_based_test_system, test_serialization_incomplete_budget_coin) { + Fr fr_val; + fr_val.set_ulong(100); + libutt::api::Coin c(d, + Fr::random_element().to_words(), + Fr::random_element().to_words(), + fr_val.to_words(), + Fr::random_element().to_words(), + libutt::api::Coin::Type::Budget, + Fr::random_element().to_words()); + std::vector c_serialized = libutt::api::serialize(c); + libutt::api::Coin c_deserialized = libutt::api::deserialize(c_serialized); + ASSERT_TRUE(c.getNullifier() == c_deserialized.getNullifier()); + ASSERT_TRUE(c.hasSig() == c_deserialized.hasSig()); + ASSERT_TRUE(c.getType() == c_deserialized.getType()); + ASSERT_TRUE(c.getVal() == c_deserialized.getVal()); + ASSERT_TRUE(c.getSN() == c_deserialized.getSN()); + ASSERT_TRUE(c.getExpDateAsCurvePoint() == c_deserialized.getExpDateAsCurvePoint()); +} + +TEST_F(ibe_based_test_system, test_serialization_incomplete_budget_op) { + libutt::api::operations::Budget b( + d, Fr::random_element().to_words(), Fr::random_element().to_words(), 100, 987654321); + std::vector b_serialized = libutt::api::serialize(b); + libutt::api::operations::Budget b_deserialized = + libutt::api::deserialize(b_serialized); + ASSERT_TRUE(b.getHashHex() == b_deserialized.getHashHex()); + const auto b_coin = b.getCoin(); + const auto c_deserialized_coin = b_deserialized.getCoin(); + ASSERT_TRUE(b_coin.getNullifier() == c_deserialized_coin.getNullifier()); + ASSERT_TRUE(b_coin.hasSig() == c_deserialized_coin.hasSig()); + ASSERT_TRUE(b_coin.getType() == c_deserialized_coin.getType()); + ASSERT_TRUE(b_coin.getVal() == c_deserialized_coin.getVal()); + ASSERT_TRUE(b_coin.getSN() == c_deserialized_coin.getSN()); + ASSERT_TRUE(b_coin.getExpDateAsCurvePoint() == c_deserialized_coin.getExpDateAsCurvePoint()); +} + +TEST_F(ibe_based_test_system, test_serialization_mint_op) { + for (auto& c : clients) { + std::vector> rsigs; + std::string simulatonOfUniqueTxHash = std::to_string(((uint64_t)rand()) * ((uint64_t)rand()) * ((uint64_t)rand())); + auto mint = Mint(simulatonOfUniqueTxHash, 100, c.getPid()); + std::vector mint_serialized = libutt::api::serialize(mint); + auto mint_deserialized = libutt::api::deserialize(mint_serialized); + ASSERT_TRUE(mint.getHash() == mint_deserialized.getHash()); + ASSERT_TRUE(mint.getVal() == mint_deserialized.getVal()); + ASSERT_TRUE(mint.getRecipentID() == mint_deserialized.getRecipentID()); + } +} + +TEST_F(ibe_based_test_system_minted, test_serialization_burn_op) { + for (auto& c : clients) { + Burn b_op{d, c, coins[c.getPid()].front()}; + std::vector burn_serialized = libutt::api::serialize(b_op); + auto burn_deserialized = libutt::api::deserialize(burn_serialized); + ASSERT_TRUE(b_op.getNullifier() == burn_deserialized.getNullifier()); + const auto& burn_coin = b_op.getCoin(); + const auto& burn_deserialized_coin = burn_deserialized.getCoin(); + ASSERT_TRUE(burn_coin.getNullifier() == burn_deserialized_coin.getNullifier()); + ASSERT_TRUE(burn_coin.hasSig() == burn_deserialized_coin.hasSig()); + ASSERT_TRUE(burn_coin.getType() == burn_deserialized_coin.getType()); + ASSERT_TRUE(burn_coin.getVal() == burn_deserialized_coin.getVal()); + ASSERT_TRUE(burn_coin.getSN() == burn_deserialized_coin.getSN()); + ASSERT_TRUE(burn_coin.getExpDateAsCurvePoint() == burn_deserialized_coin.getExpDateAsCurvePoint()); + } +} + +TEST_F(ibe_based_test_system_minted, test_serialization_tx_op) { + const auto& client1 = clients[0]; + const auto& client2 = clients[1]; + auto coin = coins[client1.getPid()].front(); + auto bcoin = bcoins[client1.getPid()].front(); + auto encryptor = std::make_shared(rsk.toPK().mpk); + Transaction tx(d, client1, {coin}, {bcoin}, {{client1.getPid(), 50}, {client2.getPid(), 50}}, *(encryptor)); + + // Test Transaction de/serialization + std::vector serialized_tx = libutt::api::serialize(tx); + auto deserialized_tx = libutt::api::deserialize(serialized_tx); + ASSERT_TRUE(tx.hasBudgetCoin() == deserialized_tx.hasBudgetCoin()); + ASSERT_TRUE(tx.getBudgetExpirationDate() == deserialized_tx.getBudgetExpirationDate()); + ASSERT_TRUE(tx.getNullifiers() == deserialized_tx.getNullifiers()); +} + +TEST_F(ibe_based_test_system_minted, test_transaction) { + std::unordered_map> encryptors_; + for (size_t i = 0; i < clients.size(); i++) { + encryptors_[i] = std::make_shared(rsk.toPK().mpk); + } + for (size_t i = 0; i < clients.size(); i++) { + auto& issuer = clients[i]; + auto& receiver = clients[(i + 1) % clients.size()]; + Transaction tx(d, + issuer, + {coins[issuer.getPid()].front()}, + {bcoins[issuer.getPid()].front()}, + {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, + *(encryptors_.at((i + 1) % clients.size()))); + coins[issuer.getPid()].erase(coins[issuer.getPid()].begin()); + bcoins.erase(issuer.getPid()); + for (auto& b : banks) { + ASSERT_TRUE(b->validate(d, tx)); + } + std::map> shares; + std::vector shares_signers; + for (size_t i = 0; i < banks.size(); i++) { + shares[i] = banks[i]->sign(tx); + shares_signers.push_back(banks[i]->getId()); + } + size_t num_coins = shares[0].size(); + for (size_t i = 0; i < num_coins; i++) { + std::vector share_sigs(n); + auto sbs = getSubset((uint32_t)n, (uint32_t)n); + for (auto s : sbs) { + share_sigs[s] = shares[s][i]; + } + for (auto& b : banks) { + for (size_t k = 0; k < share_sigs.size(); k++) { + auto& sig = share_sigs[k]; + auto& signer = shares_signers[k]; + ASSERT_TRUE(b->validatePartialSignature(signer, sig, i, tx)); + } + } + } + + std::vector sigs; + for (size_t i = 0; i < num_coins; i++) { + std::map share_sigs; + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + for (auto s : sbs) { + share_sigs[s] = shares[s][i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {share_sigs}); + sigs.emplace_back(std::move(blinded_sig)); + } + auto issuer_coins = issuer.claimCoins(tx, d, sigs); + for (auto& coin : issuer_coins) { + ASSERT_TRUE(issuer.validate(coin)); + if (coin.getType() == api::Coin::Type::Normal) { + coins[issuer.getPid()].emplace_back(std::move(coin)); + } else { + ASSERT_TRUE(bcoins.find(issuer.getPid()) == bcoins.end()); + bcoins[issuer.getPid()] = {coin}; + } + } + auto receiver_coins = receiver.claimCoins(tx, d, sigs); + for (auto& coin : receiver_coins) { + ASSERT_TRUE(receiver.validate(coin)); + ASSERT_TRUE(coin.getType() == api::Coin::Type::Normal); + coins[receiver.getPid()].emplace_back(std::move(coin)); + } + } + + // At the end of these phaese, the budget of each client is 950 and it has two coins of 50 each + for (const auto& c : clients) { + ASSERT_TRUE(coins[c.getPid()].size() == 2); + for (const auto& coin : coins[c.getPid()]) ASSERT_TRUE(coin.getVal() == 50U); + ASSERT_TRUE(bcoins[c.getPid()].front().getVal() == 950U); + } +} + +TEST_F(rsa_based_test_system_minted, test_transaction) { + for (size_t i = 0; i < clients.size(); i++) { + auto& issuer = clients[i]; + auto& receiver = clients[(i + 1) % clients.size()]; + std::map tx_pub_keys; + tx_pub_keys[issuer.getPid()] = libutt::api::testing::pkeys[i]; + tx_pub_keys[receiver.getPid()] = libutt::api::testing::pkeys[(i + 1) % clients.size()]; + libutt::RSAEncryptor tx_encryptor(tx_pub_keys); + Transaction tx(d, + issuer, + {coins[issuer.getPid()].front()}, + {bcoins[issuer.getPid()].front()}, + {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, + tx_encryptor); + coins[issuer.getPid()].erase(coins[issuer.getPid()].begin()); + bcoins.erase(issuer.getPid()); + for (auto& b : banks) { + ASSERT_TRUE(b->validate(d, tx)); + } + std::map> shares; + std::vector shares_signers; + for (size_t i = 0; i < banks.size(); i++) { + shares[i] = banks[i]->sign(tx); + shares_signers.push_back(banks[i]->getId()); + } + size_t num_coins = shares[0].size(); + for (size_t i = 0; i < num_coins; i++) { + std::vector share_sigs(n); + auto sbs = getSubset((uint32_t)n, (uint32_t)n); + for (auto s : sbs) { + share_sigs[s] = shares[s][i]; + } + for (auto& b : banks) { + for (size_t k = 0; k < share_sigs.size(); k++) { + auto& sig = share_sigs[k]; + auto& signer = shares_signers[k]; + ASSERT_TRUE(b->validatePartialSignature(signer, sig, i, tx)); + } + } + } + + std::vector sigs; + for (size_t i = 0; i < num_coins; i++) { + std::map share_sigs; + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + for (auto s : sbs) { + share_sigs[s] = shares[s][i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {share_sigs}); + sigs.emplace_back(std::move(blinded_sig)); + } + auto issuer_coins = issuer.claimCoins(tx, d, sigs); + for (auto& coin : issuer_coins) { + ASSERT_TRUE(issuer.validate(coin)); + if (coin.getType() == api::Coin::Type::Normal) { + coins[issuer.getPid()].emplace_back(std::move(coin)); + } else { + ASSERT_TRUE(bcoins.find(issuer.getPid()) == bcoins.end()); + bcoins[issuer.getPid()] = {coin}; + } + } + auto receiver_coins = receiver.claimCoins(tx, d, sigs); + for (auto& coin : receiver_coins) { + ASSERT_TRUE(receiver.validate(coin)); + ASSERT_TRUE(coin.getType() == api::Coin::Type::Normal); + coins[receiver.getPid()].emplace_back(std::move(coin)); + } + } + + // At the end of these phaese, the budget of each client is 950 and it has two coins of 50 each + for (const auto& c : clients) { + ASSERT_TRUE(coins[c.getPid()].size() == 2); + for (const auto& coin : coins[c.getPid()]) ASSERT_TRUE(coin.getVal() == 50U); + ASSERT_TRUE(bcoins[c.getPid()].front().getVal() == 950U); + } +} + +TEST_F(ibe_based_test_system_minted, test_transaction_without_budget) { + std::unordered_map> encryptors_; + for (size_t i = 0; i < clients.size(); i++) { + encryptors_[i] = std::make_shared(rsk.toPK().mpk); + } + for (size_t i = 0; i < clients.size(); i++) { + auto& issuer = clients[i]; + auto& receiver = clients[(i + 1) % clients.size()]; + // We let the client to cheat and create a non-budgeted transaction + d.budget_policy = false; + Transaction tx(d, + issuer, + {coins[issuer.getPid()].front()}, + std::nullopt, + {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, + *(encryptors_.at((i + 1) % clients.size()))); + + ASSERT_ANY_THROW(Transaction(d, + issuer, + {coins[issuer.getPid()].front()}, + {bcoins[issuer.getPid()].front()}, + {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, + *(encryptors_.at((i + 1) % clients.size())))); + + d.budget_policy = true; + for (auto& b : banks) { + ASSERT_FALSE(b->validate(d, tx)); + } + + Transaction tx2(d, + issuer, + {coins[issuer.getPid()].front()}, + {bcoins[issuer.getPid()].front()}, + {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, + *(encryptors_.at((i + 1) % clients.size()))); + + d.budget_policy = false; + for (auto& b : banks) { + ASSERT_FALSE(b->validate(d, tx2)); + } + } +} + +TEST_F(ibe_based_test_system_minted_budget_policy_disabled, test_transaction) { + std::unordered_map> encryptors_; + for (size_t i = 0; i < clients.size(); i++) { + encryptors_[i] = std::make_shared(rsk.toPK().mpk); + } + for (size_t i = 0; i < clients.size(); i++) { + auto& issuer = clients[i]; + auto& receiver = clients[(i + 1) % clients.size()]; + Transaction tx(d, + issuer, + {coins[issuer.getPid()].front()}, + std::nullopt, + {{issuer.getPid(), 50}, {receiver.getPid(), 50}}, + *(encryptors_.at((i + 1) % clients.size()))); + coins[issuer.getPid()].erase(coins[issuer.getPid()].begin()); + std::map> shares; + std::vector shares_signers; + for (size_t i = 0; i < banks.size(); i++) { + shares[i] = banks[i]->sign(tx); + shares_signers.push_back(banks[i]->getId()); + } + size_t num_coins = shares[0].size(); + for (size_t i = 0; i < num_coins; i++) { + std::vector share_sigs(n); + auto sbs = getSubset((uint32_t)n, (uint32_t)n); + for (auto s : sbs) { + share_sigs[s] = shares[s][i]; + } + for (auto& b : banks) { + for (size_t k = 0; k < share_sigs.size(); k++) { + auto& sig = share_sigs[k]; + auto& signer = shares_signers[k]; + ASSERT_TRUE(b->validatePartialSignature(signer, sig, i, tx)); + } + } + } + + std::vector sigs; + for (size_t i = 0; i < num_coins; i++) { + std::map share_sigs; + auto sbs = getSubset((uint32_t)n, (uint32_t)thresh); + for (auto s : sbs) { + share_sigs[s] = shares[s][i]; + } + auto blinded_sig = Utils::aggregateSigShares((uint32_t)n, {share_sigs}); + sigs.emplace_back(std::move(blinded_sig)); + } + auto issuer_coins = issuer.claimCoins(tx, d, sigs); + for (auto& coin : issuer_coins) { + ASSERT_TRUE(issuer.validate(coin)); + ASSERT_TRUE(coin.getType() == api::Coin::Type::Normal); + coins[issuer.getPid()].emplace_back(std::move(coin)); + } + auto receiver_coins = receiver.claimCoins(tx, d, sigs); + for (auto& coin : receiver_coins) { + ASSERT_TRUE(receiver.validate(coin)); + ASSERT_TRUE(coin.getType() == api::Coin::Type::Normal); + coins[receiver.getPid()].emplace_back(std::move(coin)); + } + } + + // At the end of these phaese, the budget of each client is 950 and it has two coins of 50 each + for (const auto& c : clients) { + ASSERT_TRUE(coins[c.getPid()].size() == 2); + for (const auto& coin : coins[c.getPid()]) ASSERT_TRUE(coin.getVal() == 50U); + } +} + +} // namespace + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/utt/tests/testUtils.hpp b/utt/tests/testUtils.hpp index 9df2241dcf..8261748ec8 100644 --- a/utt/tests/testUtils.hpp +++ b/utt/tests/testUtils.hpp @@ -22,132 +22,229 @@ #include #include #include + +#include "gtest/gtest.h" + using namespace libutt; using namespace libutt::api; namespace libutt::api::testing { -struct GpData { - libutt::CommKey cck; - libutt::CommKey rck; -}; -std::vector getSubset(uint32_t n, uint32_t size) { - std::srand((unsigned int)std::chrono::system_clock::now().time_since_epoch().count()); - std::map ret; - for (uint32_t i = 0; i < n; i++) ret[i] = i; - for (uint32_t i = 0; i < n - size; i++) { - uint32_t index = (uint32_t)(std::rand()) % (uint32_t)(ret.size()); - ret.erase(index); - } - std::vector rret; - for (const auto& [k, v] : ret) { - (void)k; - rret.push_back(v); - } - return rret; -} -std::tuple init(size_t n, size_t thresh) { - UTTParams::BaseLibsInitData base_libs_init_data; - UTTParams::initLibs(base_libs_init_data); - auto dkg = RandSigDKG(thresh, n, Params::NumMessages); - auto rc = RegAuthSK::generateKeyAndShares(thresh, n); - GpData gp_data{dkg.getCK(), rc.ck_reg}; - UTTParams d = UTTParams::create((void*)(&gp_data)); - rc.setIBEParams(d.getParams().ibe); - return {d, dkg, rc}; -} -std::vector> GenerateRegistrators(size_t n, const RegAuthSK& rsk) { - std::vector> registrators; - std::map validation_keys; - for (size_t i = 0; i < n; i++) { - auto& sk = rsk.shares[i]; - validation_keys[(uint16_t)i] = serialize(sk.toPK()); - } - for (size_t i = 0; i < n; i++) { - registrators.push_back(std::make_shared( - (uint16_t)i, serialize(rsk.shares[i]), validation_keys, serialize(rsk.toPK()))); - } - return registrators; -} +std::string privatek1 = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIICXQIBAAKBgQDAyUwjK+m4EXyFKEha2NfQUY8eIqurRujNMqI1z7B3bF/8Bdg0\n" + "wffIYJUjiXo13hB6yOq+gD62BGAPRjmgReBniT3d2rLU0Z72zQ64Gof66jCGQt0W\n" + "0sfwDUv0XsfXXW9p1EGBYwIkgW6UGiABnkZcIUW4dzP2URoRCN/VIypkRwIDAQAB\n" + "AoGAa3VIvSoTAoisscQ8YHcSBIoRjiihK71AsnAQvpHfuRFthxry4qVjqgs71i0h\n" + "M7lt0iL/xePSEL7rlFf+cvnAFL4/j1R04ImBjRzWGnaNE8I7nNGGzJo9rL5I1oi3\n" + "zN2yUucTSGm7qR0MCNVy26zNmCuS/FdBPsfdZ017OTsHtPECQQDlWXAJG6nHyw2o\n" + "2cLYHzlyrrYgnWJkgFSKzr7VFNlHxfQSWXJ4zuDwhqkm3d176bVm4eHhDDv6f413\n" + "iQGraKvTAkEA1zAzpxfI7LAqd3sObWYstQb03IXE7yddMgbhoMDCT3gXhNaHKfjT\n" + "Z/GIk49jh8kyitN2FeYXXi9TiwrXStfhPQJBAMNea6ymjvstwoYKcgsOli5WG7ku\n" + "uEkqdFoGAdObvfeA7gfPgE7e1AiwfVkpd+l9TVTFqFe/xzv8+fJQmEZ+lJcCQQDN\n" + "5I7nh7h1zzEy1Qk+345TP262OT/u26kuHqtv1j+VLgDC10jIfg443D+jgITo/Tdg\n" + "4WeRGHCva3TyCtNoBxq5AkA9KZpKof4ripad4oIuCJpR/ZhQATgQwR9f+FlAxgP0\n" + "ABmBPCoxy4uGMtSBMqiiGpImbDuivYkhlBl7D8u8vn26\n" + "-----END RSA PRIVATE KEY-----\n"; +std::string publick1 = + "-----BEGIN PUBLIC KEY-----\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAyUwjK+m4EXyFKEha2NfQUY8e\n" + "IqurRujNMqI1z7B3bF/8Bdg0wffIYJUjiXo13hB6yOq+gD62BGAPRjmgReBniT3d\n" + "2rLU0Z72zQ64Gof66jCGQt0W0sfwDUv0XsfXXW9p1EGBYwIkgW6UGiABnkZcIUW4\n" + "dzP2URoRCN/VIypkRwIDAQAB\n" + "-----END PUBLIC KEY-----\n"; -std::vector> GenerateCommitters(size_t n, const RandSigDKG& dkg, const RegAuthPK& rvk) { - std::vector> banks; - std::map share_verification_keys_; - for (size_t i = 0; i < n; i++) { - share_verification_keys_[(uint16_t)i] = serialize(dkg.skShares[i].toPK()); +std::string privatek2 = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIICXAIBAAKBgQDMw+qeJXQ/ZxrqSqjRXoN2wFYNwhljh7RTBJuIzaqK2pDL+zaK\n" + "aREzgufG/7r2uk8njWW7EJbriLcmj1oJA913LPl4YWUEORKl7Cb6wLoPO/E5gAt1\n" + "JJ0dhDeCRU+E7vtNQ4pLyy2dYS2GHO1Tm88yuFegS3gkINPYgzNggGJ2cQIDAQAB\n" + "AoGAScyCjqTpFMDQTojB91OdBfukCClghSKvtwv+EnwtbwX/EcVkjtX3QR1485vP\n" + "goT7akHn3FfKTPFlMRyRUpZ2Bov1whQ1ztuboIonFQ7ohbDTLE3QzUv4L3e8cEbY\n" + "51MSe8tEUVRxKu53nD3asWxAi/CEyqWvRCzf4s3Q6Xw3h5ECQQD8mBT6ervLr1Qk\n" + "oKmaAuPTDyZDaSjipD0/d1p1vG8Wb8go14tq89Ts+UIWzH5aGlidxTK9j/luQqlR\n" + "YVVGNkC3AkEAz4a8jtg2++fvWT0PDz6OBfw/iHJQzSmynlzKQzpRac02UBCPo4an\n" + "o7wl9uEnucXuVpCSo0JdSf+x4r9dwmCKFwJBAPWlGNG2xicBbPzp2cZTBSheVUG9\n" + "ZOtz+bRc5/YTuJzDPI6rf4QVeH60sNbnLAGIGaHlAsFi4Jmf7nWcCIftfuUCQEbx\n" + "hJxAhetvyn7zRKatd9fL99wpWD4Ktyk0B2EcGqDUqnCMeM4qRjzPIRtYtT/oziWB\n" + "nt943HNjmeguC1tbrVkCQBMd+kbpcoFHKKrC577FM24maWRTfXJeu2/o6pxUFIUY\n" + "kzkDZ2k+FfvXWaY+N5q5bJCayor8W1QeruHzewrQmgY=\n" + "-----END RSA PRIVATE KEY-----\n"; +std::string publick2 = + "-----BEGIN PUBLIC KEY-----\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMw+qeJXQ/ZxrqSqjRXoN2wFYN\n" + "whljh7RTBJuIzaqK2pDL+zaKaREzgufG/7r2uk8njWW7EJbriLcmj1oJA913LPl4\n" + "YWUEORKl7Cb6wLoPO/E5gAt1JJ0dhDeCRU+E7vtNQ4pLyy2dYS2GHO1Tm88yuFeg\n" + "S3gkINPYgzNggGJ2cQIDAQAB\n" + "-----END PUBLIC KEY-----\n"; + +std::string privatek3 = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIICXAIBAAKBgQCnmdqW7f/rg8CiUzLugxc0jPMqzphhtl40IqINAk3pasCQsA3T\n" + "eHSa1fcKocFMY9bfQRvqiKpnK74d0V0fujFUaPIMlKMLWXEVefwe1fOYrDXOoz7N\n" + "3Go7x9u/LXwCA6HehXtIavtTPQs1yHldb/bDocEhjfGvU3TLXkAuGWsnmQIDAQAB\n" + "AoGBAJ6fRHyYICB8f7Kh35BRTYMU64fWI+5GtX3OUWTSi36g5EOL/GnqlSF94+OS\n" + "F+n+i/ycGJmuYuhmQ/bgkaxXghsDYb7fsdMJ8DEqUJAKbxeOosn8fxwmJkNAJ07J\n" + "+oAg/xkJ+ukyYnPf0P3UTuTZl0EFEpwu/vnX09QJGtuXgmQhAkEA0c0Co9MdP62r\n" + "/ybXXeqgaol2YVGzFr/bMz3hhlviV9IOGPRZmeQ8v+f1/lSsqZ8wSP5P8dkBo4UB\n" + "NSLaHAUL/QJBAMyB72EyHZUEFy3o241myqamfVtN+Dzo6qdPn/PfF/BLjwsEApCO\n" + "oUObmDDo/yiSSb00XSnn23bGYH1VJJDNJs0CQE1aG+YQ+VC4FJkfVfpvfjOpePcK\n" + "q0/w7r2mzBbAm+QrMz1qIfsGVoue12itCXgElEXlVc5iZyNF75sKvYXlKnUCQHHC\n" + "tc5zelEyfVJkff0ieQhLBOCNdtErH50Chg+6wi5BWcje6i5PqRVasEZE1etTtQEy\n" + "58Av4b0ojPQrMLP76uECQEz0c1RPDwMvznwT3BJxl8t4tixPML0nyBMD8ttnZswG\n" + "K/1CYV1uMNbchmuVQb2Kd2JyE1gQF8s3ShsbteMc5og=\n" + "-----END RSA PRIVATE KEY-----\n"; + +std::string publick3 = + "-----BEGIN PUBLIC KEY-----\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnmdqW7f/rg8CiUzLugxc0jPMq\n" + "zphhtl40IqINAk3pasCQsA3TeHSa1fcKocFMY9bfQRvqiKpnK74d0V0fujFUaPIM\n" + "lKMLWXEVefwe1fOYrDXOoz7N3Go7x9u/LXwCA6HehXtIavtTPQs1yHldb/bDocEh\n" + "jfGvU3TLXkAuGWsnmQIDAQAB\n" + "-----END PUBLIC KEY-----\n"; + +std::vector pr_keys{privatek1, privatek2, privatek3}; +std::vector pkeys{publick1, publick2, publick3}; + +class test_utt_instance : public ::testing::Test { + public: + protected: + struct GpData { + libutt::CommKey cck; + libutt::CommKey rck; + bool budget_policy = true; + }; + void setUp(bool ibe, bool register_clients, bool budget_policy) { + UTTParams::BaseLibsInitData base_libs_init_data; + UTTParams::initLibs(base_libs_init_data); + auto dkg = RandSigDKG(thresh, n, Params::NumMessages); + rsk = RegAuthSK::generateKeyAndShares(thresh, n); + GpData gp_data{dkg.getCK(), rsk.ck_reg, budget_policy}; + d = UTTParams::create((void*)(&gp_data)); + rsk.setIBEParams(d.getParams().ibe); + rvk = rsk.toPK(); + bvk = dkg.getPK(); + + std::map validation_keys; + for (size_t i = 0; i < n; i++) { + auto& sk = rsk.shares[i]; + validation_keys[(uint16_t)i] = serialize(sk.toPK()); + } + for (size_t i = 0; i < n; i++) { + registrators.push_back(std::make_shared( + (uint16_t)i, serialize(rsk.shares[i]), validation_keys, serialize(rsk.toPK()))); + } + + std::map share_verification_keys_; + for (size_t i = 0; i < n; i++) { + share_verification_keys_[(uint16_t)i] = serialize(dkg.skShares[i].toPK()); + } + for (size_t i = 0; i < n; i++) { + banks.push_back(std::make_shared((uint16_t)i, + serialize(dkg.skShares[i]), + serialize(dkg.getPK()), + share_verification_keys_, + serialize(rvk))); + } + + if (ibe) { + GenerateIbeClients(); + } else { + GenerateRsaClients(pr_keys); + } + + if (register_clients) { + for (auto& c : clients) { + registerClient(c); + } + } } - for (size_t i = 0; i < n; i++) { - banks.push_back(std::make_shared((uint16_t)i, - serialize(dkg.skShares[i]), - serialize(dkg.getPK()), - share_verification_keys_, - serialize(rvk))); + std::vector getSubset(uint32_t n, uint32_t size) { + std::srand((unsigned int)std::chrono::system_clock::now().time_since_epoch().count()); + std::map ret; + for (uint32_t i = 0; i < n; i++) ret[i] = i; + for (uint32_t i = 0; i < n - size; i++) { + uint32_t index = (uint32_t)(std::rand()) % (uint32_t)(ret.size()); + ret.erase(index); + } + std::vector rret; + for (const auto& [k, v] : ret) { + (void)k; + rret.push_back(v); + } + return rret; } - return banks; -} -std::vector GenerateClients(size_t c, const RandSigPK& bvk, const RegAuthPK& rvk, const RegAuthSK& rsk) { - std::vector clients; - std::string bpk = libutt::serialize(bvk); - std::string rpk = libutt::serialize(rvk); - - for (size_t i = 0; i < c; i++) { - std::string pid = "client_" + std::to_string(i); - auto sk = rsk.msk.deriveEncSK(rsk.p, pid); - auto mpk = rsk.toPK().mpk; - std::string csk = libutt::serialize(sk); - std::string cmpk = libutt::serialize(mpk); - clients.push_back(Client(pid, bpk, rpk, csk, cmpk)); + void GenerateRsaClients(const std::vector& pk) { + c = pr_keys.size(); + std::vector clients; + std::string bpk = libutt::serialize(bvk); + std::string rpk = libutt::serialize(rvk); + + for (size_t i = 0; i < c; i++) { + std::string pid = "client_" + std::to_string(i); + clients.push_back(Client(pid, bpk, rpk, pk.at(i))); + } } - return clients; -} -std::vector GenerateClients(size_t c, - const RandSigPK& bvk, - const RegAuthPK& rvk, - const std::vector& pk) { - std::vector clients; - std::string bpk = libutt::serialize(bvk); - std::string rpk = libutt::serialize(rvk); + void GenerateIbeClients() { + std::string bpk = libutt::serialize(bvk); + std::string rpk = libutt::serialize(rvk); - for (size_t i = 0; i < c; i++) { - std::string pid = "client_" + std::to_string(i); - clients.push_back(Client(pid, bpk, rpk, pk.at(i))); - } - return clients; -} -void registerClient(const UTTParams& d, - Client& c, - std::vector>& registrators, - size_t thresh) { - size_t n = registrators.size(); - std::vector> shares; - std::vector shares_signers; - auto prf = c.getPRFSecretKey(); - Fr fr_s2 = Fr::random_element(); - types::CurvePoint s2; - auto rcm1 = c.generateInputRCM(); - for (auto& r : registrators) { - auto [ret_s2, sig] = r->signRCM(c.getPidHash(), fr_s2.to_words(), rcm1); - shares.push_back(sig); - shares_signers.push_back(r->getId()); - if (s2.empty()) { - s2 = ret_s2; + for (size_t i = 0; i < c; i++) { + std::string pid = "client_" + std::to_string(i); + auto sk = rsk.msk.deriveEncSK(rsk.p, pid); + auto mpk = rsk.toPK().mpk; + std::string csk = libutt::serialize(sk); + std::string cmpk = libutt::serialize(mpk); + clients.push_back(Client(pid, bpk, rpk, csk, cmpk)); } } - for (auto& r : registrators) { - for (size_t i = 0; i < shares.size(); i++) { - auto& sig = shares[i]; - auto& signer = shares_signers[i]; - assertTrue(r->validatePartialRCMSig(signer, c.getPidHash(), s2, rcm1, sig)); + + void registerClient(Client& c) { + size_t n = registrators.size(); + std::vector> shares; + std::vector shares_signers; + auto prf = c.getPRFSecretKey(); + Fr fr_s2 = Fr::random_element(); + types::CurvePoint s2; + auto rcm1 = c.generateInputRCM(); + for (auto& r : registrators) { + auto [ret_s2, sig] = r->signRCM(c.getPidHash(), fr_s2.to_words(), rcm1); + shares.push_back(sig); + shares_signers.push_back(r->getId()); + if (s2.empty()) { + s2 = ret_s2; + } } + for (auto& r : registrators) { + for (size_t i = 0; i < shares.size(); i++) { + auto& sig = shares[i]; + auto& signer = shares_signers[i]; + assertTrue(r->validatePartialRCMSig(signer, c.getPidHash(), s2, rcm1, sig)); + } + } + auto sids = getSubset((uint32_t)n, (uint32_t)thresh); + std::map> rsigs; + for (auto i : sids) { + rsigs[i] = shares[i]; + } + auto sig = Utils::aggregateSigShares((uint32_t)n, rsigs); + sig = + Utils::unblindSignature(d, Commitment::Type::REGISTRATION, {Fr::zero().to_words(), Fr::zero().to_words()}, sig); + c.setRCMSig(d, s2, sig); } - auto sids = getSubset((uint32_t)n, (uint32_t)thresh); - std::map> rsigs; - for (auto i : sids) { - rsigs[i] = shares[i]; - } - auto sig = Utils::aggregateSigShares((uint32_t)n, rsigs); - sig = Utils::unblindSignature(d, Commitment::Type::REGISTRATION, {Fr::zero().to_words(), Fr::zero().to_words()}, sig); - c.setRCMSig(d, s2, sig); -} + + public: + size_t thresh = 2; + size_t n = 4; + size_t c = 10; + libutt::RandSigPK bvk; + libutt::RegAuthPK rvk; + libutt::RegAuthSK rsk; + libutt::api::UTTParams d; + + std::vector> banks; + std::vector> registrators; + std::vector clients; +}; } // namespace libutt::api::testing \ No newline at end of file diff --git a/utt/utt-client-api/include/utt-client-api/User.hpp b/utt/utt-client-api/include/utt-client-api/User.hpp index e3a4b7ceae..189feb1d3f 100644 --- a/utt/utt-client-api/include/utt-client-api/User.hpp +++ b/utt/utt-client-api/include/utt-client-api/User.hpp @@ -46,6 +46,9 @@ class User { User(); // Default empty user object ~User(); + /// @brief Creates a privacy budget locally - used only for testing. + void createPrivacyBudgetLocal(const utt::Configuration& config, uint64_t amount); + /// @brief Creates an input registration commitment. Multiple calls generate the same object. /// @return The user's registration input object UserRegistrationInput getRegistrationInput() const; @@ -54,14 +57,12 @@ class User { /// @param pk The system sent public key for the registration (must be equal to the user's public key) /// @param rs A signature on the user's registration /// @param s2 A system generated part of the user's nullifier secret key - /// @return True if the registration is accepted by the user - bool updateRegistration(const std::string& pk, const utt::RegistrationSig& rs, const utt::S2& s2); + void updateRegistration(const std::string& pk, const utt::RegistrationSig& rs, const utt::S2& s2); /// @brief Updates the privacy budget of the user /// @param token The budget token object /// @param sig The budget token signature - /// @return True if the budget token is accepted - bool updatePrivacyBudget(const utt::PrivacyBudget& budget, const utt::PrivacyBudgetSig& sig); + void updatePrivacyBudget(const utt::PrivacyBudget& budget, const utt::PrivacyBudgetSig& sig); /** * @brief Get the total value of unspent UTT tokens @@ -92,25 +93,28 @@ class User { /// @param txNum The transaction number /// @param tx A transfer transaction /// @param sigs The signatures on the transaction outputs - /// @return - bool updateTransferTx(uint64_t txNum, const utt::Transaction& tx, const utt::TxOutputSigs& sigs); + void updateTransferTx(uint64_t txNum, const utt::Transaction& tx, const utt::TxOutputSigs& sigs); /// @brief Update the user's state with the effects of a mint transaction /// @param txNum The transaction number /// @param tx A mint transaction /// @param sig The signature on the transaction output (we assume a mint tx has a single output) - /// @return - bool updateMintTx(uint64_t txNum, const utt::Transaction& tx, const utt::TxOutputSig& sig); + void updateMintTx(uint64_t txNum, const utt::Transaction& tx, const utt::TxOutputSig& sig); /// @brief Update the user's state with the effects of a burn transaction /// @param txNum The transaction number /// @param tx A burn transaction - /// @return - bool updateBurnTx(uint64_t txNum, const utt::Transaction& tx); + void updateBurnTx(uint64_t txNum, const utt::Transaction& tx); /// @brief The user records the tx as a no-op and skips it /// @param txNum - bool updateNoOp(uint64_t txNum); + void updateNoOp(uint64_t txNum); + + /// @brief Creates a transaction to mint the requested amount + utt::Transaction mint(uint64_t amount) const; + + /// @brief Creates a transaction to mint the requested budget amount + utt::Transaction mintPrivacyBudget(uint64_t amount) const; /// @brief Ask to burn some amount of tokens. This function needs to be called repeatedly until the final burn /// transaction is produced. @@ -128,6 +132,8 @@ class User { /// to transfer the desired amount. TransferResult transfer(const std::string& userId, const std::string& pk, uint64_t amount) const; + void debugOutput() const; + private: // Users can be created only by the top-level ClientApi functions friend std::unique_ptr createUser(const std::string& userId, diff --git a/utt/utt-client-api/src/ClientApi.cpp b/utt/utt-client-api/src/ClientApi.cpp index f92b97995e..4384d4affd 100644 --- a/utt/utt-client-api/src/ClientApi.cpp +++ b/utt/utt-client-api/src/ClientApi.cpp @@ -59,7 +59,7 @@ Configuration generateConfig(const ConfigInputParams& inputParams) { const uint16_t n = (uint16_t)inputParams.validatorPublicKeys.size(); const uint16_t t = inputParams.threshold; - auto config = libutt::api::Configuration(n, t); + auto config = libutt::api::Configuration(n, t, inputParams.useBudget); // [TODO-UTT] Use the validator's public keys to encrypt the configuration secrets for each validator diff --git a/utt/utt-client-api/src/User.cpp b/utt/utt-client-api/src/User.cpp index 65044e6bab..74dea0745b 100644 --- a/utt/utt-client-api/src/User.cpp +++ b/utt/utt-client-api/src/User.cpp @@ -36,6 +36,37 @@ #include "utt-client-api/PickCoins.hpp" +using namespace libutt; + +#define logdbg_user logdbg << ((pImpl_->client_) ? pImpl_->client_->getPid() : "!!!uninitialized user!!!") << ' ' + +namespace { +std::string dbgPrintCoins(const std::vector& coins) { + if (coins.empty()) return "[empty]"; + std::stringstream ss; + ss << '['; + for (const auto& coin : coins) { + ss << "type:" << (coin.getType() == libutt::api::Coin::Type::Budget ? "budget" : "normal"); + ss << "|val:" << coin.getVal(); + ss << "|exp:" << coin.getExpDate(); + ss << "|null:" << coin.getNullifier() << ", "; + } + ss << ']'; + return ss.str(); +} + +std::string dbgPrintRecipients(const std::vector>& recips) { + if (recips.empty()) return "[empty]"; + std::stringstream ss; + ss << '['; + for (const auto& recip : recips) { + ss << std::get<0>(recip) << ": " << std::get<1>(recip) << ", "; + } + ss << ']'; + return ss.str(); +} +} // namespace + namespace utt::client { struct User::Impl { @@ -74,6 +105,8 @@ struct User::Impl { }; utt::Transaction User::Impl::createTx_Burn(const libutt::api::Coin& coin) { + logdbg << client_->getPid() << " creates burn tx with input coins: " << dbgPrintCoins({coin}) << endl; + utt::Transaction tx; tx.type_ = utt::Transaction::Type::Burn; auto burn = libutt::api::operations::Burn(params_, *client_, coin); @@ -86,11 +119,15 @@ utt::Transaction User::Impl::createTx_Self1to2(const libutt::api::Coin& coin, ui recip.emplace_back(client_->getPid(), amount); recip.emplace_back(client_->getPid(), coin.getVal() - amount); + logdbg << client_->getPid() << " creates self-split tx with input coins: " << dbgPrintCoins({coin}) + << " and recipients: " << dbgPrintRecipients(recip) << endl; + auto uttTx = libutt::api::operations::Transaction(params_, *client_, {coin}, std::nullopt, recip, *selfTxEncryptor_); utt::Transaction tx; tx.type_ = utt::Transaction::Type::Transfer; tx.data_ = libutt::api::serialize(uttTx); + tx.numOutputs_ = uttTx.getNumOfOutputCoins(); return tx; } @@ -98,11 +135,15 @@ utt::Transaction User::Impl::createTx_Self2to1(const std::vector> recip; recip.emplace_back(client_->getPid(), coins[0].getVal() + coins[1].getVal()); + logdbg << client_->getPid() << " creates self-merge with input coins: " << dbgPrintCoins(coins) + << " and recipients: " << dbgPrintRecipients(recip) << endl; + auto uttTx = libutt::api::operations::Transaction(params_, *client_, coins, std::nullopt, recip, *selfTxEncryptor_); utt::Transaction tx; tx.type_ = utt::Transaction::Type::Transfer; tx.data_ = libutt::api::serialize(uttTx); + tx.numOutputs_ = uttTx.getNumOfOutputCoins(); return tx; } @@ -111,11 +152,15 @@ utt::Transaction User::Impl::createTx_Self2to2(const std::vectorgetPid(), amount); recip.emplace_back(client_->getPid(), (coins[0].getVal() + coins[1].getVal()) - amount); + logdbg << client_->getPid() << " creates 2-to-2 self-split with input coins: " << dbgPrintCoins(coins) + << " and recipients: " << dbgPrintRecipients(recip) << endl; + auto uttTx = libutt::api::operations::Transaction(params_, *client_, coins, std::nullopt, recip, *selfTxEncryptor_); utt::Transaction tx; tx.type_ = utt::Transaction::Type::Transfer; tx.data_ = libutt::api::serialize(uttTx); + tx.numOutputs_ = uttTx.getNumOfOutputCoins(); return tx; } @@ -125,12 +170,17 @@ utt::Transaction User::Impl::createTx_1to1(const libutt::api::Coin& coin, std::vector> recip; recip.emplace_back(userId, coin.getVal()); + logdbg << client_->getPid() << " creates 1-to-1 transfer with input coins: " << dbgPrintCoins({coin}) + << " and recipients: " << dbgPrintRecipients(recip) << endl; + auto encryptor = createRsaEncryptorForTransferToOther(userId, pk); - auto uttTx = libutt::api::operations::Transaction(params_, *client_, {coin}, budgetCoin_, recip, encryptor); + auto uttTx = libutt::api::operations::Transaction( + params_, *client_, {coin}, params_.getBudgetPolicy() ? budgetCoin_ : std::nullopt, recip, encryptor); utt::Transaction tx; tx.type_ = utt::Transaction::Type::Transfer; tx.data_ = libutt::api::serialize(uttTx); + tx.numOutputs_ = uttTx.getNumOfOutputCoins(); return tx; } @@ -142,12 +192,17 @@ utt::Transaction User::Impl::createTx_1to2(const libutt::api::Coin& coin, recip.emplace_back(userId, amount); recip.emplace_back(client_->getPid(), coin.getVal() - amount); + logdbg << client_->getPid() << " creates 1-to-2 transfer with input coins: " << dbgPrintCoins({coin}) + << " and recipients: " << dbgPrintRecipients(recip) << endl; + auto encryptor = createRsaEncryptorForTransferToOther(userId, pk); - auto uttTx = libutt::api::operations::Transaction(params_, *client_, {coin}, budgetCoin_, recip, encryptor); + auto uttTx = libutt::api::operations::Transaction( + params_, *client_, {coin}, params_.getBudgetPolicy() ? budgetCoin_ : std::nullopt, recip, encryptor); utt::Transaction tx; tx.type_ = utt::Transaction::Type::Transfer; tx.data_ = libutt::api::serialize(uttTx); + tx.numOutputs_ = uttTx.getNumOfOutputCoins(); return tx; } @@ -157,12 +212,17 @@ utt::Transaction User::Impl::createTx_2to1(const std::vector& std::vector> recip; recip.emplace_back(userId, coins[0].getVal() + coins[1].getVal()); + logdbg << client_->getPid() << "creates 2-to-1 transfer with input coins: " << dbgPrintCoins(coins) + << " and recipients: " << dbgPrintRecipients(recip) << endl; + auto encryptor = createRsaEncryptorForTransferToOther(userId, pk); - auto uttTx = libutt::api::operations::Transaction(params_, *client_, coins, budgetCoin_, recip, encryptor); + auto uttTx = libutt::api::operations::Transaction( + params_, *client_, coins, params_.getBudgetPolicy() ? budgetCoin_ : std::nullopt, recip, encryptor); utt::Transaction tx; tx.type_ = utt::Transaction::Type::Transfer; tx.data_ = libutt::api::serialize(uttTx); + tx.numOutputs_ = uttTx.getNumOfOutputCoins(); return tx; } @@ -174,12 +234,17 @@ utt::Transaction User::Impl::createTx_2to2(const std::vector& recip.emplace_back(userId, amount); recip.emplace_back(client_->getPid(), (coins[0].getVal() + coins[1].getVal()) - amount); + logdbg << client_->getPid() << " creates 2-to-2 transfer with input coins: " << dbgPrintCoins(coins) + << " and recipients: " << dbgPrintRecipients(recip) << endl; + auto encryptor = createRsaEncryptorForTransferToOther(userId, pk); - auto uttTx = libutt::api::operations::Transaction(params_, *client_, coins, budgetCoin_, recip, encryptor); + auto uttTx = libutt::api::operations::Transaction( + params_, *client_, coins, params_.getBudgetPolicy() ? budgetCoin_ : std::nullopt, recip, encryptor); utt::Transaction tx; tx.type_ = utt::Transaction::Type::Transfer; tx.data_ = libutt::api::serialize(uttTx); + tx.numOutputs_ = uttTx.getNumOfOutputCoins(); return tx; } @@ -239,40 +304,39 @@ UserRegistrationInput User::getRegistrationInput() const { return libutt::api::serialize(pImpl_->client_->generateInputRCM()); } -bool User::updateRegistration(const std::string& pk, const RegistrationSig& rs, const S2& s2) { - if (!pImpl_->client_) return false; - if (!(pImpl_->pk_ == pk)) return false; // Expect a matching public key - if (rs.empty() || s2.empty()) return false; +void User::updateRegistration(const std::string& pk, const RegistrationSig& rs, const S2& s2) { + if (!pImpl_->client_) throw std::runtime_error("User not initialized!"); + if (rs.empty() || s2.empty() || pk.empty()) throw std::runtime_error("updateRegistration: invalid arguments!"); + if (!(pImpl_->pk_ == pk)) throw std::runtime_error("Public key mismatch!"); // Un-blind the signature std::vector randomness = {libutt::Fr::zero().to_words(), libutt::Fr::zero().to_words()}; auto unblindedSig = libutt::api::Utils::unblindSignature(pImpl_->params_, libutt::api::Commitment::REGISTRATION, randomness, rs); - if (unblindedSig.empty()) return false; + if (unblindedSig.empty()) throw std::runtime_error("Failed to unblind reg signature!"); // [TODO-UTT] What if we already updated a registration? How do we check it? pImpl_->client_->setRCMSig(pImpl_->params_, s2, unblindedSig); - - return true; } -bool User::updatePrivacyBudget(const PrivacyBudget& budget, const PrivacyBudgetSig& sig) { - if (!pImpl_->client_) return false; +void User::updatePrivacyBudget(const PrivacyBudget& budget, const PrivacyBudgetSig& sig) { + if (!pImpl_->client_) throw std::runtime_error("User not initialized!"); + + logdbg_user << "updating privacy budget" << endl; auto claimedCoins = pImpl_->client_->claimCoins(libutt::api::deserialize(budget), pImpl_->params_, std::vector{sig}); // Expect a single budget token to be claimed by the user - if (claimedCoins.size() != 1) return false; - + if (claimedCoins.size() != 1) throw std::runtime_error("Expected single budget token!"); if (!pImpl_->client_->validate(claimedCoins[0])) throw std::runtime_error("Invalid initial budget coin!"); // [TODO-UTT] Requires atomic, durable write through IUserStorage pImpl_->budgetCoin_ = claimedCoins[0]; - return true; + logdbg_user << "claimed budget token " << dbgPrintCoins({*pImpl_->budgetCoin_}) << endl; } uint64_t User::getBalance() const { @@ -294,12 +358,15 @@ const std::string& User::getPK() const { return pImpl_->pk_; } uint64_t User::getLastExecutedTxNum() const { return pImpl_->lastExecutedTxNum_; } -bool User::updateTransferTx(uint64_t txNum, const Transaction& tx, const TxOutputSigs& sigs) { - if (tx.type_ != Transaction::Type::Transfer) return false; - if (txNum != pImpl_->lastExecutedTxNum_ + 1) return false; +void User::updateTransferTx(uint64_t txNum, const Transaction& tx, const TxOutputSigs& sigs) { + if (!pImpl_->client_) throw std::runtime_error("User not initialized!"); + if (tx.type_ != Transaction::Type::Transfer) throw std::runtime_error("Transfer tx type mismatch!"); + if (txNum != pImpl_->lastExecutedTxNum_ + 1) throw std::runtime_error("Transfer tx number is not consecutive!"); auto uttTx = libutt::api::deserialize(tx.data_); + logdbg_user << "executing transfer tx: " << txNum << endl; + // [TODO-UTT] Requires atomic, durable write batch through IUserStorage pImpl_->lastExecutedTxNum_ = txNum; @@ -312,6 +379,7 @@ bool User::updateTransferTx(uint64_t txNum, const Transaction& tx, const TxOutpu return coin.getNullifier() == null; }); if (it != pImpl_->coins_.end()) { + logdbg_user << "slashing spent coin " << dbgPrintCoins({*it}) << endl; pImpl_->coins_.erase(it); } } @@ -321,68 +389,153 @@ bool User::updateTransferTx(uint64_t txNum, const Transaction& tx, const TxOutpu for (auto& coin : claimedCoins) { if (coin.getType() == libutt::api::Coin::Type::Normal) { if (!pImpl_->client_->validate(coin)) throw std::runtime_error("Invalid normal coin in transfer!"); + logdbg_user << "claimed normal coin: " << dbgPrintCoins({coin}) << endl; pImpl_->coins_.emplace_back(std::move(coin)); } else if (coin.getType() == libutt::api::Coin::Type::Budget) { // Replace budget coin if (!pImpl_->client_->validate(coin)) throw std::runtime_error("Invalid budget coin in transfer!"); if (coin.getVal() > 0) { + logdbg_user << "claimed budget coin: " << dbgPrintCoins({coin}) << endl; pImpl_->budgetCoin_ = std::move(coin); } else { pImpl_->budgetCoin_ = std::nullopt; } } } - - return true; } -bool User::updateMintTx(uint64_t txNum, const Transaction& tx, const TxOutputSig& sig) { - if (tx.type_ != Transaction::Type::Mint) return false; - if (txNum != pImpl_->lastExecutedTxNum_ + 1) return false; - pImpl_->lastExecutedTxNum_ = txNum; +void User::updateMintTx(uint64_t txNum, const Transaction& tx, const TxOutputSig& sig) { + if (!pImpl_->client_) throw std::runtime_error("User not initialized!"); + if (tx.type_ != Transaction::Type::Mint) throw std::runtime_error("Mint tx type mismatch!"); + if (txNum != pImpl_->lastExecutedTxNum_ + 1) throw std::runtime_error("Mint tx number is not consecutive!"); auto mint = libutt::api::deserialize(tx.data_); - auto claimedCoins = - pImpl_->client_->claimCoins(mint, pImpl_->params_, std::vector{sig}); + logdbg_user << "executing mint tx: " << txNum << endl; - // Expect a single token to be claimed by the user - if (claimedCoins.size() != 1) return false; + if (mint.getRecipentID() != pImpl_->client_->getPid()) { + logdbg_user << "ignores mint transaction for different user: " << mint.getRecipentID() << endl; + } else { + auto claimedCoins = + pImpl_->client_->claimCoins(mint, pImpl_->params_, std::vector{sig}); - if (!pImpl_->client_->validate(claimedCoins[0])) throw std::runtime_error("Invalid minted coin!"); + // Expect a single token to be claimed by the user + if (claimedCoins.size() != 1) throw std::runtime_error("Expected single coin in mint tx!"); + if (!pImpl_->client_->validate(claimedCoins[0])) throw std::runtime_error("Invalid minted coin!"); + + pImpl_->coins_.emplace_back(std::move(claimedCoins[0])); + } // [TODO-UTT] Requires atomic, durable write batch through IUserStorage pImpl_->lastExecutedTxNum_ = txNum; - pImpl_->coins_.emplace_back(std::move(claimedCoins[0])); - - return true; } -// [TODO-UTT] Do we actually need the whole BurnTx or we can simply use the nullifer to slash? -bool User::updateBurnTx(uint64_t txNum, const Transaction& tx) { - if (tx.type_ != Transaction::Type::Burn) return false; - if (txNum != pImpl_->lastExecutedTxNum_ + 1) return false; +// [TODO-UTT] Do we actually need the whole BurnTx or we can simply use the nullifier to slash? +void User::updateBurnTx(uint64_t txNum, const Transaction& tx) { + if (!pImpl_->client_) throw std::runtime_error("User not initialized!"); + if (tx.type_ != Transaction::Type::Burn) throw std::runtime_error("Burn tx type mismatch!"); + if (txNum != pImpl_->lastExecutedTxNum_ + 1) throw std::runtime_error("Burn tx number is not consecutive!"); auto burn = libutt::api::deserialize(tx.data_); - auto nullifier = burn.getNullifier(); - if (nullifier.empty()) return false; - auto it = std::find_if(pImpl_->coins_.begin(), pImpl_->coins_.end(), [&nullifier](const libutt::api::Coin& coin) { - return coin.getNullifier() == nullifier; - }); - if (it == pImpl_->coins_.end()) return false; + logdbg_user << "executing burn tx: " << txNum << endl; + + if (burn.getOwnerPid() != pImpl_->client_->getPid()) { + logdbg_user << "ignores burn tx for different user: " << burn.getOwnerPid() << endl; + } else { + auto nullifier = burn.getNullifier(); + if (nullifier.empty()) throw std::runtime_error("Burn tx has empty nullifier!"); + + auto it = std::find_if(pImpl_->coins_.begin(), pImpl_->coins_.end(), [&nullifier](const libutt::api::Coin& coin) { + return coin.getNullifier() == nullifier; + }); + if (it == pImpl_->coins_.end()) throw std::runtime_error("Burned token missing in wallet!"); + pImpl_->coins_.erase(it); + } // [TODO-UTT] Requires atomic, durable write batch through IUserStorage pImpl_->lastExecutedTxNum_ = txNum; - pImpl_->coins_.erase(it); - - return true; } -bool User::updateNoOp(uint64_t txNum) { - if (txNum != pImpl_->lastExecutedTxNum_ + 1) return false; +void User::updateNoOp(uint64_t txNum) { + if (!pImpl_->client_) throw std::runtime_error("User not initialized!"); + if (txNum != pImpl_->lastExecutedTxNum_ + 1) throw std::runtime_error("Noop tx number is not consecutive!"); + + logdbg_user << "executing noop tx: " << txNum << endl; + pImpl_->lastExecutedTxNum_ = txNum; - return true; +} + +utt::Transaction User::mint(uint64_t amount) const { + std::stringstream ss; + ss << Fr::random_element(); + auto randomHash = ss.str(); + + logdbg_user << "creating a mint tx with random hash: " << randomHash << endl; + + auto mint = libutt::api::operations::Mint(randomHash, amount, pImpl_->client_->getPid()); + + utt::Transaction tx; + tx.type_ = utt::Transaction::Type::Mint; + tx.data_ = libutt::api::serialize(mint); + + return tx; +} + +void User::createPrivacyBudgetLocal(const utt::Configuration& config, uint64_t amount) { + if (config.empty()) throw std::runtime_error("Privacy app config cannot be empty!"); + if (amount == 0) throw std::runtime_error("Positive privacy budget amount required!"); + + logdbg_user << "creates " << amount << " privacy budget locally" << endl; + + auto uttConfig = libutt::api::deserialize(config); + + // Create coins signers from config + auto commitVerificationKey = uttConfig.getPublicConfig().getCommitVerificationKey(); + auto registrationVerificationKey = uttConfig.getPublicConfig().getRegistrationVerificationKey(); + std::map commitVerificationKeyShares; + std::map registrationVerificationKeyShares; + std::vector coinsSigners; + + for (uint16_t i = 0; i < uttConfig.getNumValidators(); ++i) { + commitVerificationKeyShares.emplace(i, uttConfig.getCommitVerificationKeyShare(i)); + registrationVerificationKeyShares.emplace(i, uttConfig.getRegistrationVerificationKeyShare(i)); + } + + // Create coins signers + for (uint16_t i = 0; i < uttConfig.getNumValidators(); ++i) { + coinsSigners.emplace_back(i, + uttConfig.getCommitSecret(i), + commitVerificationKey, + commitVerificationKeyShares, + registrationVerificationKey); + } + + // Create budget + auto budget = libutt::api::operations::Budget(pImpl_->params_, *pImpl_->client_, amount, 0 /*expirationDate*/); + + // Sign budget + const uint16_t n = uttConfig.getNumValidators(); + const uint16_t t = uttConfig.getThreshold(); + + // We need only 'threshold' signers to sign + std::map> shareSubset; + for (size_t idx = 0; idx < t; ++idx) { + shareSubset.emplace((uint32_t)idx, coinsSigners[idx].sign(budget).front()); + } + + auto sig = libutt::api::Utils::aggregateSigShares(n, shareSubset); + + // Claim and update + auto claimedCoins = + pImpl_->client_->claimCoins(budget, pImpl_->params_, std::vector{sig}); + + // Expect a single budget token to be claimed by the user + if (claimedCoins.size() != 1) throw std::runtime_error("Expected single budget token!"); + if (!pImpl_->client_->validate(claimedCoins[0])) throw std::runtime_error("Invalid local budget coin!"); + + // [TODO-UTT] Requires atomic, durable write through IUserStorage + pImpl_->budgetCoin_ = claimedCoins[0]; } BurnResult User::burn(uint64_t amount) const { @@ -418,9 +571,10 @@ TransferResult User::transfer(const std::string& userId, const std::string& dest const uint64_t balance = getBalance(); if (balance < amount) throw std::runtime_error("User has insufficient balance!"); - const size_t budget = getPrivacyBudget(); - if (budget < amount) throw std::runtime_error("User has insufficient privacy budget!"); - + if (pImpl_->params_.getBudgetPolicy()) { + const size_t budget = getPrivacyBudget(); + if (budget < amount) throw std::runtime_error("User has insufficient privacy budget!"); + } auto pickedCoins = PickCoinsPreferExactMatch(pImpl_->coins_, amount); if (pickedCoins.empty()) throw std::runtime_error("Coin strategy didn't pick any coins!"); @@ -441,4 +595,31 @@ TransferResult User::transfer(const std::string& userId, const std::string& dest } } +void User::debugOutput() const { + std::cout << "------ USER DEBUG OUTPUT START -------------\n"; + if (!pImpl_->client_) { + std::cout << "User's libutt::api::client object not initialized!\n"; + } + std::cout << "lastExecutedTxNum:" << pImpl_->lastExecutedTxNum_ << '\n'; + std::cout << "coins: [\n"; + + auto dbgOutputCoin = [](const libutt::api::Coin& coin) { + std::cout << "type: " << (coin.getType() == libutt::api::Coin::Type::Budget ? "Budget" : "Normal") << ' '; + std::cout << "value: " << coin.getVal() << ' '; + std::cout << "expire: " << coin.getExpDate() << ' '; + std::cout << "null: " << coin.getNullifier() << ' '; + std::cout << "hasSig: " << coin.hasSig() << ' '; + std::cout << '\n'; + }; + + for (const auto& coin : pImpl_->coins_) { + dbgOutputCoin(coin); + } + std::cout << "]\n"; + std::cout << "budget coin: [\n"; + if (pImpl_->budgetCoin_) dbgOutputCoin(*pImpl_->budgetCoin_); + std::cout << "]\n"; + std::cout << "------ USER DEBUG OUTPUT END -------------\n"; +} + } // namespace utt::client \ No newline at end of file diff --git a/utt/utt-client-api/tests/TestUTTClientApi.cpp b/utt/utt-client-api/tests/TestUTTClientApi.cpp index cd13de36aa..dfceb4b66b 100644 --- a/utt/utt-client-api/tests/TestUTTClientApi.cpp +++ b/utt/utt-client-api/tests/TestUTTClientApi.cpp @@ -51,8 +51,6 @@ struct PrivacyBudgetResponse { struct ExecutedTx { utt::Transaction tx_; utt::TxOutputSigs sigs_; - std::string publicUserId_; // Burns and mints are public transactions that expose the user id, we save it here for - // convenience }; struct ServerMock { @@ -169,14 +167,15 @@ struct ServerMock { return resp; } - uint64_t mint(const std::string& userId, uint64_t amount) { + uint64_t mint(const std::string& userId, uint64_t amount, const utt::Transaction& tx) { assertFalse(userId.empty()); assertTrue(amount > 0); + assertTrue(tx.type_ == utt::Transaction::Type::Mint); + assertTrue(!tx.data_.empty()); - auto pidHash = libutt::api::Utils::curvePointFromHash(userId); - auto uniqueHash = "mint|" + std::to_string(++lastTokenId_); - - auto mintTx = libutt::api::operations::Mint(uniqueHash, amount, userId); + auto mintTx = libutt::api::deserialize(tx.data_); + assertTrue(mintTx.getRecipentID() == userId); + assertTrue(mintTx.getVal() == amount); std::vector> shares; for (const auto& signer : coinsSigners_) { @@ -193,19 +192,22 @@ struct ServerMock { } ExecutedTx executedTx; - executedTx.tx_.type_ = utt::Transaction::Type::Mint; - executedTx.tx_.data_ = libutt::api::serialize(mintTx); + executedTx.tx_ = tx; executedTx.sigs_.emplace_back(libutt::api::Utils::aggregateSigShares(n, shareSubset)); - executedTx.publicUserId_ = userId; ledger_.emplace_back(std::move(executedTx)); return ledger_.size(); } - uint64_t burn(const std::string& userId, const utt::Transaction& tx) { + uint64_t burn(const utt::Transaction& tx) { assertTrue(tx.type_ == utt::Transaction::Type::Burn); auto burn = libutt::api::deserialize(tx.data_); + + for (const auto& signer : coinsSigners_) { + assertTrue(signer.validate(config_->getPublicConfig().getParams(), burn)); + } + auto null = burn.getNullifier(); assertTrue(nullifiers_.count(null) == 0); nullifiers_.emplace(std::move(null)); @@ -213,7 +215,6 @@ struct ServerMock { ExecutedTx executedTx; executedTx.tx_.type_ = utt::Transaction::Type::Burn; executedTx.tx_.data_ = libutt::api::serialize(burn); - executedTx.publicUserId_ = userId; ledger_.emplace_back(std::move(executedTx)); return ledger_.size(); @@ -223,6 +224,11 @@ struct ServerMock { assertTrue(tx.type_ == utt::Transaction::Type::Transfer); auto uttTx = libutt::api::deserialize(tx.data_); + + for (const auto& signer : coinsSigners_) { + assertTrue(signer.validate(config_->getPublicConfig().getParams(), uttTx)); + } + for (auto&& null : uttTx.getNullifiers()) { assertTrue(nullifiers_.count(null) == 0); nullifiers_.emplace(std::move(null)); @@ -259,10 +265,7 @@ struct ServerMock { } }; -int main(int argc, char* argv[]) { - (void)argc; - (void)argv; - +void testCaseWithBudgetEnforced() { // Note that this test assumes the client and server-side parts of the code work under the same initialization of // libutt. utt::client::Initialize(); @@ -296,29 +299,16 @@ int main(int argc, char* argv[]) { const auto& executedTx = serverMock.getExecutedTx(txNum); switch (executedTx.tx_.type_) { case utt::Transaction::Type::Mint: { - if (executedTx.publicUserId_ == users[i]->getUserId()) { - assertTrue(executedTx.sigs_.size() == 1); - auto result = users[i]->updateMintTx(txNum, executedTx.tx_, executedTx.sigs_.front()); - assertTrue(result); - } else { - auto result = users[i]->updateNoOp(txNum); - assertTrue(result); - } + assertTrue(executedTx.sigs_.size() == 1); + users[i]->updateMintTx(txNum, executedTx.tx_, executedTx.sigs_.front()); } break; case utt::Transaction::Type::Burn: { - if (executedTx.publicUserId_ == users[i]->getUserId()) { - assertTrue(executedTx.sigs_.empty()); - auto result = users[i]->updateBurnTx(txNum, executedTx.tx_); - assertTrue(result); - } else { - auto result = users[i]->updateNoOp(txNum); - assertTrue(result); - } + assertTrue(executedTx.sigs_.empty()); + users[i]->updateBurnTx(txNum, executedTx.tx_); } break; case utt::Transaction::Type::Transfer: { assertFalse(executedTx.sigs_.empty()); - auto result = users[i]->updateTransferTx(txNum, executedTx.tx_, executedTx.sigs_); - assertTrue(result); + users[i]->updateTransferTx(txNum, executedTx.tx_, executedTx.sigs_); } break; default: assertFail("Unknown tx type!"); @@ -347,8 +337,7 @@ int main(int argc, char* argv[]) { assertFalse(resp.sig.empty()); // Note: the user's pk is usually recorded by the system and returned as part of the registration - auto result = users[i]->updateRegistration(users[i]->getPK(), resp.sig, resp.s2); - assertTrue(result); + users[i]->updateRegistration(users[i]->getPK(), resp.sig, resp.s2); } // Create budgets @@ -358,8 +347,7 @@ int main(int argc, char* argv[]) { assertFalse(resp.budget.empty()); assertFalse(resp.sig.empty()); - auto result = users[i]->updatePrivacyBudget(resp.budget, resp.sig); - assertTrue(result); + users[i]->updatePrivacyBudget(resp.budget, resp.sig); assertTrue(users[i]->getPrivacyBudget() == initialBudget[i]); } @@ -367,7 +355,8 @@ int main(int argc, char* argv[]) { { loginfo << "Minting tokens" << endl; for (size_t i = 0; i < C; ++i) { - auto txNum = serverMock.mint(users[i]->getUserId(), initialBalance[i]); + auto tx = users[i]->mint(initialBalance[i]); + auto txNum = serverMock.mint(users[i]->getUserId(), initialBalance[i], tx); assertTrue(txNum == serverMock.getLastExecutedTxNum()); } @@ -412,7 +401,146 @@ int main(int argc, char* argv[]) { auto result = users[i]->burn(balance); if (result.requiredTx_.type_ == utt::Transaction::Type::Burn) { assertTrue(result.isFinal_); - auto txNum = serverMock.burn(users[i]->getUserId(), result.requiredTx_); + auto txNum = serverMock.burn(result.requiredTx_); + assertTrue(txNum == serverMock.getLastExecutedTxNum()); + syncUsersWithServer(); + break; // We can stop processing after burning the coin + } else if (result.requiredTx_.type_ == utt::Transaction::Type::Transfer) { + assertFalse(result.isFinal_); + // We need to process a self transaction (split/merge) + auto txNum = serverMock.transfer(result.requiredTx_); + assertTrue(txNum == serverMock.getLastExecutedTxNum()); + syncUsersWithServer(); + } + } + } + + for (size_t i = 0; i < C; ++i) { + assertTrue(users[i]->getBalance() == 0); + } +} + +void testCaseWithoutBudgetEnforced() { + // Note that this test assumes the client and server-side parts of the code work under the same initialization of + // libutt. + utt::client::Initialize(); + + utt::client::ConfigInputParams cfgInputParams; + + // Create a UTT system tolerating F faulty validators + const uint16_t F = 1; + cfgInputParams.validatorPublicKeys = std::vector(3 * F + 1, "placeholderForPublicKey"); + cfgInputParams.threshold = F + 1; + cfgInputParams.useBudget = false; + // Create a new UTT instance config + auto config = utt::client::generateConfig(cfgInputParams); + + // Create a valid server-side mock based on the config + auto serverMock = ServerMock::createFromConfig(config); + + // Create new users by using the public config + utt::client::TestUserPKInfrastructure pki; + auto testUserIds = pki.getUserIds(); + const size_t C = testUserIds.size(); + loginfo << "Test users: " << C << '\n'; + assertTrue(C >= 3); // At least 3 test users expected + + std::vector> users; + + auto syncUsersWithServer = [&]() { + loginfo << "Synchronizing users with server" << endl; + for (size_t i = 0; i < C; ++i) { + for (uint64_t txNum = users[i]->getLastExecutedTxNum() + 1; txNum <= serverMock.getLastExecutedTxNum(); ++txNum) { + const auto& executedTx = serverMock.getExecutedTx(txNum); + switch (executedTx.tx_.type_) { + case utt::Transaction::Type::Mint: { + assertTrue(executedTx.sigs_.size() == 1); + users[i]->updateMintTx(txNum, executedTx.tx_, executedTx.sigs_.front()); + } break; + case utt::Transaction::Type::Burn: { + assertTrue(executedTx.sigs_.empty()); + users[i]->updateBurnTx(txNum, executedTx.tx_); + } break; + case utt::Transaction::Type::Transfer: { + assertFalse(executedTx.sigs_.empty()); + users[i]->updateTransferTx(txNum, executedTx.tx_, executedTx.sigs_); + } break; + default: + assertFail("Unknown tx type!"); + } + } + } + }; + + // Create new users by using the public config + utt::client::IUserStorage storage; + auto publicConfig = libutt::api::serialize(serverMock.config_->getPublicConfig()); + std::vector initialBalance; + + for (size_t i = 0; i < C; ++i) { + users.emplace_back(utt::client::createUser(testUserIds[i], publicConfig, pki, storage)); + initialBalance.emplace_back((i + 1) * 100); + } + + // Register users + loginfo << "Registering users" << endl; + for (size_t i = 0; i < C; ++i) { + auto resp = serverMock.registerUser(users[i]->getUserId(), users[i]->getRegistrationInput()); + assertFalse(resp.s2.empty()); + assertFalse(resp.sig.empty()); + + // Note: the user's pk is usually recorded by the system and returned as part of the registration + users[i]->updateRegistration(users[i]->getPK(), resp.sig, resp.s2); + } + + // Mint test + { + loginfo << "Minting tokens" << endl; + for (size_t i = 0; i < C; ++i) { + auto tx = users[i]->mint(initialBalance[i]); + auto txNum = serverMock.mint(users[i]->getUserId(), initialBalance[i], tx); + assertTrue(txNum == serverMock.getLastExecutedTxNum()); + } + + syncUsersWithServer(); + + for (size_t i = 0; i < C; ++i) { + assertTrue(users[i]->getBalance() == initialBalance[i]); + } + } + + // Each user sends to the next one (wrapping around to the first) some amount + { + const uint64_t amount = 50; + for (size_t i = 0; i < C; ++i) { + size_t nextUserIdx = (i + 1) % C; + std::string nextUserId = "user-" + std::to_string(nextUserIdx + 1); + loginfo << "Sending " << amount << " from " << users[i]->getUserId() << " to " << nextUserId << endl; + assertTrue(amount <= users[i]->getBalance()); + auto result = users[i]->transfer(nextUserId, pki.getPublicKey(nextUserId), amount); + assertTrue(result.isFinal_); + auto txNum = serverMock.transfer(result.requiredTx_); + assertTrue(txNum == serverMock.getLastExecutedTxNum()); + } + syncUsersWithServer(); + + for (size_t i = 0; i < C; ++i) { + assertTrue(users[i]->getBalance() == + initialBalance[i]); // Unchanged - each user sent X and received X from another user + } + } + + // All users burn their private funds + loginfo << "Burning user's tokens" << endl; + for (size_t i = 0; i < C; ++i) { + const uint64_t balance = users[i]->getBalance(); + assertTrue(balance > 0); + + while (true) { + auto result = users[i]->burn(balance); + if (result.requiredTx_.type_ == utt::Transaction::Type::Burn) { + assertTrue(result.isFinal_); + auto txNum = serverMock.burn(result.requiredTx_); assertTrue(txNum == serverMock.getLastExecutedTxNum()); syncUsersWithServer(); break; // We can stop processing after burning the coin @@ -429,6 +557,12 @@ int main(int argc, char* argv[]) { for (size_t i = 0; i < C; ++i) { assertTrue(users[i]->getBalance() == 0); } +} +int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + testCaseWithBudgetEnforced(); + testCaseWithoutBudgetEnforced(); return 0; } \ No newline at end of file diff --git a/utt/utt-common-api/include/utt-common-api/CommonApi.hpp b/utt/utt-common-api/include/utt-common-api/CommonApi.hpp index 9c7d220061..bebd9d7f56 100644 --- a/utt/utt-common-api/include/utt-common-api/CommonApi.hpp +++ b/utt/utt-common-api/include/utt-common-api/CommonApi.hpp @@ -53,6 +53,8 @@ using TxOutputSigs = std::vector; struct Transaction { enum class Type { Undefined = 0, Mint = 1, Transfer = 2, Burn = 3 } type_ = Type::Undefined; std::vector data_; + uint32_t numOutputs_ = + 0; // [TODO-UTT] This can be removed if the virtual contract provides an API to get the number of outputs of a tx }; } // namespace utt \ No newline at end of file diff --git a/utt/wallet-cli/CMakeLists.txt b/utt/wallet-cli/CMakeLists.txt index f8b2da2851..825b874c4c 100644 --- a/utt/wallet-cli/CMakeLists.txt +++ b/utt/wallet-cli/CMakeLists.txt @@ -2,11 +2,12 @@ add_subdirectory("proto") set(utt-wallet-cli-src src/main.cpp + src/wallet.cpp ) add_executable(utt-wallet-cli ${utt-wallet-cli-src}) -target_include_directories(utt-wallet-cli PUBLIC ../utt-client-api/include ../utt-common-api/include) +target_include_directories(utt-wallet-cli PUBLIC include/ ../utt-client-api/include ../utt-common-api/include) target_link_libraries(utt-wallet-cli PUBLIC utt-wallet-api-proto utt_client_api diff --git a/utt/wallet-cli/include/wallet.hpp b/utt/wallet-cli/include/wallet.hpp new file mode 100644 index 0000000000..0449269c38 --- /dev/null +++ b/utt/wallet-cli/include/wallet.hpp @@ -0,0 +1,88 @@ +// UTT Client API +// +// Copyright (c) 2020-2022 VMware, Inc. All Rights Reserved. +// +// This product is licensed to you under the Apache 2.0 license (the "License"). +// You may not use this product except in compliance with the Apache 2.0 +// License. +// +// This product may include a number of subcomponents with separate copyright +// notices and license terms. Your use of these subcomponents is subject to the +// terms and conditions of the sub-component's license, as noted in the LICENSE +// file. + +#pragma once + +#include +#include + +#include +#include "api.grpc.pb.h" // Generated from utt/wallet/proto/api + +#include + +namespace WalletApi = vmware::concord::utt::wallet::api::v1; + +class Wallet { + public: + using Connection = std::unique_ptr; + using Channel = std::unique_ptr>; + + static Connection newConnection(); + + /// @brief Get the configuration for a privacy application + /// @return The full and public configurations of the deployed application + /// [TODO-UTT] We only need the public config but the full config is returned + /// because we needed for createPrivacyBudgetLocal which should also be removed + /// in favor of a system-created budget tokens by the admin + static std::pair getConfigs(Channel& chan); + + bool isRegistered() const; + + /// [TODO-UTT] Create privacy budget locally because the system can't process budget requests yet. + /// @brief Create a privacy budget locally for the user. This function is only for testing. + /// @param config A Privacy app configuration + /// @param amount The amount of privacy budget to create + void createPrivacyBudgetLocal(const utt::Configuration& config, uint64_t amount); + + Wallet(std::string userId, utt::client::TestUserPKInfrastructure& pki, const utt::PublicConfig& config); + + void showInfo(Channel& chan); + + /// @brief Request registration of the current user + void registerUser(Channel& chan); + + /// @brief Request the creation of a privacy budget. The amount of the budget is predetermined by the deployed app. + /// This operation could be performed entirely by an administrator, but we add it in the wallet + /// for demo purposes. + void createPrivacyBudget(Channel& chan); + + /// @brief Requests the minting of public funds to private funds. + /// @param amount the amount of public funds + void mint(Channel& chan, uint64_t amount); + + /// @brief Transfers the desired amount anonymously to the recipient + /// @param amount The amount to transfer + /// @param recipient The user id of the recipient + void transfer(Channel& chan, uint64_t amount, const std::string& recipient); + + /// @brief Burns the desired amount of private funds and converts them to public funds. + /// @param amount The amount of private funds to burn. + void burn(Channel& chan, uint64_t amount); + + void debugOutput() const; + + private: + /// @brief Sync up to the last known tx number. If the last known tx number is zero (or not provided) the + /// last signed transaction number will be fetched from the system + void syncState(Channel& chan, uint64_t lastKnownTxNum = 0); + + struct DummyUserStorage : public utt::client::IUserStorage {}; + + DummyUserStorage storage_; + std::string userId_; + utt::client::TestUserPKInfrastructure& pki_; + std::unique_ptr user_; + uint64_t publicBalance_ = 0; + bool registered_ = false; +}; \ No newline at end of file diff --git a/utt/wallet-cli/proto/api/v1/api.proto b/utt/wallet-cli/proto/api/v1/api.proto index c91a7093c1..c7ce6c964e 100644 --- a/utt/wallet-cli/proto/api/v1/api.proto +++ b/utt/wallet-cli/proto/api/v1/api.proto @@ -7,61 +7,30 @@ syntax = "proto3"; package vmware.concord.utt.wallet.api.v1; // Privacy Wallet Service Interface -// This interface follows the UTT contract interface as close as possible. -// [TODO-UTT] Consider if we should use a simplified version of this api that assumes synchronous requests -// - This would mean that a request will receive a response only when signatures are computed. service WalletService { - // Temporary adding the deployment of privacy app to the wallet service - // [TODO-UTT] This operation should be performed by an administrator-only tool - rpc deployPrivacyApp(DeployPrivacyAppRequest) returns (DeployPrivacyAppResponse); - - rpc registerUser(RegisterUserRequest) returns (RegisterUserResponse); - rpc getUserRegistration(GetUserRegistrationRequest) returns (GetUserRegistrationResponse); - - // Note: creating a public budget is done by an administrator - rpc getLatestPublicBudget(GetLatestPublicBudgetRequest) returns (GetLatestPublicBudgetResponse); - - // [TODO-UTT] Consider if we need to get the budget sig alone. - // Here is the problem: only one budget coin can exist for a user at the any time, but the signature - // is not ready at the same time as the budget coin. - // If we call getLatestPublicBudget and get back both a budget coin and a sig then we can use the coin. - // If we get just the budget coin the sig is still pending. - // But when we get the sig eventually we're not guaranteed that its for the same budget coin. - // Concurrently some administrator may have created a new budget coin. - // The safest bet is to call getLatestPublicBudgetSig until you get both a budget coin and a sig. - // Otherwise we need to verify that the coin and sig match and if not - request the coin and/or sig until they do. - rpc getLatestPublicBudgetSig(GetLatestPublicBudgetSigRequest) returns (GetLatestPublicBudgetSigResponse); - - rpc transfer(TransferRequest) returns (TransferResponse); - - // Note: mint is supposed to convert public funds (Ether, ERC-20 tokens, etc.) to private funds (UTT tokens). - // Here we make an assumption that the wallet service keeps track of our public funds but in reality - // we need a way to use one or more Ethereum accounts representing the public funds of the user. - rpc mint(MintRequest) returns (MintResponse); - - // Note: burn is supposed to convert private funds (UTT tokens) to public funds (Ether, ERC-20 tokens, etc.). - // Here we make an assumption that the wallet service keeps track of our public funds but in reality - // we need a way to use one or more Ethereum accounts representing the public funds of the user. - rpc burn(BurnRequest) returns (BurnResponse); - - // Same as getNumOfLastAddedTranscation from spec - rpc getLastAddedTxNumber(GetLastAddedTxNumberRequest) returns (GetLastAddedTxNumberResponse); - - // Same as getNumOfLastSignedTranscation from spec - rpc getLastSignedTxNumber(GetLastSignedTxNumberRequest) returns (GetLastSignedTxNumberResponse); - - rpc getTransaction(GetTransactionRequest) returns (GetTransactionResponse); + rpc walletChannel(stream WalletRequest) returns (stream WalletResponse); +} - rpc getTransactionSig(GetTransactionSigRequest) returns (GetTransactionSigResponse); +enum TxType { + UNDEFINED = 0; + MINT = 1; + BURN = 2; + TRANSFER = 3; } -message DeployPrivacyAppRequest { - optional bytes config = 1; +message ConfigureRequest { } -message DeployPrivacyAppResponse { - optional string err = 1; // Returns any error generated during deployment - optional string app_id = 2; // Some way to identify the deployed application +message ConfigureResponse { + optional string err = 1; + optional string privacy_contract_addr = 2; + optional string token_contract_addr = 3; + optional bytes public_config = 4; + + // [TODO-UTT] Remove the whole configuration. We need only the public config and not the full config. + // The full config is only needed to create budget tokens locally, but budget tokens should be + // create by a request from an admin to the system. + optional bytes config = 5; } message RegisterUserRequest { @@ -70,119 +39,121 @@ message RegisterUserRequest { optional bytes input_rcm = 3; // The user's input registration commitment } -// [TODO-UTT] Consider if we should include the complete result in the response -// instead of indicating success and then periodically requesting the registration data message RegisterUserResponse { - optional string err = 1; // Returns any error generated during registration - optional bytes signature = 2; // Signature on the user's registration - repeated uint64 s2 = 3; // Second part of the user's nullifier key -} - -message GetUserRegistrationRequest { - optional string user_id = 1; -} - -message GetUserRegistrationResponse { optional string err = 1; - optional string user_id = 2; - optional bytes user_pk = 3; // User's public key - optional bytes s2 = 4; // System-generated part of the user's PRF key (used for nullifiers) - optional bytes rs = 5; // A signature on the full registration commitment + optional bytes signature = 2; + repeated uint64 s2 = 3; // Second part of the user's nullifier key } -message GetLatestPublicBudgetRequest { +message CreatePrivacyBudgetRequest { optional string user_id = 1; } -message GetLatestPublicBudgetResponse { - optional bytes budget_coin = 1; - // [TODO-UTT] Consider returning the signature in the same response as the budget coin, - // optional bytes signature = 2; // Could be empty -} - -message GetLatestPublicBudgetSigRequest { - optional string user_id = 1; -} - -message GetLatestPublicBudgetSigResponse { - optional bytes signature = 1; +message CreatePrivacyBudgetResponse { + optional string err = 1; + optional bytes budget = 2; + optional bytes signature = 3; } message TransferRequest { - optional TransferTx transfer = 1; + optional bytes tx_data = 1; + optional uint32 num_outputs = 2; } message TransferResponse { - optional uint64 tx_number = 1; + optional string err = 1; + optional uint64 last_added_tx_number = 2; } message MintRequest { optional string user_id = 1; optional uint64 value = 2; + optional bytes tx_data = 3; } message MintResponse { - optional uint64 tx_number = 1; + optional string err = 1; + optional uint64 last_added_tx_number = 2; } message BurnRequest { optional string user_id = 1; - optional bytes burn_tx = 2; optional uint64 value = 3; + optional bytes tx_data = 2; } message BurnResponse { - optional uint64 tx_number = 1; + optional string err = 1; + optional uint64 last_added_tx_number = 2; } message GetLastAddedTxNumberRequest { } message GetLastAddedTxNumberResponse { - optional uint64 tx_number = 1; + optional string err = 1; + optional uint64 tx_number = 2; } message GetLastSignedTxNumberRequest { } message GetLastSignedTxNumberResponse { - optional uint64 tx_number = 1; -} - -message GetTransactionRequest { - optional uint64 tx_number = 1; + optional string err = 1; + optional uint64 tx_number = 2; } -message GetTransactionResponse { +message GetSignedTransactionRequest { optional uint64 tx_number = 1; - oneof tx { - TransferTx transfer = 2; - MintTx mint = 3; - BurnTx burn = 4; - } -} - -message TransferTx { - // [TODO-UTT] What else is in a transfer tx? - // -- should be mostly represented as bytes since this is an anonymous tx. - optional bytes tx = 1; } -message MintTx { - // [TODO-UTT] What else is in a mint tx? - optional bytes tx = 1; -} - -message BurnTx { - // [TODO-UTT] What else is in a burn tx? - optional bytes tx = 1; +message GetSignedTransactionResponse { + optional string err = 1; + optional uint64 tx_number = 2; + optional TxType tx_type = 3; + optional bytes tx_data = 4; + repeated bytes sigs = 5; + optional uint32 tx_data_size = 6; } -message GetTransactionSigRequest { - optional uint64 tx_number = 1; +message GetPublicBalanceRequest { + optional string user_id = 1; } -message GetTransactionSigResponse { - optional uint64 tx_number = 1; - optional bytes signatures = 2; +message GetPublicBalanceResponse { + optional string err = 1; + optional uint64 public_balance = 2; +} + +message WalletRequest { + oneof req { + // init + ConfigureRequest configure = 1; + RegisterUserRequest register_user = 2; + // transact + TransferRequest transfer = 3; + MintRequest mint = 4; + BurnRequest burn = 5; + // sync + GetPublicBalanceRequest get_public_balance = 6; + GetLastAddedTxNumberRequest get_last_added_tx_number = 7; + GetSignedTransactionRequest get_signed_tx = 8; + } +} + +message WalletResponse { + optional string err = 1; + oneof resp { + // init + ConfigureResponse configure = 2; + RegisterUserResponse register_user = 3; + // transact + TransferResponse transfer = 4; + MintResponse mint = 5; + BurnResponse burn = 6; + // sync + GetPublicBalanceResponse get_public_balance = 7; + GetLastAddedTxNumberResponse get_last_added_tx_number = 8; + GetSignedTransactionResponse get_signed_tx = 9; + } } \ No newline at end of file diff --git a/utt/wallet-cli/src/main.cpp b/utt/wallet-cli/src/main.cpp index 065b439241..58c25a27a0 100644 --- a/utt/wallet-cli/src/main.cpp +++ b/utt/wallet-cli/src/main.cpp @@ -1,227 +1,196 @@ -#include - -#include -#include "api.grpc.pb.h" // Generated from utt/wallet/proto/api +// UTT Client API +// +// Copyright (c) 2020-2022 VMware, Inc. All Rights Reserved. +// +// This product is licensed to you under the Apache 2.0 license (the "License"). +// You may not use this product except in compliance with the Apache 2.0 +// License. +// +// This product may include a number of subcomponents with separate copyright +// notices and license terms. Your use of these subcomponents is subject to the +// terms and conditions of the sub-component's license, as noted in the LICENSE +// file. -#include "utt-client-api/ClientApi.hpp" - -#include +#include #include #include -using namespace vmware::concord::utt::wallet::api::v1; - -using GrpcWalletService = std::unique_ptr; - -GrpcWalletService connectToGrpcWalletService() { - std::string grpcServerAddr = "127.0.0.1:49000"; - - std::cout << "Connecting to gRPC server at " << grpcServerAddr << " ...\n"; - - auto chan = grpc::CreateChannel(grpcServerAddr, grpc::InsecureChannelCredentials()); - - if (!chan) { - throw std::runtime_error("Failed to create gRPC channel."); - } - auto timeoutSec = std::chrono::seconds(5); - if (chan->WaitForConnected(std::chrono::system_clock::now() + timeoutSec)) { - std::cout << "Connected.\n"; - } else { - throw std::runtime_error("Failed to connect to gRPC server after " + std::to_string(timeoutSec.count()) + - " seconds."); - } - - return WalletService::NewStub(chan); -} +#include "wallet.hpp" + +// [TODO-UTT] The wallet should use RocksDB to store UTT assets. +// [TODO-UTT] The wallet should use some RSA cryptography to generate public/private +// keys (used by the UTT Client Library) + +// [TODO-UTT] Initial registration +// Upon first launch (no records in RocksDB) the wallet asks the user to register +// > Choose a unique user identifier: +// After this prompt the wallet sends a registration request and waits for the response +// Upon successful registration the user can use any of the following commands. + +// [TODO-UTT] Startup Sequence +// A. Confirm registration -- check that the user is registered, otherwise go to "Initial registration". +// B. Synchronize +// 1) Ask about the latest signed transaction number and compare with +// the latest executed transaction in the wallet. Determine the range of tx numbers to be retrieved. +// 2) For each tx number to execute request the transaction and signature (can be combined) +// 3) Apply the transaction to the wallet state +// a. If it's a burn or a mint transaction matching our user-id +// b. IF it's an anonymous tx that we can claim outputs from or slash spent coins (check the nullifiers) +// [TODO-UTT] Synchronization can be optimized to require fewer requests by batching tx requests and/or filtering by +// user-id for burns and mints + +// [TODO-UTT] Periodic synchronization +// We need to periodically sync with the wallet service - we can either detect this when we send requests +// (we see that there are multiple transactions that happened before ours) or we do it periodically or before +// attempt an operation. + +// Note: Limited recovery from liveness issues +// In a single machine demo setting liveness issues will not be created due to the network, +// so we don't need to implement the full range of precautions to handle liveness issues +// such as timeouts. void printHelp() { std::cout << "\nCommands:\n"; - std::cout << "deploy app -- generates a privacy app config and deploys it to the blockchain.\n"; - std::cout - << "register -- creates a new user and registers it in a previously deployed privacy app instance\n"; - std::cout << "show -- prints information about a user created by this wallet\n"; - - // mint -- convert some amount of public funds (ERC20 tokens) to private funds (UTT tokens) - // burn -- convert some amount of private funds (UTT tokens) to public funds (ERC20 tokens) - // transfer -- transfers anonymously some amount of private funds (UTT tokens) to another user - // public balance -- print the number of ERC20 tokens the user has (needs a gRPC method to retrieve this value from - // the wallet service) private balance -- print the number of UTT tokens the user has currently (compute locally) - // budget -- print the currently available anonymity budget (a budget is created in advance for each user) + std::cout << "config -- configures wallets with the privacy application.\n"; + std::cout << "show -- prints information about the user managed by this wallet\n"; + std::cout << "register -- requests user registration required for spending coins\n"; + std::cout << "create-budget -- requests creation of a privacy budget, the amount is decided by the " + "system.\n"; + std::cout << "mint -- mint the requested amount of public funds.\n"; + std::cout << "transfer -- transfers the specified amount between users.\n"; + std::cout << "burn -- burns the specified amount of private funds to public funds.\n"; + std::cout << '\n'; } -class Wallet { - public: - void showUser(const std::string& userId) { - auto it = users_.find(userId); - if (it == users_.end()) { - std::cout << "User '" << userId << "' does not exist.\n"; - return; - } +struct CLIApp { + std::string userId; + grpc::ClientContext ctx; + Wallet::Connection conn; + Wallet::Channel chan; + utt::Configuration config; + utt::client::TestUserPKInfrastructure pki; + std::unique_ptr wallet; + + CLIApp() { + conn = Wallet::newConnection(); + if (!conn) throw std::runtime_error("Failed to create wallet connection!"); + + chan = conn->walletChannel(&ctx); + if (!chan) throw std::runtime_error("Failed to create wallet streaming channel!"); + } - std::cout << "User Id: " << it->first << '\n'; - std::cout << "Private balance: " << it->second->getBalance() << '\n'; - std::cout << "Privacy budget: " << it->second->getPrivacyBudget() << '\n'; - std::cout << "Last executed transaction number: " << it->second->getLastExecutedTxNum() << '\n'; + ~CLIApp() { + std::cout << "Closing wallet streaming channel... "; + chan->WritesDone(); + auto status = chan->Finish(); + std::cout << " Done.\n"; + // std::cout << "gRPC error code: " << status.error_code() << '\n'; + // std::cout << "gRPC error msg: " << status.error_message() << '\n'; + // std::cout << "gRPC error details: " << status.error_details() << '\n'; } - void deployApp(GrpcWalletService& grpc) { - if (!deployedAppId_.empty()) { - std::cout << "Privacy app '" << deployedAppId_ << "' already deployed\n"; + void configure() { + if (wallet) { + std::cout << "The wallet is already configured.\n"; return; } - // Generate a privacy config for a N=4 replica system tolerating F=1 failures - utt::client::ConfigInputParams params; - params.validatorPublicKeys = std::vector{4, "placeholderPublicKey"}; // N = 3 * F + 1 - params.threshold = 2; // F + 1 - auto config = utt::client::generateConfig(params); - if (config.empty()) throw std::runtime_error("Failed to generate a privacy app configuration!"); + auto configs = Wallet::getConfigs(chan); + config = configs.first; - grpc::ClientContext ctx; - - DeployPrivacyAppRequest req; - req.set_config(config.data(), config.size()); - - DeployPrivacyAppResponse resp; - grpc->deployPrivacyApp(&ctx, req, &resp); + wallet = std::make_unique(userId, pki, configs.second); + } - // Note that keeping the config around in memory is just for a demo purpose and should not happen in real system - if (resp.has_err()) { - std::cout << "Failed to deploy privacy app: " << resp.err() << '\n'; - } else if (resp.app_id().empty()) { - std::cout << "Failed to deploy privacy app: empty app id!\n"; + void registerUserCmd() { + if (!wallet->isRegistered()) { + wallet->registerUser(chan); + wallet->showInfo(chan); } else { - deployedAppId_ = resp.app_id(); - // We need the public config part which can typically be obtained from the service, but we keep it for simplicity - deployedPublicConfig_ = std::make_unique(utt::client::getPublicConfig(config)); - std::cout << "Successfully deployed privacy app with id: " << deployedAppId_ << '\n'; + std::cout << "Wallet is already registered.\n"; } } - void registerUser(const std::string& userId, GrpcWalletService& grpc) { - if (userId.empty()) throw std::runtime_error("Cannot register user with empty user id!"); - if (deployedAppId_.empty()) { - std::cout << "You must first deploy a privacy app to register a user. Use command 'deploy app'\n"; + void createBudgetCmd() { + wallet->createPrivacyBudgetLocal(config, 10000); + wallet->showInfo(chan); + } + + void showCmd() { wallet->showInfo(chan); } + + void mintCmd(const std::vector& cmdTokens) { + if (cmdTokens.size() != 2) { + std::cout << "Usage: mint \n"; return; } - - if (users_.find(userId) != users_.end()) { - std::cout << "User '" << userId << " is already registered!\n"; + int amount = std::atoi(cmdTokens[1].c_str()); + if (amount <= 0) { + std::cout << "Expected a positive mint amount!\n"; return; } + wallet->mint(chan, (uint64_t)amount); + wallet->showInfo(chan); + } - // Print out the list of valid user ids with pre-generated keys if we don't have a match. - // This is a temp code until we can generate keys on demand for every user id. - auto userIds = pki_.getUserIds(); - auto it = std::find(userIds.begin(), userIds.end(), userId); - if (it == userIds.end()) { - std::cout << "Please use one of the following userIds:\n["; - for (const auto& userId : userIds) std::cout << userId << ' '; - std::cout << "]\n"; + void transferCmd(const std::vector& cmdTokens) { + if (cmdTokens.size() != 3) { + std::cout << "Usage: transfer \n"; return; } - - auto user = utt::client::createUser(userId, *deployedPublicConfig_, pki_, storage_); - if (!user) throw std::runtime_error("Failed to create user!"); - - auto userRegInput = user->getRegistrationInput(); - if (userRegInput.empty()) throw std::runtime_error("Failed to create user registration input!"); - - grpc::ClientContext ctx; - - RegisterUserRequest req; - req.set_user_id(userId); - req.set_input_rcm(userRegInput.data(), userRegInput.size()); - req.set_user_pk(user->getPK()); - - RegisterUserResponse resp; - grpc->registerUser(&ctx, req, &resp); - - if (resp.has_err()) { - std::cout << "Failed to register user: " << resp.err() << '\n'; - } else { - utt::RegistrationSig sig = std::vector(resp.signature().begin(), resp.signature().end()); - std::cout << "Got sig for registration with size: " << sig.size() << '\n'; - - utt::S2 s2 = std::vector(resp.s2().begin(), resp.s2().end()); - std::cout << "Got S2 for registration: ["; - for (const auto& val : s2) std::cout << val << ' '; - std::cout << "]\n"; - - if (!user->updateRegistration(user->getPK(), sig, s2)) { - std::cout << "Failed to update user's registration!\n"; - } else { - std::cout << "Successfully registered user with id '" << user->getUserId() << "'\n"; - users_.emplace(userId, std::move(user)); - } + int amount = std::atoi(cmdTokens[1].c_str()); + if (amount <= 0) { + std::cout << "Expected a positive transfer amount!\n"; + return; } + wallet->transfer(chan, (uint64_t)amount, cmdTokens[2]); + wallet->showInfo(chan); } - private: - struct DummyUserStorage : public utt::client::IUserStorage {}; + void burnCmd(const std::vector& cmdTokens) { + if (cmdTokens.size() != 2) { + std::cout << "Usage: burn \n"; + return; + } + int amount = std::atoi(cmdTokens[1].c_str()); + if (amount <= 0) { + std::cout << "Expected a positive burn amount!\n"; + return; + } + wallet->burn(chan, (uint64_t)amount); + wallet->showInfo(chan); + } - std::unique_ptr deployedPublicConfig_; - std::string deployedAppId_; - utt::client::TestUserPKInfrastructure pki_; - DummyUserStorage storage_; - std::map> users_; + void debugCmd() { wallet->debugOutput(); } }; int main(int argc, char* argv[]) { (void)argc; (void)argv; - std::cout << "Sample Privacy Wallet CLI Application.\n"; - // [TODO-UTT] The wallet should use RocksDB to store UTT assets. - // [TODO-UTT] The wallet should use some RSA cryptography to generate public/private - // keys (used by the UTT Client Library) - - // [TODO-UTT] Initial registration - // Upon first launch (no records in RocksDB) the wallet asks the user to register - // > Choose a unique user identifier: - // After this prompt the wallet sends a registration request and waits for the response - // Upon successful registration the user can use any of the following commands. - - // [TODO-UTT] Startup Sequence - // A. Confirm registration -- check that the user is registered, otherwise go to "Initial registration". - // B. Synchronize - // 1) Ask about the latest signed transaction number and compare with - // the latest executed transaction in the wallet. Determine the range of tx numbers to be retrieved. - // 2) For each tx number to execute request the transaction and signature (can be combined) - // 3) Apply the transaction to the wallet state - // a. If it's a burn or a mint transaction matching our user-id - // b. IF it's an anonymous tx that we can claim outputs from or slash spent coins (check the nullifiers) - // [TODO-UTT] Synchronization can be optimized to require fewer requests by batching tx requests and/or filtering by - // user-id for burns and mints - - // [TODO-UTT] Periodic synchronization - // We need to periodically sync with the wallet service - we can either detect this when we send requests - // (we see that there are multiple transactions that happened before ours) or we do it periodically or before - // attempt an operation. - - // Note: Limited recovery from liveness issues - // In a single machine demo setting liveness issues will not be created due to the network, - // so we don't need to implement the full range of precautions to handle liveness issues - // such as timeouts. - - // [TODO-UTT] Commands: - // mint -- convert some amount of public funds (ERC20 tokens) to private funds (UTT tokens) - // burn -- convert some amount of private funds (UTT tokens) to public funds (ERC20 tokens) - // transfer -- transfers anonymously some amount of private funds (UTT tokens) to another user - // public balance -- print the number of ERC20 tokens the user has (needs a gRPC method to retrieve this value from - // the wallet service) private balance -- print the number of UTT tokens the user has currently (compute locally) - // budget -- print the currently available anonymity budget (a budget is created in advance for each user) + if (argc != 2) { + std::cout << "Provide a user-id.\n"; + return 0; + } - try { - auto grpc = connectToGrpcWalletService(); - if (!grpc) throw std::runtime_error("Failed to create gRPC client stub."); + CLIApp app; + app.userId = argv[1]; - utt::client::Initialize(); + const auto userIds = app.pki.getUserIds(); + if (std::find(userIds.begin(), userIds.end(), app.userId) == userIds.end()) { + std::cout << "Selected user id has no pre-generated keys!\n"; + std::cout << "Valid user ids: ["; + for (const auto& userId : userIds) std::cout << userId << ", "; + std::cout << "]\n"; + return 0; + } + + std::cout << "\nSample Privacy Wallet CLI Application.\n"; + std::cout << "UserId: " << app.userId << '\n'; - auto wallet = Wallet(); + try { + utt::client::Initialize(); while (true) { - std::cout << "Enter command (type 'h' for commands 'Ctr-D' to quit):\n > "; + std::cout << "\nEnter command (type 'h' for commands 'Ctr-D' to quit):\n > "; std::string cmd; std::getline(std::cin, cmd); @@ -232,36 +201,39 @@ int main(int argc, char* argv[]) { if (cmd == "h") { printHelp(); - } else if (cmd == "deploy app") { - wallet.deployApp(grpc); + } else if (cmd == "config") { + app.configure(); + } else if (!app.wallet) { + std::cout << "You must first configure the wallet. Use the 'config' command.\n"; } else { - // Tokenize command + // Tokenize params std::vector cmdTokens; - { - std::stringstream ss(cmd); - std::string t; - while (std::getline(ss, t, ' ')) cmdTokens.emplace_back(std::move(t)); - } + std::string token; + std::stringstream ss(cmd); + while (std::getline(ss, token, ' ')) cmdTokens.emplace_back(token); if (cmdTokens.empty()) continue; if (cmdTokens[0] == "register") { - if (cmdTokens.size() != 2) { - std::cout << "Usage: specify the user id to register.\n"; - } else { - wallet.registerUser(cmdTokens[1], grpc); - } + app.registerUserCmd(); + } else if (!app.wallet->isRegistered()) { + std::cout << "You must first register the user. Use the 'register' command.\n"; + } else if (cmdTokens[0] == "create-budget") { + app.createBudgetCmd(); } else if (cmdTokens[0] == "show") { - if (cmdTokens.size() != 2) { - std::cout << "Usage: specify the user id to show,\n"; - } else { - wallet.showUser(cmdTokens[1]); - } + app.showCmd(); + } else if (cmdTokens[0] == "mint") { + app.mintCmd(cmdTokens); + } else if (cmdTokens[0] == "transfer") { + app.transferCmd(cmdTokens); + } else if (cmdTokens[0] == "burn") { + app.burnCmd(cmdTokens); + } else if (cmdTokens[0] == "debug") { + app.debugCmd(); } else { std::cout << "Unknown command '" << cmd << "'\n"; } } } - } catch (const std::runtime_error& e) { std::cout << "Error (exception): " << e.what() << '\n'; return 1; diff --git a/utt/wallet-cli/src/wallet.cpp b/utt/wallet-cli/src/wallet.cpp new file mode 100644 index 0000000000..61c28f2d4e --- /dev/null +++ b/utt/wallet-cli/src/wallet.cpp @@ -0,0 +1,387 @@ +// UTT Client API +// +// Copyright (c) 2020-2022 VMware, Inc. All Rights Reserved. +// +// This product is licensed to you under the Apache 2.0 license (the "License"). +// You may not use this product except in compliance with the Apache 2.0 +// License. +// +// This product may include a number of subcomponents with separate copyright +// notices and license terms. Your use of these subcomponents is subject to the +// terms and conditions of the sub-component's license, as noted in the LICENSE +// file. + +#include "wallet.hpp" + +#include + +using namespace vmware::concord::utt::wallet::api::v1; + +Wallet::Wallet(std::string userId, utt::client::TestUserPKInfrastructure& pki, const utt::PublicConfig& config) + : userId_{std::move(userId)}, pki_{pki} { + user_ = utt::client::createUser(userId_, config, pki_, storage_); + if (!user_) throw std::runtime_error("Failed to create user!"); +} + +Wallet::Connection Wallet::newConnection() { + std::string grpcServerAddr = "127.0.0.1:49001"; + + std::cout << "Connecting to gRPC server at " << grpcServerAddr << "... "; + + auto chan = grpc::CreateChannel(grpcServerAddr, grpc::InsecureChannelCredentials()); + + if (!chan) { + throw std::runtime_error("Failed to create gRPC channel."); + } + auto timeoutSec = std::chrono::seconds(5); + if (chan->WaitForConnected(std::chrono::system_clock::now() + timeoutSec)) { + std::cout << "Connected.\n"; + } else { + throw std::runtime_error("Failed to connect to gRPC server after " + std::to_string(timeoutSec.count()) + + " seconds."); + } + + return WalletService::NewStub(chan); +} + +void Wallet::showInfo(Channel& chan) { + std::cout << '\n'; + syncState(chan); + std::cout << "--------- " << userId_ << " ---------\n"; + std::cout << "Public balance: " << publicBalance_ << '\n'; + std::cout << "Private balance: " << user_->getBalance() << '\n'; + std::cout << "Privacy budget: " << user_->getPrivacyBudget() << '\n'; + std::cout << "Last executed tx number: " << user_->getLastExecutedTxNum() << '\n'; +} + +std::pair Wallet::getConfigs(Channel& chan) { + WalletRequest req; + req.mutable_configure(); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_configure()) throw std::runtime_error("Expected configure response from wallet service!"); + const auto& configureResp = resp.configure(); + + // Note that keeping the config around in memory is just a temp solution and should not happen in real system + if (configureResp.has_err()) throw std::runtime_error("Failed to configure: " + resp.err()); + + std::cout << "\nSuccessfully configured privacy application\n"; + std::cout << "---------------------------------------------------\n"; + std::cout << "Privacy contract: " << configureResp.privacy_contract_addr() << '\n'; + std::cout << "Token contract: " << configureResp.token_contract_addr() << '\n'; + + if (configureResp.config().empty()) throw std::runtime_error("The full config is empty!"); + if (configureResp.public_config().empty()) throw std::runtime_error("The public config is empty!"); + + utt::Configuration config(configureResp.config().begin(), configureResp.config().end()); + utt::PublicConfig publicConfig(configureResp.public_config().begin(), configureResp.public_config().end()); + + auto otherPublicConfig = utt::client::getPublicConfig(config); + if (publicConfig != otherPublicConfig) { + throw std::runtime_error("The public config doesn't correspond to the one in the full config!"); + } + + return std::pair{std::move(config), std::move(publicConfig)}; +} + +void Wallet::createPrivacyBudgetLocal(const utt::Configuration& config, uint64_t amount) { + user_->createPrivacyBudgetLocal(config, amount); + std::cout << "Successfully created budget with value " << amount << '\n'; +} + +bool Wallet::isRegistered() const { return registered_; } + +void Wallet::registerUser(Channel& chan) { + if (registered_) throw std::runtime_error("Wallet is already registered!"); + + auto userRegInput = user_->getRegistrationInput(); + if (userRegInput.empty()) throw std::runtime_error("Failed to create user registration input!"); + + WalletRequest req; + auto& registerReq = *req.mutable_register_user(); + registerReq.set_user_id(userId_); + registerReq.set_input_rcm(userRegInput.data(), userRegInput.size()); + registerReq.set_user_pk(user_->getPK()); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_register_user()) throw std::runtime_error("Expected register response from wallet service!"); + const auto& regUserResp = resp.register_user(); + + if (regUserResp.has_err()) { + std::cout << "Failed to register user: " << regUserResp.err() << '\n'; + } else { + utt::RegistrationSig sig = std::vector(regUserResp.signature().begin(), regUserResp.signature().end()); + if (sig.empty()) throw std::runtime_error("Registration signature is empty!"); + + utt::S2 s2 = std::vector(regUserResp.s2().begin(), regUserResp.s2().end()); + if (s2.empty()) throw std::runtime_error("Registration param s2 is empty!"); + + user_->updateRegistration(user_->getPK(), sig, s2); + + registered_ = true; + + std::cout << "Successfully registered user.\n"; + } +} + +void Wallet::createPrivacyBudget(Channel& chan) { + (void)chan; + // [TODO-UTT] Create budget is done locally, should be done by the system + // grpc::ClientContext ctx; + + // CreatePrivacyBudgetRequest req; + // req.set_user_id(userId_); + + // CreatePrivacyBudgetResponse resp; + // conn->createPrivacyBudget(&ctx, req, &resp); + + // if (resp.has_err()) { + // std::cout << "Failed to create privacy budget:" << resp.err() << '\n'; + // } else { + // utt::PrivacyBudget budget = std::vector(resp.budget().begin(), resp.budget().end()); + // utt::RegistrationSig sig = std::vector(resp.signature().begin(), resp.signature().end()); + + // std::cout << "Got budget " << budget.size() << " bytes.\n"; + // std::cout << "Got budget sig " << sig.size() << " bytes.\n"; + + // user_->updatePrivacyBudget(budget, sig); + // } +} + +void Wallet::mint(Channel& chan, uint64_t amount) { + auto mintTx = user_->mint(amount); + + grpc::ClientContext ctx; + + WalletRequest req; + auto& mintReq = *req.mutable_mint(); + mintReq.set_user_id(userId_); + mintReq.set_value(amount); + mintReq.set_tx_data(mintTx.data_.data(), mintTx.data_.size()); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_mint()) throw std::runtime_error("Expected mint response from wallet service!"); + const auto& mintResp = resp.mint(); + + if (mintResp.has_err()) { + std::cout << "Failed to mint:" << mintResp.err() << '\n'; + } else { + std::cout << "Successfully sent mint tx. Last added tx number:" << mintResp.last_added_tx_number() << '\n'; + + syncState(chan, mintResp.last_added_tx_number()); + } +} + +void Wallet::transfer(Channel& chan, uint64_t amount, const std::string& recipient) { + if (userId_ == recipient) { + std::cout << "Cannot transfer to self directly!\n"; + return; + } + + if (user_->getBalance() < amount) { + std::cout << "Insufficient private balance!\n"; + return; + } + + if (user_->getPrivacyBudget() < amount) { + std::cout << "Insufficient privacy budget!\n"; + return; + } + + std::cout << "Processing an anonymous transfer of " << amount << " to " << recipient << "...\n"; + + // Process the transfer until we get the final transaction + // On each iteration we also sync up to the tx number of our request + while (true) { + auto result = user_->transfer(recipient, pki_.getPublicKey(recipient), amount); + + WalletRequest req; + auto& transferReq = *req.mutable_transfer(); + transferReq.set_tx_data(result.requiredTx_.data_.data(), result.requiredTx_.data_.size()); + transferReq.set_num_outputs(result.requiredTx_.numOutputs_); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_transfer()) throw std::runtime_error("Expected transfer response from wallet service!"); + const auto& transferResp = resp.transfer(); + + if (transferResp.has_err()) { + std::cout << "Failed to transfer:" << resp.err() << '\n'; + } else { + std::cout << "Successfully sent transfer tx. Last added tx number:" << transferResp.last_added_tx_number() + << '\n'; + + syncState(chan, transferResp.last_added_tx_number()); + } + + if (result.isFinal_) break; // Done + } + + std::cout << "Anonymous transfer done.\n"; +} + +void Wallet::burn(Channel& chan, uint64_t amount) { + if (user_->getBalance() < amount) { + std::cout << "Insufficient private balance!\n"; + return; + } + + std::cout << "Processing a burn operation for " << amount << "...\n"; + + // Process the transfer until we get the final transaction + // On each iteration we also sync up to the tx number of our request + while (true) { + auto result = user_->burn(amount); + + if (result.isFinal_) { + WalletRequest req; + auto& burnReq = *req.mutable_burn(); + burnReq.set_user_id(user_->getUserId()); + burnReq.set_value(amount); + burnReq.set_tx_data(result.requiredTx_.data_.data(), result.requiredTx_.data_.size()); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_burn()) throw std::runtime_error("Expected burn response from wallet service!"); + const auto& burnResp = resp.burn(); + + if (burnResp.has_err()) { + std::cout << "Failed to do burn:" << resp.err() << '\n'; + } else { + std::cout << "Successfully sent burn tx. Last added tx number:" << burnResp.last_added_tx_number() << '\n'; + + syncState(chan, burnResp.last_added_tx_number()); + } + + break; // Done + } else { + WalletRequest req; + auto& transferReq = *req.mutable_transfer(); + transferReq.set_tx_data(result.requiredTx_.data_.data(), result.requiredTx_.data_.size()); + transferReq.set_num_outputs(result.requiredTx_.numOutputs_); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_transfer()) throw std::runtime_error("Expected transfer response from wallet service!"); + const auto& transferResp = resp.transfer(); + + if (transferResp.has_err()) { + std::cout << "Failed to do self-transfer as part of burn:" << resp.err() << '\n'; + return; + } else { + std::cout << "Successfully sent self-transfer tx as part of burn. Last added tx number:" + << transferResp.last_added_tx_number() << '\n'; + + syncState(chan, transferResp.last_added_tx_number()); + } + // Continue with the next transaction in the burn process + } + } + + std::cout << "Burn operation done.\n"; +} + +void Wallet::syncState(Channel& chan, uint64_t lastKnownTxNum) { + std::cout << "Synchronizing state... "; + + // Update public balance + { + WalletRequest req; + req.mutable_get_public_balance()->set_user_id(userId_); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_get_public_balance()) + throw std::runtime_error("Expected get public balance response from wallet service!"); + const auto& getPubBalanceResp = resp.get_public_balance(); + + if (getPubBalanceResp.has_err()) { + std::cout << "Failed to get public balance:" << resp.err() << '\n'; + } else { + publicBalance_ = getPubBalanceResp.public_balance(); + } + } + + // Sync to latest state + if (lastKnownTxNum == 0) { + WalletRequest req; + req.mutable_get_last_added_tx_number(); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_get_last_added_tx_number()) + throw std::runtime_error("Expected get last added tx number response from wallet service!"); + const auto& getLastAddedTxNumResp = resp.get_last_added_tx_number(); + + if (getLastAddedTxNumResp.has_err()) { + std::cout << "Failed to get last added tx number:" << resp.err() << '\n'; + } else { + lastKnownTxNum = getLastAddedTxNumResp.tx_number(); + } + } + + for (uint64_t txNum = user_->getLastExecutedTxNum() + 1; txNum <= lastKnownTxNum; ++txNum) { + WalletRequest req; + req.mutable_get_signed_tx()->set_tx_number(txNum); + chan->Write(req); + + WalletResponse resp; + chan->Read(&resp); + if (!resp.has_get_signed_tx()) throw std::runtime_error("Expected get signed tx response from wallet service!"); + const auto& getSignedTxResp = resp.get_signed_tx(); + + if (getSignedTxResp.has_err()) { + std::cout << "Failed to get signed tx with number " << txNum << ':' << resp.err() << '\n'; + return; + } + + if (!getSignedTxResp.has_tx_number()) { + std::cout << "Missing tx number in GetSignedTransactionResponse!\n"; + return; + } + + utt::Transaction tx; + std::copy(getSignedTxResp.tx_data().begin(), getSignedTxResp.tx_data().end(), std::back_inserter(tx.data_)); + + utt::TxOutputSigs sigs; + sigs.reserve((size_t)getSignedTxResp.sigs_size()); + for (const auto& sig : getSignedTxResp.sigs()) { + sigs.emplace_back(std::vector(sig.begin(), sig.end())); + } + + // Apply transaction + switch (getSignedTxResp.tx_type()) { + case TxType::MINT: { + tx.type_ = utt::Transaction::Type::Mint; + if (sigs.size() != 1) throw std::runtime_error("Expected single signature in mint tx!"); + user_->updateMintTx(getSignedTxResp.tx_number(), tx, sigs[0]); + } break; + case TxType::TRANSFER: { + tx.type_ = utt::Transaction::Type::Transfer; + user_->updateTransferTx(getSignedTxResp.tx_number(), tx, sigs); + } break; + case TxType::BURN: { + tx.type_ = utt::Transaction::Type::Burn; + if (!sigs.empty()) throw std::runtime_error("Expected no signatures for burn tx!"); + user_->updateBurnTx(getSignedTxResp.tx_number(), tx); + } break; + default: + throw std::runtime_error("Unexpected tx type!"); + } + } + + std::cout << "Ok. (Last known tx number: " << lastKnownTxNum << ")\n"; +} + +void Wallet::debugOutput() const { user_->debugOutput(); } \ No newline at end of file