Skip to content

fix: shelley systemStart must match the shifted byron startTime (devnet Plutus clock skew) - #186

Open
nau wants to merge 1 commit into
bloxbean:mainfrom
nau:fix-shelley-system-start-skew
Open

fix: shelley systemStart must match the shifted byron startTime (devnet Plutus clock skew)#186
nau wants to merge 1 commit into
bloxbean:mainfrom
nau:fix-shelley-system-start-skew

Conversation

@nau

@nau nau commented Jul 29, 2026

Copy link
Copy Markdown

Problem

When shiftStartTimeBehind is enabled with conwayHardForkAtEpoch > 0 (the default devnet configuration), ClusterStartService.setupFirstRun backdates byron-genesis startTime by one epoch (600s with default parameters) so the cluster boots directly in Conway. However, shelley-genesis systemStart is still written as the unshifted current time.

On every public Cardano network byron.startTime and shelley.systemStart describe the same instant. The devnet violates that invariant, and cardano-node ends up with two clocks that disagree by exactly the backdate:

  • Slot counter (phase-1 validation, tip): anchored on byron startTime. Verified on a live devnet: cardano-cli query tip slot is exactly wall_clock - byron_startTime.
  • Plutus script context times (phase-2 validation): the ledger translates slots to POSIXTime for txInfoValidRange from shelley systemStart. Verified from a node script failure dump: both bounds of txInfoValidRange equal shelley_systemStart + slot * 1s, including shelley's sub-second fraction, i.e. 600s later than the same slots under the byron anchor.

Impact

Every POSIXTime a Plutus script sees in its validity range is shifted ~600s into the future relative to the slots the transaction was built and phase-1-validated with. Scripts that only compare the range bounds to each other never notice. Scripts that cross-check txInfoValidRange against externally computed times (deadline validators, oracles that timestamp datum entries, anything doing off-chain/on-chain time agreement) fail phase-2 on the devnet with ValidationTagMismatch (IsValid True) (FailedUnexpectedly ...), while the same transaction validates fine against a client-side evaluator and would validate on public networks.

We hit this with a Bitcoin oracle contract that records validity_range_end - block_timestamp deltas in its datum: the node recomputed the delta 600s larger than the datum value and rejected every update transaction. The failure is very confusing to debug because the transaction is phase-1 valid, the local evaluator says IsValid True, and only the node's CEK evaluation disagrees.

Concrete numbers from the failing devnet (yaci-cli 0.10.0-preview2, but the code path is unchanged on current master):

byron-genesis.json   startTime    = 1785318057            (09:40:57Z)
shelley-genesis.json systemStart  = 2026-07-29T09:50:57.677671Z   (+600.677s)
cardano-cli query tip slot        = wall_clock - 1785318057        (byron anchor)
txInfoValidRange in PlutusFailure = [1785319160677, 1785319760677) (shelley anchor: slot 503..1103 + .677 fraction)

Fix

Write shelley systemStart from the (possibly shifted) byronStartTime instead of a fresh Instant.now(), so both genesis files describe the same chain origin, exactly as on public networks. The unshifted case is unchanged (byronStartTime is Instant.now() there).

Note the Yano companion service already enforces this invariant in the opposite direction after a time-travel bootstrap (YanoCompanionService syncs byron startTime to the shifted shelley systemStart), so this brings the plain devnet path in line with it.

Verification

  • applications/cli compiles with the change (./gradlew compileJava).
  • The two-clock skew and its mechanism were verified against a live devnet as described above; a time-sensitive contract test suite that fails on current images passes once the client compensates for exactly this skew (shelley-anchored slot-to-time conversion plus real-tip validity anchoring), confirming the diagnosed root cause is the only remaining discrepancy.

This issue was explored, diagnosed and the fix prepared by Claude (Claude Code), driven by @nau.

🤖 Generated with Claude Code

…Time

When shiftStartTimeBehind backdates byron-genesis startTime (by one epoch
with the default devnet settings) so the cluster boots directly in Conway,
shelley-genesis systemStart was still written as the current time. The node
derives its slot counter from the byron startTime but translates slots to
POSIXTime for the Plutus script context from the shelley systemStart, so the
two clocks disagreed by exactly the backdate (600s by default): every
POSIXTime a Plutus script sees in txInfoValidRange is shifted by that amount
against the slots the transaction was actually built and accepted with.

Any validator that cross-checks the validity range against externally
computed times then fails phase-2 validation on the devnet while passing
against a consistent local evaluator and on public networks, where
byron.startTime and shelley.systemStart always describe the same instant.

Write systemStart from byronStartTime so both genesis files describe the
same chain origin, matching public network genesis layouts.
@satran004

Copy link
Copy Markdown
Member

@nau Please check my comment in the issue #185

Ideally, the shiftStartTimeBehind and conwayHardForkAtEpoch configurations should not be present in the latest version, as the devnet is now bootstrapped by Yano and handed over to the Haskell node during creation. However, the fix in this PR may still be valid.

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.

2 participants