// Copyright (c) 2014-2018, 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 #include #include #include #include "common/unordered_containers_boost_serialization.h" #include "common/command_line.h" #include "common/varint.h" #include "serialization/crypto.h" #include "cryptonote_basic/cryptonote_boost_serialization.h" #include "cryptonote_core/tx_pool.h" #include "cryptonote_core/cryptonote_core.h" #include "cryptonote_core/blockchain.h" #include "blockchain_db/blockchain_db.h" #include "blockchain_db/db_types.h" #include "wallet/ringdb.h" #include "version.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "bcutil" namespace po = boost::program_options; using namespace epee; using namespace cryptonote; static const char zerokey[8] = {0}; static const MDB_val zerokval = { sizeof(zerokey), (void *)zerokey }; static uint64_t records_per_sync = 200; static uint64_t db_flags = 0; static MDB_dbi dbi_relative_rings; static MDB_dbi dbi_outputs; static MDB_dbi dbi_processed_txidx; static MDB_dbi dbi_spent; static MDB_dbi dbi_ring_instances; static MDB_dbi dbi_stats; static MDB_env *env = NULL; struct output_data { uint64_t amount; uint64_t index; output_data(): amount(0), index(0) {} output_data(uint64_t a, uint64_t i): amount(a), index(i) {} bool operator==(const output_data &other) const { return other.amount == amount && other.index == index; } }; // // relative_rings: key_image -> vector // outputs: 128 bits -> set of key images // processed_txidx: string -> uint64_t // spent: 128 bits, zerokval // ring_instances: vector -> uint64_t // stats: string -> arbitrary // static bool parse_db_sync_mode(std::string db_sync_mode) { std::vector options; boost::trim(db_sync_mode); boost::split(options, db_sync_mode, boost::is_any_of(" :")); for(const auto &option : options) MDEBUG("option: " << option); // default to fast:async:1 uint64_t DEFAULT_FLAGS = DBF_FAST; if(options.size() == 0) { // default to fast:async:1 db_flags = DEFAULT_FLAGS; } bool safemode = false; if(options.size() >= 1) { if(options[0] == "safe") { safemode = true; db_flags = DBF_SAFE; } else if(options[0] == "fast") { db_flags = DBF_FAST; } else if(options[0] == "fastest") { db_flags = DBF_FASTEST; records_per_sync = 1000; // default to fastest:async:1000 } else db_flags = DEFAULT_FLAGS; } if(options.size() >= 2 && !safemode) { char *endptr; uint64_t bps = strtoull(options[1].c_str(), &endptr, 0); if (*endptr == '\0') records_per_sync = bps; } return true; } static std::string get_default_db_path() { boost::filesystem::path dir = tools::get_default_data_dir(); // remove .bitmonero, replace with .shared-ringdb dir = dir.remove_filename(); dir /= ".shared-ringdb"; return dir.string(); } static std::string get_cache_filename(boost::filesystem::path filename) { if (!boost::filesystem::is_directory(filename)) filename.remove_filename(); return filename.string(); } static int compare_hash32(const MDB_val *a, const MDB_val *b) { const uint32_t *va = (const uint32_t*) a->mv_data; const uint32_t *vb = (const uint32_t*) b->mv_data; for (int n = 7; n >= 0; n--) { if (va[n] == vb[n]) continue; return va[n] < vb[n] ? -1 : 1; } return 0; } int compare_uint64(const MDB_val *a, const MDB_val *b) { const uint64_t va = *(const uint64_t *)a->mv_data; const uint64_t vb = *(const uint64_t *)b->mv_data; return (va < vb) ? -1 : va > vb; } static int compare_double64(const MDB_val *a, const MDB_val *b) { const uint64_t va = *(const uint64_t*) a->mv_data; const uint64_t vb = *(const uint64_t*) b->mv_data; if (va == vb) { const uint64_t va = ((const uint64_t*) a->mv_data)[1]; const uint64_t vb = ((const uint64_t*) b->mv_data)[1]; return va < vb ? -1 : va > vb; } return va < vb ? -1 : va > vb; } static int resize_env(const char *db_path) { MDB_envinfo mei; MDB_stat mst; int ret; size_t needed = 1000ul * 1024 * 1024; // at least 1000 MB ret = mdb_env_info(env, &mei); if (ret) return ret; ret = mdb_env_stat(env, &mst); if (ret) return ret; uint64_t size_used = mst.ms_psize * mei.me_last_pgno; uint64_t mapsize = mei.me_mapsize; if (size_used + needed > mei.me_mapsize) { try { boost::filesystem::path path(db_path); boost::filesystem::space_info si = boost::filesystem::space(path); if(si.available < needed) { MERROR("!! WARNING: Insufficient free space to extend database !!: " << (si.available >> 20L) << " MB available"); return ENOSPC; } } catch(...) { // print something but proceed. MWARNING("Unable to query free disk space."); } mapsize += needed; } return mdb_env_set_mapsize(env, mapsize); } static void init(std::string cache_filename) { MDB_txn *txn; bool tx_active = false; int dbr; MINFO("Creating blackball cache in " << cache_filename); tools::create_directories_if_necessary(cache_filename); int flags = 0; if (db_flags & DBF_FAST) flags |= MDB_NOSYNC; if (db_flags & DBF_FASTEST) flags |= MDB_NOSYNC | MDB_WRITEMAP | MDB_MAPASYNC; dbr = mdb_env_create(&env); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LDMB environment: " + std::string(mdb_strerror(dbr))); dbr = mdb_env_set_maxdbs(env, 6); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to set max env dbs: " + std::string(mdb_strerror(dbr))); const std::string actual_filename = get_cache_filename(cache_filename); dbr = mdb_env_open(env, actual_filename.c_str(), flags, 0664); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open rings database file '" + actual_filename + "': " + std::string(mdb_strerror(dbr))); dbr = mdb_txn_begin(env, NULL, 0, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);}); tx_active = true; dbr = mdb_dbi_open(txn, "relative_rings", MDB_CREATE | MDB_INTEGERKEY, &dbi_relative_rings); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); mdb_set_compare(txn, dbi_relative_rings, compare_hash32); dbr = mdb_dbi_open(txn, "outputs", MDB_CREATE | MDB_INTEGERKEY, &dbi_outputs); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); mdb_set_compare(txn, dbi_outputs, compare_double64); dbr = mdb_dbi_open(txn, "processed_txidx", MDB_CREATE, &dbi_processed_txidx); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); dbr = mdb_dbi_open(txn, "spent", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, &dbi_spent); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); mdb_set_dupsort(txn, dbi_spent, compare_double64); dbr = mdb_dbi_open(txn, "ring_instances", MDB_CREATE, &dbi_ring_instances); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); dbr = mdb_dbi_open(txn, "stats", MDB_CREATE, &dbi_stats); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); dbr = mdb_txn_commit(txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to commit txn creating/opening database: " + std::string(mdb_strerror(dbr))); tx_active = false; } static void close() { if (env) { mdb_dbi_close(env, dbi_relative_rings); mdb_dbi_close(env, dbi_outputs); mdb_dbi_close(env, dbi_processed_txidx); mdb_dbi_close(env, dbi_spent); mdb_dbi_close(env, dbi_ring_instances); mdb_dbi_close(env, dbi_stats); mdb_env_close(env); env = NULL; } } static std::string compress_ring(const std::vector &ring, std::string s = "") { for (uint64_t out: ring) s += tools::get_varint_data(out); return s; } static std::string compress_ring(uint64_t amount, const std::vector &ring) { return compress_ring(ring, tools::get_varint_data(amount)); } static std::vector decompress_ring(const std::string &s) { std::vector ring; int read = 0; for (std::string::const_iterator i = s.begin(); i != s.cend(); std::advance(i, read)) { uint64_t out; std::string tmp(i, s.cend()); read = tools::read_varint(tmp.begin(), tmp.end(), out); CHECK_AND_ASSERT_THROW_MES(read > 0 && read <= 256, "Internal error decompressing ring"); ring.push_back(out); } return ring; } static bool for_all_transactions(const std::string &filename, uint64_t &start_idx, uint64_t &n_txes, const std::function &f) { MDB_env *env; MDB_dbi dbi; MDB_txn *txn; MDB_cursor *cur; int dbr; bool tx_active = false; MDB_val k; MDB_val v; dbr = mdb_env_create(&env); if (dbr) throw std::runtime_error("Failed to create LDMB environment: " + std::string(mdb_strerror(dbr))); dbr = mdb_env_set_maxdbs(env, 2); if (dbr) throw std::runtime_error("Failed to set max env dbs: " + std::string(mdb_strerror(dbr))); const std::string actual_filename = filename; dbr = mdb_env_open(env, actual_filename.c_str(), 0, 0664); if (dbr) throw std::runtime_error("Failed to open rings database file '" + actual_filename + "': " + std::string(mdb_strerror(dbr))); dbr = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); if (dbr) throw std::runtime_error("Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);}); tx_active = true; dbr = mdb_dbi_open(txn, "txs_pruned", MDB_INTEGERKEY, &dbi); if (dbr) dbr = mdb_dbi_open(txn, "txs", MDB_INTEGERKEY, &dbi); if (dbr) throw std::runtime_error("Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); dbr = mdb_cursor_open(txn, dbi, &cur); if (dbr) throw std::runtime_error("Failed to create LMDB cursor: " + std::string(mdb_strerror(dbr))); MDB_stat stat; dbr = mdb_stat(txn, dbi, &stat); if (dbr) throw std::runtime_error("Failed to query m_block_info: " + std::string(mdb_strerror(dbr))); n_txes = stat.ms_entries; bool fret = true; k.mv_size = sizeof(uint64_t); k.mv_data = &start_idx; MDB_cursor_op op = MDB_SET; while (1) { int ret = mdb_cursor_get(cur, &k, &v, op); op = MDB_NEXT; if (ret == MDB_NOTFOUND) break; if (ret) throw std::runtime_error("Failed to enumerate transactions: " + std::string(mdb_strerror(ret))); if (k.mv_size != sizeof(uint64_t)) throw std::runtime_error("Bad key size"); const uint64_t idx = *(uint64_t*)k.mv_data; if (idx < start_idx) continue; cryptonote::transaction_prefix tx; blobdata bd; bd.assign(reinterpret_cast(v.mv_data), v.mv_size); std::stringstream ss; ss << bd; binary_archive ba(ss); bool r = do_serialize(ba, tx); CHECK_AND_ASSERT_MES(r, false, "Failed to parse transaction from blob"); start_idx = *(uint64_t*)k.mv_data; if (!f(tx)) { fret = false; break; } } mdb_cursor_close(cur); mdb_txn_commit(txn); tx_active = false; mdb_dbi_close(env, dbi); mdb_env_close(env); return fret; } static uint64_t find_first_diverging_transaction(const std::string &first_filename, const std::string &second_filename) { MDB_env *env[2]; MDB_dbi dbi[2]; MDB_txn *txn[2]; MDB_cursor *cur[2]; int dbr; bool tx_active[2] = { false, false }; uint64_t n_txes[2]; MDB_val k; MDB_val v[2]; epee::misc_utils::auto_scope_leave_caller txn_dtor[2]; for (int i = 0; i < 2; ++i) { dbr = mdb_env_create(&env[i]); if (dbr) throw std::runtime_error("Failed to create LDMB environment: " + std::string(mdb_strerror(dbr))); dbr = mdb_env_set_maxdbs(env[i], 2); if (dbr) throw std::runtime_error("Failed to set max env dbs: " + std::string(mdb_strerror(dbr))); const std::string actual_filename = i ? second_filename : first_filename; dbr = mdb_env_open(env[i], actual_filename.c_str(), 0, 0664); if (dbr) throw std::runtime_error("Failed to open rings database file '" + actual_filename + "': " + std::string(mdb_strerror(dbr))); dbr = mdb_txn_begin(env[i], NULL, MDB_RDONLY, &txn[i]); if (dbr) throw std::runtime_error("Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); txn_dtor[i] = epee::misc_utils::create_scope_leave_handler([&, i](){if (tx_active[i]) mdb_txn_abort(txn[i]);}); tx_active[i] = true; dbr = mdb_dbi_open(txn[i], "txs_pruned", MDB_INTEGERKEY, &dbi[i]); if (dbr) dbr = mdb_dbi_open(txn[i], "txs", MDB_INTEGERKEY, &dbi[i]); if (dbr) throw std::runtime_error("Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); dbr = mdb_cursor_open(txn[i], dbi[i], &cur[i]); if (dbr) throw std::runtime_error("Failed to create LMDB cursor: " + std::string(mdb_strerror(dbr))); MDB_stat stat; dbr = mdb_stat(txn[i], dbi[i], &stat); if (dbr) throw std::runtime_error("Failed to query m_block_info: " + std::string(mdb_strerror(dbr))); n_txes[i] = stat.ms_entries; } if (n_txes[0] == 0 || n_txes[1] == 0) throw std::runtime_error("No transaction in the database"); uint64_t lo = 0, hi = std::min(n_txes[0], n_txes[1]) - 1; while (lo <= hi) { uint64_t mid = (lo + hi) / 2; k.mv_size = sizeof(uint64_t); k.mv_data = (void*)∣ dbr = mdb_cursor_get(cur[0], &k, &v[0], MDB_SET); if (dbr) throw std::runtime_error("Failed to query transaction: " + std::string(mdb_strerror(dbr))); dbr = mdb_cursor_get(cur[1], &k, &v[1], MDB_SET); if (dbr) throw std::runtime_error("Failed to query transaction: " + std::string(mdb_strerror(dbr))); if (v[0].mv_size == v[1].mv_size && !memcmp(v[0].mv_data, v[1].mv_data, v[0].mv_size)) lo = mid + 1; else hi = mid - 1; } for (int i = 0; i < 2; ++i) { mdb_cursor_close(cur[i]); mdb_txn_commit(txn[i]); tx_active[i] = false; mdb_dbi_close(env[i], dbi[i]); mdb_env_close(env[i]); } return hi; } static std::vector canonicalize(const std::vector &v) { std::vector c; c.reserve(v.size()); c.push_back(v[0]); for (size_t n = 1; n < v.size(); ++n) { if (v[n] != 0) c.push_back(v[n]); } if (c.size() < v.size()) { MINFO("Ring has duplicate member(s): " << boost::join(v | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " ")); } return c; } static uint64_t get_num_spent_outputs() { MDB_txn *txn; bool tx_active = false; int dbr = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);}); tx_active = true; MDB_cursor *cur; dbr = mdb_cursor_open(txn, dbi_spent, &cur); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open cursor for spent outputs: " + std::string(mdb_strerror(dbr))); MDB_val k, v; mdb_size_t count = 0; dbr = mdb_cursor_get(cur, &k, &v, MDB_FIRST); if (dbr != MDB_NOTFOUND) { CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get first spent output: " + std::string(mdb_strerror(dbr))); dbr = mdb_cursor_count(cur, &count); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to count entries: " + std::string(mdb_strerror(dbr))); } mdb_cursor_close(cur); dbr = mdb_txn_commit(txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to commit txn: " + std::string(mdb_strerror(dbr))); tx_active = false; return count; } static void add_spent_output(MDB_txn *txn, const output_data &od) { MDB_cursor *cur; int dbr = mdb_cursor_open(txn, dbi_spent, &cur); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open cursor for spent outputs: " + std::string(mdb_strerror(dbr))); MDB_val v = {sizeof(od), (void*)&od}; dbr = mdb_cursor_put(cur, (MDB_val *)&zerokval, &v, MDB_NODUPDATA); CHECK_AND_ASSERT_THROW_MES(!dbr || dbr == MDB_KEYEXIST, "Failed to add spent output: " + std::string(mdb_strerror(dbr))); mdb_cursor_close(cur); } static bool is_output_spent(MDB_txn *txn, const output_data &od) { MDB_cursor *cur; int dbr = mdb_cursor_open(txn, dbi_spent, &cur); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open cursor for spent outputs: " + std::string(mdb_strerror(dbr))); MDB_val v = {sizeof(od), (void*)&od}; dbr = mdb_cursor_get(cur, (MDB_val *)&zerokval, &v, MDB_GET_BOTH); CHECK_AND_ASSERT_THROW_MES(!dbr || dbr == MDB_NOTFOUND, "Failed to get spent output: " + std::string(mdb_strerror(dbr))); bool spent = dbr == 0; mdb_cursor_close(cur); return spent; } static std::vector get_spent_outputs(MDB_txn *txn) { MDB_cursor *cur; int dbr = mdb_cursor_open(txn, dbi_spent, &cur); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open cursor for spent outputs: " + std::string(mdb_strerror(dbr))); MDB_val k, v; uint64_t count = 0; dbr = mdb_cursor_get(cur, &k, &v, MDB_FIRST); if (dbr != MDB_NOTFOUND) { CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get first spent output: " + std::string(mdb_strerror(dbr))); dbr = mdb_cursor_count(cur, &count); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to count entries: " + std::string(mdb_strerror(dbr))); } std::vector outs; outs.reserve(count); while (1) { const output_data *od = (const output_data*)v.mv_data; outs.push_back(*od); dbr = mdb_cursor_get(cur, &k, &v, MDB_NEXT); if (dbr == MDB_NOTFOUND) break; CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get next spent output: " + std::string(mdb_strerror(dbr))); } mdb_cursor_close(cur); return outs; } static uint64_t get_processed_txidx(const std::string &name) { MDB_txn *txn; bool tx_active = false; int dbr = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);}); tx_active = true; uint64_t height = 0; MDB_val k, v; k.mv_data = (void*)name.c_str(); k.mv_size = name.size(); dbr = mdb_get(txn, dbi_processed_txidx, &k, &v); if (dbr != MDB_NOTFOUND) { CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get processed height: " + std::string(mdb_strerror(dbr))); height = *(const uint64_t*)v.mv_data; } dbr = mdb_txn_commit(txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to commit txn: " + std::string(mdb_strerror(dbr))); tx_active = false; return height; } static void set_processed_txidx(MDB_txn *txn, const std::string &name, uint64_t height) { MDB_val k, v; k.mv_data = (void*)name.c_str(); k.mv_size = name.size(); v.mv_data = (void*)&height; v.mv_size = sizeof(height); int dbr = mdb_put(txn, dbi_processed_txidx, &k, &v, 0); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to set processed height: " + std::string(mdb_strerror(dbr))); } static bool get_relative_ring(MDB_txn *txn, const crypto::key_image &ki, std::vector &ring) { const std::string sring = compress_ring(ring); MDB_val k, v; k.mv_data = (void*)&ki; k.mv_size = sizeof(ki); int dbr = mdb_get(txn, dbi_relative_rings, &k, &v); if (dbr == MDB_NOTFOUND) return false; CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get relative ring: " + std::string(mdb_strerror(dbr))); ring = decompress_ring(std::string((const char*)v.mv_data, v.mv_size)); return true; } static void set_relative_ring(MDB_txn *txn, const crypto::key_image &ki, const std::vector &ring) { const std::string sring = compress_ring(ring); MDB_val k, v; k.mv_data = (void*)&ki; k.mv_size = sizeof(ki); v.mv_data = (void*)sring.c_str(); v.mv_size = sring.size(); int dbr = mdb_put(txn, dbi_relative_rings, &k, &v, 0); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to set relative ring: " + std::string(mdb_strerror(dbr))); } static std::string keep_under_511(const std::string &s) { if (s.size() <= 511) return s; crypto::hash hash; crypto::cn_fast_hash(s.data(), s.size(), hash); return std::string((const char*)&hash, 32); } static uint64_t get_ring_instances(MDB_txn *txn, uint64_t amount, const std::vector &ring) { const std::string sring = keep_under_511(compress_ring(amount, ring)); MDB_val k, v; k.mv_data = (void*)sring.data(); k.mv_size = sring.size(); int dbr = mdb_get(txn, dbi_ring_instances, &k, &v); if (dbr == MDB_NOTFOUND) return 0; CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get ring instances: " + std::string(mdb_strerror(dbr))); return *(const uint64_t*)v.mv_data; } static uint64_t get_ring_subset_instances(MDB_txn *txn, uint64_t amount, const std::vector &ring) { uint64_t instances = get_ring_instances(txn, amount, ring); if (ring.size() > 11) return instances; uint64_t extra = 0; for (uint64_t mask = 1; mask < (1u << ring.size()) - 1; ++mask) { std::vector subset; subset.reserve(ring.size()); for (size_t i = 0; i < ring.size(); ++i) if ((mask >> i) & 1) subset.push_back(ring[i]); extra += get_ring_instances(txn, amount, subset); } return instances + extra; } static void set_ring_instances(MDB_txn *txn, uint64_t amount, const std::vector &ring, uint64_t count) { const std::string sring = keep_under_511(compress_ring(amount, ring)); MDB_val k, v; k.mv_data = (void*)sring.data(); k.mv_size = sring.size(); v.mv_data = &count; v.mv_size = sizeof(count); int dbr = mdb_put(txn, dbi_ring_instances, &k, &v, 0); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get ring instances: " + std::string(mdb_strerror(dbr))); } static std::vector get_key_images(MDB_txn *txn, const output_data &od) { MDB_val k, v; k.mv_data = (void*)&od; k.mv_size = sizeof(od); int dbr = mdb_get(txn, dbi_outputs, &k, &v); CHECK_AND_ASSERT_THROW_MES(!dbr || dbr == MDB_NOTFOUND, "Failed to get output: " + std::string(mdb_strerror(dbr))); if (dbr == MDB_NOTFOUND) return {}; CHECK_AND_ASSERT_THROW_MES(v.mv_size % 32 == 0, "Unexpected record size"); std::vector key_images; key_images.reserve(v.mv_size / 32); const crypto::key_image *ki = (const crypto::key_image*)v.mv_data; for (size_t n = 0; n < v.mv_size / 32; ++n) key_images.push_back(*ki++); return key_images; } static void add_key_image(MDB_txn *txn, const output_data &od, const crypto::key_image &ki) { MDB_val k, v; k.mv_data = (void*)&od; k.mv_size = sizeof(od); int dbr = mdb_get(txn, dbi_outputs, &k, &v); CHECK_AND_ASSERT_THROW_MES(!dbr || dbr == MDB_NOTFOUND, "Failed to get output"); std::string data; if (!dbr) { CHECK_AND_ASSERT_THROW_MES(v.mv_size % 32 == 0, "Unexpected record size"); data = std::string((const char*)v.mv_data, v.mv_size); } data += std::string((const char*)&ki, sizeof(ki)); v.mv_data = (void*)data.data(); v.mv_size = data.size(); dbr = mdb_put(txn, dbi_outputs, &k, &v, 0); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to set outputs: " + std::string(mdb_strerror(dbr))); } static bool get_stat(MDB_txn *txn, const char *key, uint64_t &data) { MDB_val k, v; k.mv_data = (void*)key; k.mv_size = strlen(key); int dbr = mdb_get(txn, dbi_stats, &k, &v); if (dbr == MDB_NOTFOUND) return false; CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to get stat record"); CHECK_AND_ASSERT_THROW_MES(v.mv_size == sizeof(uint64_t), "Unexpected record size"); data = *(const uint64_t*)v.mv_data; return true; } static void set_stat(MDB_txn *txn, const char *key, uint64_t data) { MDB_val k, v; k.mv_data = (void*)key; k.mv_size = strlen(key); v.mv_data = (void*)&data; v.mv_size = sizeof(uint64_t); int dbr = mdb_put(txn, dbi_stats, &k, &v, 0); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to set stat record"); } static void inc_stat(MDB_txn *txn, const char *key) { uint64_t data; if (!get_stat(txn, key, data)) data = 0; ++data; set_stat(txn, key, data); } static void open_db(const std::string &filename, MDB_env **env, MDB_txn **txn, MDB_cursor **cur, MDB_dbi *dbi) { tools::create_directories_if_necessary(filename); int flags = MDB_RDONLY; if (db_flags & DBF_FAST) flags |= MDB_NOSYNC; if (db_flags & DBF_FASTEST) flags |= MDB_NOSYNC | MDB_WRITEMAP | MDB_MAPASYNC; int dbr = mdb_env_create(env); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LDMB environment: " + std::string(mdb_strerror(dbr))); dbr = mdb_env_set_maxdbs(*env, 1); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to set max env dbs: " + std::string(mdb_strerror(dbr))); const std::string actual_filename = filename; MINFO("Opening monero blockchain at " << actual_filename); dbr = mdb_env_open(*env, actual_filename.c_str(), flags, 0664); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open rings database file '" + actual_filename + "': " + std::string(mdb_strerror(dbr))); dbr = mdb_txn_begin(*env, NULL, MDB_RDONLY, txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); dbr = mdb_dbi_open(*txn, "output_amounts", MDB_CREATE | MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, dbi); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); mdb_set_dupsort(*txn, *dbi, compare_uint64); dbr = mdb_cursor_open(*txn, *dbi, cur); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB cursor: " + std::string(mdb_strerror(dbr))); } static void close_db(MDB_env *env, MDB_txn *txn, MDB_cursor *cur, MDB_dbi dbi) { mdb_txn_abort(txn); mdb_cursor_close(cur); mdb_dbi_close(env, dbi); mdb_env_close(env); } static crypto::public_key get_output_key(MDB_cursor *cur, uint64_t amount, uint64_t offset) { MDB_val k = { sizeof(amount), (void*)&amount }, v = { sizeof(offset), (void*)&offset }; int dbr = mdb_cursor_get(cur, &k, &v, MDB_GET_BOTH); if (dbr) throw std::runtime_error("Output key not found: " + std::string(mdb_strerror(dbr))); return *(const crypto::public_key*)(((const char*)v.mv_data) + sizeof(uint64_t) * 2); } static void get_num_outputs(MDB_txn *txn, MDB_cursor *cur, MDB_dbi dbi, uint64_t &pre_rct, uint64_t &rct) { uint64_t amount = 0; MDB_val k = { sizeof(amount), (void*)&amount }, v; int dbr = mdb_cursor_get(cur, &k, &v, MDB_SET); if (dbr == MDB_NOTFOUND) { rct = 0; } else { if (dbr) throw std::runtime_error("Record 0 not found: " + std::string(mdb_strerror(dbr))); mdb_size_t count = 0; dbr = mdb_cursor_count(cur, &count); if (dbr) throw std::runtime_error("Failed to count records: " + std::string(mdb_strerror(dbr))); rct = count; } MDB_stat s; dbr = mdb_stat(txn, dbi, &s); if (dbr) throw std::runtime_error("Failed to count records: " + std::string(mdb_strerror(dbr))); if (s.ms_entries < rct) throw std::runtime_error("Inconsistent records: " + std::string(mdb_strerror(dbr))); pre_rct = s.ms_entries - rct; } static crypto::hash get_genesis_block_hash(const std::string &filename) { MDB_env *env; MDB_dbi dbi; MDB_txn *txn; int dbr; bool tx_active = false; dbr = mdb_env_create(&env); if (dbr) throw std::runtime_error("Failed to create LDMB environment: " + std::string(mdb_strerror(dbr))); dbr = mdb_env_set_maxdbs(env, 1); if (dbr) throw std::runtime_error("Failed to set max env dbs: " + std::string(mdb_strerror(dbr))); const std::string actual_filename = filename; dbr = mdb_env_open(env, actual_filename.c_str(), 0, 0664); if (dbr) throw std::runtime_error("Failed to open rings database file '" + actual_filename + "': " + std::string(mdb_strerror(dbr))); dbr = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); if (dbr) throw std::runtime_error("Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);}); tx_active = true; dbr = mdb_dbi_open(txn, "block_info", MDB_INTEGERKEY | MDB_DUPSORT | MDB_DUPFIXED, &dbi); mdb_set_dupsort(txn, dbi, compare_uint64); if (dbr) throw std::runtime_error("Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); uint64_t zero = 0; MDB_val k = { sizeof(uint64_t), (void*)&zero}, v; dbr = mdb_get(txn, dbi, &k, &v); if (dbr) throw std::runtime_error("Failed to retrieve genesis block: " + std::string(mdb_strerror(dbr))); crypto::hash genesis_block_hash = *(const crypto::hash*)(((const uint64_t*)v.mv_data) + 5); mdb_dbi_close(env, dbi); mdb_txn_abort(txn); mdb_env_close(env); tx_active = false; return genesis_block_hash; } int main(int argc, char* argv[]) { TRY_ENTRY(); epee::string_tools::set_module_name_and_folder(argv[0]); std::string default_db_type = "lmdb"; std::string available_dbs = cryptonote::blockchain_db_types(", "); available_dbs = "available: " + available_dbs; uint32_t log_level = 0; tools::on_startup(); boost::filesystem::path output_file_path; po::options_description desc_cmd_only("Command line options"); po::options_description desc_cmd_sett("Command line options and settings options"); const command_line::arg_descriptor arg_blackball_db_dir = { "blackball-db-dir", "Specify blackball database directory", get_default_db_path(), }; const command_line::arg_descriptor arg_log_level = {"log-level", "0-4 or categories", ""}; const command_line::arg_descriptor arg_database = { "database", available_dbs.c_str(), default_db_type }; const command_line::arg_descriptor arg_rct_only = {"rct-only", "Only work on ringCT outputs", false}; const command_line::arg_descriptor arg_check_subsets = {"check-subsets", "Check ring subsets (very expensive)", false}; const command_line::arg_descriptor > arg_inputs = {"inputs", "Path to Monero DB, and path to any fork DBs"}; const command_line::arg_descriptor arg_db_sync_mode = { "db-sync-mode" , "Specify sync option, using format [safe|fast|fastest]:[nrecords_per_sync]." , "fast:1000" }; command_line::add_arg(desc_cmd_sett, arg_blackball_db_dir); command_line::add_arg(desc_cmd_sett, arg_log_level); command_line::add_arg(desc_cmd_sett, arg_database); command_line::add_arg(desc_cmd_sett, arg_rct_only); command_line::add_arg(desc_cmd_sett, arg_check_subsets); command_line::add_arg(desc_cmd_sett, arg_db_sync_mode); command_line::add_arg(desc_cmd_sett, arg_inputs); command_line::add_arg(desc_cmd_only, command_line::arg_help); po::options_description desc_options("Allowed options"); desc_options.add(desc_cmd_only).add(desc_cmd_sett); po::positional_options_description positional_options; positional_options.add(arg_inputs.name, -1); po::variables_map vm; bool r = command_line::handle_error_helper(desc_options, [&]() { auto parser = po::command_line_parser(argc, argv).options(desc_options).positional(positional_options); po::store(parser.run(), vm); po::notify(vm); return true; }); if (! r) return 1; if (command_line::get_arg(vm, command_line::arg_help)) { std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << ENDL << ENDL; std::cout << desc_options << std::endl; return 1; } mlog_configure(mlog_get_default_log_path("monero-blockchain-blackball.log"), true); if (!command_line::is_arg_defaulted(vm, arg_log_level)) mlog_set_log(command_line::get_arg(vm, arg_log_level).c_str()); else mlog_set_log(std::string(std::to_string(log_level) + ",bcutil:INFO").c_str()); LOG_PRINT_L0("Starting..."); output_file_path = command_line::get_arg(vm, arg_blackball_db_dir); bool opt_rct_only = command_line::get_arg(vm, arg_rct_only); bool opt_check_subsets = command_line::get_arg(vm, arg_check_subsets); std::string db_type = command_line::get_arg(vm, arg_database); if (!cryptonote::blockchain_valid_db_type(db_type)) { std::cerr << "Invalid database type: " << db_type << std::endl; return 1; } std::string db_sync_mode = command_line::get_arg(vm, arg_db_sync_mode); if (!parse_db_sync_mode(db_sync_mode)) { MERROR("Invalid db sync mode: " << db_sync_mode); return 1; } const std::vector inputs = command_line::get_arg(vm, arg_inputs); if (inputs.empty()) { LOG_PRINT_L0("No inputs given"); return 1; } const std::string cache_dir = (output_file_path / "blackball-cache").string(); init(cache_dir); LOG_PRINT_L0("Scanning for blackballable outputs..."); size_t done = 0; const uint64_t start_blackballed_outputs = get_num_spent_outputs(); tools::ringdb ringdb(output_file_path.string(), epee::string_tools::pod_to_hex(get_genesis_block_hash(inputs[0]))); bool stop_requested = false; tools::signal_handler::install([&stop_requested](int type) { stop_requested = true; }); int dbr = resize_env(cache_dir.c_str()); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to resize LMDB database: " + std::string(mdb_strerror(dbr))); // open first db MDB_env *env0; MDB_txn *txn0; MDB_dbi dbi0; MDB_cursor *cur0; open_db(inputs[0], &env0, &txn0, &cur0, &dbi0); for (size_t n = 0; n < inputs.size(); ++n) { const std::string canonical = boost::filesystem::canonical(inputs[n]).string(); uint64_t start_idx = get_processed_txidx(canonical); if (n > 0 && start_idx == 0) { start_idx = find_first_diverging_transaction(inputs[0], inputs[n]); LOG_PRINT_L0("First diverging transaction at " << start_idx); } LOG_PRINT_L0("Reading blockchain from " << inputs[n] << " from " << start_idx); MDB_txn *txn; int dbr = mdb_txn_begin(env, NULL, 0, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); size_t records = 0; const std::string filename = inputs[n]; std::vector blackballs; uint64_t n_txes; for_all_transactions(filename, start_idx, n_txes, [&](const cryptonote::transaction_prefix &tx)->bool { std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; for (const auto &in: tx.vin) { if (in.type() != typeid(txin_to_key)) continue; const auto &txin = boost::get(in); if (opt_rct_only && txin.amount != 0) continue; const std::vector absolute = cryptonote::relative_output_offsets_to_absolute(txin.key_offsets); if (n == 0) for (uint64_t out: absolute) add_key_image(txn, output_data(txin.amount, out), txin.k_image); std::vector relative_ring; std::vector new_ring = canonicalize(txin.key_offsets); const uint32_t ring_size = txin.key_offsets.size(); uint64_t instances = get_ring_instances(txn, txin.amount, new_ring); ++instances; set_ring_instances(txn, txin.amount, new_ring, instances); if (n == 0 && ring_size == 1) { const crypto::public_key pkey = get_output_key(cur0, txin.amount, absolute[0]); MINFO("Blackballing output " << pkey << ", due to being used in a 1-ring"); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; blackballs.push_back(pkey); add_spent_output(txn, output_data(txin.amount, absolute[0])); inc_stat(txn, txin.amount ? "pre-rct-ring-size-1" : "rct-ring-size-1"); } else if (n == 0 && instances == new_ring.size()) { for (size_t o = 0; o < new_ring.size(); ++o) { const crypto::public_key pkey = get_output_key(cur0, txin.amount, absolute[o]); MINFO("Blackballing output " << pkey << ", due to being used in " << new_ring.size() << " identical " << new_ring.size() << "-rings"); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; blackballs.push_back(pkey); add_spent_output(txn, output_data(txin.amount, absolute[o])); inc_stat(txn, txin.amount ? "pre-rct-duplicate-rings" : "rct-duplicate-rings"); } } else if (n == 0 && opt_check_subsets && get_ring_subset_instances(txn, txin.amount, new_ring) >= new_ring.size()) { for (size_t o = 0; o < new_ring.size(); ++o) { const crypto::public_key pkey = get_output_key(cur0, txin.amount, absolute[o]); MINFO("Blackballing output " << pkey << ", due to being used in " << new_ring.size() << " subsets of " << new_ring.size() << "-rings"); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; blackballs.push_back(pkey); add_spent_output(txn, output_data(txin.amount, absolute[o])); inc_stat(txn, txin.amount ? "pre-rct-subset-rings" : "rct-subset-rings"); } } else if (n > 0 && get_relative_ring(txn, txin.k_image, relative_ring)) { MDEBUG("Key image " << txin.k_image << " already seen: rings " << boost::join(relative_ring | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " ") << ", " << boost::join(txin.key_offsets | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " ")); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; if (relative_ring != txin.key_offsets) { MDEBUG("Rings are different"); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; const std::vector r0 = cryptonote::relative_output_offsets_to_absolute(relative_ring); const std::vector r1 = cryptonote::relative_output_offsets_to_absolute(txin.key_offsets); std::vector common; for (uint64_t out: r0) { if (std::find(r1.begin(), r1.end(), out) != r1.end()) common.push_back(out); } if (common.empty()) { MERROR("Rings for the same key image are disjoint"); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; } else if (common.size() == 1) { const crypto::public_key pkey = get_output_key(cur0, txin.amount, common[0]); MINFO("Blackballing output " << pkey << ", due to being used in rings with a single common element"); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; blackballs.push_back(pkey); add_spent_output(txn, output_data(txin.amount, common[0])); inc_stat(txn, txin.amount ? "pre-rct-key-image-attack" : "rct-key-image-attack"); } else { MDEBUG("The intersection has more than one element, it's still ok"); std::cout << "\r" << start_idx << "/" << n_txes << " \r" << std::flush; for (const auto &out: r0) if (std::find(common.begin(), common.end(), out) != common.end()) new_ring.push_back(out); new_ring = cryptonote::absolute_output_offsets_to_relative(new_ring); } } } if (n == 0) set_relative_ring(txn, txin.k_image, new_ring); } if (!blackballs.empty()) { ringdb.blackball(blackballs); blackballs.clear(); } set_processed_txidx(txn, canonical, start_idx+1); ++records; if (records >= records_per_sync) { dbr = mdb_txn_commit(txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to commit txn creating/opening database: " + std::string(mdb_strerror(dbr))); int dbr = resize_env(cache_dir.c_str()); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to resize LMDB database: " + std::string(mdb_strerror(dbr))); dbr = mdb_txn_begin(env, NULL, 0, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); records = 0; } if (stop_requested) { MINFO("Stopping scan..."); return false; } return true; }); dbr = mdb_txn_commit(txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to commit txn creating/opening database: " + std::string(mdb_strerror(dbr))); LOG_PRINT_L0("blockchain from " << inputs[n] << " processed till tx idx " << start_idx); if (stop_requested) break; } std::vector work_spent; if (stop_requested) goto skip_secondary_passes; if (get_num_spent_outputs() > start_blackballed_outputs) { MDB_txn *txn; dbr = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); work_spent = get_spent_outputs(txn); mdb_txn_abort(txn); } while (!work_spent.empty()) { LOG_PRINT_L0("Secondary pass on " << work_spent.size() << " spent outputs"); int dbr = resize_env(cache_dir.c_str()); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to resize LMDB database: " + std::string(mdb_strerror(dbr))); MDB_txn *txn; dbr = mdb_txn_begin(env, NULL, 0, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); std::vector blackballs; std::vector scan_spent = std::move(work_spent); work_spent.clear(); for (const output_data &od: scan_spent) { std::vector key_images = get_key_images(txn, od); for (const crypto::key_image &ki: key_images) { std::vector relative_ring; CHECK_AND_ASSERT_THROW_MES(get_relative_ring(txn, ki, relative_ring), "Relative ring not found"); std::vector absolute = cryptonote::relative_output_offsets_to_absolute(relative_ring); size_t known = 0; uint64_t last_unknown = 0; for (uint64_t out: absolute) { output_data new_od(od.amount, out); if (is_output_spent(txn, new_od)) ++known; else last_unknown = out; } if (known == absolute.size() - 1 && !is_output_spent(txn, output_data(od.amount, last_unknown))) { const crypto::public_key pkey = get_output_key(cur0, od.amount, last_unknown); MINFO("Blackballing output " << pkey << ", due to being used in a " << absolute.size() << "-ring where all other outputs are known to be spent"); blackballs.push_back(pkey); add_spent_output(txn, output_data(od.amount, last_unknown)); work_spent.push_back(output_data(od.amount, last_unknown)); inc_stat(txn, od.amount ? "pre-rct-chain-reaction" : "rct-chain-reaction"); } } if (stop_requested) { MINFO("Stopping secondary passes. Secondary passes are not incremental, they will re-run fully."); return false; } } if (!blackballs.empty()) { ringdb.blackball(blackballs); blackballs.clear(); } dbr = mdb_txn_commit(txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to commit txn creating/opening database: " + std::string(mdb_strerror(dbr))); } skip_secondary_passes: uint64_t diff = get_num_spent_outputs() - start_blackballed_outputs; LOG_PRINT_L0(std::to_string(diff) << " new outputs blackballed, " << get_num_spent_outputs() << " total outputs blackballed"); MDB_txn *txn; dbr = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn); CHECK_AND_ASSERT_THROW_MES(!dbr, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr))); uint64_t pre_rct = 0, rct = 0; get_num_outputs(txn0, cur0, dbi0, pre_rct, rct); MINFO("Total pre-rct outputs: " << pre_rct); MINFO("Total rct outputs: " << rct); static const struct { const char *key; uint64_t base; } stat_keys[] = { { "pre-rct-ring-size-1", pre_rct }, { "rct-ring-size-1", rct }, { "pre-rct-duplicate-rings", pre_rct }, { "rct-duplicate-rings", rct }, { "pre-rct-subset-rings", pre_rct }, { "rct-subset-rings", rct }, { "pre-rct-key-image-attack", pre_rct }, { "rct-key-image-attack", rct }, { "pre-rct-chain-reaction", pre_rct }, { "rct-chain-reaction", rct }, }; for (const auto &key: stat_keys) { uint64_t data; if (!get_stat(txn, key.key, data)) data = 0; float percent = key.base ? 100.0f * data / key.base : 0.0f; MINFO(key.key << ": " << data << " (" << percent << "%)"); } mdb_txn_abort(txn); LOG_PRINT_L0("Blockchain blackball data exported OK"); close_db(env0, txn0, cur0, dbi0); close(); return 0; CATCH_ENTRY("Error", 1); }