kernel: add btck_set_mock_time for testing time-dependent paths

Some kernel paths read the current time (e.g. header validation's future-time check, and the `btck_SynchronizationState` carried by `btck_NotifyBlockTip` / `btck_NotifyHeaderTip` callbacks), which makes them awkward to exercise deterministically in tests. This commit exposes `btck_set_mock_time` as a wrapper over the existing `SetMockTime`, mirroring what the node has via the `setmocktime` RPC.
This commit is contained in:
stringintech
2026-06-09 18:04:05 +03:30
parent 577999c2ce
commit 156f2c6c49
4 changed files with 90 additions and 0 deletions

View File

@@ -35,6 +35,7 @@
#include <util/result.h>
#include <util/signalinterrupt.h>
#include <util/task_runner.h>
#include <util/time.h>
#include <util/translation.h>
#include <validation.h>
#include <validationinterface.h>
@@ -43,6 +44,7 @@
#include <cstring>
#include <exception>
#include <functional>
#include <limits>
#include <list>
#include <memory>
#include <span>
@@ -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<uint32_t>::max()};
if (timestamp < 0 || timestamp > max_time) {
return -1;
}
SetMockTime(std::chrono::seconds{timestamp});
return 0;
}

View File

@@ -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

View File

@@ -8,6 +8,7 @@
#include <kernel/bitcoinkernel.h>
#include <array>
#include <chrono>
#include <exception>
#include <functional>
#include <memory>
@@ -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

View File

@@ -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<uint32_t>::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<TestKernelNotifications>()};
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);
}