As of today, several validation paths inside the kernel read time implicitly, i.e. by sampling a node clock. This makes tests depend on global mock time and makes validation state transitions difficult to reason about locally.
The immediate example is btck_set_mock_time(), which wraps SetMockTime() and affects all kernel time reads in the process. A chainstate scoped clock is an improvement over this because it avoids cross chainman interference, but it still hides a runtime input inside a mutable ChainstateManager.
This design proposes a north star API should make runtime inputs visible at the operation boundary:
process(chainstate, block_or_header, runtime_inputs)
-> chainstate' + validation_result + facts
chainstate is still an inout argument. The design goal here is not to make the kernel/validation pure functions everywhere, rather it is meant to require explicit contracts for the inputs that can affect mutation of validation state.
Current kernel code uses time for different reasons. For this reason alone, a generic clock API is not recommended.
| Role | Current examples | Design direction |
|---|---|---|
| Validation input | ContextualCheckBlockHeader() reads NodeClock::now() for the future-time check. TestBlockValidity() reaches the same check. |
Pass a stable operation-time value through the validation path. |
| Time-derived chainstate mutation | UpdateIBDStatus() calls CChain::IsTipRecent(), which reads Now<NodeSeconds>() and may latch m_cached_is_ibd false. |
Until this state is moved or redesigned, pass operation time into the transition that can latch it. |
| Presentation/progress | ProcessNewBlockHeaders(), ReportHeadersPresync(), and GuessVerificationProgress() use NodeClock::now() for logging, progress estimates, and notification fields. |
Expose raw facts from kernel; let node compute progress and synchronization presentation with its own clock. |
| Runtime policy/instrumentation | FlushStateToDisk() uses NodeClock::now() for periodic writes and tracepoint duration. Validation uses SteadyClock::now() for benchmark counters. ReportHeadersPresync() uses MockableSteadyClock::now() for rate limiting. |
Keep separate from validation time. Model as runtime policy, instrumentation, or node/kernel-runtime orchestration. |
| Node policy | Mempool-related GetTime() calls in validation.cpp. |
Do not let this drive the kernel validation API. Mempool behavior belongs above validation. |
These separate classifications are the key design points. A callable clock is too broad for validation and too narrow for runtime policy: validation needs values. Runtime orchestration, however, may want policy/delegates. Presentation/UX code wants facts.
For validation, define an runtime input value:
struct BlockValidationTime {
ValidationSeconds now;
};The value is supplied once at the API boundary and passed through all validation decisions reached by the operation. An explicit-time validation path must not silently fall back to NodeClock::now(), Now<NodeSeconds>(), or global mock time. This requires more thought, but a ValidationSeconds (i.e., domain typed seconds) could be a way of ensuring the code does not errenously include calls to NodeSeconds. Strong types also serve to make context more clear to a reviewer.
For the C ABI, one proposal could be to expose the value through an options object:
typedef struct btck_BlockValidationOptions btck_BlockValidationOptions;
btck_BlockValidationOptions* btck_block_validation_options_create(void);
void btck_block_validation_options_destroy(btck_BlockValidationOptions*);
int btck_block_validation_options_set_current_time(
btck_BlockValidationOptions* options,
int64_t current_time);
btck_BlockValidationState*
btck_chainstate_manager_process_block_header_with_options(
btck_ChainstateManager* chainman,
const btck_BlockHeader* header,
const btck_BlockValidationOptions* options);The existing btck_chainstate_manager_process_block_header() can remain as a convenience wrapper that captures NodeClock::now() once and delegates to the explicit path. Tests and deterministic kernel consumers should use the explicit options path. The convenience is more about minimising need to update all call sites to pass a new option in the API.
| current code | hidden input | first migration step |
|---|---|---|
btck_chainstate_manager_process_block_header() in src/kernel/bitcoinkernel.cpp |
No way to supply validation time; calls ProcessNewBlockHeaders(). |
Add btck_chainstate_manager_process_block_header_with_options(). |
btck_chainstate_manager_process_block() in src/kernel/bitcoinkernel.cpp |
No way to supply operation time; calls ProcessNewBlock(). |
Add options only after block-processing time paths are wired. |
btck_set_mock_time() in src/kernel/bitcoinkernel.{h,cpp} |
Process-global mutable time. | Replace validation tests with explicit inputs; remove or deprecate after migration. |
ContextualCheckBlockHeader() in src/validation.cpp |
NodeClock::now() for the future-time check. |
Take BlockValidationTime. |
AcceptBlockHeader() in src/validation.cpp |
Forwards to ContextualCheckBlockHeader(). |
Take and forward BlockValidationTime. |
ProcessNewBlockHeaders() in src/validation.cpp |
Header validation time plus progress logging time. | Pass validation time into AcceptBlockHeader(); separate progress from validation. |
TestBlockValidity() in src/validation.cpp |
Reaches ContextualCheckBlockHeader(). |
Take or capture BlockValidationTime. |
UpdateIBDStatus() in src/validation.cpp |
May latch m_cached_is_ibd based on current time. |
Take operation time. |
CChain::IsTipRecent() in src/chain.h |
Reads Now<NodeSeconds>(). |
Take now as an argument. |
GuessVerificationProgress() in src/validation.cpp |
Reads NodeClock::now() for presentation. |
Move toward node-computed progress from exposed facts. |
ReportHeadersPresync() in src/validation.cpp |
Uses time for rate limiting and progress logging. | Keep rate limiting as runtime policy; move progress to node. |
FlushStateToDisk() in src/validation.cpp |
Uses time for periodic persistence and tracepoints. | Treat separately as runtime policy/instrumentation. |
Introduce BlockValidationTime internally and pass it through:
ProcessNewBlockHeaders()
-> AcceptBlockHeader()
-> ContextualCheckBlockHeader()
Existing internal callers may preserve current behavior by capturing NodeClock::now() once at their boundary. The important change is that ContextualCheckBlockHeader() stops sampling ambient time.
Add tests showing that the same header is rejected or accepted deterministically under different supplied times.
Add btck_BlockValidationOptions and
btck_chainstate_manager_process_block_header_with_options().
Keep btck_chainstate_manager_process_block_header() as a wrapper for callers that do not need deterministic time. Move kernel tests off btck_set_mock_time and onto the explicit options path.
Do not add btck_chainstate_manager_process_block_with_options() until the reachable time-derived state transitions are explicit.
The first target is:
ProcessNewBlock()
-> ActivateBestChain() / ConnectTip()
-> UpdateIBDStatus()
-> IsTipRecent()
IsTipRecent() should take now. UpdateIBDStatus() should take either
BlockValidationTime or a small operation-input object containing time. After
that is true, a block-processing options API can honestly claim to control the
operation time.
Progress and synchronization state are node presentation concerns. Kernel should expose enough facts for the node to compute them:
- tip/header height, hash, time, and chain work
- minimum-chain-work status
- blockfile indexing state
- chain tx count or other inputs needed by progress estimation.
Existing callbacks can be kept for compatibility during migration, but new interfaces should avoid asking validation code to sample wall-clock time for progress text or UI/RPC state.
FlushStateToDisk(), presync rate limiting, benchmark counters, and tracepoint durations should not use BlockValidationTime. They are kernel runtime policy or instrumentation.
Refactor them independently. A later design can decide whether periodic flush decisions should take an explicit runtime-policy object, a delegated scheduler, or move further toward node orchestration. That decision should not block the validation-time API because it is a different contract.