diff --git a/src/currency_core/currency_basic.h b/src/currency_core/currency_basic.h index 4adbcdb1..347f3fc0 100644 --- a/src/currency_core/currency_basic.h +++ b/src/currency_core/currency_basic.h @@ -563,12 +563,14 @@ namespace currency uint64_t index; crypto::key_image keyimage; uint64_t block_timestamp; + uint64_t stake_unlock_time; //not for serialization uint64_t wallet_index; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(amount) KV_SERIALIZE(index) + KV_SERIALIZE(stake_unlock_time) KV_SERIALIZE(block_timestamp) KV_SERIALIZE_VAL_POD_AS_BLOB_FORCE(keyimage) END_KV_SERIALIZE_MAP() diff --git a/src/currency_core/currency_format_utils.cpp b/src/currency_core/currency_format_utils.cpp index 1078d13c..4b8d1a3a 100644 --- a/src/currency_core/currency_format_utils.cpp +++ b/src/currency_core/currency_format_utils.cpp @@ -110,18 +110,39 @@ namespace currency std::vector destinations; for (auto a : out_amounts) { - tx_destination_entry de; + tx_destination_entry de = AUTO_VAL_INIT(de); de.addr.push_back(miner_address); de.amount = a; destinations.push_back(de); } if (pos) - destinations.push_back(tx_destination_entry(pe.amount, stakeholder_address)); + destinations.push_back(tx_destination_entry(pe.amount, stakeholder_address, pe.stake_unlock_time)); return construct_miner_tx(height, median_size, already_generated_coins, current_block_size, fee, destinations, tx, extra_nonce, max_outs, pos, pe); } //------------------------------------------------------------------ + bool apply_unlock_time(const std::vector& destinations, transaction& tx) + { + currency::etc_tx_details_unlock_time2 unlock_time2 = AUTO_VAL_INIT(unlock_time2); + unlock_time2.unlock_time_array.resize(destinations.size()); + bool found_unlock_time = false; + for (size_t i = 0; i != unlock_time2.unlock_time_array.size(); i++) + { + if (destinations[i].unlock_time) + { + found_unlock_time = true; + unlock_time2.unlock_time_array[i] = destinations[i].unlock_time; + } + } + if (found_unlock_time) + { + tx.extra.push_back(unlock_time2); + } + + return true; + } + //------------------------------------------------------------------ bool construct_miner_tx(size_t height, size_t median_size, const boost::multiprecision::uint128_t& already_generated_coins, size_t current_block_size, uint64_t fee, @@ -144,8 +165,7 @@ namespace currency if (!add_tx_extra_userdata(tx, extra_nonce)) return false; - //we always add extra_padding with 2 bytes length to make possible for get_block_template to adjust cumulative size - tx.extra.push_back(extra_padding()); + txin_gen in; in.height = height; @@ -172,6 +192,11 @@ namespace currency no++; } + //at this moment we do apply_unlock_time only for coin_base transactions + apply_unlock_time(destinations, tx); + + //we always add extra_padding with 2 bytes length to make possible for get_block_template to adjust cumulative size + tx.extra.push_back(extra_padding()); tx.version = CURRENT_TRANSACTION_VERSION; set_tx_unlock_time(tx, height + CURRENCY_MINED_MONEY_UNLOCK_WINDOW); diff --git a/src/currency_core/currency_format_utils_transactions.cpp b/src/currency_core/currency_format_utils_transactions.cpp index 6f8a988b..fe9f6bf2 100644 --- a/src/currency_core/currency_format_utils_transactions.cpp +++ b/src/currency_core/currency_format_utils_transactions.cpp @@ -31,6 +31,31 @@ namespace currency return false; // 0 means it never expires return expiration_time <= expiration_ts_median + TX_EXPIRATION_MEDIAN_SHIFT; } + //--------------------------------------------------------------- + uint64_t get_tx_max_unlock_time(const transaction& tx) + { + // etc_tx_details_expiration_time have priority over etc_tx_details_expiration_time2 + uint64_t v = get_tx_x_detail(tx); + if (v) + return v; + + etc_tx_details_unlock_time2 ut2 = AUTO_VAL_INIT(ut2); + get_type_in_variant_container(tx.extra, ut2); + if (!ut2.unlock_time_array.size()) + return 0; + + uint64_t max_unlock_time = 0; + CHECK_AND_ASSERT_THROW_MES(ut2.unlock_time_array.size() == tx.vout.size(), "unlock_time_array.size=" << ut2.unlock_time_array.size() + << " is not the same as tx.vout.size =" << tx.vout.size() << " in tx: " << get_transaction_hash(tx)); + for (size_t i = 0; i != tx.vout.size(); i++) + { + if (ut2.unlock_time_array[i] > max_unlock_time) + max_unlock_time = ut2.unlock_time_array[i]; + } + + return max_unlock_time; + } + //--------------------------------------------------------------- uint64_t get_tx_unlock_time(const transaction& tx, uint64_t o_i) { diff --git a/src/currency_core/currency_format_utils_transactions.h b/src/currency_core/currency_format_utils_transactions.h index 197802b4..82a94fa1 100644 --- a/src/currency_core/currency_format_utils_transactions.h +++ b/src/currency_core/currency_format_utils_transactions.h @@ -51,17 +51,20 @@ namespace currency std::list addr; //destination address, in case of 1 address - txout_to_key, in case of more - txout_multisig size_t minimum_sigs; // if txout_multisig: minimum signatures that are required to spend this output (minimum_sigs <= addr.size()) IF txout_to_key - not used uint64_t amount_to_provide; //amount money that provided by initial creator of tx, used with partially created transactions + uint64_t unlock_time; - tx_destination_entry() : amount(0), minimum_sigs(0), amount_to_provide(0) {} - tx_destination_entry(uint64_t a, const account_public_address& ad) : amount(a), addr(1, ad), minimum_sigs(0), amount_to_provide(0) {} - tx_destination_entry(uint64_t a, const std::list& addr) : amount(a), addr(addr), minimum_sigs(addr.size()), amount_to_provide(0) {} + tx_destination_entry() : amount(0), minimum_sigs(0), amount_to_provide(0), unlock_time(0){} + tx_destination_entry(uint64_t a, const account_public_address& ad) : amount(a), addr(1, ad), minimum_sigs(0), amount_to_provide(0), unlock_time(0){} + tx_destination_entry(uint64_t a, const account_public_address& ad, uint64_t unlock_time) : amount(a), addr(1, ad), minimum_sigs(0), amount_to_provide(0) {} + tx_destination_entry(uint64_t a, const std::list& addr) : amount(a), addr(addr), minimum_sigs(addr.size()), amount_to_provide(0), unlock_time(0){} BEGIN_SERIALIZE_OBJECT() FIELD(amount) FIELD(addr) FIELD(minimum_sigs) FIELD(amount_to_provide) - END_SERIALIZE() + FIELD(unlock_time) + END_SERIALIZE() }; template @@ -80,6 +83,7 @@ namespace currency } uint64_t get_tx_unlock_time(const transaction& tx, uint64_t o_i); + uint64_t get_tx_max_unlock_time(const transaction& tx); bool get_tx_max_min_unlock_time(const transaction& tx, uint64_t& max_unlock_time, uint64_t& min_unlock_time); inline uint64_t get_tx_flags(const transaction& tx) { return get_tx_x_detail(tx); } inline uint64_t get_tx_expiration_time(const transaction& tx) {return get_tx_x_detail(tx); } @@ -88,6 +92,7 @@ namespace currency inline void set_tx_expiration_time(transaction& tx, uint64_t v) { set_tx_x_detail(tx, v); } account_public_address get_crypt_address_from_destinations(const account_keys& sender_account_keys, const std::vector& destinations); + bool is_tx_expired(const transaction& tx, uint64_t expiration_ts_median); void get_transaction_prefix_hash(const transaction_prefix& tx, crypto::hash& h); diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index d78b0a00..851d5406 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -798,6 +798,7 @@ namespace currency currency::pos_entry pe = AUTO_VAL_INIT(pe); pe.amount = req.pos_amount; pe.index = req.pos_index; + pe.stake_unlock_time = req.stake_unlock_time; //pe.keyimage key image will be set in the wallet //pe.wallet_index is not included in serialization map, TODO: refactoring here diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index 01da849a..5e3c19dd 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -778,6 +778,7 @@ namespace currency bool pos_block; //is pos block uint64_t pos_amount; // uint64_t pos_index; // + uint64_t stake_unlock_time; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(extra_text) @@ -786,6 +787,7 @@ namespace currency KV_SERIALIZE(pos_block) KV_SERIALIZE(pos_amount) KV_SERIALIZE(pos_index) + KV_SERIALIZE(stake_unlock_time) END_KV_SERIALIZE_MAP() }; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 25347722..ca011d3c 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -33,6 +33,35 @@ using namespace currency; ENABLE_CHANNEL_BY_DEFAULT("wallet") namespace tools { + + //--------------------------------------------------------------- + uint64_t wallet2::get_max_unlock_time_from_receive_indices(const currency::transaction& tx, const tools::money_transfer2_details& td) + { + uint64_t max_unlock_time = 0; + // etc_tx_details_expiration_time have priority over etc_tx_details_expiration_time2 + uint64_t major_unlock_time = get_tx_x_detail(tx); + if (major_unlock_time) + return major_unlock_time; + + etc_tx_details_unlock_time2 ut2 = AUTO_VAL_INIT(ut2); + get_type_in_variant_container(tx.extra, ut2); + if (!ut2.unlock_time_array.size()) + return 0; + + CHECK_AND_ASSERT_THROW_MES(ut2.unlock_time_array.size() == tx.vout.size(), "Internal error: wrong tx transfer details: ut2.unlock_time_array.size()" << ut2.unlock_time_array.size() << " is not equal transaction outputs vector size=" << tx.vout.size()); + + for (auto ri : td.receive_indices) + { + CHECK_AND_ASSERT_THROW_MES(ri < tx.vout.size(), "Internal error: wrong tx transfer details: reciev index=" << ri << " is greater than transaction outputs vector " << tx.vout.size()); + if (tx.vout[ri].target.type() == typeid(currency::txout_to_key)) + { + //update unlock_time if needed + if (ut2.unlock_time_array[ri] > max_unlock_time) + max_unlock_time = ut2.unlock_time_array[ri]; + } + } + return max_unlock_time; + } //---------------------------------------------------------------------------------------------------- void wallet2::fill_transfer_details(const currency::transaction& tx, const tools::money_transfer2_details& td, tools::wallet_rpc::wallet_transfer_info_details& res_td) const { @@ -48,7 +77,9 @@ void wallet2::fill_transfer_details(const currency::transaction& tx, const tools { WLT_CHECK_AND_ASSERT_MES(ri < tx.vout.size(), void(), "Internal error: wrong tx transfer details: reciev index=" << ri << " is greater than transaction outputs vector " << tx.vout.size()); if (tx.vout[ri].target.type() == typeid(currency::txout_to_key)) + { res_td.rcv.push_back(tx.vout[ri].amount); + } } } //---------------------------------------------------------------------------------------------------- @@ -258,8 +289,14 @@ void wallet2::process_new_transaction(const currency::transaction& tx, uint64_t i++; } - //check for transaction income + /* + collect unlock_time from every output that transfered coins to this account and use maximum of + all values m_payments entry, use this strict policy is required to protect exchanges from being feeded with + useless outputs + */ + uint64_t max_out_unlock_time = 0; + //check for transaction income crypto::key_derivation derivation = AUTO_VAL_INIT(derivation); r = lookup_acc_outs(m_account.get_keys(), tx, tx_pub_key, outs, tx_money_got_in_outs, derivation); THROW_IF_TRUE_WALLET_EX(!r, error::acc_outs_lookup_error, tx, tx_pub_key, m_account.get_keys()); @@ -355,6 +392,10 @@ void wallet2::process_new_transaction(const currency::transaction& tx, uint64_t if (td.m_key_image != currency::null_ki) m_key_images[td.m_key_image] = transfer_index; add_transfer_to_transfers_cache(tx.vout[o].amount, transfer_index); + + if (max_out_unlock_time < get_tx_unlock_time(tx, o)) + max_out_unlock_time = get_tx_unlock_time(tx, o); + WLT_LOG_L0("Received money, transfer #" << transfer_index << ", amount: " << print_money(td.amount()) << ", with tx: " << get_transaction_hash(tx) << ", at height " << height); } else if (tx.vout[o].target.type() == typeid(txout_multisig)) @@ -379,7 +420,7 @@ void wallet2::process_new_transaction(const currency::transaction& tx, uint64_t payment.m_tx_hash = currency::get_transaction_hash(tx); payment.m_amount = received; payment.m_block_height = height; - payment.m_unlock_time = currency::get_tx_unlock_time(tx); + payment.m_unlock_time = max_out_unlock_time; m_payments.emplace(payment_id, payment); WLT_LOG_L2("Payment found, id (hex): " << epee::string_tools::buff_to_hex_nodelimer(payment_id) << ", tx: " << payment.m_tx_hash << ", amount: " << print_money_brief(payment.m_amount)); } @@ -919,9 +960,9 @@ void wallet2::prepare_wti(wallet_rpc::wallet_transfer_info& wti, uint64_t height wti.amount = amount; wti.height = height; fill_transfer_details(tx, td, wti.td); + wti.unlock_time = get_max_unlock_time_from_receive_indices(tx, td); wti.timestamp = timestamp; wti.fee = currency::is_coinbase(tx) ? 0:currency::get_tx_fee(tx); - wti.unlock_time = get_tx_unlock_time(tx); wti.tx_blob_size = static_cast(currency::get_object_blobsize(wti.tx)); wti.tx_hash = currency::get_transaction_hash(tx); wti.is_service = currency::is_service_tx(tx); @@ -2398,15 +2439,16 @@ bool wallet2::get_transfer_address(const std::string& adr_str, currency::account return m_core_proxy->get_transfer_address(adr_str, addr, payment_id); } //---------------------------------------------------------------------------------------------------- -bool wallet2::is_transfer_okay_for_pos(const transfer_details& tr) +bool wallet2::is_transfer_okay_for_pos(const transfer_details& tr, uint64_t& stake_unlock_time) { if (!tr.is_spendable()) return false; //blockchain conditions - if (!is_transfer_unlocked(tr)) + if (!is_transfer_unlocked(tr, true, stake_unlock_time)) return false; + //prevent staking of after-last-pow-coins if (m_blockchain.size() - tr.m_ptx_wallet_info->m_block_height <= m_core_runtime_config.min_coinstake_age) return false; @@ -2436,13 +2478,15 @@ bool wallet2::get_pos_entries(currency::COMMAND_RPC_SCAN_POS::request& req) for (size_t i = 0; i != m_transfers.size(); i++) { auto& tr = m_transfers[i]; - if (!is_transfer_okay_for_pos(tr)) + uint64_t stake_unlock_time = 0; + if (!is_transfer_okay_for_pos(tr, stake_unlock_time)) continue; currency::pos_entry pe = AUTO_VAL_INIT(pe); pe.amount = tr.amount(); pe.index = tr.m_global_output_index; pe.keyimage = tr.m_key_image; pe.wallet_index = i; + pe.stake_unlock_time = stake_unlock_time; pe.block_timestamp = tr.m_ptx_wallet_info->m_block_timestamp; req.pos_entries.push_back(pe); } @@ -2619,6 +2663,7 @@ bool wallet2::build_minted_block(const currency::COMMAND_RPC_SCAN_POS::request& tmpl_req.pos_amount = req.pos_entries[rsp.index].amount; tmpl_req.pos_index = req.pos_entries[rsp.index].index; tmpl_req.extra_text = m_miner_text_info; + tmpl_req.stake_unlock_time = req.pos_entries[rsp.index].stake_unlock_time; m_core_proxy->call_COMMAND_RPC_GETBLOCKTEMPLATE(tmpl_req, tmpl_rsp); WLT_CHECK_AND_ASSERT_MES(tmpl_rsp.status == CORE_RPC_STATUS_OK, false, "Failed to create block template after kernel hash found!"); @@ -2701,16 +2746,32 @@ currency::core_runtime_config& wallet2::get_core_runtime_config() } //---------------------------------------------------------------------------------------------------- bool wallet2::is_transfer_unlocked(const transfer_details& td) const +{ + uint64_t stub = 0; + return is_transfer_unlocked(td, false, stub); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::is_transfer_unlocked(const transfer_details& td, bool for_pos_mining, uint64_t& stake_lock_time) const { if (td.m_flags&WALLET_TRANSFER_DETAIL_FLAG_BLOCKED) return false; - if (!currency::is_tx_spendtime_unlocked(get_tx_unlock_time(td.m_ptx_wallet_info->m_tx), m_blockchain.size(), m_core_runtime_config.get_core_time())) + if (td.m_ptx_wallet_info->m_block_height + WALLET_DEFAULT_TX_SPENDABLE_AGE > m_blockchain.size()) return false; - if(td.m_ptx_wallet_info->m_block_height + WALLET_DEFAULT_TX_SPENDABLE_AGE > m_blockchain.size()) - return false; + + uint64_t unlock_time = get_tx_unlock_time(td.m_ptx_wallet_info->m_tx, td.m_internal_output_index); + if (for_pos_mining && m_blockchain.size() > m_core_runtime_config.hard_fork1_starts_after_height) + { + //allowed of staking locked coins with + stake_lock_time = unlock_time; + } + else + { + if (!currency::is_tx_spendtime_unlocked(unlock_time, m_blockchain.size(), m_core_runtime_config.get_core_time())) + return false; + } return true; } //---------------------------------------------------------------------------------------------------- diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index ccd60df7..815d7129 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -683,6 +683,7 @@ namespace tools bool build_minted_block(const currency::COMMAND_RPC_SCAN_POS::request& req, const currency::COMMAND_RPC_SCAN_POS::response& rsp, const currency::account_public_address& miner_address, uint64_t new_block_expected_height = UINT64_MAX); bool reset_history(); bool is_transfer_unlocked(const transfer_details& td) const; + bool is_transfer_unlocked(const transfer_details& td, bool for_pos_mining, uint64_t& stake_lock_time) const; void get_mining_history(wallet_rpc::mining_history& hist); void set_core_runtime_config(const currency::core_runtime_config& pc); currency::core_runtime_config& get_core_runtime_config(); @@ -720,7 +721,7 @@ namespace tools void finalize_transaction(const finalize_tx_param& ftp, currency::transaction& tx, crypto::secret_key& tx_key, bool broadcast_tx); std::string get_log_prefix() const { return m_log_prefix; } - + static uint64_t get_max_unlock_time_from_receive_indices(const currency::transaction& tx, const tools::money_transfer2_details& td); private: void add_transfers_to_expiration_list(const std::vector& selected_transfers, uint64_t expiration, uint64_t change_amount, const crypto::hash& related_tx_id); void remove_transfer_from_expiration_list(uint64_t transfer_index); @@ -768,7 +769,7 @@ private: std::string get_alias_for_address(const std::string& addr); static bool build_kernel(const currency::pos_entry& pe, const currency::stake_modifier_type& stake_modifier, currency::stake_kernel& kernel, uint64_t& coindays_weight, uint64_t timestamp); bool is_connected_to_net(); - bool is_transfer_okay_for_pos(const transfer_details& tr); + bool is_transfer_okay_for_pos(const transfer_details& tr, uint64_t& stake_unlock_time); bool scan_unconfirmed_outdate_tx(); const currency::transaction& get_transaction_by_id(const crypto::hash& tx_hash); void rise_on_transfer2(const wallet_rpc::wallet_transfer_info& wti); @@ -815,6 +816,7 @@ private: const std::vector& decrypted_items, crypto::hash& ms_id, bc_services::contract_private_details& cpd, const currency::transaction& proposal_template_tx); + void fill_transfer_details(const currency::transaction& tx, const tools::money_transfer2_details& td, tools::wallet_rpc::wallet_transfer_info_details& res_td) const; void print_source_entry(const currency::tx_source_entry& src) const; @@ -954,7 +956,7 @@ namespace boost //do not store this items in the file since it's quite easy to restore it from original tx if (Archive::is_loading::value) { - x.unlock_time = currency::get_tx_unlock_time(x.tx); + x.unlock_time = wallet::get_max_unlock_time_from_receive_indices(x.tx, x.td) x.is_service = currency::is_service_tx(x.tx); x.is_mixing = currency::is_mixin_tx(x.tx); x.is_mining = currency::is_coinbase(x.tx); diff --git a/src/wallet/wallet2_escrow.cpp b/src/wallet/wallet2_escrow.cpp index 4fb9db35..9d264508 100644 --- a/src/wallet/wallet2_escrow.cpp +++ b/src/wallet/wallet2_escrow.cpp @@ -26,7 +26,7 @@ bool wallet2::validate_escrow_proposal(const wallet_rpc::wallet_transfer_info& w // I. validate escrow proposal tx const transaction& escrow_proposal_tx = wti.tx; - uint64_t escrow_proposal_tx_unlock_time = get_tx_unlock_time(escrow_proposal_tx); + uint64_t escrow_proposal_tx_unlock_time = get_tx_max_unlock_time(escrow_proposal_tx); LOC_CHK(escrow_proposal_tx_unlock_time == 0, "proposal tx unlock time is non-zero: " << escrow_proposal_tx_unlock_time); uint64_t escrow_proposal_expiration_time = get_tx_expiration_time(escrow_proposal_tx); @@ -66,7 +66,7 @@ bool wallet2::validate_escrow_proposal(const wallet_rpc::wallet_transfer_info& w uint64_t template_expiration_time = get_tx_expiration_time(prop.tx_template); LOC_CHK(template_expiration_time != 0, "template has no expiration time"); - uint64_t template_unlock_time = get_tx_unlock_time(prop.tx_template); + uint64_t template_unlock_time = get_tx_max_unlock_time(prop.tx_template); LOC_CHK(template_unlock_time == 0, "template has non-zero unlock time: " << template_unlock_time); // (3/5) outputs @@ -156,7 +156,7 @@ bool wallet2::validate_escrow_release(const transaction& tx, bool release_type_n uint64_t expiration_time = get_tx_expiration_time(tx); LOC_CHK(expiration_time == 0, "tx has non-zero expiration time: " << expiration_time); - uint64_t unlock_time = get_tx_unlock_time(tx); + uint64_t unlock_time = get_tx_max_unlock_time(tx); LOC_CHK(unlock_time == 0, "tx has non-zero unlock time: " << unlock_time); tx_service_attachment tsa = AUTO_VAL_INIT(tsa); @@ -285,7 +285,7 @@ bool wallet2::validate_escrow_contract(const wallet_rpc::wallet_transfer_info& w uint64_t tx_expiration_time = get_tx_expiration_time(wti.tx); LOC_CHK(tx_expiration_time != 0, "no or zero expiration time specified"); - uint64_t tx_unlock_time = get_tx_unlock_time(wti.tx); + uint64_t tx_unlock_time = get_tx_max_unlock_time(wti.tx); LOC_CHK(tx_unlock_time == 0, "non-zero unlock time: " << tx_unlock_time); #undef LOC_CHK @@ -341,7 +341,7 @@ bool wallet2::validate_escrow_cancel_release(const currency::transaction& tx, co uint64_t expiration_time = get_tx_expiration_time(tx); LOC_CHK(expiration_time != 0, "tx has zero or not specified expiration time"); - uint64_t unlock_time = get_tx_unlock_time(tx); + uint64_t unlock_time = get_tx_max_unlock_time(tx); LOC_CHK(unlock_time == 0, "tx has non-zero unlock time: " << unlock_time); tx_service_attachment tsa = AUTO_VAL_INIT(tsa); @@ -430,7 +430,7 @@ bool wallet2::validate_escrow_cancel_proposal(const wallet_rpc::wallet_transfer_ uint64_t flags = get_tx_flags(wti.tx); LOC_CHK(flags == 0, "invalid tx flags: " << flags); - uint64_t unlock_time = get_tx_unlock_time(cancellation_request_tx); + uint64_t unlock_time = get_tx_max_unlock_time(cancellation_request_tx); LOC_CHK(unlock_time == 0, "invalid unlock time: " << unlock_time); uint64_t expiration_time = get_tx_expiration_time(cancellation_request_tx);