From 4971219c2cd5eae7060f17be077b909b6bd4695b Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Sun, 2 Aug 2020 15:48:39 +0000 Subject: [PATCH] blockchain: deterministic UNIX time unlock checks Based on a patch by TheCharlatan --- src/cryptonote_config.h | 1 + src/cryptonote_core/blockchain.cpp | 61 ++++++++++++++++++++++-------- src/cryptonote_core/blockchain.h | 31 +++++++++------ 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index f50ab6a40..9bd1f44eb 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -180,6 +180,7 @@ #define HF_VERSION_EFFECTIVE_SHORT_TERM_MEDIAN_IN_PENALTY 12 #define HF_VERSION_EXACT_COINBASE 13 #define HF_VERSION_CLSAG 13 +#define HF_VERSION_DETERMINISTIC_UNLOCK_TIME 13 #define PER_KB_FEE_QUANTIZATION_DECIMALS 8 diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 82b82534b..93e3ef3bc 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -2267,8 +2267,9 @@ bool Blockchain::get_outs(const COMMAND_RPC_GET_OUTPUTS_BIN::request& req, COMMA MERROR("Unexpected output data size: expected " << req.outputs.size() << ", got " << data.size()); return false; } + const uint8_t hf_version = m_hardfork->get_current_version(); for (const auto &t: data) - res.outs.push_back({t.pubkey, t.commitment, is_tx_spendtime_unlocked(t.unlock_time), t.height, crypto::null_hash}); + res.outs.push_back({t.pubkey, t.commitment, is_tx_spendtime_unlocked(t.unlock_time, hf_version), t.height, crypto::null_hash}); if (req.get_txid) { @@ -2292,7 +2293,8 @@ void Blockchain::get_output_key_mask_unlocked(const uint64_t& amount, const uint key = o_data.pubkey; mask = o_data.commitment; tx_out_index toi = m_db->get_output_tx_and_index(amount, index); - unlocked = is_tx_spendtime_unlocked(m_db->get_tx_unlock_time(toi.first)); + const uint8_t hf_version = m_hardfork->get_current_version(); + unlocked = is_tx_spendtime_unlocked(m_db->get_tx_unlock_time(toi.first), hf_version); } //------------------------------------------------------------------ bool Blockchain::get_output_distribution(uint64_t amount, uint64_t from_height, uint64_t to_height, uint64_t &start_height, std::vector &distribution, uint64_t &base) const @@ -3363,7 +3365,7 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc, // make sure that output being spent matches up correctly with the // signature spending it. - if (!check_tx_input(tx.version, in_to_key, tx_prefix_hash, tx.version == 1 ? tx.signatures[sig_index] : std::vector(), tx.rct_signatures, pubkeys[sig_index], pmax_used_block_height)) + if (!check_tx_input(tx.version, in_to_key, tx_prefix_hash, tx.version == 1 ? tx.signatures[sig_index] : std::vector(), tx.rct_signatures, pubkeys[sig_index], pmax_used_block_height, hf_version)) { MERROR_VER("Failed to check ring signature for tx " << get_transaction_hash(tx) << " vin key with k_image: " << in_to_key.k_image << " sig_index: " << sig_index); if (pmax_used_block_height) // a default value of NULL is used when called from Blockchain::handle_block_to_main_chain() @@ -3756,7 +3758,7 @@ uint64_t Blockchain::get_dynamic_base_fee_estimate(uint64_t grace_blocks) const //------------------------------------------------------------------ // This function checks to see if a tx is unlocked. unlock_time is either // a block index or a unix time. -bool Blockchain::is_tx_spendtime_unlocked(uint64_t unlock_time) const +bool Blockchain::is_tx_spendtime_unlocked(uint64_t unlock_time, uint8_t hf_version) const { LOG_PRINT_L3("Blockchain::" << __func__); if(unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) @@ -3771,7 +3773,7 @@ bool Blockchain::is_tx_spendtime_unlocked(uint64_t unlock_time) const else { //interpret as time - uint64_t current_time = static_cast(time(NULL)); + const uint64_t current_time = hf_version >= HF_VERSION_DETERMINISTIC_UNLOCK_TIME ? get_adjusted_time(m_db->height()) : static_cast(time(NULL)); if(current_time + (get_current_hard_fork_version() < 2 ? CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 : CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2) >= unlock_time) return true; else @@ -3783,7 +3785,7 @@ bool Blockchain::is_tx_spendtime_unlocked(uint64_t unlock_time) const // This function locates all outputs associated with a given input (mixins) // and validates that they exist and are usable. It also checks the ring // signature for each input. -bool Blockchain::check_tx_input(size_t tx_version, const txin_to_key& txin, const crypto::hash& tx_prefix_hash, const std::vector& sig, const rct::rctSig &rct_signatures, std::vector &output_keys, uint64_t* pmax_related_block_height) const +bool Blockchain::check_tx_input(size_t tx_version, const txin_to_key& txin, const crypto::hash& tx_prefix_hash, const std::vector& sig, const rct::rctSig &rct_signatures, std::vector &output_keys, uint64_t* pmax_related_block_height, uint8_t hf_version) const { LOG_PRINT_L3("Blockchain::" << __func__); @@ -3795,14 +3797,15 @@ bool Blockchain::check_tx_input(size_t tx_version, const txin_to_key& txin, cons { std::vector& m_output_keys; const Blockchain& m_bch; - outputs_visitor(std::vector& output_keys, const Blockchain& bch) : - m_output_keys(output_keys), m_bch(bch) + const uint8_t hf_version; + outputs_visitor(std::vector& output_keys, const Blockchain& bch, uint8_t hf_version) : + m_output_keys(output_keys), m_bch(bch), hf_version(hf_version) { } bool handle_output(uint64_t unlock_time, const crypto::public_key &pubkey, const rct::key &commitment) { //check tx unlock time - if (!m_bch.is_tx_spendtime_unlocked(unlock_time)) + if (!m_bch.is_tx_spendtime_unlocked(unlock_time, hf_version)) { MERROR_VER("One of outputs for one of inputs has wrong tx.unlock_time = " << unlock_time); return false; @@ -3821,7 +3824,7 @@ bool Blockchain::check_tx_input(size_t tx_version, const txin_to_key& txin, cons output_keys.clear(); // collect output keys - outputs_visitor vi(output_keys, *this); + outputs_visitor vi(output_keys, *this, hf_version); if (!scan_outputkeys_for_indexes(tx_version, txin, vi, tx_prefix_hash, pmax_related_block_height)) { MERROR_VER("Failed to get output keys for tx with amount = " << print_money(txin.amount) << " and count indexes " << txin.key_offsets.size()); @@ -3840,12 +3843,38 @@ bool Blockchain::check_tx_input(size_t tx_version, const txin_to_key& txin, cons return true; } //------------------------------------------------------------------ -//TODO: Is this intended to do something else? Need to look into the todo there. -uint64_t Blockchain::get_adjusted_time() const +// only works on the main chain +uint64_t Blockchain::get_adjusted_time(uint64_t height) const { LOG_PRINT_L3("Blockchain::" << __func__); - //TODO: add collecting median time - return time(NULL); + + // if not enough blocks, no proper median yet, return current time + if(height < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW) + { + return static_cast(time(NULL)); + } + std::vector timestamps; + + // need most recent 60 blocks, get index of first of those + size_t offset = height - BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW; + timestamps.reserve(height - offset); + for(;offset < height; ++offset) + { + timestamps.push_back(m_db->get_block_timestamp(offset)); + } + uint64_t median_ts = epee::misc_utils::median(timestamps); + + // project the median to match approximately when the block being validated will appear + // the median is calculated from a chunk of past blocks, so we use +1 to offset onto the current block + median_ts += (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1) * DIFFICULTY_TARGET_V2 / 2; + + // project the current block's time based on the previous block's time + // we don't use the current block's time directly to mitigate timestamp manipulation + uint64_t adjusted_current_block_ts = timestamps.back() + DIFFICULTY_TARGET_V2; + + // return minimum of ~current block time and adjusted median time + // we do this since it's better to report a time in the past than a time in the future + return (adjusted_current_block_ts < median_ts ? adjusted_current_block_ts : median_ts); } //------------------------------------------------------------------ //TODO: revisit, has changed a bit on upstream @@ -3873,9 +3902,9 @@ bool Blockchain::check_block_timestamp(std::vector& timestamps, const bool Blockchain::check_block_timestamp(const block& b, uint64_t& median_ts) const { LOG_PRINT_L3("Blockchain::" << __func__); - if(b.timestamp > get_adjusted_time() + CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT) + if(b.timestamp > (uint64_t)time(NULL) + CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT) { - MERROR_VER("Timestamp of block with id: " << get_block_hash(b) << ", " << b.timestamp << ", bigger than adjusted time + 2 hours"); + MERROR_VER("Timestamp of block with id: " << get_block_hash(b) << ", " << b.timestamp << ", bigger than local time + 2 hours"); return false; } diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index 20bd3e5d3..a9b7ca1da 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -1042,6 +1042,21 @@ namespace cryptonote */ void flush_invalid_blocks(); + /** + * @brief get the "adjusted time" + * + * Computes the median timestamp of the previous 60 blocks, projects it + * onto the current block to get an 'adjusted median time' which approximates + * what the current block's timestamp should be. Also projects the previous + * block's timestamp to estimate the current block's timestamp. + * + * Returns the minimum of the two projections, or the current local time on + * the machine if less than 60 blocks are available. + * + * @return current time approximated from chain data + */ + uint64_t get_adjusted_time(uint64_t height) const; + #ifndef IN_UNIT_TESTS private: #endif @@ -1182,10 +1197,11 @@ namespace cryptonote * @param output_keys return-by-reference the public keys of the outputs in the input set * @param rct_signatures the ringCT signatures, which are only valid if tx version > 1 * @param pmax_related_block_height return-by-pointer the height of the most recent block in the input set + * @param hf_version the consensus rules version to use * * @return false if any output is not yet unlocked, or is missing, otherwise true */ - bool check_tx_input(size_t tx_version,const txin_to_key& txin, const crypto::hash& tx_prefix_hash, const std::vector& sig, const rct::rctSig &rct_signatures, std::vector &output_keys, uint64_t* pmax_related_block_height) const; + bool check_tx_input(size_t tx_version,const txin_to_key& txin, const crypto::hash& tx_prefix_hash, const std::vector& sig, const rct::rctSig &rct_signatures, std::vector &output_keys, uint64_t* pmax_related_block_height, uint8_t hf_version) const; /** * @brief validate a transaction's inputs and their keys @@ -1373,10 +1389,11 @@ namespace cryptonote * unlock_time is either a block index or a unix time. * * @param unlock_time the unlock parameter (height or time) + * @param hf_version the consensus rules version to use * * @return true if spendable, otherwise false */ - bool is_tx_spendtime_unlocked(uint64_t unlock_time) const; + bool is_tx_spendtime_unlocked(uint64_t unlock_time, uint8_t hf_version) const; /** * @brief stores an invalid block in a separate container @@ -1437,16 +1454,6 @@ namespace cryptonote bool check_block_timestamp(std::vector& timestamps, const block& b, uint64_t& median_ts) const; bool check_block_timestamp(std::vector& timestamps, const block& b) const { uint64_t median_ts; return check_block_timestamp(timestamps, b, median_ts); } - /** - * @brief get the "adjusted time" - * - * Currently this simply returns the current time according to the - * user's machine. - * - * @return the current time - */ - uint64_t get_adjusted_time() const; - /** * @brief finish an alternate chain's timestamp window from the main chain *