Await until next second for block timestamp validation#5037
Conversation
|
Hey @Frozen, please fix Lines 250 to 270 in ddc96ae From the node log: See the Lines 57 to 67 in cfe1254 |
|
Hey @Frozen after the last commit I've seeing time to time the following error message and a view change after it: I believe the reason is the following:
How is look like on the Hooray dashboard, see the 1 on the few validators: |
|
Hey @Frozen, I've tried to debug the issue on the current leader and found the diff between linux ntp aka local vs golang ntp time aka registry and found that there is a {
"level": "warn",
"shardID": 1,
"myBlock": 40411052,
"myViewID": 40422315,
"phase": "Commit",
"mode": "Normal",
"parentTimeUnix": 1777905324,
"registryNow": "2026-05-04T09:35:24.910835186-05:00",
"localNow": "2026-05-04T09:35:24.915390017-05:00",
"registryMinusLocal": -4.554831,
"targetTime": "2026-05-04T09:35:25-05:00",
"sleepByRegistry": 89.164814,
"sleepByLocalUntil": 84.609983,
"registryTimestampUnix": 1777905324,
"retryCount": 0,
"proposalBlockNum": 40411053,
"isLeader": true,
"asyncProposal": true,
"called": "/home/um/harmony/consensus/consensus_v2.go:628",
"caller": "/home/um/harmony/consensus/proposer.go:67",
"time": "2026-05-04T09:35:24.915410385-05:00",
"message": "[timestamp-guard] waiting for next second before proposing block"
}Will add more after this comment |
| // Current time is within the same second as the parent block. | ||
| // Sleep until the next second so the child timestamp is strictly | ||
| // greater, satisfying the engine.go validation check. | ||
| time.Sleep(time.Until(time.Unix(parentTime+1, 0))) |
There was a problem hiding this comment.
Here we are mixing 2 time sources together.
And my debug got the following:
registryNow = 09:35:24.999764573
localNow = 09:35:25.004318402
targetTime = 09:35:25.000000000
Meaning:
consensus clock says: 09:35:24.999764573
local clock says: 09:35:25.004318402
target is: 09:35:25.000000000
So at the exact same moment:
registry.Now() says: not yet, wait 0.235 ms
time.Now() says: already passed, no need to wait
On the last retry, this happened:
- retryCount = 2
- registry clock: still too early
- local clock: already past target
- sleep: 0ms
- continue
- loop ends
- the code never reached the proposal path
Block proposal readiness is checked with consensus.registry.Now(), but the sleep duration was previously calculated with time.Until(), which uses raw local time.Now(). When local time is slightly ahead of the registry/NTP-adjusted clock, the sleep can resolve to zero or negative near the next-second boundary while the registry clock is still within the parent block timestamp second. This can burn proposal retries and skip proposal setup. Compute the sleep duration against consensus.registry.Now() instead, then refresh the timestamp after sleeping before continuing.
|
So after test screw in both ways - -2 minutes and +2 minutes node is still validating and producing blocks using golang NTP time ✔️ |
|
@Frozen, please:
|
|
removed 0.beevik-ntp.pool.ntp.org |
…eevik-ntp.pool.ntp.org ntp - this one can be used just for library demo
|
@Frozen, I've amend commit message and force pushed, please update your local 🙏 |
fc6c0b5 to
3ea9944
Compare
|
I've updated the PR description |


This pull request makes several improvements to the block proposing logic in the consensus mechanism, primarily focusing on more precise block timestamp handling and improved sleep timing. The changes ensure that block timestamps are strictly increasing and reflect accurate wall-clock time, especially when using NTP, and refactor the code to pass the current time explicitly.
Block timestamp handling and accuracy:
ProposeNewBlockto the proposer loop (WaitForConsensusReadyV2), ensuring that the block timestamp is always strictly greater than the parent block's timestamp. The code now explicitly sleeps until the next second if necessary.nowtime value is passed explicitly toProposeNewBlock, rather than being determined inside the method, improving testability and clarity. [1] [2] [3]