diff --git a/src/blockchain_utilities/blockchain_blackball.cpp b/src/blockchain_utilities/blockchain_blackball.cpp index f824d93a6..68fd9e14a 100644 --- a/src/blockchain_utilities/blockchain_blackball.cpp +++ b/src/blockchain_utilities/blockchain_blackball.cpp @@ -415,6 +415,115 @@ static bool for_all_transactions(const std::string &filename, uint64_t &start_id return fret; } +static bool for_all_transactions(const std::string &filename, const uint64_t &start_idx, uint64_t &n_txes, const std::function &f) +{ + MDB_env *env; + MDB_dbi dbi_blocks, dbi_txs; + MDB_txn *txn; + MDB_cursor *cur_blocks, *cur_txs; + 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, 3); + 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, "blocks", MDB_INTEGERKEY, &dbi_blocks); + if (dbr) throw std::runtime_error("Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); + dbr = mdb_dbi_open(txn, "txs_pruned", MDB_INTEGERKEY, &dbi_txs); + if (dbr) throw std::runtime_error("Failed to open LMDB dbi: " + std::string(mdb_strerror(dbr))); + + dbr = mdb_cursor_open(txn, dbi_blocks, &cur_blocks); + if (dbr) throw std::runtime_error("Failed to create LMDB cursor: " + std::string(mdb_strerror(dbr))); + dbr = mdb_cursor_open(txn, dbi_txs, &cur_txs); + if (dbr) throw std::runtime_error("Failed to create LMDB cursor: " + std::string(mdb_strerror(dbr))); + + MDB_stat stat; + dbr = mdb_stat(txn, dbi_blocks, &stat); + if (dbr) throw std::runtime_error("Failed to query txs stat: " + std::string(mdb_strerror(dbr))); + uint64_t n_blocks = stat.ms_entries; + dbr = mdb_stat(txn, dbi_txs, &stat); + if (dbr) throw std::runtime_error("Failed to query txs stat: " + std::string(mdb_strerror(dbr))); + n_txes = stat.ms_entries; + + bool fret = true; + + MDB_cursor_op op_blocks = MDB_FIRST; + MDB_cursor_op op_txs = MDB_FIRST; + uint64_t tx_idx = 0; + while (1) + { + int ret = mdb_cursor_get(cur_blocks, &k, &v, op_blocks); + op_blocks = MDB_NEXT; + if (ret == MDB_NOTFOUND) + break; + if (ret) + throw std::runtime_error("Failed to enumerate blocks: " + std::string(mdb_strerror(ret))); + + if (k.mv_size != sizeof(uint64_t)) + throw std::runtime_error("Bad key size"); + uint64_t height = *(const uint64_t*)k.mv_data; + blobdata bd; + bd.assign(reinterpret_cast(v.mv_data), v.mv_size); + block b; + if (!parse_and_validate_block_from_blob(bd, b)) + throw std::runtime_error("Failed to parse block from blob retrieved from the db"); + + ret = mdb_cursor_get(cur_txs, &k, &v, op_txs); + if (ret) + throw std::runtime_error("Failed to fetch transaction " + string_tools::pod_to_hex(get_transaction_hash(b.miner_tx)) + ": " + std::string(mdb_strerror(ret))); + op_txs = MDB_NEXT; + + bool last_block = height == n_blocks - 1; + if (start_idx <= tx_idx++ && !f(last_block && b.tx_hashes.empty(), height, b.miner_tx)) + { + fret = false; + break; + } + for (size_t i = 0; i < b.tx_hashes.size(); ++i) + { + const crypto::hash& txid = b.tx_hashes[i]; + ret = mdb_cursor_get(cur_txs, &k, &v, op_txs); + if (ret) + throw std::runtime_error("Failed to fetch transaction " + string_tools::pod_to_hex(txid) + ": " + std::string(mdb_strerror(ret))); + if (start_idx <= tx_idx++) + { + cryptonote::transaction_prefix tx; + bd.assign(reinterpret_cast(v.mv_data), v.mv_size); + CHECK_AND_ASSERT_MES(parse_and_validate_tx_prefix_from_blob(bd, tx), false, "Failed to parse transaction from blob"); + if (!f(last_block && i == b.tx_hashes.size() - 1, height, tx)) + { + fret = false; + break; + } + } + } + if (!fret) + break; + } + + mdb_cursor_close(cur_blocks); + mdb_cursor_close(cur_txs); + mdb_txn_commit(txn); + tx_active = false; + mdb_dbi_close(env, dbi_blocks); + mdb_dbi_close(env, dbi_txs); + 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]; @@ -1094,6 +1203,7 @@ int main(int argc, char* argv[]) const command_line::arg_descriptor arg_extra_spent_list = {"extra-spent-list", "Optional list of known spent outputs",""}; const command_line::arg_descriptor arg_export = {"export", "Filename to export the backball list to"}; const command_line::arg_descriptor arg_force_chain_reaction_pass = {"force-chain-reaction-pass", "Run the chain reaction pass even if no new blockchain data was processed"}; + const command_line::arg_descriptor arg_historical_stat = {"historical-stat", "Report historical stat of spent outputs for every 10000 blocks window"}; command_line::add_arg(desc_cmd_sett, arg_blackball_db_dir); command_line::add_arg(desc_cmd_sett, arg_log_level); @@ -1105,6 +1215,7 @@ int main(int argc, char* argv[]) command_line::add_arg(desc_cmd_sett, arg_extra_spent_list); command_line::add_arg(desc_cmd_sett, arg_export); command_line::add_arg(desc_cmd_sett, arg_force_chain_reaction_pass); + command_line::add_arg(desc_cmd_sett, arg_historical_stat); command_line::add_arg(desc_cmd_sett, arg_inputs); command_line::add_arg(desc_cmd_only, command_line::arg_help); @@ -1145,6 +1256,7 @@ int main(int argc, char* argv[]) bool opt_check_subsets = command_line::get_arg(vm, arg_check_subsets); bool opt_verbose = command_line::get_arg(vm, arg_verbose); bool opt_force_chain_reaction_pass = command_line::get_arg(vm, arg_force_chain_reaction_pass); + bool opt_historical_stat = command_line::get_arg(vm, arg_historical_stat); std::string opt_export = command_line::get_arg(vm, arg_export); std::string extra_spent_list = command_line::get_arg(vm, arg_extra_spent_list); std::vector> extra_spent_outputs = extra_spent_list.empty() ? std::vector>() : load_outputs(extra_spent_list); @@ -1196,6 +1308,59 @@ int main(int argc, char* argv[]) MDB_cursor *cur0; open_db(inputs[0], &env0, &txn0, &cur0, &dbi0); + std::vector work_spent; + + if (opt_historical_stat) + { + if (!start_blackballed_outputs) + { + MINFO("Spent outputs database is empty. Either you haven't run the analysis mode yet, or there is really no output marked as spent."); + goto skip_secondary_passes; + } + const uint64_t STAT_WINDOW = 10000; + uint64_t outs_total = 0; + uint64_t outs_spent = 0; + std::unordered_map outs_per_amount; + uint64_t start_idx = 0, n_txes; + uint64_t prev_height = 0; + for_all_transactions(inputs[0], start_idx, n_txes, [&](bool last_tx, uint64_t height, const cryptonote::transaction_prefix &tx)->bool + { + if (height != prev_height) + { + if (height % 100 == 0) std::cout << "\r" << height << ": " << (100.0f * outs_spent / outs_total) << "% ( " << outs_spent << " / " << outs_total << " ) \r" << std::flush; + if (height % STAT_WINDOW == 0) + { + uint64_t window_front = (height / STAT_WINDOW - 1) * STAT_WINDOW; + uint64_t window_back = window_front + STAT_WINDOW - 1; + LOG_PRINT_L0(window_front << "-" << window_back << ": " << (100.0f * outs_spent / outs_total) << "% ( " << outs_spent << " / " << outs_total << " )"); + outs_total = outs_spent = 0; + } + } + prev_height = height; + for (const auto &out: tx.vout) + { + ++outs_total; + CHECK_AND_ASSERT_THROW_MES(out.target.type() == typeid(txout_to_key), "Out target type is not txout_to_key: height=" + std::to_string(height)); + uint64_t out_global_index = outs_per_amount[out.amount]++; + if (ringdb.blackballed({out.amount, out_global_index})) + ++outs_spent; + } + if (last_tx) + { + uint64_t window_front = (height / STAT_WINDOW) * STAT_WINDOW; + uint64_t window_back = height; + LOG_PRINT_L0(window_front << "-" << window_back << ": " << (100.0f * outs_spent / outs_total) << "% ( " << outs_spent << " / " << outs_total << " )"); + } + if (stop_requested) + { + MINFO("Stopping scan..."); + return false; + } + return true; + }); + goto skip_secondary_passes; + } + if (!extra_spent_outputs.empty()) { MINFO("Adding " << extra_spent_outputs.size() << " extra spent outputs"); @@ -1432,8 +1597,6 @@ int main(int argc, char* argv[]) break; } - std::vector work_spent; - if (stop_requested) goto skip_secondary_passes;