mirror of
https://github.com/ton-blockchain/ton
synced 2025-03-09 15:40:10 +00:00
Merge 6e83ded8de
into 2a68c8610b
This commit is contained in:
commit
607be9acd5
21 changed files with 819 additions and 5 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> RocksDb::open(std::string path, RocksDbOptions options) {
|
||||
rocksdb::OptimisticTransactionDB *db;
|
||||
{
|
||||
|
|
13
test/fisherman/block_manipulator/base.hpp
Normal file
13
test/fisherman/block_manipulator/base.hpp
Normal file
|
@ -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
|
32
test/fisherman/block_manipulator/factory.cpp
Normal file
32
test/fisherman/block_manipulator/factory.cpp
Normal file
|
@ -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<BaseManipulator> {
|
||||
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<std::shared_ptr<BaseManipulator>> {
|
||||
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>(HeaderCorrupter::Config::fromJson(std::move(json_config)));
|
||||
}
|
||||
if (type == "TransactionCorrupter") {
|
||||
return std::make_shared<TransactionCorrupter>(TransactionCorrupter::Config::fromJson(std::move(json_config)));
|
||||
}
|
||||
return td::Status::Error(400, PSLICE() << "Unknown manipulator type: " << type);
|
||||
}
|
||||
|
||||
} // namespace test::fisherman
|
16
test/fisherman/block_manipulator/factory.hpp
Normal file
16
test/fisherman/block_manipulator/factory.hpp
Normal file
|
@ -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<BaseManipulator>;
|
||||
|
||||
private:
|
||||
auto createImpl(td::JsonValue jv) -> td::Result<std::shared_ptr<BaseManipulator>>;
|
||||
};
|
||||
|
||||
} // namespace test::fisherman
|
115
test/fisherman/block_manipulator/header_corrupter.cpp
Normal file
115
test/fisherman/block_manipulator/header_corrupter.cpp
Normal file
|
@ -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
|
38
test/fisherman/block_manipulator/header_corrupter.hpp
Normal file
38
test/fisherman/block_manipulator/header_corrupter.hpp
Normal file
|
@ -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
|
95
test/fisherman/block_manipulator/ideas.txt
Normal file
95
test/fisherman/block_manipulator/ideas.txt
Normal file
|
@ -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, а "важные" параметры конфигурации не менялись.
|
||||
- При повторном вычислении транзакции выяснилось, что финальный хэш аккаунта не совпадает с тем, что заявлен.
|
67
test/fisherman/block_manipulator/transaction_corrupter.cpp
Normal file
67
test/fisherman/block_manipulator/transaction_corrupter.cpp
Normal file
|
@ -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<vm::CellSlice> 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<vm::CellSlice> 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<vm::Cell> 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
|
25
test/fisherman/block_manipulator/transaction_corrupter.hpp
Normal file
25
test/fisherman/block_manipulator/transaction_corrupter.hpp
Normal file
|
@ -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
|
50
test/fisherman/block_reader.cpp
Normal file
50
test/fisherman/block_reader.cpp
Normal file
|
@ -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<ton::validator::RootDb>(
|
||||
"RootDbActor", td::actor::ActorId<ton::validator::ValidatorManager>(), db_path, opts);
|
||||
});
|
||||
}
|
||||
|
||||
BlockDataLoader::~BlockDataLoader() {
|
||||
scheduler_.stop();
|
||||
}
|
||||
|
||||
td::Result<td::Ref<ton::validator::BlockData>> BlockDataLoader::load_block_data(const ton::BlockIdExt &block_id) {
|
||||
std::atomic<bool> done{false};
|
||||
td::Result<td::Ref<ton::validator::BlockData>> block_data_result;
|
||||
|
||||
scheduler_.run_in_context([&] {
|
||||
auto handle_promise = td::PromiseCreator::lambda([&](td::Result<ton::validator::ConstBlockHandle> handle_res) {
|
||||
if (handle_res.is_error()) {
|
||||
block_data_result = td::Result<td::Ref<ton::validator::BlockData>>(handle_res.move_as_error());
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
auto handle = handle_res.move_as_ok();
|
||||
|
||||
auto data_promise = td::PromiseCreator::lambda([&](td::Result<td::Ref<ton::validator::BlockData>> 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
|
25
test/fisherman/block_reader.hpp
Normal file
25
test/fisherman/block_reader.hpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#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<td::Ref<ton::validator::BlockData>> load_block_data(const ton::BlockIdExt &block_id);
|
||||
|
||||
private:
|
||||
td::actor::Scheduler scheduler_;
|
||||
td::actor::ActorOwn<ton::validator::RootDb> root_db_actor_;
|
||||
};
|
||||
|
||||
} // namespace test::fisherman
|
11
test/fisherman/configs/test.json
Normal file
11
test/fisherman/configs/test.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"block_id": {
|
||||
"workchain_id": -1,
|
||||
"shard_id": "8000000000000000",
|
||||
"seqno": 27492934
|
||||
},
|
||||
"manipulation": {
|
||||
"type": "HeaderCorrupter",
|
||||
"config": {}
|
||||
}
|
||||
}
|
75
test/fisherman/tests.cpp
Normal file
75
test/fisherman/tests.cpp
Normal file
|
@ -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<vm::Cell> 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;
|
||||
}
|
79
test/fisherman/utils.cpp
Normal file
79
test/fisherman/utils.cpp
Normal file
|
@ -0,0 +1,79 @@
|
|||
#include "utils.hpp"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace test::fisherman {
|
||||
|
||||
td::Result<td::BufferSlice> 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<size_t>(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<char *>(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<ton::BlockIdExt> 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<uint32_t>(seqno_signed), ton::RootHash::zero(),
|
||||
ton::FileHash::zero()};
|
||||
}
|
||||
|
||||
} // namespace test::fisherman
|
20
test/fisherman/utils.hpp
Normal file
20
test/fisherman/utils.hpp
Normal file
|
@ -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<td::BufferSlice> 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<ton::BlockIdExt> parse_block_id_from_json(td::JsonValue jv);
|
||||
|
||||
} // namespace test::fisherman
|
83
test/print-all-shard-states.cpp
Normal file
83
test/print-all-shard-states.cpp
Normal file
|
@ -0,0 +1,83 @@
|
|||
#include <iostream>
|
||||
#include <cstring>
|
||||
|
||||
#include "td/actor/actor.h"
|
||||
#include "td/utils/logging.h"
|
||||
|
||||
#include "validator/db/celldb.hpp"
|
||||
|
||||
static td::actor::ActorOwn<ton::validator::CellDb> g_cell_db_actor;
|
||||
static td::actor::ActorOwn<td::actor::Actor> g_loader_actor; // LoadCellActor хранить здесь
|
||||
|
||||
class PrintHashesActor : public td::actor::Actor {
|
||||
public:
|
||||
explicit PrintHashesActor(td::actor::ActorId<ton::validator::CellDb> 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<ton::validator::CellDb> 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<ton::validator::CellDb>(
|
||||
"celldb_actor",
|
||||
td::actor::ActorId<ton::validator::RootDb>(), // пустой
|
||||
celldb_path,
|
||||
opts
|
||||
);
|
||||
|
||||
auto printer_actor = td::actor::create_actor<PrintHashesActor>("printer", g_cell_db_actor.get());
|
||||
});
|
||||
|
||||
while (scheduler.run(1)) {
|
||||
}
|
||||
|
||||
scheduler.stop();
|
||||
|
||||
LOG(INFO) << "Done. Exiting.";
|
||||
return 0;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -265,6 +265,41 @@ void CellDbIn::get_cell_db_reader(td::Promise<std::shared_ptr<vm::CellDbReader>>
|
|||
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<std::pair<std::string, std::string>> CellDbIn::prepare_stats() {
|
||||
TD_PERF_COUNTER(celldb_prepare_stats);
|
||||
auto r_boc_stats = boc_->get_stats();
|
||||
|
@ -528,6 +563,15 @@ td::Result<CellDbIn::DbEntry> CellDbIn::get_block(KeyHash key_hash) {
|
|||
return DbEntry{obj.move_as_ok()};
|
||||
}
|
||||
|
||||
void CellDbIn::get_block_id_async(KeyHash key_hash, td::Promise<BlockIdExt> 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<std::shared_ptr<vm::CellDbReader>> 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<BlockIdExt> 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();
|
||||
|
|
|
@ -66,6 +66,8 @@ class CellDbIn : public CellDbBase {
|
|||
void load_cell(RootHash hash, td::Promise<td::Ref<vm::DataCell>> promise);
|
||||
void store_cell(BlockIdExt block_id, td::Ref<vm::Cell> cell, td::Promise<td::Ref<vm::DataCell>> promise);
|
||||
void get_cell_db_reader(td::Promise<std::shared_ptr<vm::CellDbReader>> promise);
|
||||
void get_block_id_async(KeyHash key_hash, td::Promise<BlockIdExt> 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<std::shared_ptr<vm::CellDbReader>> promise);
|
||||
void get_block_id(CellDbIn::KeyHash key_hash, td::Promise<BlockIdExt> promise);
|
||||
void print_all_hashes();
|
||||
|
||||
CellDb(td::actor::ActorId<RootDb> root_db, std::string path, td::Ref<ValidatorManagerOptions> opts)
|
||||
: root_db_(root_db), path_(path), opts_(opts) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue