diff --git a/CMakeLists.txt b/CMakeLists.txt index cea3fc7e..71132ce7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -224,7 +224,7 @@ endif() if (TON_ARCH AND NOT MSVC) CHECK_CXX_COMPILER_FLAG( "-march=${TON_ARCH}" COMPILER_OPT_ARCH_SUPPORTED ) if (TON_ARCH STREQUAL "apple-m1") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=${TON_ARCH}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=${TON_ARCH}") elseif(COMPILER_OPT_ARCH_SUPPORTED) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=${TON_ARCH}") elseif(NOT TON_ARCH STREQUAL "native") @@ -556,6 +556,19 @@ target_link_libraries(test-http PRIVATE tonhttp) add_executable(test-emulator test/test-td-main.cpp emulator/test/emulator-tests.cpp) target_link_libraries(test-emulator PRIVATE emulator) +add_executable(test-fisherman + test/fisherman/tests.cpp + test/fisherman/block_manipulator/header_corrupter.cpp + test/fisherman/block_manipulator/transaction_corrupter.cpp + test/fisherman/block_manipulator/factory.cpp + test/fisherman/block_reader.cpp + test/fisherman/utils.cpp +) +target_link_libraries(test-fisherman PRIVATE validator ton_validator validator ton_db) + +add_executable(print-all-shard-states test/print-all-shard-states.cpp) +target_link_libraries(print-all-shard-states PRIVATE validator ton_db) + get_directory_property(HAS_PARENT PARENT_DIRECTORY) if (HAS_PARENT) set(ALL_TEST_SOURCE diff --git a/crypto/tl/tlbc-gen-cpp.cpp b/crypto/tl/tlbc-gen-cpp.cpp index 5730f169..75c1f796 100644 --- a/crypto/tl/tlbc-gen-cpp.cpp +++ b/crypto/tl/tlbc-gen-cpp.cpp @@ -23,9 +23,9 @@ namespace tlbc { /* - * + * * C++ CODE GENERATION - * + * */ CppIdentSet global_cpp_ids; @@ -3036,7 +3036,7 @@ void CppTypeCode::generate_store_enum_method(std::ostream& os, int options) { << minl << ");\n"; } else if (minl == maxl) { if (exact) { - os << " return cb.store_long_rchk_bool(value, " << minl << ");\n"; + os << " return cb.store_ulong_rchk_bool(value, " << minl << ");\n"; } else if (incremental_cons_tags && cons_num > (1 << (minl - 1))) { os << " return cb.store_uint_less(" << cons_num << ", value);\n"; } else { diff --git a/tddb/td/db/RocksDb.cpp b/tddb/td/db/RocksDb.cpp index f1aa64a5..3d0ee63a 100644 --- a/tddb/td/db/RocksDb.cpp +++ b/tddb/td/db/RocksDb.cpp @@ -59,6 +59,7 @@ RocksDb RocksDb::clone() const { return RocksDb{db_, options_}; } +// TODO: Add support for opening the database in read-only mode. Result RocksDb::open(std::string path, RocksDbOptions options) { rocksdb::OptimisticTransactionDB *db; { diff --git a/test/fisherman/block_manipulator/base.hpp b/test/fisherman/block_manipulator/base.hpp new file mode 100644 index 00000000..c65f581e --- /dev/null +++ b/test/fisherman/block_manipulator/base.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "crypto/block/block-auto.h" + +namespace test::fisherman { + +class BaseManipulator { + public: + virtual void modify(block::gen::Block::Record &block) = 0; + virtual ~BaseManipulator() = default; +}; + +} // namespace test::fisherman diff --git a/test/fisherman/block_manipulator/factory.cpp b/test/fisherman/block_manipulator/factory.cpp new file mode 100644 index 00000000..474f1304 --- /dev/null +++ b/test/fisherman/block_manipulator/factory.cpp @@ -0,0 +1,32 @@ +#include "factory.hpp" + +#include "header_corrupter.hpp" +#include "transaction_corrupter.hpp" + +namespace test::fisherman { + +auto ManipulatorFactory::create(td::JsonValue jv) -> std::shared_ptr { + auto res = createImpl(std::move(jv)); + if (res.is_error()) { + throw std::runtime_error("Error while creating manipulator: " + res.error().message().str()); + } + return res.move_as_ok(); +} + +auto ManipulatorFactory::createImpl(td::JsonValue jv) -> td::Result> { + CHECK(jv.type() == td::JsonValue::Type::Object); + + auto &obj = jv.get_object(); + TRY_RESULT(type, td::get_json_object_string_field(obj, "type", false)); + TRY_RESULT(json_config, td::get_json_object_field(obj, "config", td::JsonValue::Type::Object, false)); + + if (type == "HeaderCorrupter") { + return std::make_shared(HeaderCorrupter::Config::fromJson(std::move(json_config))); + } + if (type == "TransactionCorrupter") { + return std::make_shared(TransactionCorrupter::Config::fromJson(std::move(json_config))); + } + return td::Status::Error(400, PSLICE() << "Unknown manipulator type: " << type); +} + +} // namespace test::fisherman diff --git a/test/fisherman/block_manipulator/factory.hpp b/test/fisherman/block_manipulator/factory.hpp new file mode 100644 index 00000000..91256162 --- /dev/null +++ b/test/fisherman/block_manipulator/factory.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "base.hpp" +#include "td/utils/JsonBuilder.h" + +namespace test::fisherman { + +class ManipulatorFactory { + public: + auto create(td::JsonValue jv) -> std::shared_ptr; + + private: + auto createImpl(td::JsonValue jv) -> td::Result>; +}; + +} // namespace test::fisherman diff --git a/test/fisherman/block_manipulator/header_corrupter.cpp b/test/fisherman/block_manipulator/header_corrupter.cpp new file mode 100644 index 00000000..b8fff430 --- /dev/null +++ b/test/fisherman/block_manipulator/header_corrupter.cpp @@ -0,0 +1,115 @@ +#include "header_corrupter.hpp" + +#include "block/block-auto.h" + +namespace test::fisherman { + +HeaderCorrupter::Config HeaderCorrupter::Config::fromJson(td::JsonValue jv) { + Config cfg; + CHECK(jv.type() == td::JsonValue::Type::Object); + auto &obj = jv.get_object(); + + cfg.distort_timestamp = td::get_json_object_bool_field(obj, "distort_timestamp", true, false).move_as_ok(); + cfg.time_offset = td::get_json_object_int_field(obj, "time_offset", true, 999999999).move_as_ok(); + + cfg.mark_subshard_of_master = + td::get_json_object_bool_field(obj, "mark_subshard_of_master", true, false).move_as_ok(); + cfg.invert_lt = td::get_json_object_bool_field(obj, "invert_lt", true, false).move_as_ok(); + cfg.mark_keyblock_on_shard = td::get_json_object_bool_field(obj, "mark_keyblock_on_shard", true, false).move_as_ok(); + + cfg.force_after_merge_for_mc = td::get_json_object_bool_field(obj, "force_after_merge_for_mc", true, false).move_as_ok(); + cfg.force_before_split_for_mc = td::get_json_object_bool_field(obj, "force_before_split_for_mc", true, false).move_as_ok(); + cfg.force_after_split_for_mc = td::get_json_object_bool_field(obj, "force_after_split_for_mc", true, false).move_as_ok(); + cfg.allow_both_after_merge_and_split = + td::get_json_object_bool_field(obj, "allow_both_after_merge_and_split", true, false).move_as_ok(); + + cfg.shard_pfx_zero_yet_after_split = + td::get_json_object_bool_field(obj, "shard_pfx_zero_yet_after_split", true, false).move_as_ok(); + + cfg.set_vert_seqno_incr = td::get_json_object_bool_field(obj, "set_vert_seqno_incr", true, false).move_as_ok(); + return cfg; +} + +HeaderCorrupter::HeaderCorrupter(Config config) : config_(std::move(config)) { +} + +void HeaderCorrupter::modify(block::gen::Block::Record &block) { + block::gen::BlockInfo::Record info_rec; + CHECK(block::gen::t_BlockInfo.cell_unpack(block.info, info_rec)); + + // 1) distort_timestamp => сдвигаем info_rec.gen_utime + if (config_.distort_timestamp) { + info_rec.gen_utime += config_.time_offset; + } + + // 2) mark_subshard_of_master => если workchain == -1, делаем shard_pfx_bits != 0, то есть блок "подшард" MC + if (config_.mark_subshard_of_master) { + block::gen::ShardIdent::Record shard_rec; + CHECK(block::gen::ShardIdent().unpack(info_rec.shard.write(), shard_rec)); + CHECK(shard_rec.workchain_id == -1 && !info_rec.not_master); + if (shard_rec.shard_pfx_bits == 0) { + shard_rec.shard_pfx_bits = 10; + shard_rec.shard_prefix = 123456ULL; + } + vm::CellBuilder cb; + CHECK(block::gen::t_ShardIdent.pack(cb, shard_rec)); + info_rec.shard.write() = cb.finalize(); + } + + // 3) invert_lt => start_lt >= end_lt + if (config_.invert_lt) { + if (info_rec.start_lt < info_rec.end_lt) { + auto tmp = info_rec.start_lt; + info_rec.start_lt = info_rec.end_lt; + info_rec.end_lt = tmp; + } + } + + // 4) mark_keyblock_on_shard => если "not_master" = true, то проставим key_block = true + if (config_.mark_keyblock_on_shard) { + CHECK(info_rec.not_master); + info_rec.key_block = true; + } + + // 5) force_after_merge / force_before_split / force_after_split для MC + if (config_.force_after_merge_for_mc) { + CHECK(!info_rec.not_master); + info_rec.after_merge = true; + } + if (config_.force_before_split_for_mc) { + CHECK(!info_rec.not_master); + info_rec.before_split = true; + } + if (config_.force_after_split_for_mc) { + CHECK(!info_rec.not_master); + info_rec.after_split = true; + } + + // 6) allow_both_after_merge_and_split => ставим after_merge=1 и after_split=1 + if (config_.allow_both_after_merge_and_split) { + info_rec.after_merge = true; + info_rec.after_split = true; + } + + // 7) shard_pfx_zero_yet_after_split => shard_pfx_bits=0, after_split=1 + if (config_.shard_pfx_zero_yet_after_split) { + info_rec.after_split = true; + block::gen::ShardIdent::Record shard_rec; + CHECK(block::gen::ShardIdent().unpack(info_rec.shard.write(), shard_rec)); + shard_rec.shard_pfx_bits = 0; + vm::CellBuilder cb; + CHECK(block::gen::t_ShardIdent.pack(cb, shard_rec)); + info_rec.shard.write() = cb.finalize(); + } + + // 8) set_vert_seqno_incr => vert_seqno_incr != 0 => ставим true + if (config_.set_vert_seqno_incr) { + info_rec.vert_seq_no = 1; + info_rec.vert_seqno_incr = true; + info_rec.prev_vert_ref = info_rec.prev_ref; + } + + CHECK(block::gen::t_BlockInfo.cell_pack(block.info, info_rec)); +} + +} // namespace test::fisherman diff --git a/test/fisherman/block_manipulator/header_corrupter.hpp b/test/fisherman/block_manipulator/header_corrupter.hpp new file mode 100644 index 00000000..43c9a84d --- /dev/null +++ b/test/fisherman/block_manipulator/header_corrupter.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "base.hpp" + +#include "td/utils/JsonBuilder.h" + +namespace test::fisherman { + +class HeaderCorrupter : public BaseManipulator { + public: + struct Config { + bool distort_timestamp = false; + td::int32 time_offset = 1'000'000'000; + + bool mark_subshard_of_master = false; + bool invert_lt = false; + bool mark_keyblock_on_shard = false; + + bool force_after_merge_for_mc = false; + bool force_before_split_for_mc = false; + bool force_after_split_for_mc = false; + bool allow_both_after_merge_and_split = false; + + bool shard_pfx_zero_yet_after_split = false; + + bool set_vert_seqno_incr = false; + + static Config fromJson(td::JsonValue jv); + }; + + explicit HeaderCorrupter(Config config); + void modify(block::gen::Block::Record &block) override; + + private: + Config config_; +}; + +} // namespace test::fisherman diff --git a/test/fisherman/block_manipulator/ideas.txt b/test/fisherman/block_manipulator/ideas.txt new file mode 100644 index 00000000..e2d4f041 --- /dev/null +++ b/test/fisherman/block_manipulator/ideas.txt @@ -0,0 +1,95 @@ +- Некорректная работа твм, т.е. результат исполнения записанный в блоке не совпадает с "локальным" исполнением, в частности +-- некорректная проверка подписи (принята подпись, которая не была бы принята в реальной сети) +-- некорректная комиссия транзакции (одна из) +-- некорректные исходящие сообщения +-- некорректный апдейт стейта +- Непоследовательное исполнение сообщений (в блок включено сообщение с большим lt/hash чем сообщения оставшиеся в очереди) +- Несоблюдение лимитов блока (по газу, размеру, lt) +- Некорректные value-flow (уничтожаются/минтятся деньги из воздуха) + +Подпись +1. Неверная подпись валидатора в заголовке. +2. Задвоенная подпись (или отсутствующая подпись при наличии). + В заголовке блока записано, что "N валидаторов подписались", + но по факту часть подписей повторяется, либо не совпадает с публичным ключом. +3. Подделка подписи внешнего сообщения. Внешнее сообщение якобы подписано кошельком пользователя, но фактически подпись неверна. + +Газ / комиссии +1. Некорректная сумма комиссии в транзакции. В том числе и обнуление комиссии. +2. Неверное распределение комиссии между валидаторами, коллатором и пр. + Сумма собранной комиссии не совпадает с тем, сколько реально поступило на счета валидаторов/коллатора. + +Исходящие сообщения +1. Неверная форма исходящего сообщения ??? + Внутреннее сообщение, сформированное контрактом, имеет некорректную структуру. +2. Сообщение отправляется на несуществующий адрес (или адрес, не соответствующий данному шарду). +3. Неверная сумма, переданная в исходящем сообщении. + Контракт "списывает" у себя X TON, отправляет их в сообщении, но на балансе контракта фактически было меньше X. + Контракт "списывает" у себя X TON, а в сообщении указано Y TON. + +Некорректный апдейт стейта +??? + +Некорректное исполнение сообщений +1. Включено сообщение с большим lt/hash, в то время как сообщения с меньшим lt не были исполнены. +2. Использование "просроченных" входящих сообщений (по timeout). + +Несоблюдение лимитов блока +1. Превышение лимита по газу. +2. Превышение лимита по размеру блока. + Фактический размер сериализованного блока больше, чем что-то ??? +3. Превышение лимита по количеству транзакций. +4. Превышение лимита по lt / изменению lt. + +Некорректные value-flow +1. Баланс контракта увеличился на 1000 TON, хотя никаких транзакций перевода и минтинга нет. +2. Баланс контракта уменьшился на 1000 TON, хотя никаких транзакций перевода и сжигания нет. +3. Некоректоное изменение баланса при отправке сообщения. + +Соответствие цепочки блоков шарда мастерчейну +1. Мастерчейн содержит более новый блок по seqno. +2. Шард содержит незарегистированную длинную цепочку. +3. В мастерчейне содердится другой последний зарегистрированный блок, нежели в шарде. + +Ошибки в заголовке и структуре самого блока +1. Неверная ссылка на предыдущий блок. +2. Несогласованность с мастерчейном + Шард-блок утверждает, что он ссылается на определённые данные в мастерчейне, которые не совпадают с реальной историей. +3. Искажение timestamp. + Ставится время из далекого "будущего" или "прошлого". +4. Невалидный шард (например битово некорректный). +5. Блок является подшардом мастерчейна. workchain == -1, shard != ShardIdFull. +6. start_lt_ >= end_lt_. + +Несоответствие глобальным настройкам сети ??? +1. Сеть поменяла лимиты по gas, а блок генерируется со старыми лимитами. + +Ошибки в медшардовых сообщениях +1. Неверный маршрут сообщения. + Указывается, что сообщение идёт в шард A, но на самом деле адрес принадлежит шару B. + +Нарушение правил "консенсуса" +1. Блок подписан валидаторами, которые не являются валидаторами этого шарда. +2. Недостаточное число подписей. + +Дополнительные идеи порчи блока на основе reject_query +- Хэш корневой ячейки блока не совпадает с тем, что заявлено в block_id. +- Внутренние структуры коллатора, например TopBlockDescrSet, Merkle proof и т.д. некорректны. +- Отметили "key block" в шардчейне, что запрещено. +- Поле info.after_merge/info.before_split/info.after_split true, но мы в мастерчейне. +- Поля info.after_merge и info.after_split оба установлены в true. +- Пустой префикс шардирования, но блок объявлен как "после сплита". info.after_split == true, но shard_pfx_len == 0. +- В BlockInfo есть поле vert_seqno_incr (!= 0). +- Указан другой публичный ключ создателя, нежели тот, что мы ожидаем. +- Для мастерчейн-блока ожидаем extra.custom->size_refs() != 0, но оказалось 0. И наоборот. +- Противоречие end_lt в header и gen_lt в ShardState. В заголовке стоит end_lt_, а в самом стейте gen_lt, и они не совпадают. +- Аналогично противоречение timestamp. info.gen_utime (во финальном стейте) != now_ (из заголовка). +- Внутри состояния (ShardState) поле seq_no и shard_id указывает на другой блок, нежели этот. +- Некорректный мерж ??? +- Несуществующий workchain_id, который отсутствует в глобальном списке. +- Workchain есть, но active == false. Или wc_info_->basic == false. + Или wc_info_->enabled_since && wc_info_->enabled_since > config_->utime. +- При store_out_msg_queue_size_ == true ожидаем, что в стейте прописан out_msg_queue_size, а его нет. + Или размер не совпадает с найденным ns_.out_msg_queue_size_.value(). +- is_key_block_ == true, а "важные" параметры конфигурации не менялись. +- При повторном вычислении транзакции выяснилось, что финальный хэш аккаунта не совпадает с тем, что заявлен. diff --git a/test/fisherman/block_manipulator/transaction_corrupter.cpp b/test/fisherman/block_manipulator/transaction_corrupter.cpp new file mode 100644 index 00000000..eff98faf --- /dev/null +++ b/test/fisherman/block_manipulator/transaction_corrupter.cpp @@ -0,0 +1,67 @@ +#include "transaction_corrupter.hpp" + +#include "block-parse.h" + +namespace test::fisherman { + +auto TransactionCorrupter::Config::fromJson(td::JsonValue jv) -> Config { + Config cfg; + CHECK(jv.type() == td::JsonValue::Type::Object); + auto& obj = jv.get_object(); + cfg.transaction_fee_change = td::get_json_object_long_field(obj, "transaction_fee_change", false).move_as_ok(); + return cfg; +} + +TransactionCorrupter::TransactionCorrupter(Config config) : config_(std::move(config)) { +} + +void TransactionCorrupter::modify(block::gen::Block::Record& block) { + block::gen::BlockExtra::Record block_extra_rec; + CHECK(block::gen::BlockExtra().cell_unpack(block.extra, block_extra_rec)); + + block::gen::ShardAccountBlocks::Record shard_account_blocks; + CHECK(block::gen::ShardAccountBlocks().cell_unpack(block_extra_rec.account_blocks, shard_account_blocks)); + + vm::AugmentedDictionary accounts_dict{shard_account_blocks.x, 256, block::tlb::aug_ShardAccountBlocks}; + vm::AugmentedDictionary new_accounts_dict{256, block::tlb::aug_ShardAccountBlocks}; + + accounts_dict.check_for_each_extra( + [&](td::Ref account_slice, auto const&, td::ConstBitPtr account_key, int account_key_len) -> bool { + block::gen::AccountBlock::Record account_block_rec; + CHECK(block::gen::AccountBlock().unpack(account_slice.write(), account_block_rec)); + vm::AugmentedDictionary tx_dict{vm::DictNonEmpty(), account_block_rec.transactions, 64, + block::tlb::aug_AccountTransactions}; + vm::AugmentedDictionary new_tx_dict{64, block::tlb::aug_AccountTransactions}; + + CHECK(tx_dict.check_for_each_extra( + [&](td::Ref tx_slice, auto const&, td::ConstBitPtr tx_key, int tx_key_len) -> bool { + block::gen::Transaction::Record tx_rec; + if (block::gen::Transaction().cell_unpack(tx_slice.write().fetch_ref(), tx_rec)) { + block::CurrencyCollection tx_currency_rec; + CHECK(tx_currency_rec.validate_unpack(tx_rec.total_fees)); + tx_currency_rec.grams += config_.transaction_fee_change; + tx_currency_rec.pack_to(tx_rec.total_fees); + + td::Ref new_tx_cell; + CHECK(block::gen::Transaction().cell_pack(new_tx_cell, tx_rec)); + new_tx_dict.set_ref(tx_key, tx_key_len, new_tx_cell, vm::Dictionary::SetMode::Add); + } + return true; + }, + false)); + + account_block_rec.transactions.write() = vm::load_cell_slice(new_tx_dict.get_root_cell()); + + vm::CellBuilder cb2; + CHECK(block::gen::AccountBlock().pack(cb2, account_block_rec)); + new_accounts_dict.set(account_key, account_key_len, cb2.finalize(), vm::Dictionary::SetMode::Add); + + return true; + }, + false); + shard_account_blocks.x = new_accounts_dict.get_root(); + CHECK(block::gen::ShardAccountBlocks().cell_pack(block_extra_rec.account_blocks, shard_account_blocks)); + CHECK(block::gen::BlockExtra().cell_pack(block.extra, block_extra_rec)); +} + +} // namespace test::fisherman diff --git a/test/fisherman/block_manipulator/transaction_corrupter.hpp b/test/fisherman/block_manipulator/transaction_corrupter.hpp new file mode 100644 index 00000000..49a1d0e3 --- /dev/null +++ b/test/fisherman/block_manipulator/transaction_corrupter.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "base.hpp" + +#include "td/utils/JsonBuilder.h" + +namespace test::fisherman { + +class TransactionCorrupter : public BaseManipulator { + public: + struct Config { + // TODO: add corruption fields and associate this manipulator with another to achieve more complex block corruptions + td::int64 transaction_fee_change; + + static auto fromJson(td::JsonValue jv) -> Config; + }; + + explicit TransactionCorrupter(Config config); + void modify(block::gen::Block::Record &block) final; + + private: + Config config_; +}; + +} // namespace test::fisherman diff --git a/test/fisherman/block_reader.cpp b/test/fisherman/block_reader.cpp new file mode 100644 index 00000000..445a55e5 --- /dev/null +++ b/test/fisherman/block_reader.cpp @@ -0,0 +1,50 @@ +#include "block_reader.hpp" + +namespace test::fisherman { + +BlockDataLoader::BlockDataLoader(const std::string &db_path) : scheduler_({1}) { + auto opts = ton::validator::ValidatorManagerOptions::create(ton::BlockIdExt{}, ton::BlockIdExt{}); + scheduler_.run_in_context([&] { + root_db_actor_ = td::actor::create_actor( + "RootDbActor", td::actor::ActorId(), db_path, opts); + }); +} + +BlockDataLoader::~BlockDataLoader() { + scheduler_.stop(); +} + +td::Result> BlockDataLoader::load_block_data(const ton::BlockIdExt &block_id) { + std::atomic done{false}; + td::Result> block_data_result; + + scheduler_.run_in_context([&] { + auto handle_promise = td::PromiseCreator::lambda([&](td::Result handle_res) { + if (handle_res.is_error()) { + block_data_result = td::Result>(handle_res.move_as_error()); + done = true; + return; + } + auto handle = handle_res.move_as_ok(); + + auto data_promise = td::PromiseCreator::lambda([&](td::Result> data_res) { + block_data_result = std::move(data_res); + done = true; + }); + + td::actor::send_closure(root_db_actor_, &ton::validator::RootDb::get_block_data, handle, std::move(data_promise)); + }); + + td::actor::send_closure(root_db_actor_, &ton::validator::RootDb::get_block_by_seqno, + ton::AccountIdPrefixFull{block_id.id.workchain, block_id.id.shard}, block_id.id.seqno, + std::move(handle_promise)); + }); + + while (!done) { + scheduler_.run(1); + } + + return block_data_result; +} + +} // namespace test::fisherman diff --git a/test/fisherman/block_reader.hpp b/test/fisherman/block_reader.hpp new file mode 100644 index 00000000..52d952d1 --- /dev/null +++ b/test/fisherman/block_reader.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +#include "validator/db/rootdb.hpp" +#include "td/actor/actor.h" + +namespace test::fisherman { + +// TODO: Verify that the database does not get corrupted when reading while the validator is running +class BlockDataLoader { + public: + explicit BlockDataLoader(const std::string &db_path); + ~BlockDataLoader(); + + td::Result> load_block_data(const ton::BlockIdExt &block_id); + + private: + td::actor::Scheduler scheduler_; + td::actor::ActorOwn root_db_actor_; +}; + +} // namespace test::fisherman diff --git a/test/fisherman/configs/test.json b/test/fisherman/configs/test.json new file mode 100644 index 00000000..d0e2bd7e --- /dev/null +++ b/test/fisherman/configs/test.json @@ -0,0 +1,11 @@ +{ + "block_id": { + "workchain_id": -1, + "shard_id": "8000000000000000", + "seqno": 27492934 + }, + "manipulation": { + "type": "HeaderCorrupter", + "config": {} + } +} diff --git a/test/fisherman/tests.cpp b/test/fisherman/tests.cpp new file mode 100644 index 00000000..362d0c0a --- /dev/null +++ b/test/fisherman/tests.cpp @@ -0,0 +1,75 @@ +#include "block_reader.hpp" + +#include "crypto/block/block-auto.h" +#include "block_manipulator/factory.hpp" +#include "utils.hpp" + +using namespace test::fisherman; + +void print_block(const block::gen::Block::Record &block_rec) { + std::ostringstream os; + td::Ref block_cell_pack; + CHECK(block::gen::t_Block.cell_pack(block_cell_pack, block_rec)); + block::gen::t_Block.print_ref(os, block_cell_pack); + LOG(INFO) << "Block = " << os.str(); +} + +auto main(int argc, char **argv) -> int { + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " /path/to/rootdb config.json\n"; + return 1; + } + + SET_VERBOSITY_LEVEL(VERBOSITY_NAME(INFO)); // TODO: add to config + + std::string db_path = argv[1]; + std::string json_file_path = argv[2]; + + auto content_res = read_file_to_buffer(json_file_path); + if (content_res.is_error()) { + std::cerr << "Error reading JSON file: " << content_res.error().message().str() << std::endl; + return 1; + } + td::BufferSlice content = content_res.move_as_ok(); + + td::Parser parser(content.as_slice()); + auto decode_result = do_json_decode(parser, 100); + if (decode_result.is_error()) { + std::cerr << "JSON parse error: " << decode_result.error().message().str() << std::endl; + return 1; + } + + auto js = decode_result.move_as_ok(); + auto &js_obj = js.get_object(); + auto blk_id_obj_res = td::get_json_object_field(js_obj, "block_id", td::JsonValue::Type::Object, false); + CHECK(blk_id_obj_res.is_ok()); + auto blk_id_res = parse_block_id_from_json(blk_id_obj_res.move_as_ok()); + if (blk_id_res.is_error()) { + std::cerr << "Error extracting BlockIdExt: " << blk_id_res.error().message().str() << std::endl; + return 1; + } + ton::BlockIdExt blk_id = blk_id_res.move_as_ok(); + + BlockDataLoader loader(db_path); + + auto blk_data_result = loader.load_block_data(blk_id); + if (blk_data_result.is_error()) { + std::cerr << "Error loading block data: " << blk_data_result.error().message().str() << std::endl; + return 1; + } + + auto blk_data = blk_data_result.move_as_ok(); + + block::gen::Block::Record block_rec; + CHECK(block::gen::t_Block.cell_unpack(blk_data->root_cell(), block_rec)); + + print_block(block_rec); + + auto manipulation_config = td::get_json_object_field(js_obj, "manipulation", td::JsonValue::Type::Object, false); + CHECK(manipulation_config.is_ok()); + ManipulatorFactory().create(manipulation_config.move_as_ok())->modify(block_rec); + + LOG(INFO) << "Block after manipulation:"; + print_block(block_rec); + return 0; +} diff --git a/test/fisherman/utils.cpp b/test/fisherman/utils.cpp new file mode 100644 index 00000000..40cdbc54 --- /dev/null +++ b/test/fisherman/utils.cpp @@ -0,0 +1,79 @@ +#include "utils.hpp" + +#include + +namespace test::fisherman { + +td::Result read_file_to_buffer(const std::string &path) { + std::ifstream in(path, std::ios::binary); + if (!in.is_open()) { + return td::Status::Error("Cannot open file: " + path); + } + + in.seekg(0, std::ios::end); + std::streamoff file_size = in.tellg(); + if (file_size < 0) { + return td::Status::Error("Failed to get file size: " + path); + } + in.seekg(0, std::ios::beg); + + auto size = static_cast(file_size); + td::BufferWriter writer(size); + + td::MutableSlice out_slice = writer.prepare_append(); + if (out_slice.size() < size) { + return td::Status::Error("Not enough memory allocated in BufferWriter"); + } + + if (!in.read(reinterpret_cast(out_slice.data()), size)) { + return td::Status::Error("Failed to read file contents: " + path); + } + writer.confirm_append(size); + + return writer.as_buffer_slice(); +} + +td::Result parse_block_id_from_json(td::JsonValue jv) { + using td::Result; + using td::Status; + + if (jv.type() != td::JsonValue::Type::Object) { + return Status::Error("Root JSON is not an object"); + } + auto &obj = jv.get_object(); + + auto res_wc = td::get_json_object_int_field(obj, PSLICE() << "workchain_id", false); + if (res_wc.is_error()) { + return Status::Error("Missing or invalid 'workchain_id'"); + } + int32_t workchain_id = res_wc.move_as_ok(); + + auto res_shard_str = td::get_json_object_string_field(obj, PSLICE() << "shard_id", false); + if (res_shard_str.is_error()) { + return Status::Error("Missing or invalid 'shard_id'"); + } + std::string shard_str = res_shard_str.move_as_ok(); + uint64_t shard_id = 0; + try { + if (shard_str.starts_with("0x")) { + shard_str.erase(0, 2); + } + shard_id = std::stoull(shard_str, nullptr, 16); + } catch (...) { + return Status::Error("Failed to parse shard_id from: " + shard_str); + } + + auto res_seqno = td::get_json_object_int_field(obj, PSLICE() << "seqno", false); + if (res_seqno.is_error()) { + return Status::Error("Missing or invalid 'seqno'"); + } + int32_t seqno_signed = res_seqno.move_as_ok(); + if (seqno_signed < 0) { + return Status::Error("seqno must be non-negative"); + } + + return ton::BlockIdExt{workchain_id, shard_id, static_cast(seqno_signed), ton::RootHash::zero(), + ton::FileHash::zero()}; +} + +} // namespace test::fisherman diff --git a/test/fisherman/utils.hpp b/test/fisherman/utils.hpp new file mode 100644 index 00000000..753d9564 --- /dev/null +++ b/test/fisherman/utils.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "td/utils/buffer.h" +#include "td/utils/Status.h" +#include "td/utils/JsonBuilder.h" +#include "ton/ton-types.h" + +namespace test::fisherman { + +td::Result read_file_to_buffer(const std::string &path); + +/// \brief Parses a JsonValue object to build a BlockIdExt. +/// \param jv A td::JsonValue that should be a JSON object with fields: +/// "workchain_id" (int), +/// "shard_id" (hex string), +/// "seqno" (int >= 0). +/// \return A td::Result containing either the constructed BlockIdExt or an error status. +td::Result parse_block_id_from_json(td::JsonValue jv); + +} // namespace test::fisherman diff --git a/test/print-all-shard-states.cpp b/test/print-all-shard-states.cpp new file mode 100644 index 00000000..fe599f05 --- /dev/null +++ b/test/print-all-shard-states.cpp @@ -0,0 +1,83 @@ +#include +#include + +#include "td/actor/actor.h" +#include "td/utils/logging.h" + +#include "validator/db/celldb.hpp" + +static td::actor::ActorOwn g_cell_db_actor; +static td::actor::ActorOwn g_loader_actor; // LoadCellActor хранить здесь + +class PrintHashesActor : public td::actor::Actor { + public: + explicit PrintHashesActor(td::actor::ActorId cell_db) + : cell_db_(cell_db) {} + + void start_up() override { + LOG(INFO) << "PrintHashesActor: calling CellDb::print_all_hashes()"; + td::actor::send_closure(cell_db_, &ton::validator::CellDb::print_all_hashes); + stop(); + } + + private: + td::actor::ActorId cell_db_; +}; + +ton::RootHash parse_hex_hash(const std::string &hex_str) { + if (hex_str.size() != 64) { + throw std::runtime_error("Root hash must be 64 hex chars"); + } + auto r = td::hex_decode(hex_str); + if (r.is_error()) { + throw std::runtime_error("Invalid hex string: " + r.error().message().str()); + } + auto data = r.move_as_ok(); + if (data.size() != 32) { + throw std::runtime_error("Hash must be 32 bytes (64 hex characters)."); + } + ton::RootHash root; + std::memcpy(root.as_slice().begin(), data.data(), 32); + return root; +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " /path/to/celldb [64-hex-hash]\n"; + return 1; + } + + std::string celldb_path = argv[1]; + bool load_hash = (argc > 2); + ton::RootHash cell_hash; + if (load_hash) { + cell_hash = parse_hex_hash(argv[2]); + LOG(INFO) << "We will load hash = " << cell_hash.to_hex(); + } + + td::actor::Scheduler scheduler({1}); // 1-thread + + scheduler.run_in_context([&] { + auto opts = ton::validator::ValidatorManagerOptions::create( + ton::BlockIdExt{ton::masterchainId, ton::shardIdAll, 0, ton::RootHash::zero(), ton::FileHash::zero()}, + ton::BlockIdExt{ton::masterchainId, ton::shardIdAll, 0, ton::RootHash::zero(), ton::FileHash::zero()} + ); + + g_cell_db_actor = td::actor::create_actor( + "celldb_actor", + td::actor::ActorId(), // пустой + celldb_path, + opts + ); + + auto printer_actor = td::actor::create_actor("printer", g_cell_db_actor.get()); + }); + + while (scheduler.run(1)) { + } + + scheduler.stop(); + + LOG(INFO) << "Done. Exiting."; + return 0; +} diff --git a/validator/db/archive-manager.cpp b/validator/db/archive-manager.cpp index d349f9d8..a598045e 100644 --- a/validator/db/archive-manager.cpp +++ b/validator/db/archive-manager.cpp @@ -890,7 +890,7 @@ void ArchiveManager::start_up() { td::WalkPath::run(db_root_ + "/archive/states/", [&](td::CSlice fname, td::WalkPath::Type t) -> void { if (t == td::WalkPath::Type::NotDir) { - LOG(ERROR) << "checking file " << fname; + LOG(DEBUG) << "checking file " << fname; auto pos = fname.rfind(TD_DIR_SLASH); if (pos != td::Slice::npos) { fname.remove_prefix(pos + 1); diff --git a/validator/db/celldb.cpp b/validator/db/celldb.cpp index 9dcecdb3..57924d17 100644 --- a/validator/db/celldb.cpp +++ b/validator/db/celldb.cpp @@ -265,6 +265,41 @@ void CellDbIn::get_cell_db_reader(td::Promise> promise.set_result(boc_->get_cell_db_reader()); } +void CellDbIn::print_all_hashes() { + LOG(INFO) << "Enumerating keys in CellDb..."; + + auto snapshot = cell_db_->snapshot(); + + auto status = snapshot->for_each([&](td::Slice raw_key, td::Slice raw_value) -> td::Status { + if (raw_key == "desczero") { + LOG(INFO) << "Found empty key: desczero"; + return td::Status::OK(); + } + + if (raw_key.size() >= 4 && std::memcmp(raw_key.data(), "desc", 4) == 0) { + if (raw_key.size() == 4 + 44) { + KeyHash khash; + LOG(INFO) << "raw_key: " << raw_key.substr(4, 44); + auto hash_part = td::base64_decode(raw_key.substr(4, 44)).move_as_ok(); + std::memcpy(khash.as_slice().begin(), hash_part.data(), 32); + auto block = get_block(khash).move_as_ok(); + + LOG(INFO) << "Found key: hash=" << block.root_hash << " d: " << block.root_hash.to_hex(); + LOG(INFO) << "Block_id = " << block.block_id.to_str(); + } else { + LOG(INFO) << "Found key with \"desc\" prefix but not 48 bytes: key.size()=" << raw_key.size(); + } + } + return td::Status::OK(); + }); + + if (status.is_error()) { + LOG(ERROR) << "Iteration error: " << status.error().message(); + } else { + LOG(INFO) << "Done enumerating CellDb keys."; + } +} + std::vector> CellDbIn::prepare_stats() { TD_PERF_COUNTER(celldb_prepare_stats); auto r_boc_stats = boc_->get_stats(); @@ -528,6 +563,15 @@ td::Result CellDbIn::get_block(KeyHash key_hash) { return DbEntry{obj.move_as_ok()}; } +void CellDbIn::get_block_id_async(KeyHash key_hash, td::Promise promise) { + auto result = get_block(key_hash); + if (result.is_error()) { + promise.set_error(result.move_as_error()); + } else { + promise.set_value(result.move_as_ok().block_id); + } +} + void CellDbIn::set_block(KeyHash key_hash, DbEntry e) { const auto key = get_key(key_hash); cell_db_->set(td::as_slice(key), e.release()).ensure(); @@ -649,6 +693,14 @@ void CellDb::get_cell_db_reader(td::Promise> p td::actor::send_closure(cell_db_, &CellDbIn::get_cell_db_reader, std::move(promise)); } +void CellDb::get_block_id(CellDbIn::KeyHash key_hash, td::Promise promise) { + td::actor::send_closure(cell_db_, &CellDbIn::get_block_id_async, key_hash, std::move(promise)); +} + +void CellDb::print_all_hashes() { + td::actor::send_closure(cell_db_, &CellDbIn::print_all_hashes); +} + void CellDb::start_up() { CellDbBase::start_up(); boc_ = vm::DynamicBagOfCellsDb::create(); diff --git a/validator/db/celldb.hpp b/validator/db/celldb.hpp index 5639b974..bbb38f71 100644 --- a/validator/db/celldb.hpp +++ b/validator/db/celldb.hpp @@ -66,6 +66,8 @@ class CellDbIn : public CellDbBase { void load_cell(RootHash hash, td::Promise> promise); void store_cell(BlockIdExt block_id, td::Ref cell, td::Promise> promise); void get_cell_db_reader(td::Promise> promise); + void get_block_id_async(KeyHash key_hash, td::Promise promise); + void print_all_hashes(); void migrate_cell(td::Bits256 hash); @@ -204,6 +206,8 @@ class CellDb : public CellDbBase { in_memory_boc_ = std::move(in_memory_boc); } void get_cell_db_reader(td::Promise> promise); + void get_block_id(CellDbIn::KeyHash key_hash, td::Promise promise); + void print_all_hashes(); CellDb(td::actor::ActorId root_db, std::string path, td::Ref opts) : root_db_(root_db), path_(path), opts_(opts) {