diff --git a/CMakeLists.txt b/CMakeLists.txt index 567c6f58..15f44567 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -294,6 +294,7 @@ add_library(supernode_common STATIC ${PROJECT_SOURCE_DIR}/src/supernode/requestdefines.cpp ${PROJECT_SOURCE_DIR}/src/supernode/requests.cpp ${PROJECT_SOURCE_DIR}/src/supernode/requests/authorize_rta_tx.cpp + ${PROJECT_SOURCE_DIR}/src/supernode/requests/auth_sample_disqualificator.cpp ${PROJECT_SOURCE_DIR}/src/supernode/requests/debug.cpp ${PROJECT_SOURCE_DIR}/src/supernode/requests/forward.cpp ${PROJECT_SOURCE_DIR}/src/supernode/requests/get_info.cpp @@ -418,6 +419,7 @@ if (OPT_BUILD_TESTS) ${PROJECT_SOURCE_DIR}/test/rta_classes_test.cpp ${PROJECT_SOURCE_DIR}/test/sys_info.cpp ${PROJECT_SOURCE_DIR}/test/strand_test.cpp + ${PROJECT_SOURCE_DIR}/test/disqualification_test.cpp ${PROJECT_SOURCE_DIR}/test/serialize_test.cpp ${PROJECT_SOURCE_DIR}/test/main.cpp ) diff --git a/include/supernode/requests/auth_sample_disqualificator.h b/include/supernode/requests/auth_sample_disqualificator.h new file mode 100644 index 00000000..4d9e49e7 --- /dev/null +++ b/include/supernode/requests/auth_sample_disqualificator.h @@ -0,0 +1,59 @@ +// Copyright (c) 2018, The Graft Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "lib/graft/inout.h" +#include "lib/graft/context.h" + +#include + +#include + +namespace graft::supernode::request +{ + +//The class is used for tests +class AuthSDisqualificator +{ +public: + //supernodes ids + using Ids = std::vector; + + //it should be called as earlier as possible, at the request when payment_id is known, to be prepared to respond to votes of disqualification requests while we are not ready yet to do disqualifications + // returns Ok + static graft::Status initDisqualify(graft::Context& ctx, const std::string& payment_id); + + //it should be called on payment timeout, when we know whom to disqualify + //block_height is required to generate correct auth sample + //ids - supernodes to be disqualified, they should be from the auth sample + //it fills output with multicast request and returns Forward. call it even if ids empty, it will clear the context and return Ok + static graft::Status startDisqualify(graft::Context& ctx, const std::string& payment_id, uint64_t block_height, const Ids& ids, graft::Output& output); +}; + +} //namespace graft::supernode::request diff --git a/include/supernode/requests/disqualificator.h b/include/supernode/requests/disqualificator.h index e49d1077..2ff9b3c8 100644 --- a/include/supernode/requests/disqualificator.h +++ b/include/supernode/requests/disqualificator.h @@ -28,7 +28,7 @@ #pragma once -#include "lib/graft/inout.h" +#include "lib/graft/router.h" #include @@ -44,7 +44,9 @@ class BBLDisqualificatorBase public: using GetSupernodeKeys = std::function; using GetBBQSandQCL = std::function& bbqs, std::vector& qcl)>; + using GetAuthSample = std::function& auths)>; using CollectTxs = std::function; + using AddPeriodic = std::function; static std::unique_ptr createTestBBLDisqualificator( GetSupernodeKeys fnGetSupernodeKeys, @@ -52,17 +54,34 @@ class BBLDisqualificatorBase CollectTxs fnCollectTxs ); + static std::unique_ptr createTestAuthSDisqualificator( + GetSupernodeKeys fnGetSupernodeKeys, + GetAuthSample fnGetAuthSample, + CollectTxs fnCollectTxs, + std::chrono::milliseconds addPeriodic_interval_ms + ); + + static void waitForTestAuthSDisqualificator(); + struct command { std::string uri; //process if empty - // uri.empty() + // if uri.empty() uint64 block_height; crypto::hash block_hash; - // !uri.empty() + // if !uri.empty() std::string body; + // if !payment_id.empty() it is command for auth sample + std::string payment_id; + std::vector ids; //ids to disqualify, startDisqualify if not empty, initDisqualify otherwise command() = default; + //for bbl disqualification command(uint64 block_height, const crypto::hash& block_hash) : block_height(block_height), block_hash(block_hash) { } + //for auth sample disqualification + command(const std::string& payment_id, const std::vector& ids, uint64 block_height, const crypto::hash& block_hash) + : payment_id(payment_id), ids(ids), block_height(block_height), block_hash(block_hash) { } + // command(const std::string& uri, const std::string& body) : uri(uri), body(body) { } }; @@ -77,8 +96,10 @@ class BBLDisqualificatorBase protected: GetSupernodeKeys fnGetSupernodeKeys; GetBBQSandQCL fnGetBBQSandQCL; + GetAuthSample fnGetAuthSample; CollectTxs fnCollectTxs; + AddPeriodic fnAddPeriodic; + std::chrono::milliseconds addPeriodic_interval_ms; }; - } //namespace graft::supernode::request diff --git a/src/supernode/requests.cpp b/src/supernode/requests.cpp index 412707ab..cf879424 100644 --- a/src/supernode/requests.cpp +++ b/src/supernode/requests.cpp @@ -23,6 +23,8 @@ namespace graft::request::system_info { void register_request(Router& router); } namespace graft::supernode::request { +void registerAuthSDisqualificatorRequest(graft::Router &router); + void registerRTARequests(graft::Router &router) { registerSaleRequest(router); @@ -37,6 +39,7 @@ void registerRTARequests(graft::Router &router) registerSendSupernodeAnnounceRequest(router); registerSendSupernodeStakesRequest(router); registerBlockchainBasedListRequest(router); + registerAuthSDisqualificatorRequest(router); } void registerWalletApiRequests(graft::Router &router) diff --git a/src/supernode/requests/auth_sample_disqualificator.cpp b/src/supernode/requests/auth_sample_disqualificator.cpp new file mode 100644 index 00000000..c12ddb88 --- /dev/null +++ b/src/supernode/requests/auth_sample_disqualificator.cpp @@ -0,0 +1,638 @@ +// Copyright (c) 2018, The Graft Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "supernode/requests/auth_sample_disqualificator.h" +#include "supernode/requests/disqualificator.h" +#include "rta/fullsupernodelist.h" +#include "supernode/requests/multicast.h" +#include "lib/graft/binary_serialize.h" +#include "lib/graft/handler_api.h" + +#include + +#include + +#include +#include + +namespace graft::supernode::request +{ + +namespace +{ + +GRAFT_DEFINE_IO_STRUCT(DisqualificationItem2, + (std::string, payment_id), + (uint64_t, block_height), + (crypto::hash, block_hash), + (std::vector, ids) //sorted, the order is important to get valid signs + ); + +GRAFT_DEFINE_IO_STRUCT(SignerItem, + (crypto::public_key, signer_id), + (crypto::signature, sign) + ); + +GRAFT_DEFINE_IO_STRUCT(Disqualification2, + (DisqualificationItem2, item), + (std::vector, signers) + ); + +GRAFT_DEFINE_IO_STRUCT(Vote, + (std::string, payment_id), + (SignerItem, si) + ); + +} //namespace + +namespace +{ + +class AuthSDisqualificatorImpl : public AuthSDisqualificator, public BBLDisqualificatorBase +{ + static constexpr size_t BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT = graft::FullSupernodeList::BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT; + static constexpr int32_t REQUIRED_AUTHS_DISQULIFICATION_VOTES = 5; + static constexpr int32_t VOTING_TIMEOUT_MS = 5000; + + using PaymentId = std::string; + + struct Collection + { + Disqualification2 di; + std::string item_str; + std::vector auths; + }; + + std::map m_map; + + crypto::public_key m_supernode_id; + crypto::secret_key m_secret_key; + std::string m_supernode_str_id; + + template + struct less_mem + { + static_assert(std::is_trivially_copyable::value); + bool operator() (const T& l, const T& r) + { + int res = std::memcmp(&l, &r, sizeof(T)); + return (res<0); + } + }; + +///////////////////// tools return error message if any +/// + static graft::Status setError(graft::Context& ctx, const std::string& msg, int code = 0) + { + count_errors(msg, code); + LOG_ERROR(msg); + ctx.local.setError(msg.c_str(), graft::Status::Error); + return graft::Status::Error; + }; + + template + static void bin_serialize(const T& t, std::string& str) + { + graft::Output out; + out.loadT(t); + str = out.body; + } + + template + static std::string bin_deserialize(const std::string& str, T& t) + { + graft::Input in; + in.body = str; + try + { + in.getT(t); + } + catch(std::exception& ex) + { + return ex.what(); + } + return ""; + } + + void sign(const std::string& str, crypto::signature& sig) + { + crypto::hash hash; + crypto::cn_fast_hash(str.data(), str.size(), hash); + crypto::generate_signature(hash, m_supernode_id, m_secret_key, sig); + } + + static bool check_sign(const std::string& str, const crypto::public_key id, const crypto::signature& sig) + { + crypto::hash hash; + crypto::cn_fast_hash(str.data(), str.size(), hash); + return crypto::check_signature(hash, id, sig); + } + +////////// + + void getSupenodeKeys(graft::Context& ctx, crypto::public_key& pub, crypto::secret_key& sec) + { + if(fnGetSupernodeKeys) + { + fnGetSupernodeKeys(pub, sec); + return; + } + //get m_supernode_id & m_supernode_str_id & m_secret_key + ctx.global.apply("supernode", + [&pub,&sec](graft::SupernodePtr& supernode)->bool + { + assert(supernode); + pub = supernode->idKey(); + sec = supernode->secretKey(); + return true; + }); + } + + bool getAuthSample(graft::Context& ctx, uint64_t& block_height, const std::string& payment_id, crypto::hash& block_hash, std::vector& auths) + { + if(fnGetAuthSample) + { + uint64_t tmp_block_number = block_height + BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT; + fnGetAuthSample(tmp_block_number, payment_id, block_hash, auths); + assert(tmp_block_number == block_height); + block_height = tmp_block_number; + return true; + } + graft::FullSupernodeList::supernode_array suAuthS; + bool res = ctx.global.apply>("fsl", + [&](boost::shared_ptr& fsl)->bool + { + if(!fsl) return false; + uint64_t tmp_block_number; + bool res = fsl->buildAuthSample(block_height+BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT, payment_id, suAuthS, tmp_block_number); + if(!res) return false; //block_height can be old + assert(tmp_block_number == block_height); + block_height = tmp_block_number; + std::string block_hash_str; + fsl->getBlockHash(block_height, block_hash_str); + bool res1 = epee::string_tools::hex_to_pod(block_hash_str, block_hash); + assert(res1); + return true; + }); + if(!res) return false; + + auths.clear(); + auths.reserve(suAuthS.size()); + for(auto& item : suAuthS) + { + auths.push_back(item->idKey()); + } + } + +/////////// phases +protected: + graft::Status initDisqualify(graft::Context& ctx, const std::string& payment_id) + { + auto it = m_map.find(payment_id); + assert(it == m_map.end()); + if(it != m_map.end()) + { + std::ostringstream oss; oss << " in initDisqualify payment id " << payment_id << " already known"; + return setError(ctx, oss.str()); + } + m_map[payment_id]; + return graft::Status::Ok; + } + + graft::Status startDisqualify(graft::Context& ctx, const std::string& payment_id, uint64_t block_height, const Ids& ids, graft::Output& output) + { + auto it = m_map.find(payment_id); + assert(it != m_map.end()); + if(it == m_map.end()) + { + std::ostringstream oss; oss << " in startDisqualify payment id " << payment_id << " unknown"; + return setError(ctx, oss.str()); + } + + if(ids.empty()) + { + m_map.erase(it); + return graft::Status::Ok; + } + + if(m_supernode_str_id.empty()) + {//get m_supernode_id & m_supernode_str_id & m_secret_key, once + getSupenodeKeys(ctx, m_supernode_id, m_secret_key); + m_supernode_str_id = epee::string_tools::pod_to_hex(m_supernode_id); + } + + //fill collection + Collection& coll = it->second; + assert(coll.di.item.payment_id.empty()); + assert(coll.item_str.empty()); + coll.di.item.payment_id = payment_id; + coll.di.item.block_height = block_height; + coll.di.item.ids = ids; + std::sort(coll.di.item.ids.begin(), coll.di.item.ids.end(), less_mem{}); + getAuthSample(ctx, block_height, payment_id, coll.di.item.block_hash, coll.auths); + std::sort(coll.auths.begin(), coll.auths.end(), less_mem{}); + {//remove me from auth sample + auto pair = std::equal_range(coll.auths.begin(), coll.auths.end(), m_supernode_id, less_mem{}); + assert(1 == std::distance(pair.first, pair.second)); + coll.auths.erase(pair.first); + } + //bin serialize coll.di.item + bin_serialize(coll.di.item, coll.item_str); + + //check already recieved signs + coll.di.signers.erase(std::remove_if(coll.di.signers.begin(), coll.di.signers.end(), + [&coll](auto& v)->bool + { + auto& [id, sign] = v; + if(!std::binary_search(coll.auths.cbegin(), coll.auths.cend(), id, less_mem{} )) + return true; //means remove + return !check_sign(coll.item_str, id, sign); //means remove + }), coll.di.signers.end()); + + Vote v; + v.payment_id = payment_id; + {//add my sign + SignerItem& si = v.si; + si.signer_id = m_supernode_id; + sign(coll.item_str, si.sign); + coll.di.signers.emplace_back( si ); + } + + //serialize + std::string v_str; + bin_serialize(v, v_str); + + //encrypt + std::string message; + graft::crypto_tools::encryptMessage(v_str, coll.auths, message); + + //multicast vote to the auth sample + MulticastRequestJsonRpc req; + for(auto& id : coll.auths) + { + req.params.receiver_addresses.push_back(epee::string_tools::pod_to_hex(id)); + } + req.method = "multicast"; + req.params.callback_uri = ROUTE_VOTES; + req.params.data = graft::utils::base64_encode(message); + req.params.sender_address = m_supernode_str_id; + + output.load(req); + output.path = "/json_rpc/rta"; + MDEBUG("multicasting: " << output.data()); + MDEBUG(__FUNCTION__ << " end"); + + auto onTimeout = [this,it](const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output)->graft::Status + { + std::lock_guard lk(m_mutex); + + Collection& coll = it->second; + bool enough = (REQUIRED_AUTHS_DISQULIFICATION_VOTES <= coll.di.signers.size()); + //TODO: remove excess votes to minimize transaction + if(enough) + { + //create extra from disqualification2 + std::vector extra; + extra.push_back(TX_EXTRA_GRAFT_DISQUALIFICATION_TAG); + std::string di_str; + bin_serialize(coll.di, di_str); + static_assert(sizeof(di_str[0]) == sizeof(uint8_t)); + std::copy(di_str.begin(), di_str.end(), std::back_inserter(extra)); + + //create transaction from extra + tools::wallet2::pending_tx ptx; + { + cryptonote::transaction& tx = ptx.tx; + tx.version = 124; + tx.extra = extra; + + crypto::public_key tmp; + crypto::generate_keys(tmp, ptx.tx_key); + + ptx.construction_data.extra = tx.extra; + ptx.construction_data.unlock_time = 0; //unlock_time; + ptx.construction_data.use_rct = false; + } + + if(fnCollectTxs) + { + std::vector txes { ptx }; + fnCollectTxs(&txes); + } + else + { + //create wallet + std::string addr = ctx.global["cryptonode_rpc_address"]; + bool testnet = ctx.global["testnet"]; + tools::wallet2 wallet(testnet); + wallet.init(addr); + wallet.set_refresh_from_block_height(coll.di.item.block_height); + wallet.set_seed_language("English"); + + try + { + wallet.commit_tx(ptx); + } + catch(std::exception& ex) + { + return setError(ctx, ex.what()); + } + } + } + + m_map.erase(it); + + return graft::Status::Stop; + }; + + bool enough = (REQUIRED_AUTHS_DISQULIFICATION_VOTES <= coll.di.signers.size()); + + if(fnAddPeriodic) + { + fnAddPeriodic( onTimeout, std::chrono::milliseconds(0), enough? std::chrono::milliseconds(1) : addPeriodic_interval_ms, 0 ); + } + else + { + assert(ctx.handlerAPI()); + ctx.handlerAPI()->addPeriodicTask( onTimeout, std::chrono::milliseconds(0), enough? std::chrono::milliseconds(1) : std::chrono::milliseconds(VOTING_TIMEOUT_MS), 0 ); + } + + return graft::Status::Forward; + } + + graft::Status handleVotes(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + MulticastRequestJsonRpc req; + bool res1 = input.get(req); + if(!res1) + { + return setError(ctx, "cannot deserialize MulticastRequestJsonRpc"); + } + + std::string message = graft::utils::base64_decode(req.params.data); + std::string plain; + bool res2 = graft::crypto_tools::decryptMessage(message, m_secret_key, plain); + if(!res2) + { + return setError(ctx, "cannot decrypt, the message is not for me"); + } + + Vote v; + std::string err = bin_deserialize(plain, v); + if(!err.empty()) + { + std::ostringstream oss; + oss << "cannot deserialize Vote, error: '" << err << "'"; + return setError(ctx, oss.str()); + } + + auto it = m_map.find(v.payment_id); + if(it == m_map.end()) + { + std::ostringstream oss; oss << "in " << __FUNCTION__ << " cannot find collection with payment id " + << v.payment_id << ". Probably such transaction already has been processed or is not started yet."; + return setError(ctx, oss.str(), 2001); + } + + Collection& coll = it->second; + + //check vote + if(!std::binary_search(coll.auths.cbegin(), coll.auths.cend(), v.si.signer_id, less_mem{} )) + { + std::ostringstream oss; oss << " signer " << v.si.signer_id + << " is not in the auth sample with payment id "<< v.payment_id; + return setError(ctx, oss.str()); + } + if(!check_sign(coll.item_str, v.si.signer_id, v.si.sign)) + { + std::ostringstream oss; oss << " invalid signature in vote of " << v.si.signer_id + << " for payment id "<< v.payment_id << ". Probably it is voting for different candidates. "; + return setError(ctx, oss.str()); + } + + //add vote + coll.di.signers.emplace_back( std::move(v.si) ); + + return graft::Status::Ok; + } + + static std::mutex m_mutex; + +public: + //supernodes ids + using Ids = std::vector; + + static graft::Status staticInitDisqualify(graft::Context& ctx, const std::string& payment_id) + { + std::lock_guard lk(m_mutex); + + if(!ctx.global.hasKey("AuthSDisqualificatorImpl")) + { + std::shared_ptr asd = std::make_shared(); + ctx.global["AuthSDisqualificatorImpl"] = asd; + } + std::shared_ptr asd = ctx.global["AuthSDisqualificatorImpl"]; + return asd->initDisqualify(ctx, payment_id); + } + + static graft::Status staticStartDisqualify(graft::Context& ctx, const std::string& payment_id, uint64_t block_height, const Ids& ids, graft::Output& output) + { + std::lock_guard lk(m_mutex); + + if(!ctx.global.hasKey("AuthSDisqualificatorImpl")) return graft::Status::Ok; + std::shared_ptr asd = ctx.global["AuthSDisqualificatorImpl"]; + return asd->startDisqualify(ctx, payment_id, block_height, ids, output); + } + + static graft::Status staticHandleVotes(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + std::lock_guard lk(m_mutex); + + if(!ctx.global.hasKey("AuthSDisqualificatorImpl")) return graft::Status::Error; + std::shared_ptr asd = ctx.global["AuthSDisqualificatorImpl"]; + return asd->handleVotes(vars, input, ctx, output); + } + + static constexpr const char* ROUTE_VOTES = "/supernode/disqualify2/votes"; + +}; + +std::mutex AuthSDisqualificatorImpl::m_mutex; + +} //namespace + +graft::Status AuthSDisqualificator::initDisqualify(graft::Context& ctx, const std::string& payment_id) +{ + return AuthSDisqualificatorImpl::staticInitDisqualify(ctx, payment_id); +} + +graft::Status AuthSDisqualificator::startDisqualify(graft::Context& ctx, const std::string& payment_id, uint64_t block_height, const Ids& ids, graft::Output& output) +{ + return AuthSDisqualificatorImpl::staticStartDisqualify(ctx, payment_id, block_height, ids, output); +} + +void registerAuthSDisqualificatorRequest(graft::Router &router) +{ + router.addRoute(AuthSDisqualificatorImpl::ROUTE_VOTES, METHOD_POST, {nullptr, AuthSDisqualificatorImpl::staticHandleVotes , nullptr}); +} + +namespace +{ + +class AuthSDisqualificatorTest : public AuthSDisqualificatorImpl +{ + graft::GlobalContextMap globalContextMap; + graft::Context ctx = graft::Context(globalContextMap); + virtual void process_command(const command& cmd, std::vector& forward, std::string& body, std::string& callback_uri) override + { + assert(!cmd.payment_id.empty()); + + graft::Status res; + graft::Output output; + if(cmd.uri.empty()) + { + if(cmd.block_height == 0) + { + res = testInitDisqualify(ctx, cmd.payment_id); + assert(res == graft::Status::Ok); + } + else + { + res = testStartDisqualify(ctx, cmd.payment_id, cmd.block_height, cmd.ids, output); + assert(cmd.ids.empty() && res == graft::Status::Ok || !cmd.ids.empty() && res == graft::Status::Forward); + } + } + else + { + graft::Input in; in.body = cmd.body; + assert(cmd.uri == ROUTE_VOTES); + res = testHandleVotes({}, in, ctx, output); + assert(cmd.ids.empty() || res == graft::Status::Ok || !cmd.ids.empty() && res == graft::Status::Forward + || res == graft::Status::Error); + } + if(res == graft::Status::Forward) + { + body = output.body; + MulticastRequestJsonRpc req; + graft::Input in; in.body = output.body; in.getT(req); + callback_uri = req.params.callback_uri; + for(auto& id_str : req.params.receiver_addresses) + { + crypto::public_key id; epee::string_tools::hex_to_pod(id_str, id); + forward.push_back(std::move(id)); + } + } + else + { + forward.clear(); body.clear(); callback_uri.clear(); + } + } + +///// tests + + graft::Status testInitDisqualify(graft::Context& ctx, const std::string& payment_id) + { + std::lock_guard lk(m_mutex); + return initDisqualify(ctx, payment_id); + } + + graft::Status testStartDisqualify(graft::Context& ctx, const std::string& payment_id, uint64_t block_height, const Ids& ids, graft::Output& output) + { + std::lock_guard lk(m_mutex); + return startDisqualify(ctx, payment_id, block_height, ids, output); + } + + graft::Status testHandleVotes(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + std::lock_guard lk(m_mutex); + return handleVotes(vars, input, ctx, output); + } + +///// + +public: + static std::mutex m_err_mutex; + static std::map m_errors; + static std::vector m_threads; + + AuthSDisqualificatorTest() + { + ctx.global["fsl"] = nullptr; + } +}; + +std::mutex AuthSDisqualificatorTest::m_err_mutex; +std::map AuthSDisqualificatorTest::m_errors; +std::vector AuthSDisqualificatorTest::m_threads; + +} //namespace + +std::unique_ptr BBLDisqualificatorBase::createTestAuthSDisqualificator( + GetSupernodeKeys fnGetSupernodeKeys, + GetAuthSample fnGetAuthSample, + CollectTxs fnCollectTxs, + std::chrono::milliseconds addPeriodic_interval_ms +) +{ + auto addPeriodic = [&](const graft::Router::Handler& h_worker, std::chrono::milliseconds interval_ms, std::chrono::milliseconds initial_interval_ms, double random_factor)->bool + { + const graft::Router::Handler worker = h_worker; + std::thread th( [worker, initial_interval_ms]()->void + { + std::this_thread::sleep_for(initial_interval_ms); + graft::Input input; + graft::GlobalContextMap globalContextMap; + graft::Context ctx = graft::Context(globalContextMap); + graft::Output output; + graft::Status res = worker({}, input, ctx, output); + assert(res == graft::Status::Stop); + } ); +// th.detach(); + AuthSDisqualificatorTest::m_threads.emplace_back(std::move(th)); + return true; + }; + + auto disq = std::make_unique(); + disq->fnGetSupernodeKeys = fnGetSupernodeKeys; + disq->fnGetAuthSample = fnGetAuthSample; + disq->fnCollectTxs = fnCollectTxs; + disq->fnAddPeriodic = addPeriodic; + disq->addPeriodic_interval_ms = addPeriodic_interval_ms; + return disq; +} + +void BBLDisqualificatorBase::waitForTestAuthSDisqualificator() +{ + for(auto& th : AuthSDisqualificatorTest::m_threads) + { + th.join(); + } + AuthSDisqualificatorTest::m_threads.clear(); +} + +} //namespace graft::supernode::request diff --git a/src/supernode/requests/blockchain_based_list.cpp b/src/supernode/requests/blockchain_based_list.cpp index 21ea62cf..6b3945f9 100644 --- a/src/supernode/requests/blockchain_based_list.cpp +++ b/src/supernode/requests/blockchain_based_list.cpp @@ -857,6 +857,14 @@ class BBLDisqualificator : public BBLDisqualificatorBase return graft::Status::Error; } public: + + //for test only + graft::Status processTest(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + std::lock_guard lk(m_mutex); + return do_process(vars, input, ctx, output); + } + static graft::Status process(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) { std::lock_guard lk(m_mutex); @@ -899,8 +907,8 @@ class BBLDisqualificator : public BBLDisqualificatorBase } #endif - static constexpr const char* ROUTE_PING_RESULT = "/cryptonode/ping_result"; - static constexpr const char* ROUTE_VOTES = "/cryptonode/votes"; + static constexpr const char* ROUTE_PING_RESULT = "/supernode/disqualify/ping_result"; + static constexpr const char* ROUTE_VOTES = "/supernode/disqualify/votes"; }; std::mutex BBLDisqualificator::m_mutex; @@ -920,7 +928,9 @@ class BBLDisqualificatorTest : public BBLDisqualificator req.params.block_hash = epee::string_tools::pod_to_hex(cmd.block_hash); graft::Output o; o.load(req); graft::Input in; in.body = o.body; - res = do_process({}, in, ctx, output); +//TODO: Something wrong, it is required attention +// res = do_process({}, in, ctx, output); + res = processTest({}, in, ctx, output); } else { diff --git a/test/disqualification_test.cpp b/test/disqualification_test.cpp index 16b4b069..8fa2e504 100644 --- a/test/disqualification_test.cpp +++ b/test/disqualification_test.cpp @@ -29,6 +29,7 @@ #include #include "supernode/requests/disqualificator.h" +#include "utils/sample_generator.h" #include @@ -155,6 +156,8 @@ TEST(Disqualificator, BBL) } } + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + for(int i = 0; i < N; ++i) { BBLDisqualificatorBase::command cmd; cmd.uri = "stop"; @@ -186,3 +189,154 @@ TEST(Disqualificator, BBL) std::cout << txs.size() << " disqualified txs \n"; EXPECT_EQ(txs.empty(), false); } + +TEST(Disqualificator, AuthS) +{ + using namespace graft::supernode::request; + + struct cmd_queue + { + std::mutex mutex; + std::deque deque; + }; + + //N - count or running SNs, DA - dead (never runnign) SNs that in auth sample + constexpr int N = 6, DA = 8-N; + static_assert(N+DA == 8); //8 == AUTH_SAMPLE_SIZE + crypto::public_key pubs[N+DA]; + crypto::secret_key secs[N+DA]; + cmd_queue queues[N+DA]; + std::map> pub2idx; + for(int i=0; i authS; + std::copy(&pubs[0], &pubs[N+DA], std::back_inserter(authS)); + + const int BLKS = 20; + crypto::hash block_hashes[BLKS]; + for(int i = 0; i < BLKS; ++i) + { + block_hashes[i] = crypto::cn_fast_hash(&i, sizeof(i)); + } + + auto getAuthS = [&](uint64_t& block_height, const std::string& payment_id, crypto::hash& block_hash, std::vector& auths) + { + block_height -= 10; //BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT + block_hash = block_hashes[block_height]; + auths = authS; + }; + + std::mutex mut_ptx; + std::vector txs; + auto collectTxs = [&](void* ptxs) + { + auto& txes = *reinterpret_cast< std::vector* >(ptxs); + std::lock_guard lk(mut_ptx); + txs.insert(txs.end(), txes.begin(), txes.end()); + }; + + auto run = [&](int i)->void + { + auto getMyKey= [&](crypto::public_key& pub, crypto::secret_key& sec)->void + { + pub = pubs[i]; sec = secs[i]; + }; + + auto disq = BBLDisqualificatorBase::createTestAuthSDisqualificator(getMyKey, getAuthS, collectTxs, std::chrono::milliseconds(2000)); + while(true) + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + BBLDisqualificatorBase::command cmd_in; + { + auto& cq = queues[i]; + std::lock_guard lk(cq.mutex); + if(cq.deque.empty()) continue; + cmd_in = cq.deque.front(); cq.deque.pop_front(); + } + if(cmd_in.uri == "stop") return; + + std::vector forward; + std::string body, callback_uri; + disq->process_command(cmd_in, forward, body, callback_uri); + for(auto& id : forward) + { + BBLDisqualificatorBase::command cmd(callback_uri, body); + cmd.payment_id = cmd_in.payment_id; + auto& cq = queues[pub2idx[id]]; + std::lock_guard lk(cq.mutex); + cq.deque.push_back(std::move(cmd)); + } + } + }; + + std::thread ths[N]; + for(int i=0; i < N; ++i) + { + ths[i] = std::thread(run, i); + } + + const std::string payment_id = "a value of payment"; + {// run initDisqualify + BBLDisqualificatorBase::command cmd( payment_id, {}, 0, block_hashes[0]); + for(int i=0; i < N; ++i) + { + auto& cq = queues[i]; + std::lock_guard lk(cq.mutex); + cq.deque.push_back(cmd); + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + {// run startDisqualify + std::vector ids; + ids.insert(ids.end(), &pubs[N], &pubs[N+DA]); + const int b_height = 10; //BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT) + BBLDisqualificatorBase::command cmd( payment_id, ids, b_height, block_hashes[b_height]); + for(int i=0; i < N; ++i) + { + auto& cq = queues[i]; + std::lock_guard lk(cq.mutex); + cq.deque.push_back(cmd); + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(3000)); + + BBLDisqualificatorBase::waitForTestAuthSDisqualificator(); + + for(int i = 0; i < N; ++i) + { + BBLDisqualificatorBase::command cmd; cmd.uri = "stop"; + auto& cq = queues[i]; + std::lock_guard lk(cq.mutex); + cq.deque.emplace_back(std::move(cmd)); + } + + for(auto& th : ths) + { + th.join(); + } + + { + auto& map = BBLDisqualificatorBase::get_errors(); + if(!map.empty()) + { + int total = 0; for(auto& v : map){ total+=v.second; } + std::cout << "\n" << total << " total errors count \n"; + for(auto& v : map) + { + std::cout << v.first << "->" << v.second << "\n"; + } + EXPECT_EQ(map.find(0), map.end()); + BBLDisqualificatorBase::clear_errors(); + } + } + + std::cout << txs.size() << " disqualified txs \n"; + EXPECT_EQ(txs.size(), N); +}