From 2e02ad369ca31f8771907cdc4426c6d24e67ce5b Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:00:19 +0400 Subject: [PATCH] Add step by step emulator --- crypto/block/transaction.cpp | 164 ++++++++++------- crypto/block/transaction.h | 84 ++++++++- crypto/smc-envelope/SmartContract.cpp | 149 +++++++++++---- crypto/smc-envelope/SmartContract.h | 15 ++ crypto/vm/vm.cpp | 119 +++++++++--- crypto/vm/vm.h | 13 ++ emulator/CMakeLists.txt | 2 +- emulator/emulator-emscripten.cpp | 115 ++++++++++++ emulator/emulator-extern.cpp | 255 ++++++++++++++++++++++++-- emulator/emulator-extern.h | 172 +++++++++++++++++ emulator/transaction-emulator.cpp | 185 ++++++++++++++----- emulator/transaction-emulator.h | 50 ++++- emulator/tvm-emulator.hpp | 27 +++ validator/impl/collator.cpp | 4 +- validator/impl/validate-query.cpp | 2 +- 15 files changed, 1153 insertions(+), 203 deletions(-) diff --git a/crypto/block/transaction.cpp b/crypto/block/transaction.cpp index ca34e14af..40c6ea984 100644 --- a/crypto/block/transaction.cpp +++ b/crypto/block/transaction.cpp @@ -27,59 +27,6 @@ #include "vm/vm.h" #include "td/utils/Timer.h" -namespace { -/** - * Logger that stores the tail of log messages. - * - * @param max_size The size of the buffer. Default is 256. - */ -class StringLoggerTail : public td::LogInterface { - public: - explicit StringLoggerTail(size_t max_size = 256) : buf(max_size, '\0') {} - - /** - * Appends a slice of data to the buffer. - * - * @param slice The slice of data to be appended. - */ - void append(td::CSlice slice) override { - if (slice.size() > buf.size()) { - slice.remove_prefix(slice.size() - buf.size()); - } - while (!slice.empty()) { - size_t s = std::min(buf.size() - pos, slice.size()); - std::copy(slice.begin(), slice.begin() + s, buf.begin() + pos); - pos += s; - if (pos == buf.size()) { - pos = 0; - truncated = true; - } - slice.remove_prefix(s); - } - } - - /** - * Retrieves the tail of the log. - * - * @returns The log as std::string. - */ - std::string get_log() const { - if (truncated) { - std::string res = buf; - std::rotate(res.begin(), res.begin() + pos, res.end()); - return res; - } else { - return buf.substr(0, pos); - } - } - - private: - std::string buf; - size_t pos = 0; - bool truncated = false; -}; -} - namespace block { using td::Ref; @@ -1775,13 +1722,60 @@ bool Transaction::run_precompiled_contract(const ComputePhaseConfig& cfg, precom } /** - * Prepares the compute phase of a transaction, which includes running TVM. + * Prepares the compute phase of a transaction and run VM. * * @param cfg The configuration for the compute phase. * * @returns True if the compute phase was successfully prepared and executed, false otherwise. */ -bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { +bool Transaction::execute_compute_phase(const ComputePhaseConfig& cfg) { + auto maybe_res = prepare_compute_phase(cfg); + if (!maybe_res) { + return false; + } + + auto res = std::move(*maybe_res); + if (res.skipped) { + return true; + } + + if (res.precompiled_impl) { + return run_precompiled_contract(cfg, *res.precompiled_impl); + } + + auto& cp = *compute_phase.get(); + return run_compute_phase(cfg, cp, res.vm, res.logger, res.precompiled); +} + +bool Transaction::prepare_debug_compute_phase(const ComputePhaseConfig& cfg, std::unique_ptr& vm, + std::unique_ptr& logger) { + auto maybe_res = prepare_compute_phase(cfg); + if (!maybe_res) { + return false; + } + + auto res = std::move(*maybe_res); + if (res.skipped) { + return true; + } + + if (res.precompiled_impl) { + return run_precompiled_contract(cfg, *res.precompiled_impl); + } + + vm = std::make_unique(std::move(res.vm)); + logger = std::move(res.logger); + return true; +} + +/** + * Prepares the compute phase of a transaction, without actually running it in VM. + * + * @param cfg The configuration for the compute phase. + * + * @returns structure with prepared data + */ +std::optional Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { // TODO: add more skip verifications + sometimes use state from in_msg to re-activate // ... compute_phase = std::make_unique(); @@ -1797,17 +1791,17 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { if (td::sgn(balance.grams) <= 0) { // no gas cp.skip_reason = ComputePhase::sk_no_gas; - return true; + return PrepareComputePhaseResult::create_skipped(); } // Compute gas limits if (!compute_gas_limits(cp, cfg)) { compute_phase.reset(); - return false; + return std::nullopt; } if (!cp.gas_limit && !cp.gas_credit) { // no gas cp.skip_reason = ComputePhase::sk_no_gas; - return true; + return PrepareComputePhaseResult::create_skipped(); } if (in_msg_state.not_null()) { LOG(DEBUG) << "HASH(in_msg_state) = " << in_msg_state->get_hash().bits().to_hex(256) @@ -1821,7 +1815,7 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { if (acc_status == Account::acc_uninit && cfg.is_address_suspended(account.workchain, account.addr)) { LOG(DEBUG) << "address is suspended, skipping compute phase"; cp.skip_reason = ComputePhase::sk_suspended; - return true; + return PrepareComputePhaseResult::create_skipped(); } use_msg_state = true; bool forbid_public_libs = @@ -1830,31 +1824,31 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { account.check_addr_rewrite_length(new_fixed_prefix_length))) { LOG(DEBUG) << "cannot unpack in_msg_state, or it has bad fixed_prefix_length; cannot init account state"; cp.skip_reason = ComputePhase::sk_bad_state; - return true; + return PrepareComputePhaseResult::create_skipped(); } if (acc_status == Account::acc_uninit && !check_in_msg_state_hash(cfg)) { LOG(DEBUG) << "in_msg_state hash mismatch, cannot init account state"; cp.skip_reason = ComputePhase::sk_bad_state; - return true; + return PrepareComputePhaseResult::create_skipped(); } if (cfg.disable_anycast && acc_status == Account::acc_uninit && new_fixed_prefix_length > cfg.size_limits.max_acc_fixed_prefix_length) { LOG(DEBUG) << "cannot init account state: too big fixed prefix length (" << new_fixed_prefix_length << ", max " << cfg.size_limits.max_acc_fixed_prefix_length << ")"; cp.skip_reason = ComputePhase::sk_bad_state; - return true; + return PrepareComputePhaseResult::create_skipped(); } } else if (acc_status != Account::acc_active) { // no state, cannot perform transactions cp.skip_reason = in_msg_state.not_null() ? ComputePhase::sk_bad_state : ComputePhase::sk_no_state; - return true; + return PrepareComputePhaseResult::create_skipped(); } else if (in_msg_state.not_null()) { if (cfg.allow_external_unfreeze) { if (in_msg_extern && account.addr != in_msg_state->get_hash().bits()) { // only for external messages with non-zero initstate in active accounts LOG(DEBUG) << "in_msg_state hash mismatch in external message"; cp.skip_reason = ComputePhase::sk_bad_state; - return true; + return PrepareComputePhaseResult::create_skipped(); } } unpack_msg_state(cfg, true); // use only libraries @@ -1863,7 +1857,7 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { if (in_msg_extern && in_msg_state.not_null() && account.addr != in_msg_state->get_hash().bits()) { LOG(DEBUG) << "in_msg_state hash mismatch in external message"; cp.skip_reason = ComputePhase::sk_bad_state; - return true; + return PrepareComputePhaseResult::create_skipped(); } } if (cfg.disable_anycast) { @@ -1883,11 +1877,11 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { cp.precompiled_gas_usage = gas_usage; if (gas_usage > cp.gas_limit) { cp.skip_reason = ComputePhase::sk_no_gas; - return true; + return PrepareComputePhaseResult::create_skipped(); } auto impl = precompiled::get_implementation(new_code->get_hash().bits()); if (impl != nullptr && !cfg.dont_run_precompiled_ && impl->required_version() <= cfg.global_version) { - return run_precompiled_contract(cfg, *impl); + return PrepareComputePhaseResult::create_precompiled(std::move(impl)); } // Contract is marked as precompiled in global config, but implementation is not available @@ -1902,7 +1896,7 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { Ref stack = prepare_vm_stack(cp); if (stack.is_null()) { compute_phase.reset(); - return false; + return std::nullopt; } // OstreamLogger ostream_logger(error_stream); // auto log = create_vm_log(error_stream ? &ostream_logger : nullptr); @@ -1943,15 +1937,31 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { LOG(DEBUG) << "starting VM"; cp.vm_init_state_hash = vm.get_state_hash(); + + return PrepareComputePhaseResult{false, std::move(vm), std::move(logger), nullptr, precompiled}; +} + +bool Transaction::run_compute_phase(const ComputePhaseConfig& cfg, ComputePhase& cp, vm::VmState& vm, + std::unique_ptr& logger, + td::optional precompiled) { td::Timer timer; cp.exit_code = ~vm.run(); double elapsed = timer.elapsed(); + const bool compute_phase_result = get_compute_phase_result(cfg, cp, vm, logger, precompiled, elapsed); + cp.vm_loaded_cells = vm.extract_loaded_cells(); + return compute_phase_result; +} + +bool Transaction::get_compute_phase_result(const ComputePhaseConfig& cfg, ComputePhase& cp, const vm::VmState& vm, + std::unique_ptr& logger, + td::optional precompiled, + double elapsed) { LOG(DEBUG) << "VM terminated with exit code " << cp.exit_code; cp.out_of_gas = (cp.exit_code == ~(int)vm::Excno::out_of_gas); cp.vm_final_state_hash = vm.get_final_state_hash(cp.exit_code); - stack = vm.get_stack_ref(); + const auto stack = vm.get_stack_ref(); cp.vm_steps = (int)vm.get_steps_count(); - gas = vm.get_gas_limits(); + const auto gas = vm.get_gas_limits(); cp.gas_used = std::min(gas.gas_consumed(), gas.gas_limit); cp.accepted = (gas.gas_credit == 0); cp.success = (cp.accepted && vm.committed()); @@ -2010,10 +2020,22 @@ bool Transaction::prepare_compute_phase(const ComputePhaseConfig& cfg) { << cfg.flat_gas_limit << "]; remaining balance=" << balance.to_str(); CHECK(td::sgn(balance.grams) >= 0); } - cp.vm_loaded_cells = vm.extract_loaded_cells(); return true; } +bool Transaction::compute_phase_step_debug(const ComputePhaseConfig& cfg, std::unique_ptr& vm, + std::unique_ptr& logger) { + td::optional res = vm->debug_step(); + if (!res) { + return false; + } + + ComputePhase& cp = *(compute_phase.get()); + cp.exit_code = ~(*res); + + return get_compute_phase_result(cfg, cp, *vm, logger, {}, 0); +} + /** * Prepares the action phase of a transaction. * diff --git a/crypto/block/transaction.h b/crypto/block/transaction.h index 1a7af2d12..219cdc222 100644 --- a/crypto/block/transaction.h +++ b/crypto/block/transaction.h @@ -24,7 +24,9 @@ #include "vm/cellslice.h" #include "vm/dict.h" #include "vm/boc.h" +#include "vm/vm.h" #include +#include #include "tl/tlblib.hpp" #include "td/utils/bits.h" #include "ton/ton-types.h" @@ -43,6 +45,57 @@ namespace transaction { struct Transaction; } // namespace transaction +/** + * Logger that stores the tail of log messages. + * + * @param max_size The size of the buffer. Default is 256. + */ +class StringLoggerTail : public td::LogInterface { +public: + explicit StringLoggerTail(size_t max_size = 256) : buf(max_size, '\0') {} + + /** + * Appends a slice of data to the buffer. + * + * @param slice The slice of data to be appended. + */ + void append(td::CSlice slice) override { + if (slice.size() > buf.size()) { + slice.remove_prefix(slice.size() - buf.size()); + } + while (!slice.empty()) { + size_t s = std::min(buf.size() - pos, slice.size()); + std::copy(slice.begin(), slice.begin() + s, buf.begin() + pos); + pos += s; + if (pos == buf.size()) { + pos = 0; + truncated = true; + } + slice.remove_prefix(s); + } + } + + /** + * Retrieves the tail of the log. + * + * @returns The log as std::string. + */ + std::string get_log() const { + if (truncated) { + std::string res = buf; + std::rotate(res.begin(), res.begin() + pos, res.end()); + return res; + } else { + return buf.substr(0, pos); + } + } + +private: + std::string buf; + size_t pos = 0; + bool truncated = false; +}; + struct CollatorError { std::string msg; CollatorError(std::string _msg) : msg(_msg) { @@ -410,7 +463,36 @@ struct Transaction { Ref prepare_vm_stack(ComputePhase& cp); std::vector> compute_vm_libraries(const ComputePhaseConfig& cfg); bool run_precompiled_contract(const ComputePhaseConfig& cfg, precompiled::PrecompiledSmartContract& precompiled); - bool prepare_compute_phase(const ComputePhaseConfig& cfg); + + bool execute_compute_phase(const ComputePhaseConfig& cfg); + bool prepare_debug_compute_phase(const ComputePhaseConfig& cfg, std::unique_ptr& vm, + std::unique_ptr& logger); + bool get_compute_phase_result(const ComputePhaseConfig& cfg, ComputePhase& cp, const vm::VmState& vm, + std::unique_ptr& logger, + td::optional precompiled, double elapsed); + + struct PrepareComputePhaseResult { + bool skipped; + vm::VmState vm{}; + std::unique_ptr logger{}; + std::unique_ptr precompiled_impl{}; + td::optional precompiled{}; + + static PrepareComputePhaseResult create_skipped() { + return {true}; + } + + static PrepareComputePhaseResult create_precompiled(std::unique_ptr precompiled_impl) { + return {false, {}, nullptr, std::move(precompiled_impl)}; + } + }; + + std::optional prepare_compute_phase(const ComputePhaseConfig& cfg); + bool run_compute_phase(const ComputePhaseConfig& cfg, ComputePhase& cp, vm::VmState& vm, + std::unique_ptr& logger, + td::optional precompiled); + bool compute_phase_step_debug(const ComputePhaseConfig& cfg, std::unique_ptr& vm, + std::unique_ptr& logger); bool prepare_action_phase(const ActionPhaseConfig& cfg); td::Status check_state_limits(const SizeLimitsConfig& size_limits, bool is_account_stat = true); bool prepare_bounce_phase(const ActionPhaseConfig& cfg); diff --git a/crypto/smc-envelope/SmartContract.cpp b/crypto/smc-envelope/SmartContract.cpp index 265c602d7..b55803b2c 100644 --- a/crypto/smc-envelope/SmartContract.cpp +++ b/crypto/smc-envelope/SmartContract.cpp @@ -227,23 +227,13 @@ std::shared_ptr try_fetch_config_from_c7(td::Ref return std::make_shared(std::move(global_config)); } -SmartContract::Answer run_smartcont(SmartContract::State state, td::Ref stack, td::Ref c7, - vm::GasLimits gas, bool ignore_chksig, td::Ref libraries, - int vm_log_verbosity, bool debug_enabled, - std::shared_ptr config) { - auto gas_credit = gas.gas_credit; +vm::VmState init_vm(SmartContract::State state, td::Ref stack, td::Ref c7, vm::GasLimits gas, + bool ignore_chksig, td::Ref libraries, int vm_log_verbosity, bool debug_enabled, + std::shared_ptr config, td::LogInterface* logger) { vm::init_vm(debug_enabled).ensure(); vm::DictionaryBase::get_empty_dictionary(); - class Logger : public td::LogInterface { - public: - void append(td::CSlice slice) override { - res.append(slice.data(), slice.size()); - } - std::string res; - }; - Logger logger; - vm::VmLog log{&logger, td::LogOptions(VERBOSITY_NAME(DEBUG), true, false)}; + vm::VmLog log{logger, td::LogOptions(VERBOSITY_NAME(DEBUG), true, false)}; if (vm_log_verbosity > 1) { log.log_mask |= vm::VmLog::ExecLocation; if (vm_log_verbosity > 2) { @@ -257,7 +247,6 @@ SmartContract::Answer run_smartcont(SmartContract::State state, td::Ref= VERBOSITY_NAME(DEBUG)) { std::ostringstream os; stack->dump(os, 2); @@ -276,20 +265,24 @@ SmartContract::Answer run_smartcont(SmartContract::State state, td::Ref= VERBOSITY_NAME(DEBUG)) { - LOG(DEBUG) << "VM log\n" << logger.res; + LOG(DEBUG) << "VM log\n" << logs; std::ostringstream os; res.stack->dump(os, 2); LOG(DEBUG) << "VM stack:\n" << os.str(); @@ -308,7 +301,60 @@ SmartContract::Answer run_smartcont(SmartContract::State state, td::Ref stack, td::Ref c7, vm::GasLimits gas, + bool ignore_chksig, td::Ref libraries, int vm_log_verbosity, bool debug_enabled, + std::shared_ptr config, std::unique_ptr& vm, + std::unique_ptr& logger) { + logger = std::make_unique(); + logger->clear(); + auto vm_ = init_vm(state, stack, c7, gas, ignore_chksig, libraries, vm_log_verbosity, debug_enabled, config, logger.get()); + if (vm_.get_code().is_null() || stack.is_null()) { + return static_cast(vm::Excno::fatal); // no ~ for unhandled exceptions + } + vm = std::make_unique(std::move(vm_)); + return 0; +} + +/** + * First step in get method execution + */ +void prepare_get_method_args(const SmartContract::State& state, SmartContract::Args& args) { + if (args.c7 && !args.config) { + args.config = try_fetch_config_from_c7(args.c7.value()); + } + if (!args.c7) { + args.c7 = prepare_vm_c7(args, state.code); + } + if (!args.limits) { + args.limits = vm::GasLimits{1000000, 1000000}; + } + if (!args.stack) { + args.stack = td::Ref(true); + } + CHECK(args.method_id); + args.stack.value().write().push_smallint(args.method_id.unwrap()); +} + +SmartContract::Answer run_smartcont(SmartContract::State state, td::Ref stack, td::Ref c7, + vm::GasLimits gas, bool ignore_chksig, td::Ref libraries, + int vm_log_verbosity, bool debug_enabled, + std::shared_ptr config) { + auto gas_credit = gas.gas_credit; + + SmartContract::Logger logger; + auto vm = init_vm(state, stack, c7, gas, ignore_chksig, libraries, vm_log_verbosity, debug_enabled, config, &logger); + + try { + vm.run(); + } catch (...) { + LOG(FATAL) << "catch unhandled exception"; + } + + SmartContract::Answer res = get_vm_result(vm, state, logger.res); + LOG_IF(ERROR, gas_credit != 0 && (res.accepted && !res.success) && !res.missing_library) << "Accepted but failed with code " << res.code << "\n" << res.gas_used << "\n"; return res; @@ -374,25 +420,19 @@ SmartContract::Answer SmartContract::run_method(Args args) { } SmartContract::Answer SmartContract::run_get_method(Args args) const { - if (args.c7 && !args.config) { - args.config = try_fetch_config_from_c7(args.c7.value()); - } - if (!args.c7) { - args.c7 = prepare_vm_c7(args, state_.code); - } - if (!args.limits) { - args.limits = vm::GasLimits{1000000, 1000000}; - } - if (!args.stack) { - args.stack = td::Ref(true); - } - CHECK(args.method_id); - args.stack.value().write().push_smallint(args.method_id.unwrap()); + prepare_get_method_args(get_state(), args); return run_smartcont(get_state(), args.stack.unwrap(), args.c7.unwrap(), args.limits.unwrap(), args.ignore_chksig, args.libraries ? args.libraries.unwrap().get_root_cell() : td::Ref{}, args.vm_log_verbosity_level, args.debug_enabled, args.config ? args.config.value() : nullptr); } +int SmartContract::run_get_method_debug(Args args, std::unique_ptr& vm, std::unique_ptr& logger) const { + prepare_get_method_args(get_state(), args); + return setup_vm(get_state(), args.stack.unwrap(), args.c7.unwrap(), args.limits.unwrap(), args.ignore_chksig, + args.libraries ? args.libraries.unwrap().get_root_cell() : td::Ref{}, + args.vm_log_verbosity_level, args.debug_enabled, args.config ? args.config.value() : nullptr, vm, logger); +} + SmartContract::Answer SmartContract::run_get_method(td::Slice method, Args args) const { return run_get_method(args.set_method_id(method)); } @@ -405,4 +445,35 @@ SmartContract::Answer SmartContract::send_internal_message(td::Ref cel return run_method( args.set_stack(prepare_vm_stack(td::make_refint(args.amount), vm::load_cell_slice_ref(cell), args, 0)).set_method_id(0)); } + +SmartContract::Answer SmartContract::get_result(const vm::VmState& vm, const Logger& logger) const { + if (vm.get_code().is_null()) { + Answer res; + res.code = static_cast(vm::Excno::fatal); + res.success = false; + res.accepted = false; + res.gas_used = 0; + res.vm_log = "VM not initialized"; + return res; + } + return get_vm_result(vm, state_, logger.res); +} + +td::optional SmartContract::debug_step(std::unique_ptr& vm, std::unique_ptr& logger) { + if (!vm || vm->get_code().is_null()) { + LOG(ERROR) << "Attempting debug step on uninitialized VM"; + return {}; + } + + td::optional rescode; + try { + rescode = vm->debug_step(); + } catch (...) { + LOG(FATAL) << "catch unhandled exception"; + } + if (!rescode) { + return {}; + } + return get_result(*vm, *logger); +} } // namespace ton diff --git a/crypto/smc-envelope/SmartContract.h b/crypto/smc-envelope/SmartContract.h index 49edb9693..74b94d6d0 100644 --- a/crypto/smc-envelope/SmartContract.h +++ b/crypto/smc-envelope/SmartContract.h @@ -33,6 +33,17 @@ class SmartContract : public td::CntObject { static td::Ref empty_slice(); public: + class Logger : public td::LogInterface { + public: + void append(td::CSlice slice) override { + res.append(slice.data(), slice.size()); + } + void clear() { + res.clear(); + } + std::string res; + }; + struct State { td::Ref code; td::Ref data; @@ -170,6 +181,10 @@ class SmartContract : public td::CntObject { Answer send_external_message(td::Ref cell, Args args = {}); Answer send_internal_message(td::Ref cell, Args args = {}); + int run_get_method_debug(Args args, std::unique_ptr& vm, std::unique_ptr& logger) const; + td::optional debug_step(std::unique_ptr& vm, std::unique_ptr& logger); + Answer get_result(const vm::VmState& vm, const Logger& logger) const; + size_t code_size() const; size_t data_size() const; static td::Ref create(State state) { diff --git a/crypto/vm/vm.cpp b/crypto/vm/vm.cpp index 356f0df22..940a44dcb 100644 --- a/crypto/vm/vm.cpp +++ b/crypto/vm/vm.cpp @@ -466,39 +466,101 @@ int VmState::step() { } } -int VmState::run_inner() { +td::optional VmState::debug_step() { + try { + if (need_restore_parent) { + restore_parent_vm(~exit_code); + need_restore_parent = false; + } + int res_inner = run_step(); + if (!res_inner) { + return {}; + } + } catch (VmNoGas &vmoog) { + ++steps; + VM_LOG(this) << "unhandled out-of-gas exception: gas consumed=" << gas.gas_consumed() + << ", limit=" << gas.gas_limit; + get_stack().clear(); + get_stack().push_smallint(gas.gas_consumed()); + exit_code = vmoog.get_errno(); // no ~ for unhandled exceptions (to make their faking impossible) + } catch (...) { + ++steps; + VM_LOG(this) << "unhandled exception during debug step"; + exit_code = static_cast(Excno::fatal); + } + + if (parent) { + // if there is a parent VM for current VM, return nullopt to continue execution of the code after RUNVM + // at the next step, we will restore parent VM and continue execution. + need_restore_parent = true; + return {}; + } + + return exit_code; +} + +/** + * Returns exit code of the execution of the whole code or the last instruction. + */ +int VmState::get_exit_code() const { + return exit_code; +} + +/** + * Executes single instruction + */ +int VmState::run_step() { int res; Guard guard(this); - do { + try { try { - try { - res = step(); - VM_LOG_MASK(this, vm::VmLog::GasRemaining) << "gas remaining: " << gas.gas_remaining; - gas.check(); - } catch (vm::CellBuilder::CellWriteError) { - throw VmError{Excno::cell_ov}; - } catch (vm::CellBuilder::CellCreateError) { - throw VmError{Excno::cell_ov}; - } catch (vm::CellSlice::CellReadError) { - throw VmError{Excno::cell_und}; - } - } catch (const VmError& vme) { - VM_LOG(this) << "handling exception code " << vme.get_errno() << ": " << vme.get_msg(); - try { - ++steps; - res = throw_exception(vme.get_errno()); - } catch (const VmError& vme2) { - VM_LOG(this) << "exception " << vme2.get_errno() << " while handling exception: " << vme.get_msg(); - return ~vme2.get_errno(); - } + res = step(); + VM_LOG_MASK(this, vm::VmLog::GasRemaining) << "gas remaining: " << gas.gas_remaining; + gas.check(); + } catch (vm::CellBuilder::CellWriteError) { + throw VmError{Excno::cell_ov}; + } catch (vm::CellBuilder::CellCreateError) { + throw VmError{Excno::cell_ov}; + } catch (vm::CellSlice::CellReadError) { + throw VmError{Excno::cell_und}; + } + } catch (const VmError& vme) { + VM_LOG(this) << "handling exception code " << vme.get_errno() << ": " << vme.get_msg(); + try { + ++steps; + res = throw_exception(vme.get_errno()); + } catch (const VmError& vme2) { + VM_LOG(this) << "exception " << vme2.get_errno() << " while handling exception: " << vme.get_msg(); + res = ~vme2.get_errno(); + exit_code = res; + return res; } - } while (!res); - if ((res | 1) == -1 && !try_commit()) { - VM_LOG(this) << "automatic commit failed (new data or action cells too deep)"; - get_stack().clear(); - get_stack().push_smallint(0); - return ~(int)Excno::cell_ov; } + + exit_code = res; + + if (res) { + // res will be non-zero if there are no more instructions to run + if ((res | 1) == -1 && !try_commit()) { + VM_LOG(this) << "automatic commit failed (new data or action cells too deep)"; + get_stack().clear(); + get_stack().push_smallint(0); + return ~(int)Excno::cell_ov; + } + return res; + } + return res; +} + +/** + * Executes instructions one by one until end + */ +int VmState::run_inner() { + int res; + Guard guard(this); + do { + res = run_step(); + } while (!res); return res; } @@ -531,6 +593,7 @@ int VmState::run() { ss << "\n"; VM_LOG(this) << ss.str(); } + exit_code = res; return res; } restore_parent = true; diff --git a/crypto/vm/vm.h b/crypto/vm/vm.h index aad67d61e..956591f44 100644 --- a/crypto/vm/vm.h +++ b/crypto/vm/vm.h @@ -108,6 +108,16 @@ class VmState final : public VmStateInterface { long long free_gas_consumed = 0; std::unique_ptr parent = nullptr; + /** + * Whenever to restore parent VM in case of RUNVM in step by step mode. + */ + bool need_restore_parent{false}; + + /** + * Result of execution of the last instructions or the whole execution + */ + int exit_code{0}; + public: enum { cell_load_gas_price = 100, @@ -253,6 +263,8 @@ class VmState final : public VmStateInterface { td::BitArray<256> get_final_state_hash(int exit_code) const; int step(); int run(); + td::optional debug_step(); + int get_exit_code() const; Stack& get_stack() { return stack.write(); } @@ -447,6 +459,7 @@ class VmState final : public VmStateInterface { private: void init_cregs(bool same_c3 = false, bool push_0 = true); int run_inner(); + int run_step(); }; struct ParentVmState { diff --git a/emulator/CMakeLists.txt b/emulator/CMakeLists.txt index 63cb28d6a..1441e00d1 100644 --- a/emulator/CMakeLists.txt +++ b/emulator/CMakeLists.txt @@ -56,7 +56,7 @@ if (USE_EMSCRIPTEN) add_executable(emulator-emscripten ${EMULATOR_EMSCRIPTEN_SOURCE}) target_link_libraries(emulator-emscripten PUBLIC emulator) target_link_options(emulator-emscripten PRIVATE -sEXPORTED_RUNTIME_METHODS=UTF8ToString,stringToUTF8,allocate,ALLOC_NORMAL,lengthBytesUTF8) - target_link_options(emulator-emscripten PRIVATE -sEXPORTED_FUNCTIONS=_emulate,_free,_malloc,_run_get_method,_create_emulator,_destroy_emulator,_emulate_with_emulator,_version) + target_link_options(emulator-emscripten PRIVATE -sEXPORTED_FUNCTIONS=_emulate,_free,_malloc,_run_get_method,_setup_sbs_get_method,_sbs_step,_sbs_get_stack,_sbs_get_code_pos,_sbs_get_method_result,_create_emulator,_destroy_emulator,_emulate_sbs,_em_sbs_step,_em_sbs_stack,_em_sbs_code_pos,_em_sbs_result,_emulate_with_emulator,_version,_destroy_tvm_emulator,_em_sbs_c7,_sbs_get_c7) target_link_options(emulator-emscripten PRIVATE -sEXPORT_NAME=EmulatorModule) target_link_options(emulator-emscripten PRIVATE -sERROR_ON_UNDEFINED_SYMBOLS=0) target_link_options(emulator-emscripten PRIVATE -Oz) diff --git a/emulator/emulator-emscripten.cpp b/emulator/emulator-emscripten.cpp index 7b8e9dd1e..b5c0dbc54 100644 --- a/emulator/emulator-emscripten.cpp +++ b/emulator/emulator-emscripten.cpp @@ -189,6 +189,62 @@ void destroy_emulator(void* em) { transaction_emulator_destroy(em); } +const char *emulate_sbs(void *em, const char* libs, const char* account, const char* message, const char* params) { + // we need to allocate logger on the heap since it outlive this function unlike `emulate_with_emulator` + StringLog* logger = new StringLog(); + + td::log_interface = logger; + SET_VERBOSITY_LEVEL(verbosity_DEBUG); + + auto decoded_params_res = decode_transaction_emulation_params(params); + if (decoded_params_res.is_error()) { + return strdup(R"({"fail":true,"message":"Can't decode other params"})"); + } + auto decoded_params = decoded_params_res.move_as_ok(); + + bool rand_seed_set = true; + if (decoded_params.rand_seed_hex) { + rand_seed_set = transaction_emulator_set_rand_seed(em, decoded_params.rand_seed_hex.unwrap().c_str()); + } + + bool prev_blocks_set = true; + if (decoded_params.prev_blocks_info) { + prev_blocks_set = transaction_emulator_set_prev_blocks_info(em, decoded_params.prev_blocks_info.unwrap().c_str()); + } + + if (!transaction_emulator_set_libs(em, libs) || + !transaction_emulator_set_lt(em, decoded_params.lt) || + !transaction_emulator_set_unixtime(em, decoded_params.utime) || + !transaction_emulator_set_ignore_chksig(em, decoded_params.ignore_chksig) || + !transaction_emulator_set_debug_enabled(em, decoded_params.debug_enabled) || + !rand_seed_set || + !prev_blocks_set) { + return strdup(R"({"fail":true,"message":"Can't set params"})"); + } + + return transaction_emulator_sbs_emulate_transaction(em, account, message); +} + +bool em_sbs_step(void *em) { + return transaction_emulator_sbs_step(em); +} + +const char *em_sbs_stack(void *em) { + return transaction_emulator_sbs_get_stack(em); +} + +const char *em_sbs_c7(void *em) { + return transaction_emulator_sbs_get_c7(em); +} + +const char *em_sbs_code_pos(void *em) { + return transaction_emulator_sbs_get_code_pos(em); +} + +const char* em_sbs_result(void *em) { + return transaction_emulator_sbs_result(em); +} + const char *emulate_with_emulator(void* em, const char* libs, const char* account, const char* message, const char* params) { StringLog logger; @@ -250,6 +306,65 @@ const char *emulate(const char *config, const char* libs, int verbosity, const c return result; } +void *setup_sbs_get_method(const char *params, const char* stack, const char* config) { + // we need to allocate logger on the heap since it outlive this function unlike `run_get_method` + StringLog* logger = new StringLog(); + + td::log_interface = logger; + SET_VERBOSITY_LEVEL(verbosity_DEBUG); + + auto decoded_params_res = decode_get_method_params(params); + if (decoded_params_res.is_error()) { + return strdup(R"({"fail":true,"message":"Can't decode params"})"); + } + auto decoded_params = decoded_params_res.move_as_ok(); + + auto tvm = tvm_emulator_create(decoded_params.code.c_str(), decoded_params.data.c_str(), decoded_params.verbosity); + + if ((decoded_params.libs && !tvm_emulator_set_libraries(tvm, decoded_params.libs.value().c_str())) || + !tvm_emulator_set_c7(tvm, decoded_params.address.c_str(), decoded_params.unixtime, decoded_params.balance, + decoded_params.rand_seed_hex.c_str(), config) || + (decoded_params.prev_blocks_info && + !tvm_emulator_set_prev_blocks_info(tvm, decoded_params.prev_blocks_info.value().c_str())) || + (decoded_params.gas_limit > 0 && !tvm_emulator_set_gas_limit(tvm, decoded_params.gas_limit)) || + !tvm_emulator_set_debug_enabled(tvm, decoded_params.debug_enabled)) { + tvm_emulator_destroy(tvm); + return strdup(R"({"fail":true,"message":"Can't set params"})"); + } + + if (const char *error = tvm_emulator_sbs_run_get_method(tvm, decoded_params.method_id, stack); error != nullptr) { + return strdup(error); + } + + return tvm; +} + +void destroy_tvm_emulator(void *tvm) { + tvm_emulator_destroy(tvm); +} + +// -------------- GET METHODS ------------------- + +bool sbs_step(void *tvm) { + return tvm_emulator_sbs_step(tvm); +} + +const char *sbs_get_stack(void *tvm) { + return tvm_emulator_sbs_get_stack(tvm); +} + +const char *sbs_get_c7(void *tvm) { + return tvm_emulator_sbs_get_c7(tvm); +} + +const char* sbs_get_code_pos(void *tvm) { + return tvm_emulator_sbs_get_code_pos(tvm); +} + +const char* sbs_get_method_result(void *tvm) { + return tvm_emulator_sbs_get_method_result(tvm); +} + const char *run_get_method(const char *params, const char* stack, const char* config) { StringLog logger; diff --git a/emulator/emulator-extern.cpp b/emulator/emulator-extern.cpp index 2addd353c..b2f709a06 100644 --- a/emulator/emulator-extern.cpp +++ b/emulator/emulator-extern.cpp @@ -101,14 +101,15 @@ void *emulator_config_create(const char *config_params_boc) { return new block::Config(config.move_as_ok()); } -const char *transaction_emulator_emulate_transaction(void *transaction_emulator, const char *shard_account_boc, const char *message_boc) { - auto emulator = static_cast(transaction_emulator); - +const char *transaction_emulator_emulate_transaction_prepare(const char *shard_account_boc, const char *message_boc, + emulator::TransactionEmulator *emulator, + td::Ref &message_cell, block::Account &account, + ton::UnixTime &now) { auto message_cell_r = boc_b64_to_cell(message_boc); if (message_cell_r.is_error()) { ERROR_RESPONSE(PSTRING() << "Can't deserialize message boc: " << message_cell_r.move_as_error()); } - auto message_cell = message_cell_r.move_as_ok(); + message_cell = message_cell_r.move_as_ok(); auto message_cs = vm::load_cell_slice(message_cell); int msg_tag = block::gen::t_CommonMsgInfo.get_tag(message_cs); @@ -157,8 +158,8 @@ const char *transaction_emulator_emulate_transaction(void *transaction_emulator, ERROR_RESPONSE(PSTRING() << "Can't extract account address"); } - auto account = block::Account(wc, addr.bits()); - ton::UnixTime now = emulator->get_unixtime(); + account = block::Account(wc, addr.bits()); + now = emulator->get_unixtime(); if (!now) { now = (unsigned)std::time(nullptr); } @@ -174,6 +175,19 @@ const char *transaction_emulator_emulate_transaction(void *transaction_emulator, account.last_trans_lt_ = shard_account.last_trans_lt; account.last_trans_hash_ = shard_account.last_trans_hash; } + return nullptr; +} + +const char *transaction_emulator_emulate_transaction(void *transaction_emulator, const char *shard_account_boc, const char *message_boc) { + auto emulator = static_cast(transaction_emulator); + + td::Ref message_cell; + block::Account account; + ton::UnixTime now; + if (const char *error = transaction_emulator_emulate_transaction_prepare(shard_account_boc, message_boc, emulator, + message_cell, account, now)) { + return error; + } auto result = emulator->emulate_transaction(std::move(account), message_cell, now, 0, block::transaction::Transaction::tr_ord); if (result.is_error()) { @@ -214,6 +228,154 @@ const char *transaction_emulator_emulate_transaction(void *transaction_emulator, std::move(actions_boc_b64), emulation_success.elapsed_time); } +const char *transaction_emulator_sbs_emulate_transaction(void *transaction_emulator, const char *shard_account_boc, const char *message_boc) { + auto emulator = static_cast(transaction_emulator); + + td::Ref message_cell; + block::Account account; + ton::UnixTime now; + if (const auto error = transaction_emulator_emulate_transaction_prepare(shard_account_boc, message_boc, emulator, + message_cell, account, now)) { + ERROR_RESPONSE(PSTRING() << "Emulate transaction failed: " << error); + } + + auto result = emulator->prepare_emulate_transaction_debug(std::move(account), message_cell, now, 0, block::transaction::Transaction::tr_ord); + if (result.is_error()) { + ERROR_RESPONSE(PSTRING() << "Emulate transaction failed: " << result.move_as_error()); + } + + td::JsonBuilder jb; + auto json_obj = jb.enter_object(); + json_obj("success", td::JsonTrue()); + json_obj.leave(); + return strdup(jb.string_builder().as_cslice().c_str()); +} + +bool transaction_emulator_sbs_step(void *transaction_emulator) { + const auto emulator = static_cast(transaction_emulator); + + auto res = emulator->debug_step(); + if (res.is_error()) { + LOG(ERROR) << "Debug step failed: " << res.error().to_string(); + return false; + } + + return res.move_as_ok(); +} + +const char *transaction_emulator_sbs_result(void *transaction_emulator) { + auto emulator = static_cast(transaction_emulator); + + auto result = emulator->get_emulation_result(); + if (result.is_error()) { + ERROR_RESPONSE(PSTRING() << "Emulate transaction failed: " << result.move_as_error()); + } + + auto emulation_result = result.move_as_ok(); + + auto external_not_accepted = dynamic_cast(emulation_result.get()); + if (external_not_accepted) { + return external_not_accepted_response(std::move(external_not_accepted->vm_log), external_not_accepted->vm_exit_code, + external_not_accepted->elapsed_time); + } + + auto emulation_success = std::move(dynamic_cast(*emulation_result)); + auto trans_boc_b64 = cell_to_boc_b64(std::move(emulation_success.transaction)); + if (trans_boc_b64.is_error()) { + ERROR_RESPONSE(PSTRING() << "Can't serialize Transaction to boc " << trans_boc_b64.move_as_error()); + } + + auto new_shard_account_cell = vm::CellBuilder().store_ref(emulation_success.account.total_state) + .store_bits(emulation_success.account.last_trans_hash_.as_bitslice()) + .store_long(emulation_success.account.last_trans_lt_).finalize(); + auto new_shard_account_boc_b64 = cell_to_boc_b64(std::move(new_shard_account_cell)); + if (new_shard_account_boc_b64.is_error()) { + ERROR_RESPONSE(PSTRING() << "Can't serialize ShardAccount to boc " << new_shard_account_boc_b64.move_as_error()); + } + + td::optional actions_boc_b64; + if (emulation_success.actions.not_null()) { + auto actions_boc_b64_result = cell_to_boc_b64(std::move(emulation_success.actions)); + if (actions_boc_b64_result.is_error()) { + ERROR_RESPONSE(PSTRING() << "Can't serialize actions list cell to boc " << actions_boc_b64_result.move_as_error()); + } + actions_boc_b64 = actions_boc_b64_result.move_as_ok(); + } + + return success_response(trans_boc_b64.move_as_ok(), new_shard_account_boc_b64.move_as_ok(), std::move(emulation_success.vm_log), + std::move(actions_boc_b64), emulation_success.elapsed_time); +} + +// -------------------------- GETTERS IMPL -------------------------------- + +const char *emulator_vm_get_stack(const vm::VmState &vm) { + vm::FakeVmStateLimits fstate(3500); // limit recursive (de)serialization calls + vm::VmStateInterface::Guard guard(&fstate); + + vm::CellBuilder stack_cb; + if (!vm.get_stack_const().serialize(stack_cb)) { + ERROR_RESPONSE(PSTRING() << "Couldn't serialize stack"); + } + + auto stack_boc = cell_to_boc_b64(stack_cb.finalize()); + if (stack_boc.is_error()) { + ERROR_RESPONSE(PSTRING() << "Couldn't serialize stack cell: " << stack_boc.move_as_error().to_string()); + } + + return strdup(stack_boc.move_as_ok().c_str()); +} + +const char *emulator_vm_get_c7(const vm::VmState &vm) { + vm::FakeVmStateLimits fstate(3500); // limit recursive (de)serialization calls + vm::VmStateInterface::Guard guard(&fstate); + + vm::StackEntry c7_entry(vm.get_c7()); + + vm::CellBuilder c7_cb; + if (!c7_entry.serialize(c7_cb)) { + ERROR_RESPONSE(PSTRING() << "Couldn't serialize c7"); + } + auto result_stack_boc = cell_to_boc_b64(c7_cb.finalize()); + if (result_stack_boc.is_error()) { + ERROR_RESPONSE(PSTRING() << "Couldn't serialize c7 cell: " << result_stack_boc.move_as_error().to_string()); + } + + return strdup(result_stack_boc.move_as_ok().c_str()); +} + +const char *emulator_vm_get_code_pos(const vm::VmState &vm) { + const auto code = vm.get_code(); + if (code.is_null()) { + return strdup("unknown:0"); + } + + std::ostringstream rs; + rs << code->get_base_cell()->get_hash().to_hex() << ":" << code->cur_pos(); + + const auto res = rs.str(); + return strdup(res.c_str()); +} + +// -------------------- TRANSACTIONS ------------------------- + +const char *transaction_emulator_sbs_get_stack(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto &vm = emulator->get_vm(); + return emulator_vm_get_stack(vm); +} + +const char *transaction_emulator_sbs_get_c7(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto &vm = emulator->get_vm(); + return emulator_vm_get_c7(vm); +} + +const char *transaction_emulator_sbs_get_code_pos(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto &vm = emulator->get_vm(); + return emulator_vm_get_code_pos(vm); +} + const char *transaction_emulator_emulate_tick_tock_transaction(void *transaction_emulator, const char *shard_account_boc, bool is_tock) { auto emulator = static_cast(transaction_emulator); @@ -434,6 +596,8 @@ void *tvm_emulator_create(const char *code, const char *data, int vm_log_verbosi return nullptr; } + vm::init_vm(true).ensure(); + auto emulator = new emulator::TvmEmulator(code_cell.move_as_ok(), data_cell.move_as_ok()); emulator->set_vm_verbosity_level(vm_log_verbosity); return emulator; @@ -596,23 +760,61 @@ bool tvm_emulator_set_debug_enabled(void *tvm_emulator, bool debug_enabled) { return true; } -const char *tvm_emulator_run_get_method(void *tvm_emulator, int method_id, const char *stack_boc) { +const char *tvm_emulator_sbs_get_stack(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto &vm = emulator->get_vm(); + return emulator_vm_get_stack(vm); +} + +const char *tvm_emulator_sbs_get_c7(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto &vm = emulator->get_vm(); + return emulator_vm_get_c7(vm); +} + +const char* tvm_emulator_sbs_get_code_pos(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto& vm = emulator->get_vm(); + return emulator_vm_get_code_pos(vm); +} + +// -------------------------- RUN GET METHODS -------------------------- + +const char *tvm_emulator_run_get_method_prepare(const char *stack_boc, td::Ref &stack) { auto stack_cell = boc_b64_to_cell(stack_boc); if (stack_cell.is_error()) { ERROR_RESPONSE(PSTRING() << "Couldn't deserialize stack cell: " << stack_cell.move_as_error().to_string()); } auto stack_cs = vm::load_cell_slice(stack_cell.move_as_ok()); - td::Ref stack; if (!vm::Stack::deserialize_to(stack_cs, stack)) { ERROR_RESPONSE(PSTRING() << "Couldn't deserialize stack"); } + return nullptr; +} - auto emulator = static_cast(tvm_emulator); - auto result = emulator->run_get_method(method_id, stack); - +const char *tvm_emulator_sbs_run_get_method(void *tvm_emulator, int method_id, const char *stack_boc) { + td::Ref stack; + if (const char *error = tvm_emulator_run_get_method_prepare(stack_boc, stack); error != nullptr) { + ERROR_RESPONSE(PSTRING() << "Couldn't prepare get method run: " << error); + } + + const auto emulator = static_cast(tvm_emulator); + const auto result = emulator->run_get_method_debug(method_id, stack); + if (result != 0) { + ERROR_RESPONSE(PSTRING() << "Couldn't prepare get method run"); + } + + td::JsonBuilder jb; + auto json_obj = jb.enter_object(); + json_obj("success", td::JsonTrue()); + json_obj.leave(); + return strdup(jb.string_builder().as_cslice().c_str()); +} + +const char *tvm_emulator_get_method_result(emulator::TvmEmulator::Answer result) { vm::FakeVmStateLimits fstate(3500); // limit recursive (de)serialization calls vm::VmStateInterface::Guard guard(&fstate); - + vm::CellBuilder stack_cb; if (!result.stack->serialize(stack_cb)) { ERROR_RESPONSE(PSTRING() << "Couldn't serialize stack"); @@ -639,6 +841,23 @@ const char *tvm_emulator_run_get_method(void *tvm_emulator, int method_id, const return strdup(jb.string_builder().as_cslice().c_str()); } +const char *tvm_emulator_run_get_method(void *tvm_emulator, int method_id, const char *stack_boc) { + td::Ref stack; + if (const char* error = tvm_emulator_run_get_method_prepare(stack_boc, stack)) { + return error; + } + + auto emulator = static_cast(tvm_emulator); + auto result = emulator->run_get_method(method_id, stack); + return tvm_emulator_get_method_result(result); +} + +const char *tvm_emulator_sbs_get_method_result(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto result = emulator->sbs_result(); + return tvm_emulator_get_method_result(result); +} + struct TvmEulatorEmulateRunMethodResponse { const char *response; @@ -722,6 +941,18 @@ void run_method_detailed_result_destroy(void *detailed_result) { delete result; } +const char *tvm_emulator_sbs_transaction_result(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto result = emulator->sbs_result(); + return tvm_emulator_get_method_result(result); +} + +bool tvm_emulator_sbs_step(void *tvm_emulator) { + const auto emulator = static_cast(tvm_emulator); + const auto result = emulator->debug_step(); + return static_cast(result); +} + const char *tvm_emulator_send_external_message(void *tvm_emulator, const char *message_body_boc) { auto message_body_cell = boc_b64_to_cell(message_body_boc); if (message_body_cell.is_error()) { diff --git a/emulator/emulator-extern.h b/emulator/emulator-extern.h index 2da767456..1a94d1bb7 100644 --- a/emulator/emulator-extern.h +++ b/emulator/emulator-extern.h @@ -119,6 +119,96 @@ EMULATOR_EXPORT bool transaction_emulator_set_prev_blocks_info(void *transaction */ EMULATOR_EXPORT const char *transaction_emulator_emulate_transaction(void *transaction_emulator, const char *shard_account_boc, const char *message_boc); +/** + * @brief Prepares transaction for step by step emulation + * @param transaction_emulator Pointer to TransactionEmulator object + * @param shard_account_boc Base64 encoded BoC serialized ShardAccount + * @param message_boc Base64 encoded BoC serialized inbound Message (internal or external) + * + * @return JSON object with one of the following shapes: + * Error result: + * @code{.json} + * { + * "success": false, + * "error": "", // human-readable description + * "external_not_accepted": false, // always false + * } + * @endcode + * Success result: + * @code{.json} + * { + * "success": true, + * } + * @endcode + * + * @note See transaction_emulator_sbs_result to get the final result after all steps + */ +EMULATOR_EXPORT const char *transaction_emulator_sbs_emulate_transaction(void *transaction_emulator, + const char *shard_account_boc, + const char *message_boc); + +/** + * @brief Performs execution step of transaction emulation + * @param transaction_emulator Pointer to TransactionEmulator object + * @return true if execution is finished and false otherwise. + * + * @note Must be called only after transaction_emulator_sbs_emulate_transaction() + */ +EMULATOR_EXPORT bool transaction_emulator_sbs_step(void *transaction_emulator); + +/** + * @brief Finish transaction emulation and returns a result + * @param transaction_emulator Pointer to TransactionEmulator object + * @return Result of emulation: + * Error result: + * @code{.json} + * { + * "success": false, + * "error": "", // human-readable description + * "external_not_accepted": , // true if external msg was rejected by VM + * "vm_exit_code": , // optional: TVM exit code + * "vm_log": "", // optional: VM execution trace + * "elapsed_time": 0 + * } + * @endcode + * Success result: + * @code{.json} + * { + * "success": true, + * "transaction": "", // Base64 encoded Transaction boc + * "shard_account": "", // Base64 encoded new ShardAccount boc + * "vm_log": "", // VM execution trace + * "actions": "", // Base64 encoded compute phase actions boc (OutList n) + * "elapsed_time": 0 + * } + * @endcode + * + * @note This function returns the same result as transaction_emulator_emulate_transaction + * in normal execution + */ +EMULATOR_EXPORT const char *transaction_emulator_sbs_result(void *transaction_emulator); + +/** + * @brief Get stack at the current step of a transaction emulation + * @param tvm_emulator Pointer to TransactionEmulator object + * @return Base64 encoded stack at the current step + */ +EMULATOR_EXPORT const char *transaction_emulator_sbs_get_stack(void *tvm_emulator); + +/** + * @brief Get c7 control register value at the current step of a transaction emulation + * @param tvm_emulator Pointer to TransactionEmulator object + * @return Base64 encoded c7 control register value at the current step + */ +EMULATOR_EXPORT const char *transaction_emulator_sbs_get_c7(void *tvm_emulator); + +/** + * @brief Get position ad code for the current step of a transaction emulation + * @param tvm_emulator Pointer to TransactionEmulator object + * @return String in "cell_hash_hex:offset" format + */ +EMULATOR_EXPORT const char *transaction_emulator_sbs_get_code_pos(void *tvm_emulator); + /** * @brief Emulate tick tock transaction * @param transaction_emulator Pointer to TransactionEmulator object @@ -244,6 +334,88 @@ EMULATOR_EXPORT bool tvm_emulator_set_debug_enabled(void *tvm_emulator, bool deb */ EMULATOR_EXPORT const char *tvm_emulator_run_get_method(void *tvm_emulator, int method_id, const char *stack_boc); +/** + * @brief Prepares get method for step by step emulation + * @param tvm_emulator Pointer to TvmEmulator object + * @param method_id Integer method id + * @param stack_boc Base64 encoded BoC serialized stack (VmStack) + * + * @return JSON object with one of the following shapes: + * Error result: + * @code{.json} + * { + * "success": false, + * "error": "" // Error description + * } + * @endcode + * Success result: + * @code{.json} + * { + * "success": true + * } + * @endcode + */ +EMULATOR_EXPORT const char *tvm_emulator_sbs_run_get_method(void *tvm_emulator, int method_id, const char *stack_boc); + +/** + * @brief Performs execution step of get method emulation + * @param tvm_emulator Pointer to TvmEmulator object + * @return true if execution is finished and false otherwise. + * + * @note Must be called only after tvm_emulator_sbs_run_get_method() + */ +EMULATOR_EXPORT bool tvm_emulator_sbs_step(void *tvm_emulator); + +/** + * @brief Finish get method emulation and returns a result + * @param tvm_emulator Pointer to TvmEmulator object + * + * @return JSON object with one of the following shapes: + * Error result: + * @code{.json} + * { + * "success": false, + * "error": "" // Error description + * } + * @endcode + * Success result: + * @code{.json} + * { + * "success": true, + * "vm_log": "", // VM execution trace + * "vm_exit_code": "", // VM exit code + * "stack": "", // Base64 encoded BoC serialized stack (VmStack) + * "gas_used": // Amount of gas used for this get method execution + * "missing_library": null, + * } + * @endcode + * + * @note This function returns the same result as tvm_emulator_run_get_method + * in normal execution + */ +EMULATOR_EXPORT const char *tvm_emulator_sbs_get_method_result(void *tvm_emulator); + +/** + * @brief Get stack at the current step of a get method emulation + * @param tvm_emulator Pointer to TvmEmulator object + * @return Base64 encoded stack at the current step + */ +EMULATOR_EXPORT const char *tvm_emulator_sbs_get_stack(void *tvm_emulator); + +/** + * @brief Get c7 control register value at the current step of a get method emulation + * @param tvm_emulator Pointer to TvmEmulator object + * @return Base64 encoded c7 control register value at the current step + */ +EMULATOR_EXPORT const char *tvm_emulator_sbs_get_c7(void *tvm_emulator); + +/** + * @brief Get position ad code for the current step of a get method emulation + * @param tvm_emulator Pointer to TvmEmulator object + * @return String in "cell_hash_hex:offset" format + */ +EMULATOR_EXPORT const char *tvm_emulator_sbs_get_code_pos(void *tvm_emulator); + /** * @brief Optimized version of "run get method" with all passed parameters in a single call * @param len Length of params_boc buffer diff --git a/emulator/transaction-emulator.cpp b/emulator/transaction-emulator.cpp index abdf0d2cf..7cca77e2d 100644 --- a/emulator/transaction-emulator.cpp +++ b/emulator/transaction-emulator.cpp @@ -8,17 +8,10 @@ using td::Ref; using namespace std::string_literals; namespace emulator { -td::Result> TransactionEmulator::emulate_transaction( - block::Account&& account, td::Ref msg_root, ton::UnixTime utime, ton::LogicalTime lt, int trans_type) { - +td::Result<> TransactionEmulator::prepare_emulation(block::Account& account, ton::UnixTime& utime, ton::LogicalTime& lt) { td::Ref old_mparams; - std::vector storage_prices; - block::StoragePhaseConfig storage_phase_cfg{&storage_prices}; - block::ComputePhaseConfig compute_phase_cfg; - block::ActionPhaseConfig action_phase_cfg; - block::SerializeConfig serialize_config; td::RefInt256 masterchain_create_fee, basechain_create_fee; - + if (!utime) { utime = unixtime_; } @@ -47,35 +40,89 @@ td::Result> TransactionEmu compute_phase_cfg.ignore_chksig = ignore_chksig_; compute_phase_cfg.with_vm_log = true; compute_phase_cfg.vm_log_verbosity = vm_log_verbosity_; + return td::Unit{}; +} + +td::Result> TransactionEmulator::finish_emulation( + block::Account&& account, double elapsed) { + if (!trans->compute_phase->accepted && trans->in_msg_extern) { + auto vm_log = trans->compute_phase->vm_log; + auto vm_exit_code = trans->compute_phase->exit_code; + cleanup_shared_state(); + return std::make_unique(std::move(vm_log), vm_exit_code, + elapsed); + } + + if (!trans->serialize(serialize_config)) { + auto error = td::Status::Error( + -669, "cannot serialize new transaction for smart contract "s + trans->account.addr.to_hex()); + cleanup_shared_state(); + return error; + } + + auto trans_root = trans->commit(account); + if (trans_root.is_null()) { + cleanup_shared_state(); + return td::Status::Error(PSLICE() << "cannot commit new transaction for smart contract"); + } + + auto result = std::make_unique( + std::move(trans_root), std::move(account), std::move(trans->compute_phase->vm_log), + std::move(trans->compute_phase->actions), elapsed); + + // Since emulator can be used many times, cleanup all shared state for clean emulation of the next transaction + cleanup_shared_state(); + return result; +} + +void TransactionEmulator::cleanup_shared_state() { + vm = nullptr; + logger = {}; + storage_prices = {}; + storage_phase_cfg = {&storage_prices}; + compute_phase_cfg = {}; + action_phase_cfg = {}; + trans = nullptr; + account_ = {}; + external = false; + serialize_config = {}; +} + +td::Result> TransactionEmulator::emulate_transaction( + block::Account&& account, td::Ref msg_root, ton::UnixTime utime, ton::LogicalTime lt, int trans_type) { + + auto prepare_res = prepare_emulation(account, utime, lt); + if (prepare_res.is_error()) { + return prepare_res.move_as_error_prefix("cannot prepare emulation"); + } double start_time = td::Time::now(); - auto res = create_transaction(msg_root, &account, utime, lt, trans_type, - &storage_phase_cfg, &compute_phase_cfg, - &action_phase_cfg); + auto res = run_transaction(msg_root, &account, utime, lt, trans_type); double elapsed = td::Time::now() - start_time; if(res.is_error()) { return res.move_as_error_prefix("cannot run message on account "); } - std::unique_ptr trans = res.move_as_ok(); - if (!trans->compute_phase->accepted && trans->in_msg_extern) { - auto vm_log = trans->compute_phase->vm_log; - auto vm_exit_code = trans->compute_phase->exit_code; - return std::make_unique(std::move(vm_log), vm_exit_code, elapsed); - } + return finish_emulation(std::move(account), elapsed); +} + +td::Result TransactionEmulator::prepare_emulate_transaction_debug( + block::Account&& account, td::Ref msg_root, ton::UnixTime utime, ton::LogicalTime lt, int trans_type) { + + account_ = std::move(account); - if (!trans->serialize(serialize_config)) { - return td::Status::Error(-669,"cannot serialize new transaction for smart contract "s + trans->account.addr.to_hex()); + auto prepare_res = prepare_emulation(account_, utime, lt); + if (prepare_res.is_error()) { + return prepare_res.move_as_error_prefix("cannot prepare emulation"); } - auto trans_root = trans->commit(account); - if (trans_root.is_null()) { - return td::Status::Error(PSLICE() << "cannot commit new transaction for smart contract"); + auto res = run_transaction_debug(msg_root, &account_, utime, lt, trans_type); + if (res.is_error()) { + return res.move_as_error_prefix("cannot run message on account "); } - return std::make_unique(std::move(trans_root), std::move(account), - std::move(trans->compute_phase->vm_log), std::move(trans->compute_phase->actions), elapsed); + return res; } td::Result TransactionEmulator::emulate_transaction(block::Account&& account, td::Ref original_trans) { @@ -175,13 +222,10 @@ bool TransactionEmulator::check_state_update(const block::Account& account, cons hash_update.new_hash == account.total_state->get_hash().bits(); } -td::Result> TransactionEmulator::create_transaction( - td::Ref msg_root, block::Account* acc, - ton::UnixTime utime, ton::LogicalTime lt, int trans_type, - block::StoragePhaseConfig* storage_phase_cfg, - block::ComputePhaseConfig* compute_phase_cfg, - block::ActionPhaseConfig* action_phase_cfg) { - bool external{false}, ihr_delivered{false}, need_credit_phase{false}; +td::Result<> TransactionEmulator::prepare_transaction( + td::Ref msg_root, block::Account* acc, ton::UnixTime utime, ton::LogicalTime lt, int trans_type) { + external = false; + bool ihr_delivered{false}, need_credit_phase{false}; if (msg_root.not_null()) { auto cs = vm::load_cell_slice(msg_root); @@ -194,10 +238,9 @@ td::Result> TransactionEmulator need_credit_phase = true; } - std::unique_ptr trans = - std::make_unique(*acc, trans_type, lt, utime, msg_root); + trans = std::make_unique(*acc, trans_type, lt, utime, msg_root); - if (msg_root.not_null() && !trans->unpack_input_msg(ihr_delivered, action_phase_cfg)) { + if (msg_root.not_null() && !trans->unpack_input_msg(ihr_delivered, &action_phase_cfg)) { if (external) { // inbound external message was not accepted return td::Status::Error(-701,"inbound external message rejected by account "s + acc->addr.to_hex() + @@ -207,7 +250,7 @@ td::Result> TransactionEmulator } if (trans->bounce_enabled) { - if (!trans->prepare_storage_phase(*storage_phase_cfg, true)) { + if (!trans->prepare_storage_phase(storage_phase_cfg, true)) { return td::Status::Error(-669,"cannot create storage phase of a new transaction for smart contract "s + acc->addr.to_hex()); } if (need_credit_phase && !trans->prepare_credit_phase()) { @@ -217,12 +260,21 @@ td::Result> TransactionEmulator if (need_credit_phase && !trans->prepare_credit_phase()) { return td::Status::Error(-669,"cannot create credit phase of a new transaction for smart contract "s + acc->addr.to_hex()); } - if (!trans->prepare_storage_phase(*storage_phase_cfg, true, need_credit_phase)) { + if (!trans->prepare_storage_phase(storage_phase_cfg, true, need_credit_phase)) { return td::Status::Error(-669,"cannot create storage phase of a new transaction for smart contract "s + acc->addr.to_hex()); } } + return td::Unit{}; +} - if (!trans->prepare_compute_phase(*compute_phase_cfg)) { +td::Result<> TransactionEmulator::run_transaction(td::Ref msg_root, block::Account* acc, ton::UnixTime utime, + ton::LogicalTime lt, int trans_type) { + auto prepare_res = prepare_transaction(msg_root, acc, utime, lt, trans_type); + if (prepare_res.is_error()) { + return prepare_res.move_as_error_prefix("cannot prepare transaction"); + } + + if (!trans->execute_compute_phase(compute_phase_cfg)) { return td::Status::Error(-669,"cannot create compute phase of a new transaction for smart contract "s + acc->addr.to_hex()); } @@ -233,17 +285,68 @@ td::Result> TransactionEmulator } } - if (trans->compute_phase->success && !trans->prepare_action_phase(*action_phase_cfg)) { + if (trans->compute_phase->success && !trans->prepare_action_phase(action_phase_cfg)) { return td::Status::Error(-669,"cannot create action phase of a new transaction for smart contract "s + acc->addr.to_hex()); } if (trans->bounce_enabled && (!trans->compute_phase->success || trans->action_phase->state_exceeds_limits || trans->action_phase->bounce) - && !trans->prepare_bounce_phase(*action_phase_cfg)) { + && !trans->prepare_bounce_phase(action_phase_cfg)) { return td::Status::Error(-669,"cannot create bounce phase of a new transaction for smart contract "s + acc->addr.to_hex()); } - return trans; + return td::Unit{}; +} + +td::Result TransactionEmulator::run_transaction_debug(td::Ref msg_root, block::Account* acc, + ton::UnixTime utime, ton::LogicalTime lt, int trans_type) { + auto prepare_res = prepare_transaction(msg_root, acc, utime, lt, trans_type); + if (prepare_res.is_error()) { + return prepare_res.move_as_error_prefix("cannot prepare transaction"); + } + + if (!trans->prepare_debug_compute_phase(compute_phase_cfg, vm, logger)) { + return td::Status::Error(-669,"cannot create compute phase of a new transaction for smart contract "s + acc->addr.to_hex()); + } + + return true; +} + +td::Result TransactionEmulator::transaction_step_debug() { + if (!trans->compute_phase_step_debug(compute_phase_cfg, vm, logger)) { + return false; + } + + if (!trans->compute_phase->accepted) { + if (!external && trans->compute_phase->skip_reason == block::ComputePhase::sk_none) { + return td::Status::Error(-669,"new ordinary transaction for smart contract "s + account_.addr.to_hex() + + " has not been accepted by the smart contract (?)"); + } + } + + if (trans->compute_phase->success && !trans->prepare_action_phase(action_phase_cfg)) { + return td::Status::Error(-669,"cannot create action phase of a new transaction for smart contract "s + account_.addr.to_hex()); + } + + if (trans->bounce_enabled + && (!trans->compute_phase->success || trans->action_phase->state_exceeds_limits || trans->action_phase->bounce) + && !trans->prepare_bounce_phase(action_phase_cfg)) { + return td::Status::Error(-669,"cannot create bounce phase of a new transaction for smart contract "s + account_.addr.to_hex()); + } + + return true; +} + +td::Result> TransactionEmulator::get_emulation_result() { + return finish_emulation(std::move(account_), 0); +} + +td::Result TransactionEmulator::debug_step() { + auto res = transaction_step_debug(); + if (res.is_error()) { + return res.move_as_error_prefix("cannot run message on account "); + } + return res; } void TransactionEmulator::set_unixtime(ton::UnixTime unixtime) { diff --git a/emulator/transaction-emulator.h b/emulator/transaction-emulator.h index 848548c32..4b7bc1b08 100644 --- a/emulator/transaction-emulator.h +++ b/emulator/transaction-emulator.h @@ -19,6 +19,25 @@ class TransactionEmulator { bool debug_enabled_; td::Ref prev_blocks_info_; + // Emulation state that persists throughout execution + + /** + * Instance of VM used in step by step mode, otherwise it is nullptr. + */ + std::unique_ptr vm{}; + /** + * Logger used in step by step mode, otherwise it is nullptr. + */ + std::unique_ptr logger{}; + std::vector storage_prices; + block::StoragePhaseConfig storage_phase_cfg{&storage_prices}; + block::ComputePhaseConfig compute_phase_cfg{}; + block::ActionPhaseConfig action_phase_cfg{}; + std::unique_ptr trans{}; + block::Account account_{}; + bool external{false}; + block::SerializeConfig serialize_config{}; + public: TransactionEmulator(std::shared_ptr config, int vm_log_verbosity = 0) : config_(std::move(config)), libraries_(256), vm_log_verbosity_(vm_log_verbosity), @@ -68,8 +87,21 @@ class TransactionEmulator { return unixtime_; } - td::Result> emulate_transaction( - block::Account&& account, td::Ref msg_root, ton::UnixTime utime, ton::LogicalTime lt, int trans_type); + const vm::VmState& get_vm() const { + return *vm; + } + + td::Result<> prepare_emulation(block::Account& account, ton::UnixTime& utime, ton::LogicalTime& lt); + void cleanup_shared_state(); + td::Result> emulate_transaction(block::Account&& account, td::Ref msg_root, + ton::UnixTime utime, ton::LogicalTime lt, + int trans_type); + td::Result> finish_emulation(block::Account&& account, + double elapsed); + td::Result prepare_emulate_transaction_debug(block::Account&& account, td::Ref msg_root, + ton::UnixTime utime, ton::LogicalTime lt, int trans_type); + td::Result debug_step(); + td::Result> get_emulation_result(); td::Result emulate_transaction(block::Account&& account, td::Ref original_trans); td::Result emulate_transactions_chain(block::Account&& account, std::vector>&& original_transactions); @@ -85,12 +117,16 @@ class TransactionEmulator { private: bool check_state_update(const block::Account& account, const block::gen::Transaction::Record& trans); + td::Result<> prepare_transaction(td::Ref msg_root, block::Account* acc, ton::UnixTime utime, + ton::LogicalTime lt, int trans_type); - td::Result> create_transaction( + td::Result<> run_transaction( td::Ref msg_root, block::Account* acc, - ton::UnixTime utime, ton::LogicalTime lt, int trans_type, - block::StoragePhaseConfig* storage_phase_cfg, - block::ComputePhaseConfig* compute_phase_cfg, - block::ActionPhaseConfig* action_phase_cfg); + ton::UnixTime utime, ton::LogicalTime lt, int trans_type); + + td::Result run_transaction_debug(td::Ref msg_root, block::Account* acc, ton::UnixTime utime, + ton::LogicalTime lt, int trans_type); + + td::Result transaction_step_debug(); }; } // namespace emulator diff --git a/emulator/tvm-emulator.hpp b/emulator/tvm-emulator.hpp index acc13627c..95b77a2fd 100644 --- a/emulator/tvm-emulator.hpp +++ b/emulator/tvm-emulator.hpp @@ -5,6 +5,14 @@ namespace emulator { class TvmEmulator { ton::SmartContract smc_; ton::SmartContract::Args args_; + /** + * Instance of VM used in step by step mode, otherwise it is nullptr. + */ + std::unique_ptr vm{}; + /** + * Logger used in step by step mode, otherwise it is nullptr. + */ + std::unique_ptr logger{}; public: using Answer = ton::SmartContract::Answer; @@ -53,6 +61,25 @@ class TvmEmulator { args_.set_debug_enabled(debug_enabled); } + const vm::VmState& get_vm() const { + return *vm; + } + + td::optional debug_step() { + return smc_.debug_step(vm, logger); + } + + int run_get_method_debug(int method_id, td::Ref stack) { + return smc_.run_get_method_debug(args_.set_stack(stack).set_method_id(method_id), vm, logger); + } + + Answer sbs_result() { + auto result = smc_.get_result(*vm, *logger); + vm = nullptr; + logger = nullptr; + return result; + } + Answer run_get_method(int method_id, td::Ref stack) { ton::SmartContract::Args args = args_; return smc_.run_get_method(args.set_stack(stack).set_method_id(method_id)); diff --git a/validator/impl/collator.cpp b/validator/impl/collator.cpp index acf9075b4..01e168c9c 100644 --- a/validator/impl/collator.cpp +++ b/validator/impl/collator.cpp @@ -3192,7 +3192,7 @@ bool Collator::create_ticktock_transaction(const ton::StdSmcAddress& smc_addr, t return fatal_error(td::Status::Error( -666, std::string{"cannot create storage phase of a new transaction for smart contract "} + smc_addr.to_hex())); } - if (!trans->prepare_compute_phase(compute_phase_cfg_)) { + if (!trans->execute_compute_phase(compute_phase_cfg_)) { return fatal_error(td::Status::Error( -666, std::string{"cannot create compute phase of a new transaction for smart contract "} + smc_addr.to_hex())); } @@ -3400,7 +3400,7 @@ td::Result> Collator::impl_crea -669, "cannot create storage phase of a new transaction for smart contract "s + acc->addr.to_hex()); } } - if (!trans->prepare_compute_phase(*compute_phase_cfg)) { + if (!trans->execute_compute_phase(*compute_phase_cfg)) { return td::Status::Error( -669, "cannot create compute phase of a new transaction for smart contract "s + acc->addr.to_hex()); } diff --git a/validator/impl/validate-query.cpp b/validator/impl/validate-query.cpp index ac7f400f3..6ecf7a933 100644 --- a/validator/impl/validate-query.cpp +++ b/validator/impl/validate-query.cpp @@ -5807,7 +5807,7 @@ bool ValidateQuery::check_one_transaction(block::Account& account, ton::LogicalT << addr.to_hex()); } } - if (!trs->prepare_compute_phase(compute_phase_cfg_)) { + if (!trs->execute_compute_phase(compute_phase_cfg_)) { return reject_query(PSTRING() << "cannot re-create compute phase of transaction " << lt << " for smart contract " << addr.to_hex()); }