Skip to content

Instantly share code, notes, and snippets.

@josibake
Created June 23, 2026 10:05
Show Gist options
  • Select an option

  • Save josibake/35ede913894239692d916b01c849e985 to your computer and use it in GitHub Desktop.

Select an option

Save josibake/35ede913894239692d916b01c849e985 to your computer and use it in GitHub Desktop.
its about time we talked about time

problem

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.

proposal

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.

many different kinds of time!

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.

target boundary

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 work

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.

migration path

1. Thread header validation time

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.

2. Add the C ABI options path

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.

3. Wire block-processing time before exposing block options

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.

4. Move presentation to facts

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.

5. Split runtime policy from validation

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment