9b9ede2e1e
Addresses seven bugs surfaced by post-landing audit of the path
validation feature.
Spec fixes (RFC 9000 §9):
- Bug 1: PATH_CHALLENGE was going out on the OLD DCID because the
writer reads conn.destinationConnectionId per packet and the
rotation only happened on PATH_RESPONSE arrival. Now rotate the
DCID inside triggerPathMigrationLocked (abrupt-migration model
appropriate for the "old path looks dead" trigger condition).
Fixes the headline feature — without this the challenge cannot
actually exercise the new path.
- Bug 2: 3 * PTO timeout dropped the failed CID without queuing a
RETIRE_CONNECTION_ID. The peer kept the routing entry forever.
checkValidationTimeout now queues the failed sequence per §5.1.2.
- Bug 3: RETIRE_CONNECTION_ID for seq 0 was silently honored. We
have no replacement SCID to give the peer (we don't issue our
own NEW_CONNECTION_ID frames), so the connection is unusable.
Close with INTERNAL_ERROR instead.
- Bug 4: triggerPathMigration had no handshake-confirmed gate;
§9.1 forbids migration before handshake confirmation. Returns
new PathMigrationResult.NotConnected when status != CONNECTED.
Implementation fixes:
- Bug 5: driver was calling Clock.System.now() directly instead
of conn.nowMillis(), breaking virtual-clock tests.
- Bug 6: PTO threshold check ran BEFORE the consecutive-PTO
counter increment, so threshold=2 actually required 3 PTOs.
Increment first; threshold semantics now match the constant.
- Bug 7: applyPeerPathResponseLocked didn't reset
consecutivePtoCount on successful validation; the next sleep
inherited a stale exponential-backoff multiplier even though
the peer just proved liveness.
Code quality:
- Rename ValidationOutcome.Validated.newConnectionIdBytes →
connectionId; PathValidationState.Validating.newCidBytes →
newConnectionId. The "Bytes" suffix was redundant.
- Drop unused PathValidator(initialActiveCidSequence) parameter.
- Drop dead coerceAtLeast(2) in pool size calculation.
- Make pendingChallenges and pendingRetireSequences internal.
- Fix stale KDoc references (activatePendingValidatedCid,
forceRetireActiveIfNeeded, "retirePriorTo decreased" — none
survived the §19.15 clamp fix).
- PathValidator.RecordResult: drop RetirePriorToRegressed
enum value (clamped, never returned).
- Surface qlogObserver.onConnectionIdRetired in both the
success and timeout paths.
Tests:
- ClientPathMigrationTest: existing fullMigrationRoundTrip
test now asserts DCID rotates AT challenge time, not on
PATH_RESPONSE.
- New retireConnectionIdForSequenceZeroClosesConnection.
- New pathResponseSuccessResetsConsecutivePtoCount.
- New triggerPathMigrationBeforeHandshakeReturnsNotConnected.
- PathValidatorTest:
validationTimeoutAfter3PtoTransitionsToFailedAndRetiresFailedCid
now asserts the failed sequence is queued for retire.
- retirePriorToRegressionIsRejected → renamed to
retirePriorToRegressionIsClampedNotRejected.
All :quic:jvmTest and :nestsClient:jvmTest pass.
https://claude.ai/code/session_01PVVhSQXvw4K4oQ46FzpgaT