diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index ba3a57d4422..6d333239be9 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -1488,3 +1490,13 @@ int btck_transaction_check(const btck_Transaction* tx, btck_TxValidationState* v const bool ok = CheckTransaction(*btck_Transaction::get(tx), state); return ok ? 1 : 0; } + +int btck_set_mock_time(int64_t timestamp) +{ + constexpr int64_t max_time{std::numeric_limits::max()}; + if (timestamp < 0 || timestamp > max_time) { + return -1; + } + SetMockTime(std::chrono::seconds{timestamp}); + return 0; +} diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h index fc0eb9e82d3..f6a6355648b 100644 --- a/src/kernel/bitcoinkernel.h +++ b/src/kernel/bitcoinkernel.h @@ -1960,6 +1960,28 @@ BITCOINKERNEL_API void btck_block_header_destroy(btck_BlockHeader* header); ///@} +/** @name Testing + * Functions intended for testing purposes only. + */ +///@{ + +/** + * @brief Override the current time with a fixed timestamp for testing. + * + * Affects all kernel time reads globally. The caller is responsible + * for gating usage (e.g. restricting to regtest) if desired. + * + * The upper bound (4294967295) matches the maximum value of a block header + * timestamp. + * + * @param[in] timestamp Unix epoch seconds, or 0 to restore the system clock. + * @return 0 on success, non-zero if timestamp is outside the + * valid [0, 4294967295] range. + */ +BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_set_mock_time(int64_t timestamp); + +///@} + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h index ff7899d1d69..aa309c6e024 100644 --- a/src/kernel/bitcoinkernel_wrapper.h +++ b/src/kernel/bitcoinkernel_wrapper.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -1355,6 +1356,13 @@ public: } }; +inline void set_mock_time(std::chrono::seconds timestamp) +{ + if (btck_set_mock_time(timestamp.count()) != 0) { + throw std::runtime_error("timestamp out of range"); + } +} + } // namespace btck #endif // BITCOIN_KERNEL_BITCOINKERNEL_WRAPPER_H diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp index 9050719c372..c69a6826842 100644 --- a/src/test/kernel/test_kernel.cpp +++ b/src/test/kernel/test_kernel.cpp @@ -1386,3 +1386,51 @@ BOOST_AUTO_TEST_CASE(btck_transaction_check_tests) "ffffffff00ffffffff000100000000000000000000000000000000000000000000000000000000" "00000000000000ffffffff010000000000000000015100000000"); } + +class KernelMockTime +{ +public: + explicit KernelMockTime(std::chrono::seconds timestamp) { set(timestamp); } + ~KernelMockTime() + { + set_mock_time(std::chrono::seconds{0}); + } + + KernelMockTime(const KernelMockTime&) = delete; + KernelMockTime& operator=(const KernelMockTime&) = delete; + + void set(std::chrono::seconds timestamp) { set_mock_time(timestamp); } +}; + +BOOST_AUTO_TEST_CASE(btck_set_mock_time_tests) +{ + // Out-of-range timestamps throw + BOOST_CHECK_EXCEPTION(set_mock_time(std::chrono::seconds{-1}), std::runtime_error, HasReason("timestamp out of range")); + constexpr std::chrono::seconds max_time{std::numeric_limits::max()}; + BOOST_CHECK_EXCEPTION(set_mock_time(max_time + std::chrono::seconds{1}), std::runtime_error, HasReason("timestamp out of range")); + + // Confirm the mock time actually takes effect by exercising the header future-time check + auto test_directory{TestDirectory{"set_mock_time_test_bitcoin_kernel"}}; + auto notifications{std::make_shared()}; + auto context{create_context(notifications, ChainType::REGTEST)}; + auto chainman{create_chainman( + test_directory, /*reindex=*/false, /*wipe_chainstate=*/false, + /*block_tree_db_in_memory=*/true, /*chainstate_db_in_memory=*/true, context)}; + + Block block{hex_string_to_byte_vec(REGTEST_BLOCK_DATA[0])}; + BlockHeader header{block.GetHeader()}; + const std::chrono::seconds block_time{header.Timestamp()}; + + // With the time set 3h before the header, the kernel must see the header as >2h in the future and reject it + KernelMockTime mock_time{block_time - std::chrono::hours{3}}; + BlockValidationState future_state{chainman->ProcessBlockHeader(header)}; + BOOST_CHECK(future_state.GetValidationMode() == ValidationMode::INVALID); + BOOST_CHECK(future_state.GetBlockValidationResult() == BlockValidationResult::TIME_FUTURE); + + // At the upper bound the header is far in the past and must be accepted; this also + // confirms the future-time check's "now + 2h" computation doesn't overflow when now is at its max. + mock_time.set(max_time); + BlockValidationState ok_state{chainman->ProcessBlockHeader(header)}; + BOOST_CHECK(ok_state.GetValidationMode() == ValidationMode::VALID); + BOOST_CHECK(ok_state.GetBlockValidationResult() == BlockValidationResult::UNSET); +}