diff options
27 files changed, 942 insertions, 299 deletions
diff --git a/.gitignore b/.gitignore index a27982af1..1f88b6ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,4 @@ local.properties .texlipse .idea/ -/testnet
\ No newline at end of file + @@ -378,3 +378,20 @@ Note: rlwrap will save things like your seed and private keys, if you supply the If you want to help out, see CONTRIBUTING for a set of guidelines. +# Debugging + +This section contains general instructions for debugging failed installs or problems encountered with Monero. First ensure you are running the latest version built from the github repo. + +## LMDB + +Instructions for debugging suspected blockchain corruption as per @HYC + +There is an `mdb_stat` command in the LMDB source that can print statistics about the database but it's not routinely built. This can be built with the following command: + +`cd ~/monero/external/db_drivers/liblmdb && make` + +The output of `mdb_stat -ea <path to blockchain dir>` will indicate inconsistencies in the blocks, block_heights and block_info table. + +The output of `mdb_dump -s blocks <path to blockchain dir>` and `mdb_dump -s block_info <path to blockchain dir>` is useful for indicating whether blocks and block_info contain the same keys. + +These records are dumped as hex data, where the first line is the key and the second line is the data. diff --git a/contrib/epee/include/console_handler.h b/contrib/epee/include/console_handler.h index 95b986ff5..2ad92b3f2 100644 --- a/contrib/epee/include/console_handler.h +++ b/contrib/epee/include/console_handler.h @@ -155,6 +155,20 @@ namespace epee else if (0 < retval) return true; } +#else + while (m_run.load(std::memory_order_relaxed)) + { + int retval = ::WaitForSingleObject(::GetStdHandle(STD_INPUT_HANDLE), 100); + switch (retval) + { + case WAIT_FAILED: + return false; + case WAIT_OBJECT_0: + return true; + default: + break; + } + } #endif return true; diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d5d22bca6..dd17f6d64 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -33,6 +33,7 @@ set(common_sources util.cpp i18n.cpp perf_timer.cpp + task_region.cpp thread_group.cpp) if (STACK_TRACE) @@ -57,6 +58,7 @@ set(common_private_headers i18n.h perf_timer.h stack_trace.h + task_region.h thread_group.h) monero_private_headers(common diff --git a/src/common/task_region.cpp b/src/common/task_region.cpp new file mode 100644 index 000000000..b53a8376a --- /dev/null +++ b/src/common/task_region.cpp @@ -0,0 +1,94 @@ +// Copyright (c) 2014-2016, The Monero 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 "common/task_region.h" + +#include <boost/thread/locks.hpp> +#include <cassert> + +/* `mark_completed` and `wait` can throw in the lock call, but its difficult to +recover from either. An exception in `wait` means the post condition of joining +all threads cannot be achieved, and an exception in `mark_completed` means +certain deadlock. `noexcept` qualifier will force a call to `std::terminate` if +locking throws an exception, which should only happen if a recursive lock +attempt is made (which is not possible since no external function is called +while holding the lock). */ + +namespace tools +{ +void task_region_handle::state::mark_completed(id task_id) noexcept { + assert(task_id != 0 && (task_id & (task_id - 1)) == 0); // power of 2 check + if (pending.fetch_and(~task_id) == task_id) { + // synchronize with wait call, but do not need to hold + boost::unique_lock<boost::mutex>{sync_on_complete}; + all_complete.notify_all(); + } +} + +void task_region_handle::state::abort() noexcept { + state* current = this; + while (current) { + current->ready = 0; + current = current->next.get(); + } +} + +void task_region_handle::state::wait() noexcept { + state* current = this; + while (current) { + { + boost::unique_lock<boost::mutex> lock{current->sync_on_complete}; + current->all_complete.wait(lock, [current] { return current->pending == 0; }); + } + current = current->next.get(); + } +} + +void task_region_handle::state::wait(thread_group& threads) noexcept { + state* current = this; + while (current) { + while (current->pending != 0) { + if (!threads.try_run_one()) { + current->wait(); + return; + } + } + current = current->next.get(); + } +} + +void task_region_handle::create_state() { + st = std::make_shared<state>(std::move(st)); + next_id = 1; +} + +void task_region_handle::do_wait() noexcept { + assert(st); + const std::shared_ptr<state> temp = std::move(st); + temp->wait(threads); +} +} diff --git a/src/common/task_region.h b/src/common/task_region.h new file mode 100644 index 000000000..e4d210661 --- /dev/null +++ b/src/common/task_region.h @@ -0,0 +1,223 @@ +// Copyright (c) 2014-2016, The Monero 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 <atomic> +#include <boost/thread/condition_variable.hpp> +#include <boost/thread/mutex.hpp> +#include <memory> +#include <type_traits> +#include <utility> + +#include "common/thread_group.h" + +namespace tools +{ + +/*! A model of the fork-join concept. `run(...)` "forks" (i.e. spawns new +tasks), and `~task_region_handle()` or `wait()` "joins" the spawned tasks. +`wait` will block until all tasks have completed, while `~task_region_handle()` +blocks until all tasks have completed or aborted. + +Do _NOT_ give this object to separate thread of execution (which includes +`task_region_handle::run(...)`) because joining on a different thread is +undesireable (potential deadlock). + +This class cannot be constructed directly, use the function +`task_region(...)` instead. +*/ +class task_region_handle +{ + struct state + { + using id = unsigned; + + explicit state(std::shared_ptr<state> next_src) noexcept + : next(std::move(next_src)) + , ready(0) + , pending(0) + , sync_on_complete() + , all_complete() { + } + + state(const state&) = default; + state(state&&) = default; + ~state() = default; + state& operator=(const state&) = default; + state& operator=(state&&) = default; + + void track_id(id task_id) noexcept { + pending |= task_id; + ready |= task_id; + } + + //! \return True only once whether a given id can execute + bool can_run(id task_id) noexcept { + return (ready.fetch_and(~task_id) & task_id); + } + + //! Mark id as completed, and synchronize with waiting threads + void mark_completed(id task_id) noexcept; + + //! Tell all unstarted functions in region to return immediately + void abort() noexcept; + + //! Blocks until all functions in region have aborted or completed. + void wait() noexcept; + + //! Same as `wait()`, except `this_thread` runs tasks while waiting. + void wait(thread_group& threads) noexcept; + + private: + /* This implementation is a bit pessimistic, it ensures that all copies + of a wrapped task can only be executed once. `thread_group` should never + do this, but some variable needs to track whether an abort should be done + anyway... */ + std::shared_ptr<state> next; + std::atomic<id> ready; //!< Tracks whether a task has been invoked + std::atomic<id> pending; //!< Tracks when a task has completed or aborted + boost::mutex sync_on_complete; + boost::condition_variable all_complete; + }; + + template<typename F> + struct wrapper + { + wrapper(state::id id_src, std::shared_ptr<state> st_src, F f_src) + : task_id(id_src), st(std::move(st_src)), f(std::move(f_src)) { + } + + wrapper(const wrapper&) = default; + wrapper(wrapper&&) = default; + wrapper& operator=(const wrapper&) = default; + wrapper& operator=(wrapper&&) = default; + + void operator()() { + if (st) { + if (st->can_run(task_id)) { + f(); + } + st->mark_completed(task_id); + } + } + + private: + const state::id task_id; + std::shared_ptr<state> st; + F f; + }; + +public: + friend struct task_region_; + + task_region_handle() = delete; + task_region_handle(const task_region_handle&) = delete; + task_region_handle(task_region_handle&&) = delete; + + //! Cancels unstarted pending tasks, and waits for them to respond. + ~task_region_handle() noexcept { + if (st) { + st->abort(); + st->wait(threads); + } + } + + task_region_handle& operator=(const task_region_handle&) = delete; + task_region_handle& operator=(task_region_handle&&) = delete; + + /*! If the group has no threads, `f` is immediately run before returning. + Otherwise, `f` is dispatched to the thread_group associated with `this` + region. If `f` is dispatched to another thread, and it throws, the process + will immediately terminate. See std::packaged_task for getting exceptions on + functions executed on other threads. */ + template<typename F> + void run(F&& f) { + if (threads.count() == 0) { + f(); + } else { + if (!st || next_id == 0) { + create_state(); + } + const state::id this_id = next_id; + next_id <<= 1; + + st->track_id(this_id); + threads.dispatch(wrapper<F>{this_id, st, std::move(f)}); + } + } + + //! Wait until all functions provided to `run` have completed. + void wait() noexcept { + if (st) { + do_wait(); + } + } + +private: + explicit task_region_handle(thread_group& threads_src) + : st(nullptr), threads(threads_src), next_id(0) { + } + + void create_state(); + void do_wait() noexcept; + + std::shared_ptr<state> st; + thread_group& threads; + state::id next_id; +}; + +/*! Function for creating a `task_region_handle`, which automatically calls +`task_region_handle::wait()` before returning. If a `thread_group` is not +provided, one is created with an optimal number of threads. The callback `f` +must have the signature `void(task_region_handle&)`. */ +struct task_region_ { + template<typename F> + void operator()(thread_group& threads, F&& f) const { + static_assert( + std::is_same<void, typename std::result_of<F(task_region_handle&)>::type>::value, + "f cannot have a return value" + ); + task_region_handle region{threads}; + f(region); + region.wait(); + } + + template<typename F> + void operator()(thread_group&& threads, F&& f) const { + (*this)(threads, std::forward<F>(f)); + } + + template<typename F> + void operator()(F&& f) const { + thread_group threads; + (*this)(threads, std::forward<F>(f)); + } +}; + +constexpr const task_region_ task_region{}; +} diff --git a/src/common/thread_group.cpp b/src/common/thread_group.cpp index aa1b64f2e..4e1cc8964 100644 --- a/src/common/thread_group.cpp +++ b/src/common/thread_group.cpp @@ -27,6 +27,7 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "common/thread_group.h" +#include <boost/thread/locks.hpp> #include <cassert> #include <limits> #include <stdexcept> @@ -35,14 +36,20 @@ namespace tools { -thread_group::thread_group(std::size_t count) : internal() { +std::size_t thread_group::optimal() { static_assert( std::numeric_limits<unsigned>::max() <= std::numeric_limits<std::size_t>::max(), "unexpected truncation" ); - count = std::min<std::size_t>(count, get_max_concurrency()); - count = count ? count - 1 : 0; + const std::size_t hardware = get_max_concurrency(); + return hardware ? (hardware - 1) : 0; +} + +std::size_t thread_group::optimal_with_max(std::size_t count) { + return count ? std::min(count - 1, optimal()) : 0; +} +thread_group::thread_group(std::size_t count) : internal() { if (count) { internal.emplace(count); } @@ -52,24 +59,21 @@ thread_group::data::data(std::size_t count) : threads() , head{nullptr} , last(std::addressof(head)) - , pending(count) , mutex() , has_work() - , finished_work() , stop(false) { threads.reserve(count); while (count--) { - threads.push_back(std::thread(&thread_group::data::run, this)); + threads.push_back(boost::thread(&thread_group::data::run, this)); } } thread_group::data::~data() noexcept { { - const std::unique_lock<std::mutex> lock(mutex); + const boost::unique_lock<boost::mutex> lock(mutex); stop = true; } has_work.notify_all(); - finished_work.notify_all(); for (auto& worker : threads) { try { worker.join(); @@ -78,10 +82,20 @@ thread_group::data::~data() noexcept { } } +std::unique_ptr<thread_group::data::work> thread_group::data::get_next() noexcept { + std::unique_ptr<work> rc = std::move(head.ptr); + if (rc != nullptr) { + head.ptr = std::move(rc->next.ptr); + if (head.ptr == nullptr) { + last = std::addressof(head); + } + } + return rc; +} -void thread_group::data::sync() noexcept { +bool thread_group::data::try_run_one() noexcept { /* This function and `run()` can both throw when acquiring the lock, or in - the dispatched function. It is tough to recover from either, particularly the + dispatched function. It is tough to recover from either, particularly the lock case. These functions are marked as noexcept so that if either call throws, the entire process is terminated. Users of the `dispatch` call are expected to make their functions noexcept, or use std::packaged_task to copy @@ -89,50 +103,25 @@ void thread_group::data::sync() noexcept { cases (std::bad_alloc). This was the existing behavior; `asio::io_service::run` propogates errors from dispatched calls, and uncaught exceptions on threads result in process termination. */ - assert(!threads.empty()); - bool not_first = false; - while (true) { - std::unique_ptr<work> next = nullptr; - { - std::unique_lock<std::mutex> lock(mutex); - pending -= std::size_t(not_first); - not_first = true; - finished_work.notify_all(); - - if (stop) { - return; - } - - next = get_next(); - if (next == nullptr) { - finished_work.wait(lock, [this] { return pending == 0 || stop; }); - return; - } - } + std::unique_ptr<work> next = nullptr; + { + const boost::unique_lock<boost::mutex> lock(mutex); + next = get_next(); + } + if (next) { assert(next->f); next->f(); + return true; } -} - -std::unique_ptr<thread_group::data::work> thread_group::data::get_next() noexcept { - std::unique_ptr<work> rc = std::move(head.ptr); - if (rc != nullptr) { - head.ptr = std::move(rc->next.ptr); - if (head.ptr == nullptr) { - last = std::addressof(head); - } - } - return rc; + return false; } void thread_group::data::run() noexcept { - // see `sync()` source for additional information + // see `try_run_one()` source for additional information while (true) { std::unique_ptr<work> next = nullptr; { - std::unique_lock<std::mutex> lock(mutex); - --pending; - finished_work.notify_all(); + boost::unique_lock<boost::mutex> lock(mutex); has_work.wait(lock, [this] { return head.ptr != nullptr || stop; }); if (stop) { return; @@ -149,15 +138,12 @@ void thread_group::data::dispatch(std::function<void()> f) { std::unique_ptr<work> latest(new work{std::move(f), node{nullptr}}); node* const latest_node = std::addressof(latest->next); { - const std::unique_lock<std::mutex> lock(mutex); + const boost::unique_lock<boost::mutex> lock(mutex); assert(last != nullptr); assert(last->ptr == nullptr); - if (pending == std::numeric_limits<std::size_t>::max()) { - throw std::overflow_error("thread_group exceeded max queue depth"); - } + last->ptr = std::move(latest); last = latest_node; - ++pending; } has_work.notify_one(); } diff --git a/src/common/thread_group.h b/src/common/thread_group.h index d8461d49a..62e82d832 100644 --- a/src/common/thread_group.h +++ b/src/common/thread_group.h @@ -25,8 +25,12 @@ // 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 <boost/optional/optional.hpp> -#include <condition_variable> +#include <boost/thread/condition_variable.hpp> +#include <boost/thread/mutex.hpp> +#include <boost/thread/thread.hpp> #include <cstddef> #include <functional> #include <thread> @@ -35,11 +39,21 @@ namespace tools { -//! Manages zero or more threads for work dispatching +//! Manages zero or more threads for work dispatching. class thread_group { public: - //! Create `min(count, get_max_concurrency()) - 1` threads + + //! \return `get_max_concurrency() ? get_max_concurrency() - 1 : 0` + static std::size_t optimal(); + + //! \return `count ? min(count - 1, optimal()) : 0` + static std::size_t optimal_with_max(std::size_t count); + + //! Create an optimal number of threads. + explicit thread_group() : thread_group(optimal()) {} + + //! Create exactly `count` threads. explicit thread_group(std::size_t count); thread_group(thread_group const&) = delete; @@ -51,30 +65,26 @@ public: thread_group& operator=(thread_group const&) = delete; thread_group& operator=(thread_group&&) = delete; - /*! Blocks until all functions provided to `dispatch` complete. Does not - destroy threads. If a dispatched function calls `this->dispatch(...)`, - `this->sync()` will continue to block until that new function completes. */ - void sync() noexcept { + //! \return Number of threads owned by `this` group. + std::size_t count() const noexcept { if (internal) { - internal->sync(); + return internal->count(); } + return 0; } - /*! Example usage: - std::unique_ptr<thread_group, thread_group::lazy_sync> sync(std::addressof(group)); - which guarantees synchronization before the unique_ptr destructor returns. */ - struct lazy_sync { - void operator()(thread_group* group) const noexcept { - if (group != nullptr) { - group->sync(); - } + //! \return True iff a function was available and executed (on `this_thread`). + bool try_run_one() noexcept { + if (internal) { + return internal->try_run_one(); } - }; + return false; + } - /*! `f` is invoked immediately if the thread_group is empty, otherwise - execution of `f` is queued for next available thread. If `f` is queued, any - exception leaving that function will result in process termination. Use - std::packaged_task if exceptions need to be handled. */ + /*! `f` is invoked immediately if `count() == 0`, otherwise execution of `f` + is queued for next available thread. If `f` is queued, any exception leaving + that function will result in process termination. Use std::packaged_task if + exceptions need to be handled. */ template<typename F> void dispatch(F&& f) { if (internal) { @@ -91,8 +101,11 @@ private: data(std::size_t count); ~data() noexcept; - void sync() noexcept; + std::size_t count() const noexcept { + return threads.size(); + } + bool try_run_one() noexcept; void dispatch(std::function<void()> f); private: @@ -116,13 +129,11 @@ private: void run() noexcept; private: - std::vector<std::thread> threads; + std::vector<boost::thread> threads; node head; node* last; - std::size_t pending; - std::condition_variable has_work; - std::condition_variable finished_work; - std::mutex mutex; + boost::condition_variable has_work; + boost::mutex mutex; bool stop; }; @@ -130,4 +141,5 @@ private: // optionally construct elements, without separate heap allocation boost::optional<data> internal; }; + } diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index ffebcd592..c2ccf3db0 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1758,7 +1758,7 @@ bool Blockchain::get_random_rct_outs(const COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::r return true; } //------------------------------------------------------------------ -bool Blockchain::get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res) const +bool Blockchain::get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res) const { LOG_PRINT_L3("Blockchain::" << __func__); CRITICAL_REGION_LOCAL(m_blockchain_lock); diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index eb7a050b2..f9ae9d8aa 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -452,7 +452,7 @@ namespace cryptonote * * @return true */ - bool get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res) const; + bool get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res) const; /** * @brief gets random ringct outputs to mix with diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 9deb8863d..84a41cfbf 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -758,7 +758,7 @@ namespace cryptonote return m_blockchain_storage.get_random_outs_for_amounts(req, res); } //----------------------------------------------------------------------------------------------- - bool core::get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res) const + bool core::get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res) const { return m_blockchain_storage.get_outs(req, res); } diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h index 5ddab4ed4..21f84cdd4 100644 --- a/src/cryptonote_core/cryptonote_core.h +++ b/src/cryptonote_core/cryptonote_core.h @@ -471,7 +471,7 @@ namespace cryptonote * * @note see Blockchain::get_outs */ - bool get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res) const; + bool get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res) const; /** * diff --git a/src/ringct/rctSigs.cpp b/src/ringct/rctSigs.cpp index df33c26b2..b773be1e5 100644 --- a/src/ringct/rctSigs.cpp +++ b/src/ringct/rctSigs.cpp @@ -30,6 +30,7 @@ #include "misc_log_ex.h" #include "common/perf_timer.h" +#include "common/task_region.h" #include "common/thread_group.h" #include "common/util.h" #include "rctSigs.h" @@ -39,22 +40,6 @@ using namespace crypto; using namespace std; namespace rct { - namespace { - struct verRangeWrapper_ { - void operator()(const key & C, const rangeSig & as, bool &result) const { - result = verRange(C, as); - } - }; - constexpr const verRangeWrapper_ verRangeWrapper{}; - - struct verRctMGSimpleWrapper_ { - void operator()(const key &message, const mgSig &mg, const ctkeyV & pubs, const key & C, bool &result) const { - result = verRctMGSimple(message, mg, pubs, C); - } - }; - constexpr const verRctMGSimpleWrapper_ verRctMGSimpleWrapper{}; - } - //Schnorr Non-linkable //Gen Gives a signature (L1, s1, s2) proving that the sender knows "x" such that xG = one of P1 or P2 //Ver Verifies that signer knows an "x" such that xG = one of P1 or P2 @@ -766,15 +751,17 @@ namespace rct { try { std::deque<bool> results(rv.outPk.size(), false); - tools::thread_group threadpool(rv.outPk.size()); // this must destruct before results + tools::thread_group threadpool(tools::thread_group::optimal_with_max(rv.outPk.size())); + + tools::task_region(threadpool, [&] (tools::task_region_handle& region) { + DP("range proofs verified?"); + for (size_t i = 0; i < rv.outPk.size(); i++) { + region.run([&, i] { + results[i] = verRange(rv.outPk[i].mask, rv.p.rangeSigs[i]); + }); + } + }); - DP("range proofs verified?"); - for (size_t i = 0; i < rv.outPk.size(); i++) { - threadpool.dispatch( - std::bind(verRangeWrapper, std::cref(rv.outPk[i].mask), std::cref(rv.p.rangeSigs[i]), std::ref(results[i])) - ); - } - threadpool.sync(); for (size_t i = 0; i < rv.outPk.size(); ++i) { if (!results[i]) { LOG_ERROR("Range proof verified failed for input " << i); @@ -804,7 +791,6 @@ namespace rct { //assumes only post-rct style inputs (at least for max anonymity) bool verRctSimple(const rctSig & rv) { PERF_TIMER(verRctSimple); - size_t i = 0; CHECK_AND_ASSERT_MES(rv.type == RCTTypeSimple, false, "verRctSimple called on non simple rctSig"); CHECK_AND_ASSERT_MES(rv.outPk.size() == rv.p.rangeSigs.size(), false, "Mismatched sizes of outPk and rv.p.rangeSigs"); @@ -813,28 +799,29 @@ namespace rct { CHECK_AND_ASSERT_MES(rv.pseudoOuts.size() == rv.mixRing.size(), false, "Mismatched sizes of rv.pseudoOuts and mixRing"); const size_t threads = std::max(rv.outPk.size(), rv.mixRing.size()); - tools::thread_group threadpool(threads); - { - std::deque<bool> results(rv.outPk.size(), false); - { - const std::unique_ptr<tools::thread_group, tools::thread_group::lazy_sync> - sync(std::addressof(threadpool)); - for (i = 0; i < rv.outPk.size(); i++) { - threadpool.dispatch( - std::bind(verRangeWrapper, std::cref(rv.outPk[i].mask), std::cref(rv.p.rangeSigs[i]), std::ref(results[i])) - ); - } - } // threadpool.sync(); - for (size_t i = 0; i < rv.outPk.size(); ++i) { - if (!results[i]) { - LOG_ERROR("Range proof verified failed for input " << i); - return false; - } + + std::deque<bool> results(threads); + tools::thread_group threadpool(tools::thread_group::optimal_with_max(threads)); + + results.clear(); + results.resize(rv.outPk.size()); + tools::task_region(threadpool, [&] (tools::task_region_handle& region) { + for (size_t i = 0; i < rv.outPk.size(); i++) { + region.run([&, i] { + results[i] = verRange(rv.outPk[i].mask, rv.p.rangeSigs[i]); + }); + } + }); + + for (size_t i = 0; i < results.size(); ++i) { + if (!results[i]) { + LOG_ERROR("Range proof verified failed for input " << i); + return false; } } key sumOutpks = identity(); - for (i = 0; i < rv.outPk.size(); i++) { + for (size_t i = 0; i < rv.outPk.size(); i++) { addKeys(sumOutpks, sumOutpks, rv.outPk[i].mask); } DP(sumOutpks); @@ -843,27 +830,25 @@ namespace rct { key message = get_pre_mlsag_hash(rv); - { - std::deque<bool> results(rv.mixRing.size(), false); - { - const std::unique_ptr<tools::thread_group, tools::thread_group::lazy_sync> - sync(std::addressof(threadpool)); - for (i = 0 ; i < rv.mixRing.size() ; i++) { - threadpool.dispatch( - std::bind(verRctMGSimpleWrapper, std::cref(message), std::cref(rv.p.MGs[i]), std::cref(rv.mixRing[i]), std::cref(rv.pseudoOuts[i]), std::ref(results[i])) - ); - } - } // threadpool.sync(); - for (size_t i = 0; i < results.size(); ++i) { - if (!results[i]) { - LOG_ERROR("verRctMGSimple failed for input " << i); - return false; - } + results.clear(); + results.resize(rv.mixRing.size()); + tools::task_region(threadpool, [&] (tools::task_region_handle& region) { + for (size_t i = 0 ; i < rv.mixRing.size() ; i++) { + region.run([&, i] { + results[i] = verRctMGSimple(message, rv.p.MGs[i], rv.mixRing[i], rv.pseudoOuts[i]); + }); + } + }); + + for (size_t i = 0; i < results.size(); ++i) { + if (!results[i]) { + LOG_ERROR("verRctMGSimple failed for input " << i); + return false; } } key sumPseudoOuts = identity(); - for (i = 0 ; i < rv.mixRing.size() ; i++) { + for (size_t i = 0 ; i < rv.mixRing.size() ; i++) { addKeys(sumPseudoOuts, sumPseudoOuts, rv.pseudoOuts[i]); } DP(sumPseudoOuts); diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index a02a2375b..5bf500733 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -246,7 +246,7 @@ namespace cryptonote return true; } //------------------------------------------------------------------------------------------------------------------------------ - bool core_rpc_server::on_get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res) + bool core_rpc_server::on_get_outs_bin(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res) { CHECK_CORE_BUSY(); res.status = "Failed"; @@ -269,6 +269,42 @@ namespace cryptonote return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool core_rpc_server::on_get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res) + { + CHECK_CORE_BUSY(); + res.status = "Failed"; + + if (m_restricted) + { + if (req.outputs.size() > MAX_RESTRICTED_GLOBAL_FAKE_OUTS_COUNT) + { + res.status = "Too many outs requested"; + return true; + } + } + + cryptonote::COMMAND_RPC_GET_OUTPUTS_BIN::request req_bin; + req_bin.outputs = req.outputs; + cryptonote::COMMAND_RPC_GET_OUTPUTS_BIN::response res_bin; + if(!m_core.get_outs(req_bin, res_bin)) + { + return true; + } + + // convert to text + for (const auto &i: res_bin.outs) + { + res.outs.push_back(cryptonote::COMMAND_RPC_GET_OUTPUTS::outkey()); + cryptonote::COMMAND_RPC_GET_OUTPUTS::outkey &outkey = res.outs.back(); + outkey.key = epee::string_tools::pod_to_hex(i.key); + outkey.mask = epee::string_tools::pod_to_hex(i.mask); + outkey.unlocked = i.unlocked; + } + + res.status = CORE_RPC_STATUS_OK; + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool core_rpc_server::on_get_random_rct_outs(const COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::request& req, COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::response& res) { CHECK_CORE_BUSY(); @@ -388,6 +424,17 @@ namespace cryptonote res.txs_as_hex.push_back(e.as_hex); if (req.decode_as_json) res.txs_as_json.push_back(e.as_json); + + // output indices too if not in pool + if (pool_tx_hashes.find(tx_hash) == pool_tx_hashes.end()) + { + bool r = m_core.get_tx_outputs_gindexs(tx_hash, e.output_indices); + if (!r) + { + res.status = "Failed"; + return false; + } + } } BOOST_FOREACH(const auto& miss_tx, missed_txs) diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index 2fdb790ab..b7f6cdd60 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -79,7 +79,7 @@ namespace cryptonote MAP_URI_AUTO_BIN2("/gethashes.bin", on_get_hashes, COMMAND_RPC_GET_HASHES_FAST) MAP_URI_AUTO_BIN2("/get_o_indexes.bin", on_get_indexes, COMMAND_RPC_GET_TX_GLOBAL_OUTPUTS_INDEXES) MAP_URI_AUTO_BIN2("/getrandom_outs.bin", on_get_random_outs, COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS) - MAP_URI_AUTO_BIN2("/get_outs.bin", on_get_outs, COMMAND_RPC_GET_OUTPUTS) + MAP_URI_AUTO_BIN2("/get_outs.bin", on_get_outs_bin, COMMAND_RPC_GET_OUTPUTS_BIN) MAP_URI_AUTO_BIN2("/getrandom_rctouts.bin", on_get_random_rct_outs, COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS) MAP_URI_AUTO_JON2("/gettransactions", on_get_transactions, COMMAND_RPC_GET_TRANSACTIONS) MAP_URI_AUTO_JON2("/is_key_image_spent", on_is_key_image_spent, COMMAND_RPC_IS_KEY_IMAGE_SPENT) @@ -97,6 +97,7 @@ namespace cryptonote MAP_URI_AUTO_JON2_IF("/out_peers", on_out_peers, COMMAND_RPC_OUT_PEERS, !m_restricted) MAP_URI_AUTO_JON2_IF("/start_save_graph", on_start_save_graph, COMMAND_RPC_START_SAVE_GRAPH, !m_restricted) MAP_URI_AUTO_JON2_IF("/stop_save_graph", on_stop_save_graph, COMMAND_RPC_STOP_SAVE_GRAPH, !m_restricted) + MAP_URI_AUTO_JON2("/get_outs", on_get_outs, COMMAND_RPC_GET_OUTPUTS) BEGIN_JSON_RPC_MAP("/json_rpc") MAP_JON_RPC("getblockcount", on_getblockcount, COMMAND_RPC_GETBLOCKCOUNT) MAP_JON_RPC_WE("on_getblockhash", on_getblockhash, COMMAND_RPC_GETBLOCKHASH) @@ -131,6 +132,7 @@ namespace cryptonote bool on_stop_mining(const COMMAND_RPC_STOP_MINING::request& req, COMMAND_RPC_STOP_MINING::response& res); bool on_mining_status(const COMMAND_RPC_MINING_STATUS::request& req, COMMAND_RPC_MINING_STATUS::response& res); bool on_get_random_outs(const COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::request& req, COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::response& res); + bool on_get_outs_bin(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMAND_RPC_GET_OUTPUTS_BIN::response& res); bool on_get_outs(const COMMAND_RPC_GET_OUTPUTS::request& req, COMMAND_RPC_GET_OUTPUTS::response& res); bool on_get_random_rct_outs(const COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::request& req, COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::response& res); bool on_get_info(const COMMAND_RPC_GET_INFO::request& req, COMMAND_RPC_GET_INFO::response& res); diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index 6d452f59d..e19238c44 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -41,7 +41,7 @@ namespace cryptonote #define CORE_RPC_STATUS_BUSY "BUSY" #define CORE_RPC_STATUS_NOT_MINING "NOT MINING" -#define CORE_RPC_VERSION 4 +#define CORE_RPC_VERSION 5 struct COMMAND_RPC_GET_HEIGHT { @@ -162,6 +162,7 @@ namespace cryptonote std::string as_json; bool in_pool; uint64_t block_height; + std::vector<uint64_t> output_indices; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash) @@ -169,6 +170,7 @@ namespace cryptonote KV_SERIALIZE(as_json) KV_SERIALIZE(in_pool) KV_SERIALIZE(block_height) + KV_SERIALIZE(output_indices) END_KV_SERIALIZE_MAP() }; @@ -291,22 +293,22 @@ namespace cryptonote }; }; //----------------------------------------------- - struct COMMAND_RPC_GET_OUTPUTS + struct get_outputs_out { - struct out - { - uint64_t amount; - uint64_t index; + uint64_t amount; + uint64_t index; - BEGIN_KV_SERIALIZE_MAP() - KV_SERIALIZE(amount) - KV_SERIALIZE(index) - END_KV_SERIALIZE_MAP() - }; + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amount) + KV_SERIALIZE(index) + END_KV_SERIALIZE_MAP() + }; + struct COMMAND_RPC_GET_OUTPUTS_BIN + { struct request { - std::vector<out> outputs; + std::vector<get_outputs_out> outputs; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(outputs) @@ -337,6 +339,42 @@ namespace cryptonote END_KV_SERIALIZE_MAP() }; }; + //----------------------------------------------- + struct COMMAND_RPC_GET_OUTPUTS + { + struct request + { + std::vector<get_outputs_out> outputs; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(outputs) + END_KV_SERIALIZE_MAP() + }; + + struct outkey + { + std::string key; + std::string mask; + bool unlocked; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(key) + KV_SERIALIZE(mask) + KV_SERIALIZE(unlocked) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::vector<outkey> outs; + std::string status; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(outs) + KV_SERIALIZE(status) + END_KV_SERIALIZE_MAP() + }; + }; struct COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS { diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 515fc5b22..462cdf58f 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -2923,57 +2923,6 @@ bool simple_wallet::submit_transfer(const std::vector<std::string> &args_) return true; } - // if more than one tx necessary, prompt user to confirm - if (m_wallet->always_confirm_transfers()) - { - uint64_t total_sent = 0; - uint64_t total_fee = 0; - uint64_t dust_not_in_fee = 0; - uint64_t dust_in_fee = 0; - for (size_t n = 0; n < ptx_vector.size(); ++n) - { - total_fee += ptx_vector[n].fee; - for (auto i: ptx_vector[n].selected_transfers) - total_sent += m_wallet->get_transfer_details(i).amount(); - total_sent -= ptx_vector[n].change_dts.amount + ptx_vector[n].fee; - - if (ptx_vector[n].dust_added_to_fee) - dust_in_fee += ptx_vector[n].dust; - else - dust_not_in_fee += ptx_vector[n].dust; - } - - std::stringstream prompt; - prompt << boost::format(tr("Sending %s. ")) % print_money(total_sent); - if (ptx_vector.size() > 1) - { - prompt << boost::format(tr("Your transaction needs to be split into %llu transactions. " - "This will result in a transaction fee being applied to each transaction, for a total fee of %s")) % - ((unsigned long long)ptx_vector.size()) % print_money(total_fee); - } - else - { - prompt << boost::format(tr("The transaction fee is %s")) % - print_money(total_fee); - } - if (dust_in_fee != 0) prompt << boost::format(tr(", of which %s is dust from change")) % print_money(dust_in_fee); - if (dust_not_in_fee != 0) prompt << tr(".") << ENDL << boost::format(tr("A total of %s from dust change will be sent to dust address")) - % print_money(dust_not_in_fee); - prompt << tr(".") << ENDL << "Full transaction details are available in the log file" << ENDL << tr("Is this okay? (Y/Yes/N/No)"); - - std::string accepted = command_line::input_line(prompt.str()); - if (std::cin.eof()) - return true; - if (accepted != "Y" && accepted != "y" && accepted != "Yes" && accepted != "yes") - { - fail_msg_writer() << tr("transaction cancelled."); - - // would like to return false, because no tx made, but everything else returns true - // and I don't know what returning false might adversely affect. *sigh* - return true; - } - } - // actually commit the transactions while (!ptx_vector.empty()) { diff --git a/src/wallet/api/transaction_history.cpp b/src/wallet/api/transaction_history.cpp index 63c4ea3cc..603739598 100644 --- a/src/wallet/api/transaction_history.cpp +++ b/src/wallet/api/transaction_history.cpp @@ -55,7 +55,8 @@ TransactionHistoryImpl::TransactionHistoryImpl(WalletImpl *wallet) TransactionHistoryImpl::~TransactionHistoryImpl() { - + for (auto t : m_history) + delete t; } int TransactionHistoryImpl::count() const diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index d21d8b900..215b61aef 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -605,6 +605,17 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const break; } } + else if (has_payment_id) { + std::string extra_nonce; + set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id_short); + bool r = add_extra_nonce_to_tx_extra(extra, extra_nonce); + if (!r) { + m_status = Status_Error; + m_errorString = tr("Failed to add short payment id: ") + epee::string_tools::pod_to_hex(payment_id_short); + break; + } + } + //std::vector<tools::wallet2::pending_tx> ptx_vector; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 15a134257..ea3994435 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -191,7 +191,7 @@ boost::optional<tools::password_container> get_password(const boost::program_opt } // Remove line breaks the user might have inserted - password.erase(std::remove(password.begin() - 1, password.end(), '\n'), password.end()); + password.erase(std::remove(password.end() - 1, password.end(), '\n'), password.end()); password.erase(std::remove(password.end() - 1, password.end(), '\r'), password.end()); return {tools::password_container(std::move(password))}; } @@ -1574,12 +1574,35 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re std::list<cryptonote::block_complete_entry> blocks; std::vector<COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> o_indices; + std::string daemon_height_err = ""; + uint64_t daemon_bc_height = get_daemon_blockchain_height(daemon_height_err); + if(daemon_height_err.size() > 0) { + throw std::runtime_error(daemon_height_err); + } + // pull the first set of blocks get_short_chain_history(short_chain_history); m_run.store(true, std::memory_order_relaxed); if (start_height > m_blockchain.size() || m_refresh_from_block_height > m_blockchain.size()) { - if (!start_height) - start_height = m_refresh_from_block_height; + + // even target_height can be zero if the daemon just started and hasn't gotten some sync + // data back from peers .. hmmm, what to do ... O.o (you can see him thinking) + // i'm going with infiniti loop until i get something bigger than zero or err ... moneromoo don't kill me + std::string daemon_target_err = ""; + uint64_t daemon_target_height = 0; + + while(daemon_target_height == 0) + { + daemon_target_height = get_daemon_blockchain_target_height(daemon_target_err); + if(daemon_target_err.size() > 0) { + daemon_target_height = get_approximate_blockchain_height(); // - x? + } + } + + if (m_refresh_from_block_height > daemon_target_height) m_refresh_from_block_height = daemon_target_height - 1; + if (!start_height) start_height = m_refresh_from_block_height; + if (start_height >= daemon_bc_height) start_height = daemon_bc_height - 1; + // we can shortcut by only pulling hashes up to the start_height fast_refresh(start_height, blocks_start_height, short_chain_history); // regenerate the history now that we've got a full set of hashes @@ -1589,53 +1612,56 @@ void wallet2::refresh(uint64_t start_height, uint64_t & blocks_fetched, bool& re // and then fall through to regular refresh processing } - pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); - // always reset start_height to 0 to force short_chain_ history to be used on - // subsequent pulls in this refresh. - start_height = 0; - - while(m_run.load(std::memory_order_relaxed)) + if(!(m_refresh_from_block_height >= daemon_bc_height)) { - try - { - // pull the next set of blocks while we're processing the current one - uint64_t next_blocks_start_height; - std::list<cryptonote::block_complete_entry> next_blocks; - std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> next_o_indices; - bool error = false; - pull_thread = boost::thread([&]{pull_next_blocks(start_height, next_blocks_start_height, short_chain_history, blocks, next_blocks, next_o_indices, error);}); - - process_blocks(blocks_start_height, blocks, o_indices, added_blocks); - blocks_fetched += added_blocks; - pull_thread.join(); - if(!added_blocks) - break; - - // switch to the new blocks from the daemon - blocks_start_height = next_blocks_start_height; - blocks = next_blocks; - o_indices = next_o_indices; + pull_blocks(start_height, blocks_start_height, short_chain_history, blocks, o_indices); + // always reset start_height to 0 to force short_chain_ history to be used on + // subsequent pulls in this refresh. + start_height = 0; - // handle error from async fetching thread - if (error) - { - throw std::runtime_error("proxy exception in refresh thread"); - } - } - catch (const std::exception&) + while(m_run.load(std::memory_order_relaxed)) { - blocks_fetched += added_blocks; - if (pull_thread.joinable()) - pull_thread.join(); - if(try_count < 3) + try { - LOG_PRINT_L1("Another try pull_blocks (try_count=" << try_count << ")..."); - ++try_count; + // pull the next set of blocks while we're processing the current one + uint64_t next_blocks_start_height; + std::list<cryptonote::block_complete_entry> next_blocks; + std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> next_o_indices; + bool error = false; + pull_thread = boost::thread([&]{pull_next_blocks(start_height, next_blocks_start_height, short_chain_history, blocks, next_blocks, next_o_indices, error);}); + + process_blocks(blocks_start_height, blocks, o_indices, added_blocks); + blocks_fetched += added_blocks; + pull_thread.join(); + if(!added_blocks) + break; + + // switch to the new blocks from the daemon + blocks_start_height = next_blocks_start_height; + blocks = next_blocks; + o_indices = next_o_indices; + + // handle error from async fetching thread + if (error) + { + throw std::runtime_error("proxy exception in refresh thread"); + } } - else + catch (const std::exception&) { - LOG_ERROR("pull_blocks failed, try_count=" << try_count); - throw; + blocks_fetched += added_blocks; + if (pull_thread.joinable()) + pull_thread.join(); + if(try_count < 3) + { + LOG_PRINT_L1("Another try pull_blocks (try_count=" << try_count << ")..."); + ++try_count; + } + else + { + LOG_ERROR("pull_blocks failed, try_count=" << try_count); + throw; + } } } } @@ -2730,7 +2756,7 @@ void wallet2::add_unconfirmed_tx(const cryptonote::transaction& tx, uint64_t amo utd.m_amount_out = 0; for (const auto &d: dests) utd.m_amount_out += d.amount; - utd.m_amount_out += change_amount; + utd.m_amount_out += change_amount; // dests does not contain change utd.m_change = change_amount; utd.m_sent_time = time(NULL); utd.m_tx = (const cryptonote::transaction_prefix&)tx; @@ -3057,7 +3083,7 @@ bool wallet2::sign_tx(const std::string &unsigned_filename, const std::string &s ptx.change_dts = sd.change_dts; ptx.selected_transfers = sd.selected_transfers; ptx.tx_key = rct::rct2sk(rct::identity()); // don't send it back to the untrusted view wallet - ptx.dests = sd.splitted_dsts; + ptx.dests = sd.dests; ptx.construction_data = sd; txs.push_back(ptx); @@ -3335,8 +3361,8 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<si LOG_PRINT_L2("base_requested_outputs_count: " << base_requested_outputs_count); // generate output indices to request - COMMAND_RPC_GET_OUTPUTS::request req = AUTO_VAL_INIT(req); - COMMAND_RPC_GET_OUTPUTS::response daemon_resp = AUTO_VAL_INIT(daemon_resp); + COMMAND_RPC_GET_OUTPUTS_BIN::request req = AUTO_VAL_INIT(req); + COMMAND_RPC_GET_OUTPUTS_BIN::response daemon_resp = AUTO_VAL_INIT(daemon_resp); size_t num_selected_transfers = 0; for(size_t idx: selected_transfers) @@ -3442,7 +3468,7 @@ void wallet2::get_outs(std::vector<std::vector<entry>> &outs, const std::list<si // sort the subsection, to ensure the daemon doesn't know wich output is ours std::sort(req.outputs.begin() + start, req.outputs.end(), - [](const COMMAND_RPC_GET_OUTPUTS::out &a, const COMMAND_RPC_GET_OUTPUTS::out &b) { return a.index < b.index; }); + [](const get_outputs_out &a, const get_outputs_out &b) { return a.index < b.index; }); } for (auto i: req.outputs) @@ -3656,6 +3682,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent ptx.construction_data.extra = tx.extra; ptx.construction_data.unlock_time = unlock_time; ptx.construction_data.use_rct = false; + ptx.construction_data.dests = dsts; } void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry> dsts, const std::list<size_t> selected_transfers, size_t fake_outputs_count, @@ -3776,6 +3803,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry ptx.construction_data.extra = tx.extra; ptx.construction_data.unlock_time = unlock_time; ptx.construction_data.use_rct = true; + ptx.construction_data.dests = dsts; } static size_t estimate_rct_tx_size(int n_inputs, int mixin, int n_outputs) @@ -4688,6 +4716,53 @@ bool wallet2::verify(const std::string &data, const cryptonote::account_public_a return crypto::check_signature(hash, address.m_spend_public_key, s); } //---------------------------------------------------------------------------------------------------- +crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const +{ + std::vector<tx_extra_field> tx_extra_fields; + if(!parse_tx_extra(td.m_tx.extra, tx_extra_fields)) + { + // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key + } + + // Due to a previous bug, there might be more than one tx pubkey in extra, one being + // the result of a previously discarded signature. + // For speed, since scanning for outputs is a slow process, we check whether extra + // contains more than one pubkey. If not, the first one is returned. If yes, they're + // checked for whether they yield at least one output + tx_extra_pub_key pub_key_field; + THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, 0), error::wallet_internal_error, + "Public key wasn't found in the transaction extra"); + const crypto::public_key tx_pub_key = pub_key_field.pub_key; + bool two_found = find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, 1); + if (!two_found) { + // easy case, just one found + return tx_pub_key; + } + + // more than one, loop and search + const cryptonote::account_keys& keys = m_account.get_keys(); + size_t pk_index = 0; + while (find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, pk_index++)) { + const crypto::public_key tx_pub_key = pub_key_field.pub_key; + crypto::key_derivation derivation; + generate_key_derivation(tx_pub_key, keys.m_view_secret_key, derivation); + + for (size_t i = 0; i < td.m_tx.vout.size(); ++i) + { + uint64_t money_transfered = 0; + bool error = false, received = false; + check_acc_out_precomp(keys.m_account_address.m_spend_public_key, td.m_tx.vout[i], derivation, i, received, money_transfered, error); + if (!error && received) + return tx_pub_key; + } + } + + // we found no key yielding an output + THROW_WALLET_EXCEPTION_IF(true, error::wallet_internal_error, + "Public key yielding at least one output wasn't found in the transaction extra"); + return cryptonote::null_pkey; +} +//---------------------------------------------------------------------------------------------------- std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key_images() const { std::vector<std::pair<crypto::key_image, crypto::signature>> ski; @@ -4713,10 +4788,8 @@ std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key { // Extra may only be partially parsed, it's OK if tx_extra_fields contains public key } - tx_extra_pub_key pub_key_field; - THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field), error::wallet_internal_error, - "Public key wasn't found in the transaction extra"); - crypto::public_key tx_pub_key = pub_key_field.pub_key; + + crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); // generate ephemeral secret key crypto::key_image ki; @@ -4845,10 +4918,9 @@ size_t wallet2::import_outputs(const std::vector<tools::wallet2::transfer_detail THROW_WALLET_EXCEPTION_IF(td.m_tx.vout.empty(), error::wallet_internal_error, "tx with no outputs at index " + boost::lexical_cast<std::string>(i)); THROW_WALLET_EXCEPTION_IF(!parse_tx_extra(td.m_tx.extra, tx_extra_fields), error::wallet_internal_error, "Transaction extra has unsupported format at index " + boost::lexical_cast<std::string>(i)); - THROW_WALLET_EXCEPTION_IF(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field), error::wallet_internal_error, - "Public key wasn't found in the transaction extra at index " + boost::lexical_cast<std::string>(i)); + crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td); - cryptonote::generate_key_image_helper(m_account.get_keys(), pub_key_field.pub_key, td.m_internal_output_index, in_ephemeral, td.m_key_image); + cryptonote::generate_key_image_helper(m_account.get_keys(), tx_pub_key, td.m_internal_output_index, in_ephemeral, td.m_key_image); td.m_key_image_known = true; THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != boost::get<cryptonote::txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key, error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key at index " + boost::lexical_cast<std::string>(i)); @@ -4889,14 +4961,15 @@ std::string wallet2::encrypt_with_view_secret_key(const std::string &plaintext, //---------------------------------------------------------------------------------------------------- std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated) const { - THROW_WALLET_EXCEPTION_IF(ciphertext.size() < sizeof(chacha8_iv), - error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key"); + const size_t prefix_size = sizeof(chacha8_iv) + (authenticated ? sizeof(crypto::signature) : 0); + THROW_WALLET_EXCEPTION_IF(ciphertext.size() < prefix_size, + error::wallet_internal_error, "Unexpected ciphertext size"); crypto::chacha8_key key; crypto::generate_chacha8_key(&skey, sizeof(skey), key); const crypto::chacha8_iv &iv = *(const crypto::chacha8_iv*)&ciphertext[0]; std::string plaintext; - plaintext.resize(ciphertext.size() - sizeof(iv) - (authenticated ? sizeof(crypto::signature) : 0)); + plaintext.resize(ciphertext.size() - prefix_size); if (authenticated) { crypto::hash hash; @@ -4907,7 +4980,7 @@ std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret THROW_WALLET_EXCEPTION_IF(!crypto::check_signature(hash, pkey, signature), error::wallet_internal_error, "Failed to authenticate criphertext"); } - crypto::chacha8(ciphertext.data() + sizeof(iv), ciphertext.size() - sizeof(iv), key, iv, &plaintext[0]); + crypto::chacha8(ciphertext.data() + sizeof(iv), ciphertext.size() - prefix_size, key, iv, &plaintext[0]); return std::move(plaintext); } //---------------------------------------------------------------------------------------------------- diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 6168873d5..b6d3250b2 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -191,11 +191,12 @@ namespace tools { std::vector<cryptonote::tx_source_entry> sources; cryptonote::tx_destination_entry change_dts; - std::vector<cryptonote::tx_destination_entry> splitted_dsts; + std::vector<cryptonote::tx_destination_entry> splitted_dsts; // split, includes change std::list<size_t> selected_transfers; std::vector<uint8_t> extra; uint64_t unlock_time; bool use_rct; + std::vector<cryptonote::tx_destination_entry> dests; // original setup, does not include change BEGIN_SERIALIZE_OBJECT() FIELD(sources) @@ -205,12 +206,16 @@ namespace tools FIELD(extra) VARINT_FIELD(unlock_time) FIELD(use_rct) + FIELD(dests) END_SERIALIZE() }; typedef std::vector<transfer_details> transfer_container; typedef std::unordered_multimap<crypto::hash, payment_details> payment_container; + // The convention for destinations is: + // dests does not include change + // splitted_dsts (in construction_data) does struct pending_tx { cryptonote::transaction tx; @@ -591,6 +596,7 @@ namespace tools template<typename entry> void get_outs(std::vector<std::vector<entry>> &outs, const std::list<size_t> &selected_transfers, size_t fake_outputs_count); bool wallet_generate_key_image_helper(const cryptonote::account_keys& ack, const crypto::public_key& tx_public_key, size_t real_output_index, cryptonote::keypair& in_ephemeral, crypto::key_image& ki); + crypto::public_key get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const; cryptonote::account_base m_account; std::string m_daemon_address; @@ -1047,6 +1053,7 @@ namespace tools ptx.construction_data.extra = tx.extra; ptx.construction_data.unlock_time = unlock_time; ptx.construction_data.use_rct = false; + ptx.construction_data.dests = dsts; } diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index f3658b9ff..5f050554f 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -50,6 +50,7 @@ set(unit_tests_sources test_format_utils.cpp test_peerlist.cpp test_protocol_pack.cpp + thread_group.cpp hardfork.cpp unbound.cpp varint.cpp diff --git a/tests/unit_tests/epee_levin_protocol_handler_async.cpp b/tests/unit_tests/epee_levin_protocol_handler_async.cpp index ca110eb59..2dd3ffe29 100644 --- a/tests/unit_tests/epee_levin_protocol_handler_async.cpp +++ b/tests/unit_tests/epee_levin_protocol_handler_async.cpp @@ -242,6 +242,7 @@ namespace m_req_head.m_cb = m_in_data.size(); m_req_head.m_have_to_return_data = true; m_req_head.m_command = expected_command; + m_req_head.m_return_code = LEVIN_OK; m_req_head.m_flags = LEVIN_PACKET_REQUEST; m_req_head.m_protocol_version = LEVIN_PROTOCOL_VER_1; diff --git a/tests/unit_tests/hardfork.cpp b/tests/unit_tests/hardfork.cpp index 7b4874811..be2885f45 100644 --- a/tests/unit_tests/hardfork.cpp +++ b/tests/unit_tests/hardfork.cpp @@ -117,7 +117,7 @@ public: blocks.push_back(blk); } virtual block get_block_from_height(const uint64_t& height) const { - return blocks[height]; + return blocks.at(height); } virtual void set_hard_fork_version(uint64_t height, uint8_t version) { if (versions.size() <= height) @@ -125,7 +125,7 @@ public: versions[height] = version; } virtual uint8_t get_hard_fork_version(uint64_t height) const { - return versions[height]; + return versions.at(height); } virtual void check_hard_fork_info() {} diff --git a/tests/unit_tests/serialization.cpp b/tests/unit_tests/serialization.cpp index dc7f7cb06..b592f456b 100644 --- a/tests/unit_tests/serialization.cpp +++ b/tests/unit_tests/serialization.cpp @@ -395,7 +395,10 @@ TEST(Serialization, serializes_transacion_signatures_correctly) // Not enough signature vectors for all inputs txin_to_key txin_to_key1; - txin_to_key1.key_offsets.resize(2); + txin_to_key1.amount = 1; + memset(&txin_to_key1.k_image, 0x42, sizeof(crypto::key_image)); + txin_to_key1.key_offsets.push_back(12); + txin_to_key1.key_offsets.push_back(3453); tx.vin.clear(); tx.vin.push_back(txin_to_key1); tx.vin.push_back(txin_to_key1); diff --git a/tests/unit_tests/thread_group.cpp b/tests/unit_tests/thread_group.cpp new file mode 100644 index 000000000..2e7a78353 --- /dev/null +++ b/tests/unit_tests/thread_group.cpp @@ -0,0 +1,177 @@ +// Copyright (c) 2014-2016, The Monero 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 "gtest/gtest.h" + +#include <atomic> +#include "common/task_region.h" +#include "common/thread_group.h" + +TEST(ThreadGroup, NoThreads) +{ + tools::task_region(tools::thread_group(0), [] (tools::task_region_handle& region) { + std::atomic<bool> completed{false}; + region.run([&] { completed = true; }); + EXPECT_TRUE(completed); + }); + { + tools::thread_group group(0); + std::atomic<bool> completed{false}; + group.dispatch([&] { completed = true; }); + EXPECT_TRUE(completed); + } +} + +TEST(ThreadGroup, OneThread) +{ + tools::thread_group group(1); + + for (unsigned i = 0; i < 3; ++i) { + std::atomic<bool> completed{false}; + tools::task_region(group, [&] (tools::task_region_handle& region) { + region.run([&] { completed = true; }); + }); + EXPECT_TRUE(completed); + } +} + + +TEST(ThreadGroup, UseActiveThreadOnSync) +{ + tools::thread_group group(1); + + for (unsigned i = 0; i < 3; ++i) { + std::atomic<bool> completed{false}; + tools::task_region(group, [&] (tools::task_region_handle& region) { + region.run([&] { while (!completed); }); + region.run([&] { completed = true; }); + }); + EXPECT_TRUE(completed); + } +} + +TEST(ThreadGroup, InOrder) +{ + tools::thread_group group(1); + + for (unsigned i = 0; i < 3; ++i) { + std::atomic<unsigned> count{0}; + std::atomic<bool> completed{false}; + tools::task_region(group, [&] (tools::task_region_handle& region) { + region.run([&] { while (!completed); }); + region.run([&] { if (count == 0) completed = true; }); + region.run([&] { ++count; }); + }); + EXPECT_TRUE(completed); + EXPECT_EQ(1u, count); + } +} + +TEST(ThreadGroup, TwoThreads) +{ + tools::thread_group group(2); + + for (unsigned i = 0; i < 3; ++i) { + std::atomic<bool> completed{false}; + tools::task_region(group, [&] (tools::task_region_handle& region) { + region.run([&] { while (!completed); }); + region.run([&] { while (!completed); }); + region.run([&] { completed = true; }); + }); + EXPECT_TRUE(completed); + } +} + +TEST(ThreadGroup, Nested) { + struct fib { + unsigned operator()(tools::thread_group& group, unsigned value) const { + if (value == 0 || value == 1) { + return value; + } + unsigned left = 0; + unsigned right = 0; + tools::task_region(group, [&, value] (tools::task_region_handle& region) { + region.run([&, value] { left = fib{}(group, value - 1); }); + region.run([&, value] { right = fib{}(group, value - 2); } ); + }); + return left + right; + } + + unsigned operator()(tools::thread_group&& group, unsigned value) const { + return (*this)(group, value); + } + }; + // be careful of depth on asynchronous version + EXPECT_EQ(6765, fib{}(tools::thread_group(0), 20)); + EXPECT_EQ(377, fib{}(tools::thread_group(1), 14)); +} + +TEST(ThreadGroup, Many) +{ + tools::thread_group group(1); + + for (unsigned i = 0; i < 3; ++i) { + std::atomic<unsigned> count{0}; + tools::task_region(group, [&] (tools::task_region_handle& region) { + for (unsigned tasks = 0; tasks < 1000; ++tasks) { + region.run([&] { ++count; }); + } + }); + EXPECT_EQ(1000u, count); + } +} + +TEST(ThreadGroup, ThrowInTaskRegion) +{ + class test_exception final : std::exception { + public: + explicit test_exception() : std::exception() {} + + virtual const char* what() const noexcept override { + return "test_exception"; + } + }; + + tools::thread_group group(1); + + for (unsigned i = 0; i < 3; ++i) { + std::atomic<unsigned> count{0}; + EXPECT_THROW( + [&] { + tools::task_region(group, [&] (tools::task_region_handle& region) { + for (unsigned tasks = 0; tasks < 1000; ++tasks) { + region.run([&] { ++count; }); + } + throw test_exception(); + }); + }(), + test_exception + ); + EXPECT_GE(1000u, count); + } +} diff --git a/utils/gpg_keys/jaquee.asc b/utils/gpg_keys/jaquee.asc index 1fa043b35..f1199dd25 100644 --- a/utils/gpg_keys/jaquee.asc +++ b/utils/gpg_keys/jaquee.asc @@ -1,30 +1,30 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v2 -mQENBFfxeegBCACggflhOp8t5kXdNc9rusZB96cNJkCCNKj2El7Q/5FCN8LmIetq -IzWd9b4hpfxZRn6g6NaOv8bO6EJPZHpk1nO1ZVIM+jWl1oY3BCgR0Loalntyk6aF -HAAEo5YUlEN6pyZ9PHL6legR/pu90YV9cp2NoDmO85o1h5o8O549XPLBPwrxQ8tH -4ZPjVKOfL6EHkkNZPid7On30S2r6mnsLjlfME4xGJkZn9+giBtYLMNjDe9DNzwsO -lrVPVfUWO9zwdOLVQQ1dgA5mlAZCuzXm54Onp/sgHH4RVjhVMrPvnaiGcLW+jybR -5t2FOdZD0oa9V4lLCFT9GNgErhU6cmkAyPoRABEBAAG0IUphY29iIEJyeWRvbGYg -PGphY29iQGJyeWRvbGYubmV0PokBOQQTAQgAIwUCV/F56AIbAwcLCQgHAwIBBhUI -AgkKCwQWAgMBAh4BAheAAAoJEN5GJGVQ0vPFi+wH/iw4Y7hPbulYgyaNQfAZTXo2 -DKHQP6iKk6zafMlgMsISTWqm1qTF/7MFa6yYjrVTRvORFrphpvEwYDGN6i/3Q4zn -5C/xdZSgw6PQaLc4xB7lS7b/tlk06QN7esLFbwDXorx7w1WAB6GTdmWxLzCsKUaZ -9M8r5JtwndtVMqlMrjqBFPBiOJlgTsDc7L7cNpZXK/PqRA6Xd8Q93NJ9Y0w7I/sl -X0AL509dVCHyo5tTQP3/paDDjenYHhqy/IOKLWKLm65L76vqw9eA9vdldjb0am2g -djBenHZKcnVLir+r/C3FD7BxqWL1/ZR7A3JaAACfSIawOUs0NAVhZE7c+JGHgwG5 -AQ0EV/F56AEIANNgpM+Y3DL+TTjcs9GN4ZcaBitxpSOtei0b8hmQx82KWSMJxb4+ -pH+HTCsFmSX744+B7nvvyaYLfVWnh3n2khjPaae/e9BoCmCIVQGnxAbhPUvHALPr -FN+s2wpLYx2eXuVvZaaafrXinRx8uhdwZNw40K1BcfbOxFhnJy/URWGpQ5vFltMY -i/YDbcLgRQm1aRaF6uYldwImHqIBwZeETTKL5YdAZxpnKvJkpEb6LXCQd00H7jo9 -s4U7WHl6RofirnlsWiYqSoAcIzUxAuqaijd000PkgDHGVbQUTLsyws5UBuoe1Zb8 -+/DfS7eOba7fXvLG63AkEkVTydLHrtG7wO0AEQEAAYkBHwQYAQgACQUCV/F56AIb -DAAKCRDeRiRlUNLzxZlbB/4npqwsObodmOBW0b78xcDR8kmSvVyjThVoqsjkaqi1 -hP29/o+bmA6OhyET7EjTkCBlCd2zXcqAoUwvEvcSt/qnMrv02zTCvUkYHy2DtxDl -BmMPdbtX2EaKfHtQ+7/LEiQzckVwYsJrX2U23oGaRvwVwlF7ARTE8whVYfngvcgF -2zgStg9YfH9Gs7V0A14gr7tnaZsCj+BnF4BiDuClBiCn38cBVM0SKzb2QqEX68Wt -qRIfOexu99/Gt3Px+JvpKOf0uCOTRezpCuL9Nm7cxRzR69bMFO0rWfPZUMPHzHx3 -NJW9mHeFMvJVD3tEXMB6MaGHNVtc16lVUcgeqUKcDlqH -=B9zn ------END PGP PUBLIC KEY BLOCK----- +mQENBFg14koBCACxrLtcmKK1X9haFa90Vr04PfHK9hXDeUrwUS8xIhCojj2WccdR +6hJLUCR27G03gb36xadQhQVCYk2tAwOKLjZVlsUtJ5rLASjPv9mcmgZnL5nU66Qi +9lHFKBD/Mrf7X6Bg89nWT2dUshIUuvkmbAAh9o9i8CoA/P62/9TG4uWc+vbNGEzV +jb6UtPQc6DHgZ8HeagC9WLE4bTx8tIxNmimwyLc5fow0G0LE0eb3V8jPMUZkdG1h +byHOLeqbSkFJd8GVYz96o/XNzFPPv/60bihn5wDMZqL9J64odoCqzligP1OprhhF +wEsbt0QQ60R9AlCjCgz7pgH8QIJSeX1a121HABEBAAG0IEphcXVlZSA8amFxdWVl +Lm1vbmVyb0BnbWFpbC5jb20+iQE5BBMBCAAjBQJYNeJKAhsDBwsJCAcDAgEGFQgC +CQoLBBYCAwECHgECF4AACgkQOE5SsJ9F3DmyqggArNrQzE4jopaDzxNy+VKTKf7z +chMk9zzY6TSKQ9YfQe0zHKcL7GyfQ/plVmoPMZQglVvC54Vw0aLA8PGt1a/ZU0mD +KWLTGbdIHasGatkY7J3B/vjDsPxfWMKai4Bpesk+gI4/nb2V8/CVsSuVlX5PrQD4 +buXhv7S7Mn6ezzzRZD/oYN5WaokgAxu8rnTlJN8yQP3uat71ply+JN3u3TwFxMeX +WcrDTwq/XqJD7OUWrBm378v7M1Dd8kWQTwyWMPCDZhaYgiSlSwKfbDsq4e7d55jv +2ucKL1AOGnpn6hJ7ip0sn9rLr53qD7eqHl7yzsrrRZkYH6AYiXvk45Srx76DSbkB +DQRYNeJKAQgAvMTf6cGiBTJzj9DfATqQlLEPWJHbqfoFTIbRlHmYdCBbLH2M/GCr +HK4YdhF+9i+Wnq5E9fxnxJzhm22dfSWgw9GuyX8LxBanVhhQiE+9eH/H0ULHjokN +PVQOg90XdAQWahvl5YCOmqt7H7z6+HxHIhCHzo688+lPC4zol0PYNEWcHYBrVsax +cEQ2LObD9J7bBY1BqLXppzHATTwxuO/bp9Bd9LmCaFBinLyF34oDYttEVOq2RNko +vNeWbHWpmdzoagHxLGMU1ae+yCMuGHSdfLnLWTUdVLepVLmzblXxgTd7SS4Bm3Y0 +1jBgBni8msSTR1ICvyMcQuRZnK/yAgM+TQARAQABiQEfBBgBCAAJBQJYNeJKAhsM +AAoJEDhOUrCfRdw5hmsH/0Moh1Z4kPXbvfincDarDDDe5wUImey/7WwOrSsiPwTI +8AkRts496o3XHcsSWLXZ5Ab+cGKnys5TRfX+RqMrG4p+TXMt2vWDnF5gjeRcwpcz +UI/TCFkCg+x3QAEvlNFNLK4APd2fwR/KQJjmn4a4/0boqrrn09vLSx7/+bdsfYbZ +eKSuF58aYVROY9FUNhFsvgjGI/sYNQFj0DoexLe28f4/cFFVpZvb/kJYqm4QauyK +5SMyvTTu8PD+pZpLM9MFoQ+Jk+EmDzSUlarwnt75O6kMLNLPFJ8hCOiN2RNK2PKk +iTpY5CQtKsGQDRA7sT6ZaiWO7kkQGO0HSBfmWnZq2G0= +=Ky09 +-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file |