Skip to content

backport: make adjusted time type-safe (bitcoin#25786)#7459

Open
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:codex/backport-25786
Open

backport: make adjusted time type-safe (bitcoin#25786)#7459
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:codex/backport-25786

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Backports bitcoin/bitcoin#25786, which makes adjusted network time type-safe by returning a NodeClock::time_point and using chrono types for time arithmetic.

Dash has additional adjusted-time consumers in CoinJoin, governance, spork, LLMQ, and RPC code. Those consumers must be converted without changing timestamps that form part of serialized, signed, hashed, or RPC-facing data.

What was done?

  • Backported both upstream commits from refactor: Make adjusted time type safe bitcoin/bitcoin#25786 as one commit, preserving the upstream changes to block/index time helpers, consensus target spacing, adjusted-time callbacks, validation, mining, and net processing.
  • Preserved Dash-specific net-processing behavior, including nMaxTipAge and the existing relay guard.
  • Added a separate Dash adaptation commit for nearby CoinJoin, governance, spork, LLMQ, RPC, and unit-test code.
  • Kept serialized, signed, hashed, and RPC timestamp fields as integer Unix seconds, with explicit conversions at those boundaries.
  • Used NodeClock, NodeSeconds, and chrono durations for internal comparisons and arithmetic. No GetTime() workaround was introduced.
  • Reviewed the complete two-commit unit against upstream with the backport-review workflow; no missing prerequisites or incorrectly resolved upstream hunks were found.

How Has This Been Tested?

Tested on macOS arm64 (aarch64-apple-darwin25.3.0) with:

./configure --without-gui --disable-bench --disable-fuzz-binary --disable-hardening
make -C src -j"$(sysctl -n hw.ncpu)" test/test_dash

The following focused unit-test suites passed:

  • coinjoin_inouts_tests
  • coinjoin_queue_tests
  • timedata_tests
  • governance_validators_tests
  • governance_vote_wire_tests
  • governance_inv_tests
  • miner_tests
  • validation_chainstatemanager_tests
  • llmq_dkg_tests

git diff --check upstream/develop..HEAD also passed.

Breaking Changes

None. Serialized, signed, hashed, and RPC timestamp representations remain integer Unix seconds.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation (not applicable: internal type-safety refactor with no user-facing behavior change)
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

@PastaPastaPasta
PastaPastaPasta force-pushed the codex/backport-25786 branch 2 times, most recently from e2693d1 to f00fdbb Compare July 14, 2026 06:33
Upstream commits:

fa3be79 Add time helpers
eeee5ad Make adjusted time type safe
@PastaPastaPasta
PastaPastaPasta force-pushed the codex/backport-25786 branch 3 times, most recently from 03b9dcf to a1df653 Compare July 14, 2026 06:55
@PastaPastaPasta
PastaPastaPasta marked this pull request as ready for review July 14, 2026 07:08
@thepastaclaw

thepastaclaw commented Jul 14, 2026

Copy link
Copy Markdown

⛔ Blockers found — Sonnet deferred (commit 40e554c)
Canonical validated blockers: 1

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Potential PR merge conflicts

This is advisory only. It does not block CI, but it marks PRs that will likely need a rebase depending on merge order.

If this PR merges first

These open PRs will likely need a rebase:

If these PRs merge first

This PR will likely need a rebase:

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5c78a034-10be-4d8a-95f5-f736a3fe9756

📥 Commits

Reviewing files that changed from the base of the PR and between f5e834c and 40e554c.

📒 Files selected for processing (20)
  • src/Makefile.test.include
  • src/coinjoin/coinjoin.cpp
  • src/coinjoin/coinjoin.h
  • src/governance/governance.cpp
  • src/governance/governance.h
  • src/governance/object.cpp
  • src/governance/object.h
  • src/governance/vote.cpp
  • src/governance/vote.h
  • src/llmq/debug.cpp
  • src/llmq/debug.h
  • src/rpc/governance.cpp
  • src/spork.cpp
  • src/spork.h
  • src/test/coinjoin_inouts_tests.cpp
  • src/test/coinjoin_queue_tests.cpp
  • src/test/governance_validators_tests.cpp
  • src/test/governance_vote_wire_tests.cpp
  • src/test/spork_tests.cpp
  • test/util/data/non-backported.txt
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/test/coinjoin_inouts_tests.cpp
  • src/rpc/governance.cpp
  • src/governance/vote.cpp
  • src/test/governance_vote_wire_tests.cpp
  • src/governance/vote.h
  • src/spork.h
  • src/governance/object.cpp
  • src/llmq/debug.h
  • src/spork.cpp
  • src/coinjoin/coinjoin.cpp
  • src/llmq/debug.cpp
  • src/coinjoin/coinjoin.h
  • src/governance/object.h
  • src/governance/governance.h
  • src/governance/governance.cpp
  • src/test/coinjoin_queue_tests.cpp

Walkthrough

The pull request migrates node time handling from integer Unix timestamps to NodeClock::time_point, NodeSeconds, and chrono durations across adjusted-time callbacks, validation, block processing, networking, CoinJoin, governance, LLMQ debugging, and spork checks. Governance serialization retains historical integer-second disk encoding, with tests covering timestamp conversion, wire compatibility, and extreme values.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • dashpay/dash#7330: Both changes modify governance vote processing and time-based rate limiting.
  • dashpay/dash#7361: Both changes update chainstate adjusted-time callback interfaces.
  • dashpay/dash#7387: Both changes modify governance propagation timing around RELIABLE_PROPAGATION_TIME.

Suggested reviewers: thepastaclaw, udjinm6, knst

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: backporting a type-safe adjusted-time refactor.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the backport and Dash-specific adaptations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The backport has no missing-prerequisite gap, but the Dash adaptation introduces signed-overflow undefined behavior when extreme serialized timestamps are compared with native-resolution NodeClock values. The intermediate upstream commit is also non-buildable until the following Dash adaptation commit, violating the repository's atomic-commit requirement.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (failed), gpt-5.6-sol — dash-core-commit-history (failed), gpt-5.6-sol — dash-core-commit-history (completed), gpt-5.6-sol — backport-reviewer (failed), gpt-5.6-sol — backport-reviewer (failed), gpt-5.6-sol — backport-reviewer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 3 blocking

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/spork.cpp`:
- [BLOCKING] src/spork.cpp:128: Peer-supplied spork timestamps overflow before authentication
  TimeSigned() represents the serialized int64_t timestamp as NodeSeconds, while GetAdjustedTime() uses NodeClock's finer native duration. Chrono comparison converts both operands to that finer duration, overflowing when an extreme timestamp such as INT64_MAX is multiplied by the microsecond or nanosecond scale. This happens before signer recovery or authentication in the SPORK network handler, so an unauthenticated peer can reach it. A minimal build using the project's debug -ftrapv setting terminated with exit 133; optimized builds retain signed-overflow undefined behavior. Normalize adjusted time to seconds before comparing. The activation comparison at line 229 has the same unsafe conversion for signed SporkValue timestamps.

In `src/governance/vote.cpp`:
- [BLOCKING] src/governance/vote.cpp:161-162: Extreme governance timestamps overflow chrono comparisons
  CGovernanceVote::Time() wraps the signed 64-bit wire timestamp in NodeSeconds, but max_time retains NodeClock's finer native duration. Comparing them implicitly scales the wire value and overflows for values such as INT64_MAX. A peer can reach this before signature verification by using a known governance parent, valid signal fields, and a public masternode outpoint. The same PR-introduced conversion occurs before governance-object signature verification in MasternodeRateCheck at governance.cpp lines 742-749, and RPC/GUI proposal validation at object.cpp line 147 also accepts an int64_t epoch. Cast adjusted time to NodeSeconds at each serialized-time comparison and add extreme-value coverage.

In `<commit:ccb8bcebfca>`:
- [BLOCKING] <commit:ccb8bcebfca>:1: Squash the Dash adaptation into the type-change commit
  Commit ccb8bcebfca changes GetAdjustedTime() to return NodeClock::time_point while leaving Dash callers that assign it directly to int64_t, use it as an int64_t default argument, or add integer values to it. Examples at that revision include governance.cpp:286, coinjoin.h:220, vote.cpp:94, and rpc/governance.cpp:120; these expressions have no valid implicit chrono-to-integer conversion, so the intermediate revision cannot compile. The following commit fixes those consumers, but the repository guide requires every commit to build independently. Fold the required Dash adaptations into the type-change commit while retaining the upstream identifiers and provenance.

Comment thread src/spork.cpp Outdated
std::optional<CKeyID> CSporkManager::GetValidSporkSigner(const CSporkMessage& spork) const
{
if (spork.nTimeSigned > GetAdjustedTime() + 2 * 60 * 60) {
if (spork.TimeSigned() > GetAdjustedTime() + 2h) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Peer-supplied spork timestamps overflow before authentication

TimeSigned() represents the serialized int64_t timestamp as NodeSeconds, while GetAdjustedTime() uses NodeClock's finer native duration. Chrono comparison converts both operands to that finer duration, overflowing when an extreme timestamp such as INT64_MAX is multiplied by the microsecond or nanosecond scale. This happens before signer recovery or authentication in the SPORK network handler, so an unauthenticated peer can reach it. A minimal build using the project's debug -ftrapv setting terminated with exit 133; optimized builds retain signed-overflow undefined behavior. Normalize adjusted time to seconds before comparing. The activation comparison at line 229 has the same unsafe conversion for signed SporkValue timestamps.

Suggested change
if (spork.TimeSigned() > GetAdjustedTime() + 2h) {
const auto max_time{std::chrono::time_point_cast<std::chrono::seconds>(GetAdjustedTime() + 2h)};
if (spork.TimeSigned() > max_time) {

source: ['codex']

Comment thread src/governance/vote.cpp Outdated
Comment on lines +161 to +162
const auto max_time{GetAdjustedTime() + 1h};
if (Time() > max_time) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Extreme governance timestamps overflow chrono comparisons

CGovernanceVote::Time() wraps the signed 64-bit wire timestamp in NodeSeconds, but max_time retains NodeClock's finer native duration. Comparing them implicitly scales the wire value and overflows for values such as INT64_MAX. A peer can reach this before signature verification by using a known governance parent, valid signal fields, and a public masternode outpoint. The same PR-introduced conversion occurs before governance-object signature verification in MasternodeRateCheck at governance.cpp lines 742-749, and RPC/GUI proposal validation at object.cpp line 147 also accepts an int64_t epoch. Cast adjusted time to NodeSeconds at each serialized-time comparison and add extreme-value coverage.

Suggested change
const auto max_time{GetAdjustedTime() + 1h};
if (Time() > max_time) {
const auto max_time{std::chrono::time_point_cast<std::chrono::seconds>(GetAdjustedTime() + 1h)};
if (Time() > max_time) {

source: ['codex']

Convert nearby CoinJoin, governance, spork, LLMQ, RPC, and test code to chrono time points and durations. Preserve serialized, signed, hashed, and RPC-facing timestamps as integer Unix seconds with explicit conversions at those boundaries.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

Carried-forward prior findings: prior-3 is STILL_VALID because ccb8bce changes GetAdjustedTime() to NodeClock::time_point while leaving incompatible Dash-only consumers until the following commit. Latest-delta reconciliation: 4baa5e1fa44e and e011eb8a496e are FIXED through seconds-resolution comparisons and focused extreme-timestamp tests; no new latest-delta findings were found. The remaining non-buildable commit boundary is blocking, so Sonnet coverage remains deferred by the Codex blocker gate.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed), gpt-5.6-sol — backport-reviewer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `<commit:ccb8bcebfca>`:
- [BLOCKING] <commit:ccb8bcebfca>:1: Dash adaptations still follow the type-changing cherry-pick
  Commit ccb8bcebfca changes GetAdjustedTime() from int64_t to NodeClock::time_point but leaves Dash-only consumers unchanged. At that revision, governance.cpp:286 and rpc/governance.cpp:120 assign the result to int64_t, vote.cpp:94 initializes an integer timestamp from it, and coinjoin.h:220 uses it as an int64_t default argument; these conversions are ill-formed. Commit 40e554c929c supplies the required adaptations only afterward, leaving ccb8bcebfca as a non-buildable permanent bisect point. Fold the Dash adaptations into ccb8bcebfca, or otherwise restructure the stack so every revision builds.

Comment thread src/coinjoin/coinjoin.cpp
return current_time - nTime > COINJOIN_QUEUE_TIMEOUT ||
nTime - current_time > COINJOIN_QUEUE_TIMEOUT;
const auto queue_time{Time()};
if (current_time < NodeSeconds{} || queue_time < NodeSeconds{}) return true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: technically, it's valid code, but looks a bit strange without 0s

if (current_time < NodeSeconds{0} || queue_time < NodeSeconds{0}) return true;

Comment thread src/coinjoin/coinjoin.h
Comment on lines +222 to +223
[[nodiscard]] bool IsTimeOutOfBounds(NodeSeconds current_time) const;
[[nodiscard]] bool IsTimeOutOfBounds() const;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 methods with the same name is a bit strange too.

consider renaming one from IsTimeOutOfBounds to IsTimeOutOfBoundsAtTime or something

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's for the overload?

int64_t nNow = GetAdjustedTime();
int64_t nSuperblockCycleSeconds = Params().GetConsensus().nSuperblockCycle * Params().GetConsensus().nPowTargetSpacing;
const auto timestamp{govobj.CreationTime()};
const auto now{std::chrono::time_point_cast<std::chrono::seconds>(GetAdjustedTime())};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why not just

const std::chrono::time_point_cast<std::chrono::seconds> now{(GetAdjustedTime())};

Type is already specified, auto seems useless here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time point cast is a function not a type

// Perform additional relays for triggers
int64_t nNow = GetAdjustedTime();
int64_t nSuperblockCycleSeconds = Params().GetConsensus().nSuperblockCycle * Params().GetConsensus().nPowTargetSpacing;
const auto now{std::chrono::time_point_cast<std::chrono::seconds>(GetAdjustedTime())};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same here for auto - type is already specified on the same line

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same; time_point_cast is a function not a type

Comment thread src/governance/object.h
static constexpr int64_t GOVERNANCE_FEE_CONFIRMATIONS = 6;
static constexpr int64_t GOVERNANCE_MIN_RELAY_FEE_CONFIRMATIONS = 1;
static constexpr int64_t GOVERNANCE_UPDATE_MIN = 60 * 60;
static constexpr std::chrono::hours GOVERNANCE_UPDATE_MIN{1};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unify style with our code base, see for example net_processing.cpp

static constexpr auto HEADERS_DOWNLOAD_TIMEOUT_PER_HEADER = 1ms;
-static constexpr std::chrono::hours GOVERNANCE_UPDATE_MIN{1};
+static constexpr auto GOVERNANCE_UPDATE_MIN{1h};

@knst knst left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK 40e554c

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants