Change:
unblock the server until it is certain that the server has finished its address validation (see Section 8 of QUIC-TRANSPORT).  That is, the client MUST set the probe timer if the client has not received an <del> acknowledgement for one of its Handshake or 1-RTT packets, and has not received a HANDSHAKE_DONE frame.  If Handshake keys are available to the client, it MUST send a Handshake packet, and otherwise it MUST send an Initial packet in a UDP datagram of at least 1200 bytes. A client could have received and acknowledged a Handshake packet, causing it to discard state for the Initial packet number space, but not sent any ack-eliciting Handshake packets.  In this case, the PTO timer is armed from the time that the Initial packet number space is discarded. </del> <ins> acknowledgement for one of its Handshake or 1-RTT packets and has not received a HANDSHAKE_DONE frame, even if there are no packets in flight.  When the PTO fires, the client MUST send a Handshake packet if it has Handshake keys, otherwise it MUST send an Initial packet in a UDP datagram of at least 1200 bytes. </ins> 6.2.3.

Discussion:
I tried adding more text, but I think it made it more confusing, so I moved text up for the one case you set PTO from the current time. Suggestions welcome. Later updated to go back to using "current time" which NAME didn't like in . It seems fairly clear to me, and the original fix doesn't work for the Initial packet number space, which is why we're back to current time.
The pseudo code says the following: With the change introduced in , the spec says the following for the time from which the timer should be armed if there are no outstanding packets in the Handshake packet number space:
This code is called from where Initial keys are discarded, so I believe it still matches the text, which only discusses the "(has handshake keys)" case.
That being said, it feels odd we mention one case of using now() and not the other, so I updated PR
This paragraph does not really provide any context that would allow you to infer what the "current time" is. Based on the pseudocode, this time is when SetLossDetectionTimer() is invoked, but that covers a range of cases: When a packet is received and the endpoint is at its anti-amplification limit When an in-flight packet is sent When an ACK is received When the loss detection timer pops When a packet number space is discarded Of these, the "current time" clause only really applies to the last two, and the last one is the real baseline (because the other only matters if the first was set). I think that this can be more precisely worded to say "In this case, the PTO is set from the time at which the Initial packet number space is discarded." That's far more direct, but I would like others to check my work to ensure that it catches all the cases properly.
Setting the time from when the Initial PN space is discarded is a good clarification, and conveniently it lines up with the pseudocode. The pseudocode for OnPacketNumberSpaceDiscarded(pn_space) calls SetLossDetectionTimer().
There are actually two cases now() is used, so we can't solely rely on handshake keys being discarded. See for a larger change.
Assume the following situation: The server sends 0.5-RTT data, but never receives an acknowledgement for that packet.

When the handshake completes, the only outstanding packet is that 0.5-RTT packet. The server now needs to arm a PTO timer. This seems to imply that the timer should be set to , whereas seems to imply that the timer should be set to . I think I'd prefer the former interpretation of the latter one. For example, if the entire first flight is lost, the server won't retransmit the Initial for 1s. When the handshake then completes, the RTT is most likely << 1s, which would mean that the timer is set to a time in the past, if we base it on the time the 0.5-RTT packet was sent.
IIUC, what needs to be applied is the latter. The former is merely pointing out that ptotimeout is defined as the minimum of among all the PN spaces, and therefore that it needs to be recalculated when PN spaces are discarded. Generally speaking, I think that this is the correct behavior. If an endpoint has sent a tail, and has not received an ACK for more than 1 RTT + alpha from the moment it sent the packet, it is natural to assume that the packet might have been lost. There is no problem in the endpoint realizing that it should send a probe immediately, when the quality of the RTT estimate improves. That said, I do see problem with sending a PTO when the peer is possibly stuck in calculating the traffic keys; see .
So, NAME is your assertion that in this case you would unblock the sending of PTO, notice that the time has passed, and send a probe immediately?
The problem is that this is not a "real" tail. It's only a tail because we have different packet number spaces. Note that the server completes the handshake when receiving the CFIN, which is also when it derives the client 1-RTT secret. This means there's no way it could have received and processed an ACK for any 1-RTT packet.
NAME Yes. NAME I would point out that the example is a corner case. It is about the first flight of the server getting lost, then the first set of PTO probes reaching the client. The assumption is that there were some 0.5 RTT data in the first flight but none in the first set of PTO probes.

While that could happen in practice, as the amount of data the server is allowed to send is different between the two occasions, I would point out that the amount of Initial and Handshake data could be less than the amount of data you could send as a PTO probe (e.g., when resuming a connection or when certificate compression is used). In such case, the server would be sending 0.5 RTT data in the first PTO event. I think that is a fine thing to do. To paraphrase, the example is about the server sending 0.5 RTT probes later than it might have, due to the space of probes being restricted. That's why I do not think it's important to consider about the example. As previously stated, I do think that there is are issues regarding the interaction of handshake and PTO probes, but my point is that there is nothing wrong with how the PTO timeout is being calculated.
[[Based on the discussion with NAME this comment has been moved to a dedicated issue: ]]
Reconsidering the original point, is the assumption that persistent congestion period is calculated per PN space? The question is if that assumption has been the intent of the specification. Pseudo-code might imply that (as takes - a per-PN-space value - as an argument), but I do not see any mention regarding PN space in the text itself.
As commented elsewhere, persistent congestion is a feature of the congestion controller, so it's across all PN spaces. I also agree with NAME that the current text indicates setting the alarm to outstanding0.5RTTpacketsenttime + PTO. Only in the case of no in-flight packets is Now(in this case handshakecompletiontime) used. I actually think the existing text doesn't imply you'd start from then handshake completion time, but I can write an editorial PR to make it completely clear.
A couple of suggestions