From 2b182186dea4746ce8cf1d2132f56f16e3440ed4 Mon Sep 17 00:00:00 2001 From: SpyCheese Date: Fri, 7 Mar 2025 17:48:07 +0300 Subject: [PATCH] GETEXTRABALANCE instruction, changes in reserve --- crypto/block/mc-config.h | 1 + crypto/block/transaction.cpp | 34 +++++++++++++++-- crypto/fift/lib/Asm.fif | 2 + crypto/vm/tonops.cpp | 72 +++++++++++++++++++++++++++++++++++- crypto/vm/vm.cpp | 19 ++++++---- crypto/vm/vm.h | 17 ++++++++- doc/GlobalVersions.md | 12 +++++- 7 files changed, 143 insertions(+), 14 deletions(-) diff --git a/crypto/block/mc-config.h b/crypto/block/mc-config.h index 98e6a26d..7e06d002 100644 --- a/crypto/block/mc-config.h +++ b/crypto/block/mc-config.h @@ -398,6 +398,7 @@ struct SizeLimitsConfig { td::uint32 max_acc_public_libraries = 256; td::uint32 defer_out_queue_size_limit = 256; td::uint32 max_msg_extra_currencies = 2; + td::uint32 max_reserve_extra_currencies = 2; }; struct CatchainValidatorsConfig { diff --git a/crypto/block/transaction.cpp b/crypto/block/transaction.cpp index 34d23511..f1f9a950 100644 --- a/crypto/block/transaction.cpp +++ b/crypto/block/transaction.cpp @@ -2823,6 +2823,20 @@ int Transaction::try_action_reserve_currency(vm::CellSlice& cs, ActionPhase& ap, } int mode = rec.mode; LOG(INFO) << "in try_action_reserve_currency(" << mode << ")"; + if (cfg.extra_currency_v2) { + CurrencyCollection value; + if (!value.unpack(rec.currency)) { + LOG(DEBUG) << "invalid value in action_reserve_currency"; + return -1; + } + if (!CurrencyCollection::remove_zero_extra_currencies(value.extra, cfg.size_limits.max_reserve_extra_currencies)) { + LOG(DEBUG) << "invalid extra currencies in action_reserve_currency: too many currencies (max " + << cfg.size_limits.max_reserve_extra_currencies << ")"; + // Dict should be valid, since it was checked in t_OutListNode.validate_ref, so error here means limit exceeded + return -1; + } + rec.currency = value.pack(); + } CurrencyCollection reserve, newc; if (!reserve.validate_unpack(std::move(rec.currency))) { LOG(DEBUG) << "cannot parse currency field in action_reserve_currency"; @@ -2832,9 +2846,17 @@ int Transaction::try_action_reserve_currency(vm::CellSlice& cs, ActionPhase& ap, << ", balance=" << ap.remaining_balance.to_str() << ", original balance=" << original_balance.to_str(); if (mode & 4) { if (mode & 8) { - reserve = original_balance - reserve; + if (cfg.extra_currency_v2) { + reserve.grams = original_balance.grams - reserve.grams; + } else { + reserve = original_balance - reserve; + } } else { - reserve += original_balance; + if (cfg.extra_currency_v2) { + reserve.grams += original_balance.grams; + } else { + reserve += original_balance; + } } } else if (mode & 8) { LOG(DEBUG) << "invalid reserve mode " << mode; @@ -2847,7 +2869,7 @@ int Transaction::try_action_reserve_currency(vm::CellSlice& cs, ActionPhase& ap, if (mode & 2) { if (cfg.reserve_extra_enabled) { if (!reserve.clamp(ap.remaining_balance)) { - LOG(DEBUG) << "failed to clamp reserve amount" << mode; + LOG(DEBUG) << "failed to clamp reserve amount " << mode; return -1; } } else { @@ -2868,7 +2890,11 @@ int Transaction::try_action_reserve_currency(vm::CellSlice& cs, ActionPhase& ap, newc.grams = ap.remaining_balance.grams - reserve.grams; if (mode & 1) { // leave only res_grams, reserve everything else - std::swap(newc, reserve); + if (cfg.extra_currency_v2) { + std::swap(newc.grams, reserve.grams); + } else { + std::swap(newc, reserve); + } } // set remaining_balance to new_grams and new_extra ap.remaining_balance = std::move(newc); diff --git a/crypto/fift/lib/Asm.fif b/crypto/fift/lib/Asm.fif index 976093f8..d18bc41b 100644 --- a/crypto/fift/lib/Asm.fif +++ b/crypto/fift/lib/Asm.fif @@ -1327,6 +1327,8 @@ x{F840} @Defop GETGLOBVAR x{F860} @Defop SETGLOBVAR { dup 1 31 @rangechk get_stack(); + auto id = (td::uint32)stack.pop_long_range((1LL << 32) - 1); + + auto tuple = st->get_c7(); + tuple = tuple_index(tuple, 0).as_tuple_range(255); + if (tuple.is_null()) { + throw VmError{Excno::type_chk, "intermediate value is not a tuple"}; + } + tuple = tuple_index(tuple, 7).as_tuple_range(255); // Balance + if (tuple.is_null()) { + throw VmError{Excno::type_chk, "intermediate value is not a tuple"}; + } + auto dict_root = tuple_index(tuple, 1); + if (!dict_root.is_cell() && !dict_root.is_null()) { + throw VmError{Excno::type_chk, "intermediate value is not cell or null"}; + } + + class LocalVmState : public VmStateInterface { + public: + explicit LocalVmState(VmState* st) : st_(st) { + } + ~LocalVmState() override = default; + + Ref load_library(td::ConstBitPtr hash) override { + return st_->load_library(hash); + } + void register_cell_load(const CellHash& cell_hash) override { + auto new_cell = st_->register_cell_load_free(cell_hash); + consume_gas(new_cell ? VmState::cell_load_gas_price : VmState::cell_reload_gas_price); + } + void register_cell_create() override { + // Not expected in this operation + } + int get_global_version() const override { + return st_->get_global_version(); + } + + private: + VmState* st_; + long long remaining = VmState::get_extra_balance_cheap_max_gas_price; + + void consume_gas(long long gas) { + long long consumed = std::min(gas, remaining); + st_->consume_gas(consumed); + remaining -= consumed; + if (remaining == 0) { + st_->consume_free_gas(gas - consumed); + } + } + }; + bool cheap = st->register_get_extra_balance_call(); + LocalVmState local_vm_state{st}; + VmStateInterface::Guard guard{cheap ? local_vm_state : *st}; + + Dictionary dict{dict_root.as_cell(), 32}; + Ref cs = dict.lookup(td::BitArray<32>(id)); + if (cs.is_null()) { + stack.push_smallint(0); + } else { + td::RefInt256 x; + util::load_var_integer_q(cs.write(), x, /* len_bits = */ 5, /* sgnd = */ false, /* quiet = */ false); + stack.push_int(std::move(x)); + } + + return 0; +} + void register_ton_config_ops(OpcodeTable& cp0) { using namespace std::placeholders; cp0.insert(OpcodeInstr::mkfixedrange(0xf820, 0xf823, 16, 4, instr::dump_1c("GETPARAM "), exec_get_var_param)) @@ -391,7 +460,8 @@ void register_ton_config_ops(OpcodeTable& cp0) { .insert(OpcodeInstr::mksimple(0xf840, 16, "GETGLOBVAR", exec_get_global_var)) .insert(OpcodeInstr::mkfixedrange(0xf841, 0xf860, 16, 5, instr::dump_1c_and(31, "GETGLOB "), exec_get_global)) .insert(OpcodeInstr::mksimple(0xf860, 16, "SETGLOBVAR", exec_set_global_var)) - .insert(OpcodeInstr::mkfixedrange(0xf861, 0xf880, 16, 5, instr::dump_1c_and(31, "SETGLOB "), exec_set_global)); + .insert(OpcodeInstr::mkfixedrange(0xf861, 0xf880, 16, 5, instr::dump_1c_and(31, "SETGLOB "), exec_set_global)) + .insert(OpcodeInstr::mksimple(0xf880, 16, "GETEXTRABALANCE", exec_get_extra_currency_balance)->require_version(10)); } static constexpr int randseed_idx = 6; diff --git a/crypto/vm/vm.cpp b/crypto/vm/vm.cpp index 3c1118c6..cecc4208 100644 --- a/crypto/vm/vm.cpp +++ b/crypto/vm/vm.cpp @@ -656,12 +656,12 @@ bool VmState::register_library_collection(Ref lib) { } void VmState::register_cell_load(const CellHash& cell_hash) { - if (cell_load_gas_price == cell_reload_gas_price) { - consume_gas(cell_load_gas_price); - } else { - auto ok = loaded_cells.insert(cell_hash); // check whether this is the first time this cell is loaded - consume_gas(ok.second ? cell_load_gas_price : cell_reload_gas_price); - } + auto new_cell = loaded_cells.insert(cell_hash).second; // check whether this is the first time this cell is loaded + consume_gas(new_cell ? cell_load_gas_price : cell_reload_gas_price); +} + +bool VmState::register_cell_load_free(const CellHash& cell_hash) { + return loaded_cells.insert(cell_hash).second; } void VmState::register_cell_create() { @@ -715,10 +715,13 @@ void VmState::run_child_vm(VmState&& new_state, bool return_data, bool return_ac if (!isolate_gas) { new_state.loaded_cells = std::move(loaded_cells); } else { - consume_gas(std::min(chksgn_counter, chksgn_free_count) * chksgn_gas_price); + consume_gas(free_gas_consumed); chksgn_counter = 0; + get_extra_balance_counter = 0; } new_state.chksgn_counter = chksgn_counter; + new_state.free_gas_consumed = free_gas_consumed; + new_state.get_extra_balance_counter = get_extra_balance_counter; auto new_parent = std::make_unique(); new_parent->return_data = return_data; @@ -743,6 +746,8 @@ void VmState::restore_parent_vm(int res) { loaded_cells = std::move(child_state.loaded_cells); } chksgn_counter = child_state.chksgn_counter; + get_extra_balance_counter = child_state.get_extra_balance_counter; + free_gas_consumed = child_state.free_gas_consumed; VM_LOG(this) << "Child VM finished. res: " << res << ", steps: " << child_state.steps << ", gas: " << child_state.gas_consumed(); diff --git a/crypto/vm/vm.h b/crypto/vm/vm.h index a171ef27..a0ca4a4b 100644 --- a/crypto/vm/vm.h +++ b/crypto/vm/vm.h @@ -103,6 +103,8 @@ class VmState final : public VmStateInterface { td::uint16 max_data_depth = 512; // Default value int global_version{0}; size_t chksgn_counter = 0; + size_t get_extra_balance_counter = 0; + long long free_gas_consumed = 0; std::unique_ptr parent = nullptr; public: @@ -161,7 +163,10 @@ class VmState final : public VmStateInterface { bls_g2_multiexp_coef2_gas_price = 22840, bls_pairing_base_gas_price = 20000, - bls_pairing_element_gas_price = 11800 + bls_pairing_element_gas_price = 11800, + + get_extra_balance_cheap_count = 5, + get_extra_balance_cheap_max_gas_price = 200 }; VmState(); VmState(Ref _code, int global_version, Ref _stack, const GasLimits& _gas, int flags = 0, Ref _data = {}, @@ -214,6 +219,9 @@ class VmState final : public VmStateInterface { consume_stack_gas((unsigned)stk->depth()); } } + void consume_free_gas(long long amount) { + free_gas_consumed += amount; + } GasLimits get_gas_limits() const { return gas; } @@ -226,6 +234,7 @@ class VmState final : public VmStateInterface { Ref load_library( td::ConstBitPtr hash) override; // may throw a dictionary exception; returns nullptr if library is not found void register_cell_load(const CellHash& cell_hash) override; + bool register_cell_load_free(const CellHash& cell_hash); void register_cell_create() override; bool init_cp(int new_cp); bool set_cp(int new_cp); @@ -420,9 +429,15 @@ class VmState final : public VmStateInterface { ++chksgn_counter; if (chksgn_counter > chksgn_free_count) { consume_gas(chksgn_gas_price); + } else { + consume_free_gas(chksgn_gas_price); } } } + bool register_get_extra_balance_call() { + ++get_extra_balance_counter; + return get_extra_balance_counter <= get_extra_balance_cheap_count; + } private: void init_cregs(bool same_c3 = false, bool push_0 = true); diff --git a/doc/GlobalVersions.md b/doc/GlobalVersions.md index 77963e95..fbf25cb3 100644 --- a/doc/GlobalVersions.md +++ b/doc/GlobalVersions.md @@ -3,7 +3,7 @@ Global version is a parameter specified in `ConfigParam 8` ([block.tlb](https:// Various features are enabled depending on the global version. ## Version 4 -New features of version 4 are desctibed in detail in [the documentation](https://docs.ton.org/v3/documentation/tvm/changelog/tvm-upgrade-2023-07). +New features of version 4 are described in detail in [the documentation](https://docs.ton.org/v3/documentation/tvm/changelog/tvm-upgrade-2023-07). ### New TVM instructions * `PREVMCBLOCKS`, `PREVKEYBLOCK` @@ -152,7 +152,17 @@ Example: if the last masterchain block seqno is `19071` then the list contains b - `SENDMSG` does not check the number of extra currencies. - Extra currency dictionary is not counted in the account size and does not affect storage fees. - Accounts with already existing extra currencies will get their sizes recomputed without EC only after modifying `AccountState`. +- Reserve modes `+1`, `+4` and `+8` ("reserve all except", "add original balance" and "negate amount") now only affect TONs, but not extra currencies. +- One reserve action can have at most 2 different extra currencies. Amount of an extra currency in the dictionary can be zero. ### TVM changes - `SENDMSG` calculates messages size and fees without extra currencies, uses new +64 and +128 mode behavior. - `SENDMSG` does not check the number of extra currencies. +- New instruction `GETEXTRABALANCE` (`id - amount`). Takes id of the extra currency (integer in range `0..2^32-1`), returns the amount of this extra currency on the account balance. + - This is equivalent to taking the extra currency dictionary (`BALANCE SECOND`), loading value (`UDICTGET`) and parsing it (`LDVARUINT32`). If `id` is not present in the dictionary, `0` is returned. + - `GETEXTRABALANCE` has special gas cost that allows writing gas-efficient code with predictable gas usage even if there are a lot of different extra currencies. + - The full gas cost of `GETEXTRABALANCE` is `26` (normal instruction cost) plus gas for loading cells (up to `3300` if the dictionary has maximum depth). + - However, the first `5` executions of `GETEXTRABALANCE` cost at most `26+200` gas units. All subsequent executions cost the full price. + - `RUNVM` interacts with this instructions in the following way: + - Without "isolate gas" mode, the child VM shares `GETEXTRABALANCE` counter with the parent vm. + - With "isolate gas" mode, in the beginning of `RUNVM` the parent VM spends full gas for all already executed `GETEXTRABALANCE` and resets the counter. \ No newline at end of file