Task
wallet_reset_coin_db computes its own now_ms inline from SystemTime::now()
(crates/dig-node-service/src/control.rs:2601) and never routes through WalletBackend, so it
bypasses the ClockGovernor entirely for that one RPC.
#532 (PR #539) disciplines every reservation "now" read through
WalletBackend::reservation_now_ms() so a wall-clock jump cannot make a live hold be misjudged.
Six production call sites were migrated. This one was not, because it lives in a different crate and
reads the clock directly rather than asking the backend.
Consequence: the clock-jump protection #532 exists to provide is absent on this path. A wall
clock stepped forward while a reset is in flight is evaluated against an undisciplined now, which
is precisely the state ClockGovernor was built to prevent — so the guard is real everywhere except
here, and nothing says so.
Why this is not a defect in #539
Pre-existing, blamed to #454, and it predates both #528 and #539. It is in dig-node-service, while
#539's change is in dig-wallet. The #539 lane surfaced it rather than widening its own PR, which
was the right call — an in-scope fix would have crossed a crate boundary and grown the diff past
what its gate covered.
Done condition (finite and checkable)
Constraints carried forward from #532/#528
Evidence
Task
wallet_reset_coin_dbcomputes its ownnow_msinline fromSystemTime::now()(
crates/dig-node-service/src/control.rs:2601) and never routes throughWalletBackend, so itbypasses the
ClockGovernorentirely for that one RPC.#532 (PR #539) disciplines every reservation "now" read through
WalletBackend::reservation_now_ms()so a wall-clock jump cannot make a live hold be misjudged.Six production call sites were migrated. This one was not, because it lives in a different crate and
reads the clock directly rather than asking the backend.
Consequence: the clock-jump protection #532 exists to provide is absent on this path. A wall
clock stepped forward while a reset is in flight is evaluated against an undisciplined
now, whichis precisely the state
ClockGovernorwas built to prevent — so the guard is real everywhere excepthere, and nothing says so.
Why this is not a defect in #539
Pre-existing, blamed to #454, and it predates both #528 and #539. It is in
dig-node-service, while#539's change is in
dig-wallet. The #539 lane surfaced it rather than widening its own PR, whichwas the right call — an in-scope fix would have crossed a crate boundary and grown the diff past
what its gate covered.
Done condition (finite and checkable)
wallet_reset_coin_dbobtains itsnow_msthrough the same disciplined path as the other sixreservation call sites (
WalletBackend::reservation_now_ms()or an equivalent that routesthrough
ClockGovernor), or it is documented at the call site why an undisciplined read iscorrect for a reset specifically.
reserve_spend/prune_reservations-style calls rather than a hand-placed row — a fixture in astate production cannot reach would pass under the defect.
SystemTime::now()directly. If themigration missed one site it may have missed others; the sweep is part of the fix, not a
follow-up.
Constraints carried forward from #532/#528
rpc.rs:863andcontrol.rs:2601both carry thecomment that a caller-supplied clock "would be a lapse oracle". Whatever this becomes, it must
not take
nowfrom a request parameter.double-spend direction; holding one too long is a liveness cost. They are not symmetric, and the
fix must say which way a wrong version fails.
Evidence
crates/dig-node-service/src/control.rs:2601— the inlineSystemTime::now().dig-walletproduction sites confirmed routing throughreservation_now_ms()by grep after the fix(wallet): bound a reservation deadline by the observing clock, not only by the first push's #528 merge.