diff --git a/CMakeLists.txt b/CMakeLists.txt index a7e3fef8..293cb807 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -306,6 +306,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 @@ -430,6 +431,8 @@ 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/lib/graft/binary_serialize.h b/include/lib/graft/binary_serialize.h new file mode 100644 index 00000000..6674b4e0 --- /dev/null +++ b/include/lib/graft/binary_serialize.h @@ -0,0 +1,273 @@ +// 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 +#include +#include + +#include "lib/graft/inout.h" + +namespace graft { namespace serializer { + +namespace binary_details +{ + +template +using naked_t = std::remove_cv_t< std::remove_reference_t >; + +template +using to_void = void; // maps everything to void, used in non-evaluated contexts + +template +struct is_container_exact : std::false_type +{}; + +template +struct is_container_exact().begin()), + decltype(std::declval().end()), + typename C::value_type + >> : std::true_type // will be enabled for iterable objects +{}; + +template +using is_container = is_container_exact< naked_t >; + +static_assert( is_container::value ); +static_assert( is_container>::value ); +static_assert( is_container>::value ); +static_assert( is_container&>::value ); + +template +inline constexpr bool is_container_v = is_container::value; + +template +inline constexpr bool is_serializable_v = std::is_base_of>, naked_t>::value; + +template +struct is_trivial_class { static constexpr bool value = std::is_trivially_copyable>::value && std::is_class>::value; }; + +template +inline constexpr bool is_trivial_class_v = is_trivial_class::value; + +/////////////////////// +/*! \brief write_varint adopted from cryptonode. + */ +template +// Requires T to be both an integral type and unsigned, should be a compile error if it is not +void write_varint(Arch& ar, V i) { + // Make sure that there is one after this + while (i >= 0x80) { + char ch = (static_cast(i) & 0x7f) | 0x80; +// ++dest; + ar << ch; + i >>= 7; // I should be in multiples of 7, this should just get the next part + } + // writes the last one to dest +/* + *dest = static_cast(i); + dest++; // Seems kinda pointless... +*/ + ar << static_cast(i); +} + + +/*! \brief read_varint adopted from cryptonode. + */ +template +bool read_varint(Arch& ar, V& write) +{ + constexpr int bits = std::numeric_limits::digits; + int read = 0; + write = 0; + for (int shift = 0;; shift += 7) { +/* + if (first == last) + { + return read; + } +*/ +/* + unsigned char byte = *first; + ++first; +*/ + unsigned char byte; + ar >> byte; + + ++read; + + if (shift + 7 >= bits && byte >= 1 << (bits - shift)) { + return -1; //EVARINT_OVERFLOW; + } + if (byte == 0 && shift != 0) { + return -2; //EVARINT_REPRESENT; + } + + write |= static_cast(byte & 0x7f) << shift; /* Does the actualy placing into write, stripping the first bit */ + + /* If there is no next */ + if ((byte & 0x80) == 0) { + break; + } + } + return read; +} + +////////////// +// forward declarations +template +void bserialize(Arch& ar, V& v); + +template +void bdeserialize(Arch& ar, V& v); +////////////// + +template +typename std::enable_if< !is_serializable_v && !is_container_v && !is_trivial_class_v >::type +ser(Arch& ar, V& v) +{ + ar << v; +} + +template +typename std::enable_if< !is_serializable_v && !is_container_v && is_trivial_class_v >::type +ser(Arch& ar, V& v) +{ + const uint8_t* p = reinterpret_cast(&v); + for(int i=0; i +typename std::enable_if< is_container_v >::type //it means also !is_serializable_v +ser(Arch& ar, V& v) +{ + size_t size = v.size(); + write_varint(ar, size); + std::for_each(v.begin(), v.end(), [&](auto& item) + { + bserialize(ar, item); + }); +} + +template +typename std::enable_if< is_serializable_v >::type +ser(Arch& ar, V& v) +{ + boost::hana::for_each(boost::hana::keys(v), [&](auto key) + { + const auto& member = boost::hana::at_key(v, key); + ser(ar, member); + }); +} + +template +void bserialize(Arch& ar, V& v) +{ + ser(ar, v); +} + +template +typename std::enable_if< !is_serializable_v && !is_container_v && !is_trivial_class_v >::type +deser(Arch& ar, V& v) +{ + ar >> v; +} + +template +typename std::enable_if< !is_serializable_v && !is_container_v && is_trivial_class_v >::type +deser(Arch& ar, V& v) +{ + uint8_t* p = reinterpret_cast(&v); + for(int i=0; i> *p; + } +} + +template +typename std::enable_if< is_container_v >::type //it means also !is_serializable_v +deser(Arch& ar, V& v) +{ + size_t size; + read_varint(ar, size); + v.resize(size); + std::for_each(v.begin(), v.end(), [&](auto& item) + { + bdeserialize(ar, item); + }); +} + +template +typename std::enable_if< is_serializable_v >::type +deser(Arch& ar, V& v) +{ + boost::hana::for_each(boost::hana::keys(v), [&](auto key) + { + auto& member = boost::hana::at_key(v, key); + deser(ar, member); + }); +} + + +template +void bdeserialize(Arch& ar, V& v) +{ + deser(ar, v); +} + +} //namespace binary_details + +//It is expected that serialize and deserialize throw exception on error +template +class Binary +{ +public: + static std::string serialize(const T& t) + { + std::ostringstream oss; + boost::archive::binary_oarchive ar(oss, boost::archive::no_header); + binary_details::bserialize(ar, t); + return oss.str(); + } + + static void deserialize(const std::string& s, T& t) + { + std::istringstream iss(s); + boost::archive::binary_iarchive ar(iss, boost::archive::no_header); + binary_details::bdeserialize(ar,t); + } +}; + +} } //namespace graft { namespace serializer + + diff --git a/include/rta/fullsupernodelist.h b/include/rta/fullsupernodelist.h index 44c8a8c3..01d10e0c 100644 --- a/include/rta/fullsupernodelist.h +++ b/include/rta/fullsupernodelist.h @@ -1,7 +1,4 @@ -#ifndef FULLSUPERNODELIST_H -#define FULLSUPERNODELIST_H - -#include +#pragma once #include "rta/supernode.h" #include "rta/DaemonRpcClient.h" @@ -9,19 +6,18 @@ #include #include #include -#include #include #include -#include - namespace graft { +//TODO: is it required? +/* namespace utils { class ThreadPool; } - +*/ class FullSupernodeList { @@ -29,8 +25,11 @@ class FullSupernodeList static constexpr int32_t TIERS = 4; static constexpr int32_t ITEMS_PER_TIER = 2; static constexpr int32_t AUTH_SAMPLE_SIZE = TIERS * ITEMS_PER_TIER; + static constexpr int32_t DISQUALIFICATION_SAMPLE_SIZE = AUTH_SAMPLE_SIZE; + static constexpr int32_t DISQUALIFICATION_CANDIDATES_SIZE = AUTH_SAMPLE_SIZE; static constexpr int64_t AUTH_SAMPLE_HASH_HEIGHT = 20; // block number for calculating auth sample should be calculated as current block height - AUTH_SAMPLE_HASH_HEIGHT; static constexpr int64_t ANNOUNCE_TTL_SECONDS = 60 * 60; // if more than ANNOUNCE_TTL_SECONDS passed from last annouce - supernode excluded from auth sample selection + static constexpr size_t BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT = 10; // count of blocks to step back in history to get stable picture of supernodes subsets, it used for generation of BBQS and QCL FullSupernodeList(const std::string &daemon_address, bool testnet = false); ~FullSupernodeList(); @@ -98,6 +97,16 @@ class FullSupernodeList bool buildAuthSample(const std::string& payment_id, supernode_array &out, uint64_t &out_auth_block_number); + /*! + * \brief buildDisqualificationSamples - builds disqualification samples for given block height + * \param height - block height used to perform selectio + * \param out_bbqs - vector of supernode pointers which should check other nodes + * \param out_qcl - vector of supernode pointers which should be checked + * \param out_block_number - block number which was used for samples + * \return - true on success + */ + bool buildDisqualificationSamples(uint64_t height, supernode_array &out_bbqs, supernode_array &out_qcl, uint64_t &out_block_number); + /*! * \brief items - returns address list of known supernodes * \return @@ -118,7 +127,8 @@ class FullSupernodeList * * \return - std::future to wait for result */ - std::future refreshAsync(); + //TODO: is it required? +// std::future refreshAsync(); /*! * \brief refreshedItems - returns number of refreshed supernodes @@ -143,8 +153,18 @@ class FullSupernodeList }; typedef std::vector blockchain_based_list_tier; - typedef std::vector blockchain_based_list; - typedef std::shared_ptr blockchain_based_list_ptr; + typedef std::vector blockchain_based_list_tier_array; + + struct blockchain_based_list + { + std::string block_hash; + blockchain_based_list_tier_array tiers; + + blockchain_based_list() {} + blockchain_based_list(const std::string& in_block_hash) : block_hash(in_block_hash) {} + }; + + typedef std::shared_ptr blockchain_based_list_ptr; /*! * \brief setBlockchainBasedList - updates full list of supernodes @@ -203,7 +223,7 @@ class FullSupernodeList private: // bool loadWallet(const std::string &wallet_path); void addImpl(SupernodePtr item); - bool selectSupernodes(size_t items_count, const std::string& payment_id, const blockchain_based_list_tier& src_array, supernode_array& dst_array); + bool buildSample(const blockchain_based_list& bbl, size_t sample_size, const char* prefix, supernode_array &out); typedef std::unordered_map blockchain_based_list_map; @@ -214,12 +234,11 @@ class FullSupernodeList bool m_testnet; mutable DaemonRpcClient m_rpc_client; mutable boost::shared_mutex m_access; - std::unique_ptr m_tp; +// std::unique_ptr m_tp; std::atomic_size_t m_refresh_counter; uint64_t m_blockchain_based_list_max_block_number; uint64_t m_stakes_max_block_number; blockchain_based_list_map m_blockchain_based_lists; - std::mt19937_64 m_rng; boost::posix_time::ptime m_next_recv_stakes; boost::posix_time::ptime m_next_recv_blockchain_based_list; }; @@ -230,5 +249,3 @@ std::ostream& operator<<(std::ostream& os, const std::vector super } // namespace graft - -#endif // FULLSUPERNODELIST_H 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/blockchain_based_list.h b/include/supernode/requests/blockchain_based_list.h index 73f79170..e6a4f483 100644 --- a/include/supernode/requests/blockchain_based_list.h +++ b/include/supernode/requests/blockchain_based_list.h @@ -18,6 +18,7 @@ GRAFT_DEFINE_IO_STRUCT_INITED(BlockchainBasedListTier, GRAFT_DEFINE_IO_STRUCT_INITED(BlockchainBasedList, (uint64_t, block_height, uint64_t()), + (std::string, block_hash, std::string()), (std::vector, tiers, std::vector()) ); diff --git a/include/supernode/requests/disqualificator.h b/include/supernode/requests/disqualificator.h new file mode 100644 index 00000000..2ff9b3c8 --- /dev/null +++ b/include/supernode/requests/disqualificator.h @@ -0,0 +1,105 @@ +// 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/router.h" + +#include + +#include +#include + +namespace graft::supernode::request +{ + +//The class is used for tests +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, + GetBBQSandQCL fnGetBBQSandQCL, + 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 + // if uri.empty() + uint64 block_height; + crypto::hash block_hash; + // 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) { } + }; + + virtual void process_command(const command& cmd, std::vector& forward, std::string& body, std::string& callback_uri) + { + assert(false); + } + + static void count_errors(const std::string& msg, int code=0); + static const std::map& get_errors(); + static void clear_errors(); +protected: + GetSupernodeKeys fnGetSupernodeKeys; + GetBBQSandQCL fnGetBBQSandQCL; + GetAuthSample fnGetAuthSample; + CollectTxs fnCollectTxs; + AddPeriodic fnAddPeriodic; + std::chrono::milliseconds addPeriodic_interval_ms; +}; + +} //namespace graft::supernode::request diff --git a/include/supernode/requests/send_supernode_stakes.h b/include/supernode/requests/send_supernode_stakes.h index b098deab..064e1aa3 100644 --- a/include/supernode/requests/send_supernode_stakes.h +++ b/include/supernode/requests/send_supernode_stakes.h @@ -17,7 +17,8 @@ GRAFT_DEFINE_IO_STRUCT_INITED(SupernodeStake, GRAFT_DEFINE_IO_STRUCT_INITED(SupernodeStakes, (uint64_t, block_height, 0), - (std::vector, stakes, std::vector()) + (std::vector, stakes, std::vector()), + (std::vector, disqualifications, {}) //tx_extra_graft_disqualification; ); GRAFT_DEFINE_IO_STRUCT_INITED(SendSupernodeStakesResponse, diff --git a/src/rta/fullsupernodelist.cpp b/src/rta/fullsupernodelist.cpp index f671725a..3c540338 100644 --- a/src/rta/fullsupernodelist.cpp +++ b/src/rta/fullsupernodelist.cpp @@ -1,5 +1,7 @@ #include "rta/fullsupernodelist.h" +#include "lib/graft/graft_exception.h" +#include #include #include #include @@ -10,14 +12,15 @@ #include #include -#include +//TODO: is it required? +//#include +//#include #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "supernode.fullsupernodelist" constexpr size_t STAKES_RECV_TIMEOUT_SECONDS = 600; constexpr size_t BLOCKCHAIN_BASED_LIST_RECV_TIMEOUT_SECONDS = 180; -constexpr size_t BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT = 10; constexpr size_t REPEATED_REQUEST_DELAY_SECONDS = 10; namespace fs = boost::filesystem; @@ -64,10 +67,12 @@ namespace { return result; } -} +} //namespace namespace graft { +//TODO: is it required? +/* namespace utils { class ThreadPool @@ -121,7 +126,52 @@ std::future ThreadPool::runAsync() } } // namespace utils; +*/ + +namespace +{ + +using TI = std::pair; //tier, index in the tier +constexpr int32_t TIERS = FullSupernodeList::TIERS; +using blockchain_based_list = FullSupernodeList::blockchain_based_list; +using supernode_array = FullSupernodeList::supernode_array; +using m_list_t = std::unordered_map; + +std::array, TIERS> makeBBLindexes(const blockchain_based_list& bbl) +{ + std::array, TIERS> res; + for(size_t t=0; tTI{ return std::make_pair(t, idx++); } ); + } + return res; +} +supernode_array fromIndexes(const blockchain_based_list& bbl, const std::vector& idxs, const m_list_t& m_list) +{ + supernode_array res; + res.reserve(idxs.size()); + for(auto& [t,i] : idxs) + { + auto& supernode_public_id = bbl.tiers[t][i].supernode_public_id; + + auto supernode_it = m_list.find(supernode_public_id); + if(supernode_it == m_list.end()) + { + std::ostringstream oss; + oss << "attempt to select unknown supernode " << supernode_public_id; + throw graft::exit_error(oss.str()); + } + const SupernodePtr& supernode = supernode_it->second; + res.push_back(supernode); + } + return res; +} + +} //namespace #ifndef __cpp_inline_variables constexpr int32_t FullSupernodeList::TIERS, FullSupernodeList::ITEMS_PER_TIER, FullSupernodeList::AUTH_SAMPLE_SIZE; @@ -132,7 +182,7 @@ FullSupernodeList::FullSupernodeList(const string &daemon_address, bool testnet) : m_daemon_address(daemon_address) , m_testnet(testnet) , m_rpc_client(daemon_address, "", "") - , m_tp(new utils::ThreadPool()) +// , m_tp(new utils::ThreadPool()) , m_blockchain_based_list_max_block_number() , m_stakes_max_block_number() , m_next_recv_stakes(boost::date_time::not_a_date_time) @@ -240,44 +290,6 @@ SupernodePtr FullSupernodeList::get(const string &address) const return SupernodePtr(nullptr); } -bool FullSupernodeList::selectSupernodes(size_t items_count, const std::string& payment_id, const blockchain_based_list_tier& src_array, supernode_array& dst_array) -{ - size_t src_array_size = src_array.size(); - - if (items_count > src_array_size) - items_count = src_array_size; - - for (size_t i=0; isecond; - - size_t random_value = m_rng(); - - MDEBUG(".....select random value " << random_value << " items count is " << items_count << " with clamp to " << (src_array_size - i) << " items; result is " << (random_value % (src_array_size - i))); - - random_value %= src_array_size - i; - - if (random_value >= items_count) - continue; - - MDEBUG(".....supernode " << src_array[i].supernode_public_id << " has been selected"); - - dst_array.push_back(supernode); - - items_count--; - } - - return true; -} - uint64_t FullSupernodeList::getBlockchainBasedListForAuthSample(uint64_t block_number, blockchain_based_list& list) const { boost::shared_lock readerLock(m_access); @@ -292,7 +304,9 @@ uint64_t FullSupernodeList::getBlockchainBasedListForAuthSample(uint64_t block_n blockchain_based_list result; blockchain_based_list_ptr bbl = it->second; - for (blockchain_based_list_tier& src : *bbl) + result.block_hash = bbl->block_hash; + + for (blockchain_based_list_tier& src : bbl->tiers) { blockchain_based_list_tier dst; @@ -312,14 +326,26 @@ uint64_t FullSupernodeList::getBlockchainBasedListForAuthSample(uint64_t block_n return true; }); - result.emplace_back(std::move(dst)); + result.tiers.emplace_back(std::move(dst)); } - list.swap(result); + list = std::move(result); return blockchain_based_list_height; } +bool FullSupernodeList::buildSample(const blockchain_based_list& bbl, size_t sample_size, const char* prefix, supernode_array &out) +{ + std::array, TIERS> idxs = makeBBLindexes(bbl); + + std::vector dst; + bool res = generator::selectSample(sample_size, idxs, dst, prefix); + + supernode_array sa = fromIndexes(bbl, dst, m_list); + out.swap(sa); + return res; +} + bool FullSupernodeList::buildAuthSample(uint64_t height, const std::string& payment_id, supernode_array &out, uint64_t &out_auth_block_number) { blockchain_based_list bbl; @@ -335,103 +361,49 @@ bool FullSupernodeList::buildAuthSample(uint64_t height, const std::string& paym MDEBUG("building auth sample for height " << height << " (blockchain_based_list_height=" << out_auth_block_number << ") and PaymentID '" << payment_id << "'"); - std::array tier_supernodes; - { - boost::unique_lock writerLock(m_access); - - //seed RNG - - std::seed_seq seed(reinterpret_cast(payment_id.c_str()), - reinterpret_cast(payment_id.c_str() + payment_id.size())); - - m_rng.seed(seed); - - //select supernodes for a full supernode list - - MDEBUG("use blockchain based list for height " << out_auth_block_number); - int t = 1; - for (const blockchain_based_list_tier& l : bbl) - { - MDEBUG("...tier #" << t); - int j=0; - for (const blockchain_based_list_entry& e : l) - MDEBUG(".....[" << j++ << "]=" << e.supernode_public_id); - t++; - } + boost::unique_lock writerLock(m_access); - for (size_t i=0, tiers_count=bbl.size(); i select; - select.fill(ITEMS_PER_TIER); - // If we are short of the needed SNs on any tier try selecting additional SNs from the highest - // tier with surplus SNs. For example, if tier 2 is short by 1, look for a surplus first at - // tier 4, then tier 3, then tier 1. - for (int i = 0; i < TIERS; i++) { - int deficit_i = select[i] - int(tier_supernodes[i].size()); - for (int j = TIERS-1; deficit_i > 0 && j >= 0; j--) { - if (i == j) continue; - int surplus_j = int(tier_supernodes[j].size()) - select[j]; - if (surplus_j > 0) { - // Tier j has more SNs than needed, so select an extra SN from tier j to make up for - // the deficiency at tier i. - int transfer = std::min(deficit_i, surplus_j); - select[i] -= transfer; - select[j] += transfer; - deficit_i -= transfer; - } - } - // If we still have a deficit then no other tier has a surplus; we'll just have to work with - // a smaller sample because there aren't enough SNs on the entire network. - if (deficit_i > 0) - select[i] -= deficit_i; - } + out_block_number = getBlockchainBasedListForAuthSample(height, bbl); - out.clear(); - out.reserve(ITEMS_PER_TIER * TIERS); - auto out_it = back_inserter(out); - for (int i = 0; i < TIERS; i++) { - std::copy(tier_supernodes[i].begin(), tier_supernodes[i].begin() + select[i], out_it); + if (!out_block_number) + { + LOG_ERROR("unable to build disqualification samples for block height " << height << " (blockchain_based_list_height=" << (height - BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT) << ") " + ". Blockchain based list for this block is absent, latest block is " << getBlockchainBasedListMaxBlockNumber()); + return false; } - if (VLOG_IS_ON(2)) { - std::string auth_sample_str, tier_sample_str; - for (const auto &a : out) { - auth_sample_str += a->idKeyAsString() + "\n"; - } - for (size_t i = 0; i < select.size(); i++) { - if (i > 0) tier_sample_str += ", "; - tier_sample_str += std::to_string(select[i]) + " T" + std::to_string(i+1); - } - MDEBUG("selected " << tier_sample_str << " supernodes of " << size() << " for auth sample"); - MTRACE("auth sample: \n" << auth_sample_str); - } + MDEBUG("building disqualification samples for height " << height << " (blockchain_based_list_height=" << out_block_number << ") and hash=" << bbl.block_hash); - if (out.size() > AUTH_SAMPLE_SIZE) - out.resize(AUTH_SAMPLE_SIZE); + std::array, TIERS> idxs = makeBBLindexes(bbl); - MDEBUG("..." << out.size() << " supernodes has been selected"); + boost::unique_lock writerLock(m_access); - return out.size() == AUTH_SAMPLE_SIZE; -} + crypto::hash block_hash; + bool ok = epee::string_tools::hex_to_pod(bbl.block_hash, block_hash); + assert(ok); -bool FullSupernodeList::buildAuthSample(const string &payment_id, FullSupernodeList::supernode_array &out, uint64_t &out_auth_block_number) -{ - return buildAuthSample(getBlockchainBasedListMaxBlockNumber(), payment_id, out, out_auth_block_number); + std::vector idxs_bbqs, idxs_qcl; + bool res = generator::select_BBQS_QCL(block_hash, idxs, idxs_bbqs, idxs_qcl); + + out_bbqs = fromIndexes(bbl, idxs_bbqs, m_list); + out_qcl = fromIndexes(bbl, idxs_qcl, m_list); + + return res; } vector FullSupernodeList::items() const @@ -451,6 +423,8 @@ bool FullSupernodeList::getBlockHash(uint64_t height, string &hash) return result; } +//TODO: is it required? +/* std::future FullSupernodeList::refreshAsync() { m_refresh_counter = 0; @@ -469,6 +443,7 @@ std::future FullSupernodeList::refreshAsync() return m_tp->runAsync(); } +*/ size_t FullSupernodeList::refreshedItems() const { @@ -575,17 +550,6 @@ void FullSupernodeList::setBlockchainBasedList(uint64_t block_number, const bloc { boost::unique_lock writerLock(m_access); - MDEBUG("update blockchain based list for height " << block_number); - int t = 1; - for (const blockchain_based_list_tier& l : *list) - { - MDEBUG("...tier #" << t); - int j=0; - for (const blockchain_based_list_entry& e : l) - MDEBUG(".....[" << j++ << "]=" << e.supernode_public_id); - t++; - } - blockchain_based_list_map::iterator it = m_blockchain_based_lists.find(block_number); if (it != m_blockchain_based_lists.end()) @@ -637,7 +601,7 @@ size_t FullSupernodeList::getSupernodeBlockchainBasedListTier(const std::string& size_t tier_number = 1; - for (const blockchain_based_list_tier& tier : *list) + for (const blockchain_based_list_tier& tier : list->tiers) { for (const blockchain_based_list_entry& sn : tier) if (sn.supernode_public_id == supernode_public_id) 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..b0d6b1ba --- /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? cryptonote::TESTNET : cryptonote::MAINNET); + 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/authorize_rta_tx.cpp b/src/supernode/requests/authorize_rta_tx.cpp index bb18381f..92fb9f70 100644 --- a/src/supernode/requests/authorize_rta_tx.cpp +++ b/src/supernode/requests/authorize_rta_tx.cpp @@ -51,7 +51,7 @@ namespace graft::supernode::request { GRAFT_DEFINE_IO_STRUCT_INITED(SupernodeSignature, (std::string, id_key, std::string()), - (std::string, result_signature, std::string()), // signarure for tx_id + result + (std::string, result_signature, std::string()), // signature for tx_id + result (std::string, tx_signature, std::string()) // signature for tx_id only ); diff --git a/src/supernode/requests/blockchain_based_list.cpp b/src/supernode/requests/blockchain_based_list.cpp index 8cc82696..fb76d3cf 100644 --- a/src/supernode/requests/blockchain_based_list.cpp +++ b/src/supernode/requests/blockchain_based_list.cpp @@ -26,21 +26,987 @@ // 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. +/////////////// phases scheme +/// on the trigger of blockchain increased +/// n+1 ----------------------- +/// get Blockchain-based List +/// get hash(es) of previous Blockchain-based List(s) +/// create BBQS & QCL from BBL +/// n+2 ----------------------- +/// if the supernode in the QCL it Forward "multicast" MRME (self sing (SN ID | block_height)) to BBQS +/// n+3 ----------------------- +/// if SN is in BBQS +/// make list of non-answered IDs (DL(n+1) = (QCL(n+1) - (answered IDs)) +/// Forward "multicast" MRME (DL(n+)) to BBQS +/// n+4 ----------------------- +/// if SN is in BBQS +/// make disqualification list with sign set of each item. (id (signes > 66% of BBQS)) +/// create disqualification transaction (type 1) +/// send it to the blockchain +/// +/// n+5 = (n+4) + 1 +/// +/// a handler that validates and collects "ping" responses (if SN is in BBQS) [n+1..n+3). +/// +/// a handler that validates and collects DLs (if SN is in BBQS) [n+2..n+4). +/// + #include "supernode/requests/blockchain_based_list.h" #include "supernode/requestdefines.h" +#include "supernode/requests/multicast.h" +#include "supernode/requests/disqualificator.h" #include "rta/fullsupernodelist.h" #include "rta/supernode.h" +#include "lib/graft/binary_serialize.h" #include #include +#include + +#define tst false + #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "supernode.blockchainbasedlistrequest" +namespace +{ + +static const char* PATH = "/blockchain_based_list"; + +GRAFT_DEFINE_IO_STRUCT(PingMessage, + (uint64_t, block_height), + (crypto::hash, block_hash), + (crypto::public_key, id) + ); + +GRAFT_DEFINE_IO_STRUCT(SignedPingMessage, + (PingMessage, pm), + (crypto::signature, self_sign) + ); + +GRAFT_DEFINE_IO_STRUCT(DisqualificationItem, + (uint64_t, block_height), + (crypto::hash, block_hash), + (crypto::public_key, id) + ); + +GRAFT_DEFINE_IO_STRUCT(DisqualificationVotes, + (uint64_t, block_height), + (crypto::hash, block_hash), + (crypto::public_key, signer_id), + (std::vector, ids), + (std::vector, signs) //signs of DisqualificationItem with correcponding ids + ); + +GRAFT_DEFINE_IO_STRUCT(SignerItem, + (crypto::public_key, signer_id), + (crypto::signature, sign) + ); + +GRAFT_DEFINE_IO_STRUCT(Disqualification, + (DisqualificationItem, item), + (std::vector, signers) + ); + +} //namespace + +namespace graft::supernode::request { + namespace { - static const char* PATH = "/blockchain_based_list"; + +class BBLDisqualificator : public BBLDisqualificatorBase +{ + static constexpr int32_t DESIRED_BBQS_SIZE = graft::FullSupernodeList::DISQUALIFICATION_SAMPLE_SIZE; + static constexpr int32_t DESIRED_QCL_SIZE = graft::FullSupernodeList::DISQUALIFICATION_CANDIDATES_SIZE; + static constexpr int32_t REQUIRED_BBQS_VOTES = (DESIRED_BBQS_SIZE*2 + (3-1))/3; + static constexpr size_t BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT = graft::FullSupernodeList::BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT; + static constexpr size_t DISQUALIFICATION_DURATION_BLOCK_COUNT = 10; + + enum Phases : int + { + phase_1, + phase_2, + phase_3, + phase_4, + phases_count + }; + + static std::mutex m_mutex; + + bool m_started = false; + + uint64_t m_block_height = 0; + crypto::hash m_block_hash; + //Blockchain Based Qualification Sample, exclude itself + std::vector m_bbqs_ids; + //Qualification Candidate List, exclude itself + std::vector m_qcl_ids; + + std::vector m_answered_ids; + + crypto::public_key m_supernode_id; + crypto::secret_key m_secret_key; + + std::vector m_bbqs_str_ids; + std::string m_supernode_str_id; + + + bool m_in_bbqs = false; + bool m_in_qcl = false; + bool m_collectPings = false; + bool m_collectVotes = false; + + using DisqId = crypto::public_key; + using SignerId = crypto::public_key; + using Sign = crypto::signature; + + 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); + } + }; + + std::map>, less_mem > m_votes; + +///////////////////// 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 ""; + } + + template + static void json_serialize(const T& t, std::string& str) + { + graft::Output out; + out.load(t); + str = out.body; + } + + template + static bool json_deserialize(const std::string& str, T& t) + { + graft::Input in; + in.body = str; + return in.get(t); + } + + 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); + } + + 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 getBBQSandQCL(graft::Context& ctx, uint64_t& block_height, crypto::hash& block_hash, std::vector& bbqs, std::vector& qcl) + { + if(fnGetBBQSandQCL) + { + uint64_t tmp_block_number = block_height + BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT; + fnGetBBQSandQCL(tmp_block_number, block_hash, bbqs, qcl); + assert(tmp_block_number == block_height); + block_height = tmp_block_number; + return true; + } + graft::FullSupernodeList::supernode_array suBBQS, suQCL; + bool res = ctx.global.apply>("fsl", + [&](boost::shared_ptr& fsl)->bool + { + if(!fsl) return false; + uint64_t tmp_block_number; + bool res = fsl->buildDisqualificationSamples(block_height+BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT, suBBQS, suQCL, 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; + + bbqs.clear(); + bbqs.reserve(suBBQS.size()); + for(auto& item : suBBQS) + { + bbqs.push_back(item->idKey()); + } + + qcl.clear(); + qcl.reserve(suQCL.size()); + for(auto& item : suQCL) + { + qcl.push_back(item->idKey()); + } + } + +///////////////////// phases +protected: + graft::Status do_phase1(graft::Context& ctx, uint64_t block_height) + { + m_started = true; + m_block_height = block_height; + + {//get m_supernode_id & m_supernode_str_id & m_secret_key + getSupenodeKeys(ctx, m_supernode_id, m_secret_key); + m_supernode_str_id = epee::string_tools::pod_to_hex(m_supernode_id); + } + + {//generate BBQS & QCL + m_block_height = block_height; + bool res = getBBQSandQCL(ctx, m_block_height, m_block_hash, m_bbqs_ids, m_qcl_ids); + if(!res) return graft::Status::Error; + } + + std::sort(m_bbqs_ids.begin(), m_bbqs_ids.end(), less_mem{}); + std::sort(m_qcl_ids.begin(), m_qcl_ids.end(), less_mem{}); + + {//set m_in_bbqs + auto pair = std::equal_range(m_bbqs_ids.begin(), m_bbqs_ids.end(), m_supernode_id, less_mem{}); + assert(pair.first == pair.second || std::distance(pair.first, pair.second) == 1 ); + m_in_bbqs = (pair.first != pair.second); + if(m_in_bbqs) + { + m_bbqs_ids.erase(pair.first, pair.second); + } + } + {//set m_in_qcl + auto pair = std::equal_range(m_qcl_ids.begin(), m_qcl_ids.end(), m_supernode_id, less_mem{}); + assert(pair.first == pair.second || std::distance(pair.first, pair.second) == 1 ); + m_in_qcl = (pair.first != pair.second); + if(m_in_qcl) + { + m_qcl_ids.erase(pair.first, pair.second); + } + } + + //make m_bbqs_str_ids + m_bbqs_str_ids.reserve(m_bbqs_ids.size()); + for(auto& id : m_bbqs_ids) + { + m_bbqs_str_ids.push_back( epee::string_tools::pod_to_hex(id) ); + } + + m_answered_ids.clear(); + m_collectPings = m_in_bbqs; + + return graft::Status::Ok; + } + + graft::Status do_phase2(graft::Context& ctx, graft::Output& output) + { + if(!m_started) return graft::Status::Ok; + m_collectVotes = m_in_bbqs; + if(m_collectVotes) m_votes.clear(); + if(!m_in_qcl) return graft::Status::Ok; + + std::string spm_str; + {//prepare bin serialized SignedPingMessage + SignedPingMessage spm; + spm.pm.block_height = m_block_height; + spm.pm.block_hash = m_block_hash; + spm.pm.id = m_supernode_id; + + //sign + std::string pm_str; + bin_serialize(spm.pm, pm_str); + sign(pm_str, spm.self_sign); + + //serialize + bin_serialize(spm, spm_str); + } + //encrypt + std::string message; + graft::crypto_tools::encryptMessage(spm_str, m_bbqs_ids, message); + + //multicast to m_bbqs_ids + MulticastRequestJsonRpc req; + req.params.receiver_addresses = m_bbqs_str_ids; + req.method = "multicast"; + req.params.callback_uri = ROUTE_PING_RESULT; //"/cryptonode/ping_result"; + 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"); + return graft::Status::Forward; + } + + graft::Status handle_phase2(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + if(!m_started || !m_collectPings) return graft::Status::Ok; + if(!m_in_bbqs) + { + return setError(ctx, "unexpected call of handle_phase2, not in BBQS"); + } + + 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"); + } + + SignedPingMessage spm; + std::string err = bin_deserialize(plain, spm); + if(!err.empty()) + { + std::ostringstream oss; + oss << "cannot deserialize SignedPingMessage, error: '" << err << "'"; + return setError(ctx, oss.str()); + } + + //check reliability + if(spm.pm.block_height != m_block_height) + { + std::ostringstream oss; + oss << "invalid block_height " << spm.pm.block_height << " expected " << m_block_height; + return setError(ctx, oss.str(), 201); + } + if(spm.pm.block_hash != m_block_hash) + { + std::ostringstream oss; + oss << "invalid block block_hash " << spm.pm.block_hash << " expected " << m_block_hash; + return setError(ctx, oss.str()); + } + if(std::none_of(m_qcl_ids.begin(), m_qcl_ids.end(), [&spm](auto& it){ return it == spm.pm.id; } )) + { + std::ostringstream oss; oss << "the id '" << spm.pm.id << "' is not in QCL"; + return setError(ctx, oss.str()); + } + + {//check sign of spm + std::string pm_str; + bin_serialize(spm.pm, pm_str); + bool res = check_sign(pm_str, spm.pm.id, spm.self_sign); + if(!res) + { + std::ostringstream oss; + oss << "invalid self signature '" << spm.self_sign << "' of id '" << spm.pm.id << "'"; + return setError(ctx, oss.str()); + } + + } + + //save SN id + m_answered_ids.push_back(spm.pm.id); + + return graft::Status::Ok; + } + + graft::Status do_phase3(graft::Context& ctx, graft::Output& output) + { + if(!m_started || !m_in_bbqs) return graft::Status::Ok; + m_collectPings = false; + + //find difference (m_qcl_ids) - (m_answered_ids) + std::sort(m_qcl_ids.begin(), m_qcl_ids.end(), less_mem{}); + m_qcl_ids.erase( std::unique( m_qcl_ids.begin(), m_qcl_ids.end() ), m_qcl_ids.end() ); + + std::sort(m_answered_ids.begin(), m_answered_ids.end(), less_mem{}); + m_answered_ids.erase( std::unique( m_answered_ids.begin(), m_answered_ids.end() ), m_answered_ids.end() ); + + std::vector diff; + std::set_difference(m_qcl_ids.begin(), m_qcl_ids.end(), m_answered_ids.begin(), m_answered_ids.end(), std::back_inserter(diff), less_mem{} ); + LOG_PRINT_L1("non-answered qcl size: ") << diff.size(); + if(diff.empty()) return graft::Status::Ok; + + + //sign each id in diff and push into dv + DisqualificationVotes dv; + { + dv.block_height = m_block_height; + dv.block_hash = m_block_hash; + dv.signer_id = m_supernode_id; + dv.ids.reserve(diff.size()); + dv.signs.reserve(diff.size()); + + DisqualificationItem di; + di.block_height = m_block_height; + di.block_hash = m_block_hash; + for(const auto& it : diff) + { + di.id = it; + std::string di_str; + bin_serialize(di, di_str); + crypto::signature sig; + sign(di_str, sig); + dv.ids.push_back(it); + dv.signs.push_back(std::move(sig)); + } + } + + //bin serialize dv + std::string dv_str; + bin_serialize(dv, dv_str); + //encrypt + std::string message; + graft::crypto_tools::encryptMessage(dv_str, m_bbqs_ids, message); + + //add my votes to me + for(size_t i = 0; i < dv.ids.size(); ++i) + { + const auto& id = dv.ids[i]; + const auto& sign = dv.signs[i]; + auto& vec = m_votes[id]; + vec.emplace_back( std::make_pair(dv.signer_id, sign) ); + } + + //multicast to m_bbqs_ids + MulticastRequestJsonRpc req; + req.params.receiver_addresses = m_bbqs_str_ids; + 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"); + return graft::Status::Forward; + } + + graft::Status handle_phase3(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + if(!m_started || !m_collectVotes) return graft::Status::Ok; + if(!m_in_bbqs) + { + return setError(ctx, "unexpected call of handle_phase3, not in BBQS"); + } + + MulticastRequestJsonRpc req; + bool res = input.get(req); + if(!res) + { + return setError(ctx, "cannot deserialize MulticastRequestJsonRpc"); + } + + std::string message = graft::utils::base64_decode(req.params.data); + std::string dv_str; + bool res2 = graft::crypto_tools::decryptMessage(message, m_secret_key, dv_str); + if(!res2) + { + return setError(ctx, "cannot decrypt, the message is not for me"); + } + + DisqualificationVotes dv; + std::string err = bin_deserialize(dv_str, dv); + if(!err.empty()) + { + std::ostringstream oss; + oss << "cannot deserialize DisqualificationVotes, error: '" << err << "'"; + return setError(ctx, oss.str()); + } + + //check reliability + if(dv.block_height != m_block_height) + { + std::ostringstream oss; + oss << "invalid block_height " << dv.block_height << " expected " << m_block_height; + return setError(ctx, oss.str(), 301); + } + if(dv.block_hash != m_block_hash) + { + std::ostringstream oss; + oss << " invalid block_hash " << dv.block_hash << " expected " << m_block_hash; + return setError(ctx, oss.str()); + } + if(dv.ids.empty() || dv.signs.size() != dv.ids.size()) + { + return setError(ctx, "corrupted DisqualificationVotes"); + } + //assumed m_qcl_ids and m_bbqs_ids are sorted + //check dv.signer_id in BBQS + if(!std::binary_search(m_bbqs_ids.cbegin(), m_bbqs_ids.cend(), dv.signer_id, less_mem{} )) + { + std::ostringstream oss; oss << "in DisqualificationVotes signer with id '" << dv.signer_id << "' is not in BBQS "; + return setError(ctx, oss.str()); + } + {//check that all dv.ids in QCL difference (dv.ids) - (m_qcl_ids) is empty + std::vector diff; + std::set_difference(dv.ids.begin(), dv.ids.end(), m_qcl_ids.begin(), m_qcl_ids.end(), std::back_inserter(diff), less_mem{}); + if(!diff.empty()) + { + return setError(ctx, "in DisqualificationVotes some ids is not in QCL"); + } + } + {//check signes + DisqualificationItem di; + di.block_height = m_block_height; + di.block_hash = m_block_hash; + for(size_t i = 0; i < dv.ids.size(); ++i) + { + const auto& id = dv.ids[i]; + const auto& sign = dv.signs[i]; + di.id = id; + std::string di_str; + bin_serialize(di, di_str); + bool res = check_sign(di_str, dv.signer_id, sign); + if(!res) + { + return setError(ctx, "in DisqualificationVotes at least one sign is invalid"); + } + } + } + + //save votes + for(size_t i = 0; i < dv.ids.size(); ++i) + { + const auto& id = dv.ids[i]; + const auto& sign = dv.signs[i]; + auto& vec = m_votes[id]; + vec.emplace_back( std::make_pair(dv.signer_id, sign) ); + } + + return graft::Status::Ok; + } + + graft::Status do_phase4(graft::Context& ctx, graft::Output& output) + { + if(!m_started || !m_in_bbqs) return graft::Status::Ok; + m_collectVotes = false; + + //remove entries from m_votes with not enough votes + for(auto it = m_votes.begin(), eit = m_votes.end(); it != eit;) + { + if(it->second.size() < REQUIRED_BBQS_VOTES) + { + MWARNING("Not enough votes to disqualify '") << epee::string_tools::pod_to_hex(it->first) + << "' got " << it->second.size() << " required " << REQUIRED_BBQS_VOTES; + it = m_votes.erase(it); + } + else ++it; + } + if(m_votes.empty()) return graft::Status::Ok; + + //create disqualifications + std::vector ds; + ds.reserve(m_votes.size()); + for(auto& [id, vec] : m_votes) + { + Disqualification d; + d.item.block_height = m_block_height; + d.item.block_hash = m_block_hash; + d.item.id = id; + d.signers.reserve(vec.size()); + for(auto& [siner_id, sign] : vec) + { + SignerItem si; + si.signer_id = siner_id; + si.sign = sign; + d.signers.push_back(std::move(si) ); + } + ds.push_back(std::move(d)); + } + + //create extras from disqualifications + using extra2_t = std::vector; + std::vector extra2s; + extra2s.reserve(ds.size()); + for(auto& d : ds) + { + extra2_t ext; + ext.push_back(TX_EXTRA_GRAFT_DISQUALIFICATION_TAG); + std::string d_str; + bin_serialize(d, d_str); + static_assert(sizeof(d_str[0]) == sizeof(uint8_t)); + std::copy(d_str.begin(), d_str.end(), std::back_inserter(ext)); + extra2s.push_back(std::move(ext)); + } + + //create transactions from extras + uint32_t unlock_time = m_block_height + DISQUALIFICATION_DURATION_BLOCK_COUNT; + std::vector txes; + txes.reserve(extra2s.size()); + for(auto& extra2 : extra2s) + { + tools::wallet2::pending_tx ptx; + cryptonote::transaction& tx = ptx.tx; + tx.version = 123; + tx.extra = extra2; + tx.extra2 = extra2; + + crypto::public_key tmp; + crypto::generate_keys(tmp, ptx.tx_key); + + ptx.construction_data.extra = tx.extra; + ptx.construction_data.unlock_time = unlock_time; + ptx.construction_data.use_rct = false; + + txes.push_back(std::move(ptx)); + } + + if(fnCollectTxs) + { + fnCollectTxs(&txes); + return graft::Status::Ok; + } + + //create wallet + std::string addr = ctx.global["cryptonode_rpc_address"]; + bool testnet = ctx.global["testnet"]; + tools::wallet2 wallet(testnet? cryptonote::TESTNET : cryptonote::MAINNET); + wallet.init(addr); + wallet.set_refresh_from_block_height(m_block_height); + wallet.set_seed_language("English"); + + try + { + wallet.commit_tx(txes); + } + catch(std::exception& ex) + { + return setError(ctx, ex.what()); + } + + return graft::Status::Ok; + } + +#if(tst) + graft::Status handle_test(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + if(!m_started) return graft::Status::Error; + + // + std::vector ds; + { + Disqualification d; + d.item.block_height = m_block_height; + d.item.block_hash = m_block_hash; + d.item.id = m_supernode_id; //self disqualification + { + SignerItem si; + si.signer_id = m_supernode_id; + //sign + std::string item_str; + bin_serialize(d.item, item_str); + sign(item_str, si.sign); + + d.signers.push_back(std::move(si) ); + } + ds.push_back(std::move(d)); + } + + //create extras from disqualifications + using extra2_t = std::vector; + std::vector extra2s; + extra2s.reserve(ds.size()); + for(auto& d : ds) + { + extra2_t ext; + ext.push_back(TX_EXTRA_GRAFT_DISQUALIFICATION_TAG); + std::string d_str; + bin_serialize(d, d_str); + static_assert(sizeof(d_str[0]) == sizeof(uint8_t)); + std::copy(d_str.begin(), d_str.end(), std::back_inserter(ext)); + extra2s.push_back(std::move(ext)); + } + + //create transactions from extras + uint32_t unlock_time = m_block_height + DISQUALIFICATION_DURATION_BLOCK_COUNT; + std::vector txes; + txes.reserve(extra2s.size()); + for(auto& extra2 : extra2s) + { + tools::wallet2::pending_tx ptx; + cryptonote::transaction& tx = ptx.tx; + tx.version = 123; + tx.extra = extra2; + tx.extra2 = extra2; + + crypto::public_key tmp; + crypto::generate_keys(tmp, ptx.tx_key); + + ptx.construction_data.extra = tx.extra; + ptx.construction_data.unlock_time = unlock_time; + ptx.construction_data.use_rct = false; + + txes.push_back(std::move(ptx)); + } + + //create wallet + std::string addr = ctx.global["cryptonode_rpc_address"]; + bool testnet = ctx.global["testnet"]; + tools::wallet2 wallet(testnet); + bool ok = wallet.init(addr); + if(!ok) + { + return setError(ctx, "cannot create temporal wallet"); + } + wallet.set_refresh_from_block_height(m_block_height); + wallet.set_seed_language("English"); + + try + { + wallet.commit_tx(txes); + } + catch(std::exception& ex) + { + return setError(ctx, ex.what()); + } + + return graft::Status::Ok; + } +#endif + + graft::Status do_process(const graft::Router::vars_t& vars, const graft::Input& input, graft::Context& ctx, graft::Output& output) + { + if(ctx.local.getLastStatus() == graft::Status::Forward) return graft::Status::Ok; + assert(ctx.global.hasKey("fsl")); + + BlockchainBasedListJsonRpcRequest req; + if (!input.get(req)) + { + // can't parse request + LOG_ERROR("Failed to parse request"); + return Status::Error; + } + + uint64_t res_block_height = req.params.block_height - BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT; + + if(res_block_height == m_block_height) return graft::Status::Ok; + if(res_block_height < m_block_height) + { + MDEBUG("Old block_height ") << res_block_height << " current " << m_block_height; + return graft::Status::Error; + } + + int phase = req.params.block_height % phases_count; + + switch(phase) + { + case phase_1: return do_phase1(ctx, res_block_height); + case phase_2: return do_phase2(ctx, output); + case phase_3: return do_phase3(ctx, output); + case phase_4: return do_phase4(ctx, output); + default: assert(false); + } + + 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); + + if(!ctx.global.hasKey("BBLDisqualificator")) + { + std::shared_ptr bbld = std::make_shared(); + ctx.global["BBLDisqualificator"] = bbld; + } + std::shared_ptr bbld = ctx.global["BBLDisqualificator"]; + return bbld->do_process(vars, input, ctx, output); + } + + static graft::Status phase2Handler(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("BBLDisqualificator")) return graft::Status::Ok; + std::shared_ptr bbld = ctx.global["BBLDisqualificator"]; + return bbld->handle_phase2(vars, input, ctx, output); + } + + static graft::Status phase3Handler(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("BBLDisqualificator")) return graft::Status::Ok; + std::shared_ptr bbld = ctx.global["BBLDisqualificator"]; + return bbld->handle_phase3(vars, input, ctx, output); + } + +#if(tst) + static graft::Status testHandler(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("BBLDisqualificator")) return graft::Status::Ok; + std::shared_ptr bbld = ctx.global["BBLDisqualificator"]; + return bbld->handle_test(vars, input, ctx, output); + } +#endif + + 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; + +class BBLDisqualificatorTest : public BBLDisqualificator +{ + 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 + { + graft::Status res; + graft::Output output; + if(cmd.uri.empty()) + { + BlockchainBasedListJsonRpcRequest req; + req.params.block_height = cmd.block_height; + 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; +//TODO: Something wrong, it is required attention +// res = do_process({}, in, ctx, output); + res = processTest({}, in, ctx, output); + } + else + { + graft::Input in; in.body = cmd.body; + if(cmd.uri == ROUTE_PING_RESULT) + { + res = handle_phase2({}, in, ctx, output); + } + else + { + res = handle_phase3({}, in, ctx, output); + } + } + 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(); + } + } +public: + static std::mutex m_err_mutex; + static std::map m_errors; + + BBLDisqualificatorTest() + { + ctx.global["fsl"] = nullptr; + } +}; + +std::mutex BBLDisqualificatorTest::m_err_mutex; +std::map BBLDisqualificatorTest::m_errors; + +} //namespace + +std::unique_ptr BBLDisqualificatorBase::createTestBBLDisqualificator( + GetSupernodeKeys fnGetSupernodeKeys, + GetBBQSandQCL fnGetBBQSandQCL, + CollectTxs fnCollectTxs +) +{ + auto disq = std::make_unique(); + disq->fnGetSupernodeKeys = fnGetSupernodeKeys; + disq->fnGetBBQSandQCL = fnGetBBQSandQCL; + disq->fnCollectTxs = fnCollectTxs; + return disq; +} + +void BBLDisqualificatorBase::count_errors(const std::string& msg, int code) +{ + std::lock_guard lk(BBLDisqualificatorTest::m_err_mutex); + ++BBLDisqualificatorTest::m_errors[code]; +} + +const std::map& BBLDisqualificatorBase::get_errors() +{ + return BBLDisqualificatorTest::m_errors; +} + +void BBLDisqualificatorBase::clear_errors() +{ + return BBLDisqualificatorTest::m_errors.clear(); } +} //namespace graft::supernode::request + namespace graft::supernode::request { namespace @@ -78,7 +1044,7 @@ Status blockchainBasedListHandler //handle tiers - FullSupernodeList::blockchain_based_list tiers; + FullSupernodeList::blockchain_based_list bbl(req.params.block_hash); for (const BlockchainBasedListTier& tier : req.params.tiers) { @@ -99,11 +1065,13 @@ Status blockchainBasedListHandler supernodes.emplace_back(std::move(entry)); } - tiers.emplace_back(std::move(supernodes)); + bbl.tiers.emplace_back(std::move(supernodes)); } fsl->setBlockchainBasedList(req.params.block_height, FullSupernodeList::blockchain_based_list_ptr( - new FullSupernodeList::blockchain_based_list(std::move(tiers)))); + new FullSupernodeList::blockchain_based_list(std::move(bbl)))); + + BBLDisqualificator::process(vars, input, ctx, output); return Status::Ok; } @@ -117,6 +1085,13 @@ void registerBlockchainBasedListRequest(graft::Router &router) router.addRoute(PATH, METHOD_POST, h3); LOG_PRINT_L0("route " << PATH << " registered"); + + router.addRoute(BBLDisqualificator::ROUTE_PING_RESULT, METHOD_POST, {nullptr, BBLDisqualificator::phase2Handler , nullptr}); + router.addRoute(BBLDisqualificator::ROUTE_VOTES, METHOD_POST, {nullptr, BBLDisqualificator::phase3Handler , nullptr}); + +#if(tst) + router.addRoute("/disqualTest", METHOD_GET | METHOD_POST, {nullptr, BBLDisqualificator::testHandler , nullptr}); +#endif } } diff --git a/src/supernode/requests/debug.cpp b/src/supernode/requests/debug.cpp index 68d51b12..f1787ad0 100644 --- a/src/supernode/requests/debug.cpp +++ b/src/supernode/requests/debug.cpp @@ -68,7 +68,7 @@ Status getSupernodeList(const Router::vars_t& vars, const graft::Input& input, auto is_supernode_available = [&](const std::string& supernode_public_id) { - for (const FullSupernodeList::blockchain_based_list_tier& tier : auth_sample_base_list) + for (const FullSupernodeList::blockchain_based_list_tier& tier : auth_sample_base_list.tiers) { for (const FullSupernodeList::blockchain_based_list_entry& entry : tier) if (supernode_public_id == entry.supernode_public_id) @@ -235,7 +235,7 @@ Status getBlockchainBasedListImpl(const Router::vars_t& vars, const graft::Input } } - for (const FullSupernodeList::blockchain_based_list_tier& src_tier : bbl) + for (const FullSupernodeList::blockchain_based_list_tier& src_tier : bbl.tiers) { std::vector dst_tier; diff --git a/test/cryptonode_handlers_test.cpp b/test/cryptonode_handlers_test.cpp index ae5aeb34..bf0b04c8 100644 --- a/test/cryptonode_handlers_test.cpp +++ b/test/cryptonode_handlers_test.cpp @@ -191,20 +191,25 @@ TEST_F(CryptonodeHandlersTest, sendrawtx) // open wallet const std::string wallet_path = "test_wallet"; - tools::wallet2 wallet(true); + tools::wallet2 wallet(cryptonote::TESTNET); wallet.load(wallet_path, ""); wallet.init("localhost:28881"); - wallet.refresh(); +//TODO: +// wallet.refresh(); + wallet.refresh(true); wallet.store(); - std::cout << "wallet addr: " << wallet.get_account().get_public_address_str(true) << std::endl; + std::cout << "wallet addr: " << wallet.get_account().get_public_address_str(cryptonote::TESTNET) << std::endl; const uint64_t AMOUNT_1_GRFT = 10000000000000; // send to itself - cryptonote::tx_destination_entry de (AMOUNT_1_GRFT, wallet.get_account().get_keys().m_account_address); + // TODO: modify to support subadress + cryptonote::tx_destination_entry de (AMOUNT_1_GRFT, wallet.get_account().get_keys().m_account_address, false); std::vector dsts; dsts.push_back(de); std::vector extra; - std::vector ptx = wallet.create_transactions_2(dsts, 4, 0, 0, extra, true); + std::set subaddr_indices; + // TODO: modify so it works with subaddresses + std::vector ptx = wallet.create_transactions_2(dsts, 4, 0, 0, extra, true, subaddr_indices); ASSERT_TRUE(ptx.size() == 1); SendRawTxRequest req; ASSERT_TRUE(createSendRawTxRequest(ptx.at(0), req)); diff --git a/test/disqualification_test.cpp b/test/disqualification_test.cpp new file mode 100644 index 00000000..8fa2e504 --- /dev/null +++ b/test/disqualification_test.cpp @@ -0,0 +1,342 @@ +// 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 + +#include "supernode/requests/disqualificator.h" +#include "utils/sample_generator.h" + +#include + +#include +#include +#include + +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); + } +}; + + +TEST(Disqualificator, BBL) +{ + using namespace graft::supernode::request; + + struct cmd_queue + { + std::mutex mutex; + std::deque deque; + }; + + //N - count or running SNs, DB - dead (never runnign) SNs that in BBQS, DQ - dead SNs that in QCL + const int N = 14, DB = 2, DQ = 3; + crypto::public_key pubs[N+DB+DQ]; + crypto::secret_key secs[N+DB+DQ]; + cmd_queue queues[N+DB+DQ]; + std::map> pub2idx; + for(int i=0; i BBQS; + std::copy(&pubs[3], &pubs[3+8-DB], std::back_inserter(BBQS)); + std::copy(&pubs[N], &pubs[N+DB], std::back_inserter(BBQS)); + std::vector QCL; + std::copy(&pubs[3+8], &pubs[3+8-DQ], std::back_inserter(QCL)); + std::copy(&pubs[N+DB], &pubs[N+DB+DQ], std::back_inserter(QCL)); + + 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 getBBQSandQCL = [&](uint64_t& block_height, crypto::hash& block_hash, std::vector& bbqs, std::vector& qcl) + { + block_height -= 10; //BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT + block_hash = block_hashes[block_height]; + bbqs = BBQS; + qcl = QCL; + }; + + + 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::createTestBBLDisqualificator(getMyKey, getBBQSandQCL, collectTxs); + while(true) + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + BBLDisqualificatorBase::command cmd; + { + auto& cq = queues[i]; + std::lock_guard lk(cq.mutex); + if(cq.deque.empty()) continue; + cmd = cq.deque.front(); cq.deque.pop_front(); + } + if(cmd.uri == "stop") return; + + std::vector forward; + std::string body, callback_uri; + disq->process_command(cmd, forward, body, callback_uri); + for(auto& id : forward) + { + BBLDisqualificatorBase::command cmd(callback_uri, body); + 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); + } + + for(int b_height = 10; b_height < BLKS; ++b_height) //BLOCKCHAIN_BASED_LIST_DELAY_BLOCK_COUNT) + { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + BBLDisqualificatorBase::command cmd(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(std::move(cmd)); + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + 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.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); +} diff --git a/test/graft_server_test.cpp b/test/graft_server_test.cpp index 63e13124..42de0da6 100644 --- a/test/graft_server_test.cpp +++ b/test/graft_server_test.cpp @@ -19,6 +19,7 @@ #include #include +#include GRAFT_DEFINE_IO_STRUCT(Payment, (uint64, amount), @@ -80,76 +81,6 @@ TEST(InOut, common) EXPECT_EQ(s_out, s); } -namespace graft { namespace serializer { - -template -struct Nothing -{ - static std::string serialize(const T& t) - { - return ""; - } - static void deserialize(const std::string& s, T& t) - { - } -}; - -} } - -TEST(InOut, serialization) -{ - using namespace graft; - - GRAFT_DEFINE_IO_STRUCT(J, - (int,x), - (int,y) - ); - - J j; - - Input input; - input.body = "{\"x\":1,\"y\":2}"; - j.x = 5; j.y = 6; - j = input.get>(); - EXPECT_EQ(j.x, 1); EXPECT_EQ(j.y, 2); - j = input.get(); - EXPECT_EQ(j.x, 1); EXPECT_EQ(j.y, 2); - j.x = 5; j.y = 6; - j = input.getT(); - EXPECT_EQ(j.x, 1); EXPECT_EQ(j.y, 2); - - Output output; - output.load>(j); - output.load(j); - output.load<>(j); - EXPECT_EQ(input.body, output.body); - output.body.clear(); - output.load(j); - EXPECT_EQ(input.body, output.body); - output.body.clear(); - output.loadT(j); - output.loadT(j); - output.loadT<>(j); - EXPECT_EQ(input.body, output.body); - output.body.clear(); - output.loadT(j); - EXPECT_EQ(input.body, output.body); - - struct A - { - int x; - int y; - }; - - A a; - - a = input.get>(); - a = input.getT(); - output.load>(a); - output.loadT(a); - output.loadT(a); -} - TEST(InOut, makeUri) { { diff --git a/test/serialize_test.cpp b/test/serialize_test.cpp new file mode 100644 index 00000000..4a3180dd --- /dev/null +++ b/test/serialize_test.cpp @@ -0,0 +1,405 @@ +// 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 + +#include "lib/graft/binary_serialize.h" + +#include "include_base_utils.h" +using namespace epee; + +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "serialization/binary_utils.h" + +#include "lib/graft/jsonrpc.h" +#include + +namespace +{ +GRAFT_DEFINE_IO_STRUCT(Payment, + (uint64, amount), + (uint32, block_height), + (std::string, payment_id), + (std::string, tx_hash), + (uint32, unlock_time) +); + +GRAFT_DEFINE_IO_STRUCT(NestedPayment, + (std::string, s), + (Payment, p), + (uint32, i) +); + +GRAFT_DEFINE_IO_STRUCT(WithVector, + (std::vector, vs), + (std::vector, vp) +); + +//TODO: does not work +GRAFT_DEFINE_IO_STRUCT(WithVectorInt, + (std::vector, v) +); + +struct PaymentX +{ + uint64 amount; + uint32 block_height; + std::string payment_id; + std::string tx_hash; + uint32 unlock_time; + BEGIN_SERIALIZE() + FIELD(amount) + FIELD(block_height) + FIELD(payment_id) + FIELD(tx_hash) + FIELD(unlock_time) + END_SERIALIZE() +}; + +struct NestedPaymentX +{ + std::string s; + PaymentX p; + uint32 i; + BEGIN_SERIALIZE() + FIELD(s) + FIELD(p) + FIELD(i) + END_SERIALIZE() +}; + +struct WithVectorX +{ + std::vector vs; + std::vector vp; + BEGIN_SERIALIZE() + FIELD(vs) + FIELD(vp) + END_SERIALIZE() +}; + +//TODO: does not work +struct WithVectorIntX +{ + std::vector v; + BEGIN_SERIALIZE() + FIELD(v) + END_SERIALIZE() +}; + +} //namespace + +namespace graft { namespace serializer { + +template +struct Nothing +{ + static std::string serialize(const T& t) + { + return ""; + } + static void deserialize(const std::string& s, T& t) + { + } +}; + +} } //namespace graft { namespace serializer + +TEST(InOut, serialization) +{ + using namespace graft; + namespace serial = graft::serializer; + + GRAFT_DEFINE_IO_STRUCT(J, + (int,x), + (int,y) + ); + + J j; + + Input input; + input.body = "{\"x\":1,\"y\":2}"; + j.x = 5; j.y = 6; + j = input.get>(); + j = input.get(); + EXPECT_EQ(j.x, 1); EXPECT_EQ(j.y, 2); + j = input.get(); + EXPECT_EQ(j.x, 1); EXPECT_EQ(j.y, 2); + j.x = 5; j.y = 6; + j = input.getT(); +// j = input.getT(); + EXPECT_EQ(j.x, 1); EXPECT_EQ(j.y, 2); + + Output output; + output.load>(j); + output.load(j); + output.load<>(j); + EXPECT_EQ(input.body, output.body); + output.body.clear(); + output.load(j); + EXPECT_EQ(input.body, output.body); + + output.body.clear(); + output.loadT(j); + output.loadT(j); + output.loadT<>(j); + EXPECT_EQ(input.body, output.body); + output.body.clear(); + output.loadT(j); + EXPECT_EQ(input.body, output.body); + + struct A + { + int x; + int y; + }; + + A a; + + a = input.get>(); + a = input.getT(); + output.load>(a); + output.loadT(a); + output.loadT(a); + + { + output.load>(j); + std::string s = output.body; + input.body = s; + J j1; + j1 = input.getT(); + EXPECT_EQ(j.x, j1.x); + EXPECT_EQ(j.y, j1.y); + } + + Payment p{ {}, 1, 2, "abc", "defg", 5 }; + output.load>(p); + PaymentX px{ 1, 2, "abc", "defg", 5 }; + std::string blob; + ::serialization::dump_binary(px, blob); + EXPECT_EQ(output.body, blob); + + Payment p1; + input.body = output.body; + p1 = input.get>(); + EXPECT_EQ(p.amount, p1.amount); + EXPECT_EQ(p.block_height, p1.block_height); + EXPECT_EQ(p.payment_id, p1.payment_id); + EXPECT_EQ(p.tx_hash, p1.tx_hash); + EXPECT_EQ(p.unlock_time, p1.unlock_time); + + { + NestedPayment np{ {}, "something", p, 123 }; + output.load>(np); + NestedPaymentX npx{ "something", px, 123 }; + ::serialization::dump_binary(npx, blob); + EXPECT_EQ(output.body, blob); + + NestedPayment np1; + input.body = output.body; + np1 = input.get>(); + EXPECT_EQ(np.s, np1.s); + EXPECT_EQ(np.p.amount, np1.p.amount); + EXPECT_EQ(np.p.block_height, np1.p.block_height); + EXPECT_EQ(np.p.payment_id, np1.p.payment_id); + EXPECT_EQ(np.p.tx_hash, np1.p.tx_hash); + EXPECT_EQ(np.p.unlock_time, np1.p.unlock_time); + EXPECT_EQ(np.i, np1.i); + } + { + WithVector wv{ {}, {"1","2","3","4","5"}, {p, p, p} }; + output.load>(wv); + WithVectorX wvx{ {"1","2","3","4","5"}, {px, px, px} }; + ::serialization::dump_binary(wvx, blob); + EXPECT_EQ(output.body, blob); + + input.body = output.body; + WithVector wv1 = input.get>(); + EXPECT_EQ(wv.vs, wv1.vs); + EXPECT_EQ(wv.vp.size(), wv1.vp.size()); + for(size_t i = 0; i>(wv); + WithVectorIntX wvx{ {1,2,3,4,5} }; + ::serialization::dump_binary(wvx, blob); + EXPECT_EQ(output.body, blob); + + input.body = output.body; + WithVectorInt wv1 = input.get>(); + EXPECT_EQ(wv.v, wv1.v); + } +#endif +} + +namespace +{ + +GRAFT_DEFINE_IO_STRUCT(DisqualificationItem, + (uint64_t, block_height), + (crypto::hash, block_hash), + (crypto::public_key, id) + ); + +GRAFT_DEFINE_IO_STRUCT(SignerItem, + (crypto::public_key, signer_id), + (crypto::signature, sign) + ); + +GRAFT_DEFINE_IO_STRUCT(Disqualification, + (DisqualificationItem, item), + (std::vector, signers) + ); + +struct tx_extra_graft_disqualification +{ + struct disqualification_item + { + uint64_t block_height; + crypto::hash block_hash; + crypto::public_key id; + BEGIN_SERIALIZE() + FIELD(block_height) + FIELD(block_hash) + FIELD(id) + END_SERIALIZE() + }; + + struct signer_item + { + crypto::public_key signer_id; + crypto::signature sign; + BEGIN_SERIALIZE() + FIELD(signer_id) + FIELD(sign) + END_SERIALIZE() + }; + + disqualification_item item; + std::vector signers; + BEGIN_SERIALIZE() + FIELD(item) + FIELD(signers) + END_SERIALIZE() +}; + + +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 ""; +} + +} //namespace + +TEST(InOut, serialization1) +{ + using namespace graft; + namespace serial = graft::serializer; +#define ZPOD(X) std::memset(&X, 0, sizeof(X)); + crypto::public_key pub; + crypto::secret_key sec; + crypto::generate_keys(pub, sec); + + Disqualification d; + d.item.block_height = 1; + ZPOD(d.item.block_hash) + d.item.block_hash.data[0] = 2; + ZPOD(d.item.id); + d.item.id.data[0] = 3; + { + SignerItem si; + ZPOD(si); + si.signer_id.data[0] = 4; + *(char*)&si.sign = 5; + d.signers.push_back(std::move(si)); + } + std::string d_str; + bin_serialize(d, d_str); + + Disqualification d1; + bin_deserialize(d_str, d1); + EXPECT_EQ(d.item.block_height, d1.item.block_height); + EXPECT_EQ(d.item.block_hash, d1.item.block_hash); + EXPECT_EQ(d.item.id, d1.item.id); + EXPECT_EQ(d.signers.size(), d1.signers.size()); + EXPECT_EQ(d.signers.size(), 1); + EXPECT_EQ(d.signers[0].signer_id, d1.signers[0].signer_id); + EXPECT_EQ(d.signers[0].sign, d1.signers[0].sign); + + tx_extra_graft_disqualification dx; + dx.item.block_height = 1; + ZPOD(dx.item.block_hash) + dx.item.block_hash.data[0] = 2; + ZPOD(dx.item.id); + dx.item.id.data[0] = 3; + { + tx_extra_graft_disqualification::signer_item si; + ZPOD(si); + si.signer_id.data[0] = 4; + *(char*)&si.sign = 5; + + std::string si_str; + ::serialization::dump_binary(si, si_str); + dx.signers.push_back(std::move(si)); + } + std::string dx_str; + ::serialization::dump_binary(dx, dx_str); + + EXPECT_EQ(d_str, dx_str); +}