If the sender wants to elicit a faster acknowledgement on PTO, it can skip a packet number to eliminate the ack delay. <del> When the PTO timer expires, and there is new or previously sent unacknowledged data, it MUST be sent.  A probe packet SHOULD carry new data when possible.  A probe packet MAY carry retransmitted unacknowledged data when new data is unavailable, when flow control does not permit new data to be sent, or to opportunistically reduce loss recovery delay.  Implementations MAY use alternative strategies for determining the content of probe packets, including sending new or retransmitted data based on the application's priorities. </del>  It is possible the sender has no new or previously-sent data to send. As an example, consider the following sequence of events: new

I think this is clearer, but maybe there's a larger change people would like.
Currently "Sending Probe Packets" define the priority what to be in Probe packet as follows: A probe packet SHOULD carry new data when possible. A probe packet MAY carry retransmitted unacknowledged data when new data is unavailable. And also says "Implementations MAY use alternative strategies for determining the content of probe packets, including sending new or retransmitted data based on the application's priorities." When there is no new data (case 2), the implementation need to choose which one to retransmit when there is a multiple data loss in the sender. QUIC's Probe packet approach is a combined experience of TCP TLP and RTO and I believe Probe packet is more similar to TLP. RACK draft says (URL) "The probe data segment can either be new, previously unsent data, or a retransmission of previously sent data just below SND.NXT" so it's pretty clear which one to send among the lost data (newest one). Is there any reason that it's up to the implementers when it need to choose which one to send from lost data? It's better to clarify which one to send among retransmitted unacknowledged data, from oldest or newest when there is multiple data packet lost. I think it's better to send newest (unacked) one first if I follow the reasoning of TLP (new to the old), but there may be some reason why the current draft doesn't define it clearly. URL has some background and my comment.
A lot of the reasoning behind TLP comes from TCP counting bytes and not counting packets. In TCP, new data means a new sequence number, thus a higher chance to trigger ACKs or SACKS. failing that, it is best in TCP to repeat the last transmitted bytes, because that too will trigger ACK or dup ACK or SACK. But for Quic, any new packet will have a new sequence number, so from a transport engine point of view it does not matter very much what data is in that packet. Logically it should be the most urgent data among all the available streams. So first pick the most urgent stream with something to send; if that something is old data presumed lost, send it; if there is no data presumed lost on that stream and there is new data available, send that. If there is no data available at all on that stream, then pick the next most urgent stream, etc.
I think this is the right answer but need some clarification on the draft text. I think there is 2 different approach for the content of Probe packet depending on the role of Probe packet: 1) if the tail loss is most concerning (what's what I understand from TLP) then we would send a new data if available, or newest lost to move the right edge and update ACK ranges. 2) If HoL is most concerning then we would send oldest lost data (or highest priority data) to move the left edge (but it may be delivered already, because we may lost some acks) However the draft says we always try to send a new data first regardless of lost data, so I assume 1). But if the priority is simply on the urgent data, we don't need to send a new data first.
This is a great question and I incline towards sending the newest (tail) unacknowledged data first (same as TLP in TCP). Lets say we sent packets 1,2,3,4 & 5 at the same time and assume 1 & 5 were lost. Now, PTO fires. Lets say, on PTO expiry, we retransmit the oldest packet i.e. 1. When the PTO timer fires a second time, only then we would send pn=5. OTOH, if we retransmit the tail first, i.e. pn=5, it provides faster recovery, because an arriving ACK with largest pn=4 will mark pn=1 as lost and we will retransmit that immediately. The unacknowledged packets at the head have the advantage of subsequent packets allowing them to recover via ACKs marking them as lost. Not so much for the tail packets.
NAME I'm not sure I understand the argument here. Why would the content of the retransmission matter for declaring packets lost?
We declare packets lost when So, a tail packet (with no packets after it) wouldn't be marked lost until after some earlier packets were retransmitted.
I don't understand why people think that it is important to standardize this. Do we have research that points more clearly to one choice being better than another?
That's correct, but a retransmission always has a higher packet number than earlier sent packets, so the contents of the packet don't matter, right? Or am I misunderstanding your argument? I agree with NAME We already have some advice about retransmitting packets in section 13.3:
This seems right to me; we're retransmitting frames, not packets. If folks get a chance to experiment with this and publish their results, that would be super cool! But it seems like that's somewhat orthogonal to getting this spec finished, since so far we've settled with "doing the default thing should get you functional QUIC". It seems like all options here work well enough that we don't need further text unless either we have some data or we have a principle of why one option is deterministically better?
NAME I am not saying that the contents matter. What I am saying is if we send a PTO probe for the tail packet, we would recover faster as it doesn't have to wait for ACKs for the retransmissions of earlier packets (1 as 6). With this approach we would be able to retransmit both tail (via PTO) and head (via ACK for pn 4) faster than the other approach. That being said, I am neutral towards making any change to the draft and leaving it to implementors is a fine option.
I also think that we do not need to standardize this. If I understand correctly, we do not define how data should be sent for the first time. I think the only point we make is that a QUIC implementation SHOULD provide ways in which an application can indicate the relative priority of streams (URL). I do not think that there is a baked-in assumption that the receiving application would be reading the stream from the beginning. Assuming that my understanding is correct (it could be wrong), I do not think we need to talk any more about what should be done to transmit data the second time.
Well... We retransmit information, not frames or packets. :) Those people who do retransmit frames are not strictly wrong, but they are also not going to get the best results either. Kazuho has provided what I think is the definitive response here.
My point is what to send in Probe packet, not a usual retransmission detected by loss detection algorithm. recovery 6.3.4 has the following sentences: While B) can be a research topic on which one is better to retransmit if there is multiple loss reported (oldest or newest), I wonder if A) and C) seems conflicting because "new data MUST be sent" limits C) to pick up a strategy to send which data to retransmit. I think there is some history why A) is in the first place and I believe it's from TLP definition. Initially my question was there is an ambiguity on what to retransmit from lost packet. As many people pointed out, I understand QUIC's monotonic packet number property will make better ACK ranges report coming back when we send a Probe packet. This makes less importance on sending a new data as MUST. If so, now I think we don't need to have a new data transmission as MUST.
I guess NAME has a point. There is probably a tad too much text in the recovery draft, and the MUST there would be better replaced by an RFC 6919 variant.
Having re-read these, the inconsistency isn't that inconsistent. But it could still be improved. The rules require that you send unacknowledged data, whether it was sent before or not. Then they recommend unsent data, and allow retransmissions (the latter of which is somewhat redundant). Then (C) is something about not really meaning the first MUST. If anything, this should be tuned down to a recommendation to send unacknowledged data and to steer clear of any proscription regarding new transmissions vs. retransmissions. However, we should try to seek consensus on a change because it would seem like any change here is a change to normative language.
I keep re-reading this, and I don't see any conflict of C with A, but I can create an editorial PR to reduce the likelihood of someone reading "new data MUST be sent" into A.
LGTM. Thanks!

If the sender wants to elicit a faster acknowledgement on PTO, it can skip a packet number to eliminate the ack delay.  <ins> When the PTO timer expires, an ack-eliciting packet MUST be sent.  An endpoint SHOULD include new data in this packet.  Previously sent data MAY be sent if no new data can be sent.  Implementations MAY use alternative strategies for determining the content of probe packets, including sending new or retransmitted data based on the application's priorities. </ins> It is possible the sender has no new or previously-sent data to send. As an example, consider the following sequence of events: new


field section, some endpoints could be forced to commit a large amount of available memory for header fields. <del> An endpoint can use the SETTINGS_MAX_HEADER_LIST_SIZE (settings- </del>  parameters) setting to advise peers of limits that might apply on the size of field sections.  This setting is only advisory, so endpoints MAY choose to send field sections that exceed this limit and risk

Replace SETTINGSMAXHEADERLISTSIZE with SETTINGSMAXFIELDSECTIONSIZE in one last place. Add comment to HTTP/2 identifier mapping section about rename.

field section, some endpoints could be forced to commit a large amount of available memory for header fields.  <ins> An endpoint can use the SETTINGS_MAX_FIELD_SECTION_SIZE (settings- </ins> parameters) setting to advise peers of limits that might apply on the size of field sections.  This setting is only advisory, so endpoints MAY choose to send field sections that exceed this limit and risk


requests with a Stream ID greater than or equal to the identifier received in a GOAWAY frame, those requests will not be processed. Clients can safely retry unprocessed requests on a different <del> connection. </del>  Requests on Stream IDs less than the Stream ID in a GOAWAY frame from the server might have been processed; their status cannot be server started to process that POST request if the server does not send a GOAWAY frame to indicate what streams it might have acted on. <del> A client that is unable to retry requests loses all requests that are in flight when the server closes the connection.  An endpoint MAY send multiple GOAWAY frames indicating different identifiers, but the identifier in each frame MUST NOT be greater than the identifier in any previous frame, since clients might already have retried unprocessed requests on another connection.  Receiving a GOAWAY containing a larger identifier than previously received MUST be treated as a connection error of type H3_ID_ERROR. </del>  An endpoint that is attempting to gracefully shut down a connection can send a GOAWAY frame with a value set to the maximum possible see frame-max-push-id.  A smaller value indicates the client will reject pushes with Push IDs greater than or equal to this value. Like the server, the client MAY send subsequent GOAWAY frames so long <del> as the specified Push ID is strictly smaller than all previously sent values. </del>  Even when a GOAWAY indicates that a given request or push will not be processed or accepted upon receipt, the underlying transport

Move "A client that is unable to retry requests [...]" sentence from the beginning of an entirely unrelated paragraph to somewhere more appropriate. Change Push ID requirement on subsequent GOAWAY frames. This was presumably an oversight, because "An endpoint MAY send multiple GOAWAY frames indicating different identifiers [...]" paragraph above is pretty clear and covers both directions already.

requests with a Stream ID greater than or equal to the identifier received in a GOAWAY frame, those requests will not be processed. Clients can safely retry unprocessed requests on a different  <ins> connection.  A client that is unable to retry requests loses all requests that are in flight when the server closes the connection. </ins> Requests on Stream IDs less than the Stream ID in a GOAWAY frame from the server might have been processed; their status cannot be server started to process that POST request if the server does not send a GOAWAY frame to indicate what streams it might have acted on.  <ins> An endpoint MAY send multiple GOAWAY frames indicating different identifiers, but the identifier in each frame MUST NOT be greater than the identifier in any previous frame, since clients might already have retried unprocessed requests on another connection. Receiving a GOAWAY containing a larger identifier than previously received MUST be treated as a connection error of type H3_ID_ERROR. </ins> An endpoint that is attempting to gracefully shut down a connection can send a GOAWAY frame with a value set to the maximum possible see frame-max-push-id.  A smaller value indicates the client will reject pushes with Push IDs greater than or equal to this value. Like the server, the client MAY send subsequent GOAWAY frames so long  <ins> as the specified Push ID is no greater than any previously sent value. </ins> Even when a GOAWAY indicates that a given request or push will not be processed or accepted upon receipt, the underlying transport


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 <del> is set from the current time. </del>  6.2.3.

This is a rephrase of NAME suggestion.
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.
This is good, but I wouldn't talk about "current time" at all.

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  <ins> timer is armed from the time that the Initial packet number space is discarded. </ins> 6.2.3.


packet protection. A client only receives acknowledgments for its 0-RTT packets once the <del> handshake is complete. </del>  A client MUST NOT send 0-RTT packets once it starts processing 1-RTT packets from the server.  This means that 0-RTT packets cannot

Specify that when the spec says that a "client only receives acknowledgments for its 0-RTT packets once the handshake is complete", it means "as defined in the Handshake Complete section of {{QUIC-TLS}}.".
is complete. Consequently, a server might expect 0-RTT packets to start with a However, this is not necessarily strictly true, because you might have the ACKs in 0.5RTT. Now you might say that the TLS stack reports the handshake as complete as soon as it receives Finished but that's an implementation detail
I think that this is true. The server can send before then, but we require that the client not install 1-RTT keys until it considers the handshake complete.
That's not entirely true. It's the server that needs to hold back keys (namely, the 1-RTT read key) until the handshake completes. The client can use keys as soon as they become available.
I think NAME is correct in sense that the following three events happen at the same moment on the client side: handshake is declared complete TLS stack provides the 1-RTT read and write keys TLS stack provides ClientFinished
Martin is correct. There are two different forms of handshake completion: complete when 1rtt can be sent and received, confirmed when the peer is known to be complete. The client cannot receive 0rtt acks before installing 1rtt keys, hence before hanshake is complete.
We've generally used section numbers for this.

packet protection. A client only receives acknowledgments for its 0-RTT packets once the  <ins> handshake is complete, as defined Section 4.1.1 of QUIC-TLS. </ins> A client MUST NOT send 0-RTT packets once it starts processing 1-RTT packets from the server.  This means that 0-RTT packets cannot


The encoded packet number is protected as described in Section 5.4 of QUIC-TLS. <del> The sender MUST use a packet number size able to represent more than </del>  twice as large a range than the difference between the largest acknowledged packet and packet number being sent.  A peer receiving the packet will then correctly decode the packet number, unless the packet protection. A client only receives acknowledgments for its 0-RTT packets once the <del> handshake is complete.  Consequently, a server might expect 0-RTT packets to start with a packet number of 0.  Therefore, in determining the length of the packet number encoding for 0-RTT packets, a client MUST assume that all packets up to the current packet number are in flight, starting from a packet number of 0. Thus, 0-RTT packets could need to use a longer packet number encoding. </del>  A client MUST NOT send 0-RTT packets once it starts processing 1-RTT packets from the server.  This means that 0-RTT packets cannot

I noticed the 0-RTT specific text in and thought a general clarification would be better. Conveniently, I don't think anyone has gotten this wrong in their implementation.
Thanks Ian, this is much clearer.

The encoded packet number is protected as described in Section 5.4 of QUIC-TLS.  <ins> Prior to receiving an acknowledgement for a packet number space, the full packet number MUST be included. After an acknowledgement is received for a packet number space, the sender MUST use a packet number size able to represent more than </ins> twice as large a range than the difference between the largest acknowledged packet and packet number being sent.  A peer receiving the packet will then correctly decode the packet number, unless the packet protection. A client only receives acknowledgments for its 0-RTT packets once the  <ins> handshake is complete. </ins> A client MUST NOT send 0-RTT packets once it starts processing 1-RTT packets from the server.  This means that 0-RTT packets cannot


Handshake packet from the client, it can consider the client address to have been validated.  Prior to validating the client address, servers MUST NOT send more than three times as many bytes as the number of bytes they have received.  This limits the magnitude of any amplification attack that

I'm not sure this changes anything in practice, since the client can't rely on it and the server could already do this, but explicitly saying it seems preferable.
LGTM
Suppose you have a large SH, for instance due to PQ crypto, in which case it will have to span multiple packets. If it exceeds three packets, then it will not be able to fit into a single flight. Ideally this would look something like this: However, as I read the text, the server is actually only permitted to send three packets in response to the ACK. The good news is that because the client is required to send fully padded packets, so you at least get to send 3600 octets or so per RT, but if the SH is >7K this is pretty annoying. I believe that there is a straightforward compatible change that would allow relaxing this restriction: if the server uses a reasonably long SCID, then when the client responds with an initial with DCID set to that SCID (which it is supposed to do anyway by my reading of S 7.2), then it is implicitly a proof of reachability, and the server could ignore the amplification limit.
This would be conditional on the server choosing a connection ID that has enough entropy (we seem to have decided that 64 bits is the benchmark). NAME has made the same comment on occasion.
Yes, agreed.
How would a client know how what algorithm the server used to generate the connection ID? As this seems to be a design issue anyway, here's another proposal: The server could send a token in the Initial packet, and the client would then use this token on subsequent Initial packets (replacing the token it might have used on the packet containing the ClientHello). This way, we'd be using a field that's intended to carry a high-entropy value, and not rely on a field that the server might not even have control over.
The client doesn't need to know, because it's the server's amplification limit that is at hand. And right now, the handshake would complete if the client doesn't do anything, it's just not optimal. I agree that this would work, but it seems like a significantly more invasive change, especially given the somewhat edge-casey nature of the issue.
We could also allow PATH_CHALLENGE in Initial packets, but that too has more disruptive effects on the design. Of course, this is almost something that implementations could do without permission. But if we want that, then it is better to capture it.
Yah, that was my thinking.
That's probably easier than using the Token field. The only change required for this would be to allow those two frames in Initial packets.
I agree that the change makes sense. The picoquic server already does that: if the client responds with the proper CID, then the connection is validated. The protocol police has not caught me yet, and that makes sense because the specifics of amplification response are pretty much a server implementation choice -- and yes, sticking a path challenge in the server's Initial packet would be another valid choice. The main problem is not on the server side, but rather on the client side. The last interop issue I saw came when a client was coalescing an Initial ACK with an Handshake ACK. The exchange went something like: For DCID I'm sending you an Initial ACK + Handshake but picoquic is rejecting it as too short (error code 408). My reading of the spec is this is legal, as it's a coalesced packet Answer: The text log says that the Initial packet was accepted, but after that you are sending an initial packet that is just 95 bytes long, and then an handshake packet. ... discussion showing that the coalesced datagram was just 95 bytes long, when the spec says that all initial packets should be padded to 1200 bytes. So basically the question is, should clients be authorized to NOT pad an Initial packet to 1200 bytes if the DCID of the Initial packet is set to the SCID received by the server? I would love to say that, but I am concerned about making disruptive changes during WGLC. And so my reluctant advice is to not do anything about this issue now. Developers of servers can make their own decisions, using either long CID or path challenges.
Yes, the Initial padding was discussed, and I think that as it depends on the client making an assessment of the entropy present in the connection ID, it is a non-starter.
The server could also inject entropy by selecting a large sequence number for the Initial packet. In theory nothing would break if the server picked a random 61 bit number for the first Initial packet instead of zero. If the client acks that and repeats the proper CID, then the server can legitimately assume that the client's address is validated.
You can only get 32 bits of entropy in the packet number, and you might limit your ability to send more packets if you picked a number that is too large, but yeah, there are options.
Is there a reason we would want to prohibit PATHCHALLENGE/PATHRESPONSE in Initial packets? That seems like a straightforward way to use existing mechanisms, if we want to take this design change.
I'm a bit surprised we're having this conversation at this stage in the process, but I'll throw my 2 cents in. I'm not particularly opposed to allowing PATHCHALLENGE/PATHRESPONSE, but it seems more invasive than explicitly saying something like "Servers MAY treat the receipt of a server generated connection ID with at least 64 bits of entropy as address validation."
This is my take as well. What's the disadvantage of the text Ian proposes
I like Ian's proposal and I dislike the idea of allowing the Path Challenge in Initial packets. I think allowing Path Challenge would just make things more complicated, while providing very little advantage over the Retry process. We might have debated that longer 2 years ago, but now is not the time -- better leave the spec as is.
Ian's suggestion seems fine. It's an addition to the spec, and I'm not really hearing any strong dissensions.
This is sounding like a design change, is that accurate?
Yes, this is a design change being proposed.
Is there a proposed resolution here?
NAME there is now.
Thanks, NAME Do folks agree with the proposed resolution?
NAME -- the PR needs some work, but I've only heard agreement on the resolution itself.
NAME : I think this is as well.
I see this concern, but consider a use case where it is expensive to send a large initial packet but the client has an a-priory key to generate trusted initial CIDs. In that case the client knows that entropy is sufficient and the server can validate the DCID. This can be done, for example, and deployment dependent, by using a blake-2b hash. For battery powered low-power radio devices, sending large initials can be prohibitive.
In most cases, the only reason a client will need to send another large Initial is if there is loss. In low-power conditions, coalescing of Initial and Handshake by the server can be used, at which point the client never has to send an Initial again. Unless there is loss, the client in this case only ever sends one 1200 byte packet. (And it probably never ever has to receive a packet that big ever again, assuming that the server doesn't decide to drop a 10k certificate on it.)
I can see that you might want to forego an a-priory key and pay 1200 bytes once to get a 0-RTT token. But the 1200 initial bytes still holds for 0-RTT, no? A low power device will frequently go to sleep, and might exceed to the idle timeout of the server (although the server might want to reconsider its timeout in that case). I could also see a client generating its own 0-RTT token based on a-priory keys and this way get around the 1200 bytes if 0-RTT permitted that.
QUIC still requires that the datagram be 1200 bytes, but most of that 1200 can be 0-RTT payload (after you include a token, session ticket, TLS key share, transport parameters, etc..). A new protocol specialized to this can define a different minimum packet size. That can be QUIC-compatible if it wants to be. For such a constrained deployment, that might be a good choice.
LGTM. My weak preference goes to adding a word or a phrase that indicates that this is an alternative to the strategy being described in the previous sentence (like below).I think this is a useful improvement.Thanks, NAME !

Handshake packet from the client, it can consider the client address to have been validated. <ins> Additionally, a server MAY consider the client address valididated if the client uses a connection ID chosen by the server and the connection ID contains at least 64 bits of entropy. </ins> Prior to validating the client address, servers MUST NOT send more than three times as many bytes as the number of bytes they have received.  This limits the magnitude of any amplification attack that


4.1.2. <del> Clients can cancel requests by resetting and aborting the request stream with an error code of H3_REQUEST_CANCELLED (http-error-codes). When the client aborts reading a response, it indicates that this response is no longer of interest.  Implementations SHOULD cancel requests by abruptly terminating any directions of a stream that are still open. When the server rejects a request without performing any application processing, it SHOULD abort its response stream with the error code H3_REQUEST_REJECTED.  In this context, "processed" means that some data from the stream was passed to some higher layer of software that might have taken some action as a result.  The client can treat requests rejected by the server as though they had never been sent at all, thereby allowing them to be retried later on a new connection. </del>  Servers MUST NOT use the H3_REQUEST_REJECTED error code for requests that were partially or fully processed.  When a server abandons a response after partial processing, it SHOULD abort its response stream with the error code H3_REQUEST_CANCELLED. <del> When a client resets a request with the error code H3_REQUEST_CANCELLED, a server MAY abruptly terminate the response using the error code H3_REQUEST_REJECTED if no processing was performed.  Clients MUST NOT use the H3_REQUEST_REJECTED error code, except when a server has requested closure of the request stream with this error code. </del>  <del> If a stream is cancelled after receiving a complete response, the </del>  client MAY ignore the cancellation and use the response.  However, if a stream is cancelled after receiving a partial response, the response SHOULD NOT be used.  Automatically retrying such requests is were or might be processed in this connection.  The server sends a client-initiated bidirectional Stream ID; the client sends a Push ID. Requests or pushes with the indicated identifier or greater are <del> rejected by the sender of the GOAWAY.  This identifier MAY be zero if no requests or pushes were processed. </del>  The information in the GOAWAY frame enables a client and server to agree on which requests or pushes were accepted prior to the

I still don't feel like this flows quite right, so suggestions appreciated.
it better be clarify Implementation means Client/Server or both. there are some of similar terms reset cancel reject abort (abruptly) terminate it seems better to make them more clarify what each of them describes and how they can be done.
"Implementations," throughout the document, refers to both clients and servers. Do you have a suggested place to clarify this? "reset" and "abrupt termination" are synonyms for the same operation in "cancel" and "reject" are separate actions an implementation can take, described in It looks like "abort" is used with relatively little context -- I'll try to clean that up a bit. It's intended to refer to "abort reading," another operation in .
I thought this section says about client, but suddenly appears. Clients can cancel requests by ~ When the client aborts reading a response ~ Implementations SHOULD cancel requests by ~ so I think this is also a client (that says about cancel response). but you mean this says both client/server ? server can cancel request too ? SGTM > I'll try to clean that up a bit
Yes, servers can cancel a request; that is, indicate that for some reason they will not be processing it any further. But perhaps it would be clearer to separately state that in a paragraph about server behavior.
Thank you for improving the specs!

4.1.2.  <ins> Once a request stream has been opened, the request MAY be cancelled by either endpoint.  Clients cancel requests if the response is no longer of interest; servers cancel requests if they are unable to or choose not to respond.  When possible, it is RECOMMENDED that servers send an HTTP response with an appropriate status code rather than canceling a request it has already begun processing. Implementations SHOULD cancel requests by abruptly terminating any directions of a stream that are still open.  This means resetting the sending parts of streams and aborting reading on receiving parts of streams; see Section 2.4 of QUIC-TRANSPORT. When the server cancels a request without performing any application processing, the request is considered "rejected."  The server SHOULD abort its response stream with the error code H3_REQUEST_REJECTED. In this context, "processed" means that some data from the stream was passed to some higher layer of software that might have taken some action as a result.  The client can treat requests rejected by the server as though they had never been sent at all, thereby allowing them to be retried later. </ins> Servers MUST NOT use the H3_REQUEST_REJECTED error code for requests that were partially or fully processed.  When a server abandons a response after partial processing, it SHOULD abort its response stream with the error code H3_REQUEST_CANCELLED.  <ins> Client SHOULD use the error code H3_REQUEST_CANCELLED to cancel requests.  Upon receipt of this error code, a server MAY abruptly terminate the response using the error code H3_REQUEST_REJECTED if no processing was performed.  Clients MUST NOT use the H3_REQUEST_REJECTED error code, except when a server has requested closure of the request stream with this error code. </ins>  <ins> If a stream is canceled after receiving a complete response, the </ins> client MAY ignore the cancellation and use the response.  However, if a stream is cancelled after receiving a partial response, the response SHOULD NOT be used.  Automatically retrying such requests is were or might be processed in this connection.  The server sends a client-initiated bidirectional Stream ID; the client sends a Push ID. Requests or pushes with the indicated identifier or greater are  <ins> rejected (request-cancellation) by the sender of the GOAWAY.  This identifier MAY be zero if no requests or pushes were processed. </ins> The information in the GOAWAY frame enables a client and server to agree on which requests or pushes were accepted prior to the


Packets are considered in-flight when they are ack-eliciting or contain a PADDING frame, and they have been sent but are not <del> acknowledged, declared lost, or abandoned along with old keys. </del>  3. 4.7. <del> QUIC uses a probe timeout (see pto), with a timer based on TCP's RTO computation.  QUIC's PTO includes the peer's maximum expected </del>  acknowledgement delay instead of using a fixed minimum timeout.  QUIC does not collapse the congestion window until persistent congestion (persistent-congestion) is declared, unlike TCP, which collapses the If a packet is lost, the QUIC transport needs to recover from that loss, such as by retransmitting the data, sending an updated frame, <del> or abandoning the frame.  For more information, see Section 13.3 of </del>  QUIC-TRANSPORT. Loss detection is separate per packet number space, unlike RTT implementations SHOULD NOT use a packet threshold less than 3; see RFC5681. <del> Some networks may exhibit higher degrees of reordering, causing a sender to detect spurious losses.  Algorithms that increase the reordering threshold after spuriously detecting losses, such as TCP- NCR (RFC4653), have proven to be useful in TCP and are expected to be at least as useful in QUIC.  Re-ordering could be more common with QUIC than TCP, because network elements cannot observe and fix the order of out-of-order packets. </del>  6.1.2. Packets that carry only ACK frames can be heuristically identified by observing packet size.  Acknowledgement patterns may expose <del> information about link characteristics or application behavior. Endpoints can use PADDING frames or bundle acknowledgments with other frames to reduce leaked information. </del>  8.3.

From URL, minus the question of limiting bursts, which is in Issue Section 2 " are not acknowledged, declared lost, or abandoned along with old keys." --> are not acknowledged, declared lost, or discarded along with old keys." Discarding is the term used in the rest of the doc notably in section 2.4. I see some reference to "abandon packets" in the transport draft as well. Would be good to use the same term for consistency everywhere. Section 4.4. "does not allow any acked packet to be reneged" --> "does not allow any acknowledged packet to be reneged" Section 4.7 "QUIC uses a probe timeout (see Section 6.2)," --> "QUIC uses a Probe Timeout (PTO) (see Section 6.2)," Section 5.2 "If the path's actual RTT increases, the minrtt will not adapt to it, allowing future RTT samples that are smaller than the new RTT be included in smoothedrtt." Would it safer to reset minrtt after persistent congestion is declared. This is important so we can adapt to a consistent spike in RTT as a result of a path change. Section 6.1.1 "Algorithms that increase the reordering threshold after spuriously detecting losses, such as TCP-NCR [RFC4653], have proven to be useful in TCP and are expected to at least as useful in QUIC." It might be better to refer to RACK here which has an optional feature to increase reorder threshold in time. I don't know of any major implementations that do TCP-NCR but maybe I am mistaken. Section 6.2.1 "time threshold Section 6.1.2" --> time threshold (Section 6.1.2) “The life of a connection that is experiencing consecutive PTOs is limited by the endpoint's idle timeout.” Is there any specification of recommended values for idle timeout? Any values lower than 9 seconds could adversely impact preserving of connectivity during VM live migration. Section 6.2.4 "If the sender wants to elicit a faster acknowledgement on PTO, it can skip a packet number to eliminate the ack delay." This adds some unnecessary complexity. Given that endpoint MAY send two full-sized datagrams, isn't that a better technique than skipping packet numbers? Section 7.8 "The congestion period is calculated as the time between the oldest and newest lost packets: (3 - 0) = 3." --> "The congestion period is calculated as the sent time difference between the oldest and newest lost packets: (3 - 0) = 3." It might be worth noting that persistent congestion can be declared without sending a PTO. Particularly because the example assumes PTOs. In the case where app kept on periodically sending new data and no ACKs came back for the threshold and then an ACK came back and declared all packets sent before it as lost. Section 8.2 "Endpoints can use PADDING frames or bundle acknowledgments with other frames to reduce leaked information." A reminder should be added here that PADDING counts towards in-flight so there is a possible impact on performance. Section B.8 The function AreAllPacketsLost does not have any pseudocode.
NAME The last item is pointed out in . As I wrote in , NAME suggested an example implementation. If you think it is reasonable, I will make a PR.
I've addressed the editorial bits in . The rest are addressed below. We don't have a recommended value for Idle timeout, but it the states the following: That said, the peer's is known to both sides. If you find that the peer's timeout is too short for your purposes, you can keep the connection alive through PINGs. On faster acknowledgement when PTO: yes, sending two packets works just fine. Skipping packet numbers is not something you need to do -- so the additional complexity concern is only for those who want to go do it -- it's just something that an implementation can do to get an immediate ack anytime in the connection.On PTOs and persistent congestion, there's text at the top of the section that calls this out explicitly: Nevertheless, I've added a sentence at the end of the example description. AreAllPacketsLost is in .
Marking this editorial, since the design one moved out.
One suggestion We don't do this, but there is a plan. URL shows the plan that we haven't implemented. The idea is that the TLS stack exports an encryption primitive, which then performs nonce generation within the PKCS module according to the TLS rules. PKCS has the necessary interfaces. Decryption can't work that way and so doesn't: the caller has to provide the number. I don't know if FIPS insists on anti-replay tracking within the module, but we don't do that (and likely can't).

Packets are considered in-flight when they are ack-eliciting or contain a PADDING frame, and they have been sent but are not  <ins> acknowledged, declared lost, or discarded along with old keys. </ins> 3. 4.7.  <ins> QUIC uses a probe timeout (PTO; see pto), with a timer based on TCP's RTO computation.  QUIC's PTO includes the peer's maximum expected </ins> acknowledgement delay instead of using a fixed minimum timeout.  QUIC does not collapse the congestion window until persistent congestion (persistent-congestion) is declared, unlike TCP, which collapses the If a packet is lost, the QUIC transport needs to recover from that loss, such as by retransmitting the data, sending an updated frame,  <ins> or discarding the frame.  For more information, see Section 13.3 of </ins> QUIC-TRANSPORT. Loss detection is separate per packet number space, unlike RTT implementations SHOULD NOT use a packet threshold less than 3; see RFC5681.  <ins> Some networks may exhibit higher degrees of packet reordering, causing a sender to detect spurious losses.  Additionally, packet reordering could be more common with QUIC than TCP, because network elements that could observe and fix the order of reordered TCP packets cannot do it with QUIC.  Algorithms that increase the reordering threshold after spuriously detecting losses, such as RACK RACK, have proven to be useful in TCP and are expected to at least as useful in QUIC. </ins> 6.1.2. Packets that carry only ACK frames can be heuristically identified by observing packet size.  Acknowledgement patterns may expose  <ins> information about link characteristics or application behavior.  To reduce leaked information, endpoints can bundle acknowledgments with other frames, or they can use PADDING frames at a potential cost to performance. </ins> 8.3.


This duration is computed as follows: <del> For example, assume: If an ack-eliciting packet is sent at time t = 0, the following scenario would illustrate persistent congestion: The first three packets are determined to be lost when the acknowledgement of packet 4 is received at t = 8.  The congestion period is calculated as the time between the oldest and newest lost packets: (3 - 0) = 3.  The duration for persistent congestion is equal to: (1 * kPersistentCongestionThreshold) = 3.  Because the threshold was reached and because none of the packets between the oldest and the newest packets are acknowledged, the network is considered to have experienced persistent congestion. </del>  When persistent congestion is established, the sender's congestion window MUST be reduced to the minimum congestion window

Makes the period computation a bit clearer. Also addresses NAME comment in .
Looks like there is some confusion on Slack about the case where disjoint sets of packets are lost. Do we need two examples rather than one?
NAME This confusion now has its own issue: .
I'm going to merge this. Happy to fix any more editorial issues separately.
A sender sends a tail of 10 packets, packet number 1 to 10. It now receives an acknowledgement for packets 5 and 10, which establishes the loss of all other packets. It's not clear how to perform the check for persistent congestion now. Several options come to mind: check if the period between 6 and 9 is longer than 3PTO check if either the period between packet 1 and 4 or the period between packet 6 and 9 is longer than 3PTO The ACK doesn't establish loss of all tail packets, so there's no persistent congestion. In an offline discussion, both NAME and NAME expressed preferences for different options here. I'm not sure what the right thing to do is here, but at least the draft should be clearer on what exactly establishes persistent congestion and what doesn't.
Consider the case where the sender receives an acknowledgement for packets from 5 to 10, and the period between 1 and 4 was longer than 3*PTO. IIUC, this is clearly a persistent congestion. The original example is strictly worse than that, and I think it would make sense to declare persistent congestion in the case too. In other words, I think that 3 is incorrect. That said, I do not have a strong opinion regarding if we should specifically recommend either of 1 or 2, considering that the logic (or the time threshold) being used to detect persistent congestion is merely a recommendation.
FWIW, we had some discussion regarding the design rationale behind persistent congestion in , and it seemed to me that it was mostly about the amount of time the sender cannot make any progress.
seems also related
I think we should keep it simple, which IMO is do nothing. Or if we want to add a recommendation, we can say that you start with the largest lost packet and try to detect Persistent congestion until either you have found 1 occurrence or reached the end of list.
This is fixed in , and is an editorial fix.
The example shown counts multiples of the PTO, so the maxackdelay and rttvar values chosen aren't really relevant. The first is only ever 0 during the handshake and the second is never zero unless you have faster than light packet transmission. Counting number of times the base PTO lapses is clearer, and avoids having to use unrealistic values for these two values.
Thanks Jana, this is a nice improvement.

This duration is computed as follows:  <ins> Unlike the PTO computation in pto, persistent congestion includes the max_ack_delay irrespective of the packet number spaces in which losses are established. The following example illustrates how persistent congestion can be established.  Assume: Consider the following sequence of events: Packets 2 through 8 are declared lost when the acknowledgement for packet 9 is received at t = 12.2. The congestion period is calculated as the time between the oldest and newest lost packets: 8 - 1 = 7.  The duration for establishing persistent congestion is: 2 * 3 = 6.  Because the threshold was reached and because none of the packets between the oldest and the newest lost packets were acknowledged, the network is considered to have experienced persistent congestion. While this example shows the occurrence of PTOs, they are not required for persistent congestion to be established. </ins> When persistent congestion is established, the sender's congestion window MUST be reduced to the minimum congestion window


Receivers MAY route based on the information in the first packet contained in a UDP datagram.  Senders MUST NOT coalesce QUIC packets <del> for different connections into a single UDP datagram.  Receivers </del>  SHOULD ignore any subsequent packets with a different Destination Connection ID than the first packet in the datagram.

Section 12.2 makes two quasi-conflicting statements about coalesced packets. In a connection where the sender has multiple CIDs in hand, using two different CIDs in the two coalesced packets is entirely consistent with the guidance to the sender, but would be dropped based on the guidance to the receiver. We should align these two statements.
Fixing this would require a change to normative language. I'm of the opinion that the first should change to different connection IDs rather than connections; the second might change to a MUST NOT, but it could remain a SHOULD and achieve the same end. The main reason for this is that routing will be based on the first packet. But there is also a secondary benefit in not creating a strong correlation between different connection IDs for the same connection.
But this complicates the coalescing logic, because you no longer only need to look at packet types but now also CID fields. At least in my stack, that is an ugly change to make at this stage.
The alternative, which I can now see is probably a smaller change is:
What motivated this was two observations: we don't coalesce except during the handshake, where we are not migrating some stacks generate packets for coalescing while processing, and so they might generate a datagram with multiple connection IDs
NAME writes: This is much simpler alternative than what is being proposed. Now an implementation can only drop a packet after doing a connection lookup, whereas before it could just compare the CID to the previous value. If this is just a matter of personal preference, I vote for this approach, so now the score is 1:1 (with NAME being on the opposite side). To take a step back: why is recommendation to drop packets exist in the first place? If a sender wants to coalesce packets from different connections, why not let it?
I think it should not allow having multiple packets for different connections in a single UDP datagram - please. Currently I see implementations (including mine) have something like: Receive UDP packet. Parse packet header. Check CID, find the connection handler. Pass the packet to the respective connection (thread) to handle the packet. I think it makes implementations much more complicated (already). Also I wonder it may cause security issue if having bad assumption?
The classic example would be coalescing Handshake packets, whose DCID is the SCID used by the server, and Initial or Zero-RTT packets, whose DCID is the client-chosen Initial DCID. The client MAY switch to using the server chosen CID for Initial packets. Maybe we should say that it MUST do that if it is going to coalesce Initial and Handshake packets. Not so clear about 0-RTT packets. Is it actually allowed to send them with a different DCID than the Initial DCID?
NAME : Our assumption has been that load balancers, for instance, can simply route on the CID in the first header they see in the UDP datagram. If we allow different connections to be coalesced, that complicates things. It may also start to leak some things about the way load is distributed within the server network: if some CIDs when combined cause loss and some don't, that reveals some things about how the server load is distributed. Is there a strong argument for changing this behavior? This new language in was always the intent. I suspect it didn't change when we started allowing multiple CIDs within a connection. Unless there's an argument for changing behavior, this is really an editorial PR.
NAME If I understand correctly, every time you change the connection ID, you change the connection ID for all packet number spaces. Assuming that my reading of the spec is correct in this regard, it seems like there's no compelling reason for making this change?
NAME the spec actually says (7.2): The first flight of 0-RTT packets use the same Destination Connection ID and Source Connection ID values as the client's first Initial packet. Upon first receiving an Initial or Retry packet from the server, the client uses the Source Connection ID supplied by the server as the Destination Connection ID for subsequent packets, including any 0-RTT packets. SO you are correct, there is no ambiguity.
NAME Thank you for looking this up. Then I'm opposed to making any change here. Not only is it unnecessary (that alone should be enough), it would also make it more difficult to evolve the way we deal with coalesced packets in the future: I can imagine a future QUIC version introducing a way to authenticate the entire coalesced packet, while at the same time compressing the header. This will be easier if we keep the explicit property that all connection IDs of the coalesced packets match.
NAME I'm not sure if I agree. Coalescing of QUIC packets is a feature specific to v1, entities that operate without the knowledge of the connection (e.g., load balancers, datagram dispatchers) are not expected to consult the CID values of QUIC packets that are appended. Therefore, what we are talking here does not affect what we can do in the future.
That argument was based on the thought that it will be easier in a future QUIC version because the diff to v1 would be smaller. My main point is that this change is unnecessary because when endpoints change connection IDs, they change it for all packer types at the same time.
NAME Thank you for the clarification. I think I do not agree to this, too. IIUC, the consensus we reached with was that the CID of a Handshake packet and that of the 1-RTT packet can be different. While it is true that the initial DCID set by the client is expected to change when the client receives the first "active" connection ID supplied by the server, the fact does not mean that the CIDs of all QUIC packets within a datagram has to be the same in all the cases.
I think we need to make a change, in one direction or the other. It's unfortunate to mix CIDs, since it clearly links them to any observer, but doesn't seem like a big deal. For some implementations, it sounds like it's a big deal to avoid mixing them. Therefore, while I'm content with either resolution, it seems like it makes more sense to resolve toward being more permissive; e.g. . Mixing packets from different connections seems like a generally bad idea, since it assumes an implementation structure on the far end for it to work. The more likely outcome is that coalesced packets from other connections will simply be dropped as invalid. (Of course, this very property could be used for padding / misleading observers.)
Hi Mike, Can you say more about why you don't see this as an issue? Is it because this use case is most likely during the handshake? Or is there some other linkability already present that makes this less of an issue? regards, Ted
Some implementations try to change the CID as soon as possible, to avoid linking 1RTT traffic with the CID visible during the handshake and to improve privacy. There will be a brief window when they still need to send both some handshake packets and some 1RTT packets. But coalescing a Handshake packet with the old CID and a 1RTT packet with the new CID links old and new CID and thus negates the effort. And just in case someone asks, no, you don't really need to decrypt the packets to access the second CID. You just need the coalesced packet and another 1RTT packet. Not hard.
NAME If that is the concern, wouldn't it be better for endpoints to migrate to a new CID after learning that the peer has dropped the Handshake keys? If an endpoint switches to the new CID before the handshake keys are dropped by the peer, there's chance that the peer might use a new CID in the Handshake packets that the peer sends. That at least leaks when the connection was established for that CID.
NAME yes, the simplest way to not run into coalescing problems is to not start migrations or CID changes before Handshake Done.
Apologies, I'm getting lost in the weeds here. I agree with NAME that we need to make a change, and that is really meant to fix the text to what the intent always was. NAME NAME NAME Are we all agreeing on this point?
NAME That is my view. I think to be an editorial amendment of . Regarding linkability problem, the simple and safe way of mitigating the problem (as proposed in URL) does not interfere with the proposed resolution.
NAME I personally prefer the old version, force single CID in coalesced packet. Loosening the text to allow multiple CID requires a code change in my implementation.
I'm with NAME Single CID.
Both, actually. This occurs during the handshake, when addresses are required to be stable. Unless you're running a large number of parallel connections, all CIDs used on that 4-tuple during the handshake belong to the same connection with very high probability. You can break that linkage by deliberately changing CID and port at the same time, but you can't do that until the handshake is confirmed. If you want the linkage broken, you need to do that jump post-handshake anyway.
Hi Mike, I think "high probability" is probably app-dependent, because there may be cases where there are significant numbers of parallel connections. In the case of proxied queries, for example, the use of the same 4-tuple but a different CID could turn out to be common, because the proxy may prefer to keep traffic from its different clients separate even if they could be served by the same upstream server. A DNS example: if you have an DNS resolver talking to an upstream proxy or a common authoritative server, that resolver may want to set a different Client Subnet for different query streams, so that input can be used to determine the preferred reply. In the presence of a NAT or CGNAT, there could be interesting interaction with the dwell timers in the state table of the middlebox. If you change it too late, the middlebox may recycle the most recently freed port, which could result in you getting the same 4-tuple to an observer on that part of the path. Speaking strictly as an individual, I think using a single CID per coalesced packet as Christian prefers is cleaner and leaves open app behaviors we may not have yet identified. regards Ted I'm not a big fan of Client Subnet, and these particular parallel connections may or may not pose a risk; it's just a proxy situation where the parallelism might occur.
The discussion is active, I'm marking this as design.
NAME I might be missing something, but if an endpoint wants to cut linkability between the packets used during the handshake and the packets used in 1-RTT, that endpoint has to switch to a new CID after the peer has dropped its Initial and Handshake keys, regardless of the outcome of this issue. That is because if the endpoint switches to a new CID while the peer is still sending Initial or Handshake packets, we allow the peer to use that new CID in the long header packets sent in response. Therefore, I do not think that requiring endpoints to use one DCID in all the coalesced QUIC packets help us in practice.
In my implementation, ngtcp2, it processes packet in the way that PR proposes; which means I just over looked existing text, but anyway, I think that processing a packet atomically is cleaner approach in implementation wise rather than passing a some kind of pointer to the very first packet header.
NAME Once a non-coalesced packet is passed to a connection, in my implementation I don't have to do any checking. The server already makes sure to pass packets to the correct connection based on the DCID. So there's additional validation logic required for coalesced packets. And the easiest way is to just check that all DCIDs match. Sure, it would be possible to somehow query which other DCIDs would also be valid for the same session, but it's more complicated. And given that nobody has come up with any good reason to coalesce packets with different DCIDs in the first place, I'm wondering how this additional complexitiy would be justified.
I don't foresee a large problem with the proposed change, but I also don't think it's necessary.
Do we have a proposed resolution for this one?
I think we settled on requiring connection IDs to be the same, but I haven't had the time to write that up, so not just yet.
NAME FTR, discussion happened on the mailing list . My sense is that it's still not clear to some (including me) why the current fix in (requiring "connections" to be the same) doesn't work, but nobody is arguing strongly against what NAME said above (requiring "connection IDs" to be the same). So that might be the least common denominator. URL discovered a minor "discrepancy" in our requirements for coalescing packets. We required that: senders not coalesce packets for different connections in the same datagram. receivers reject packets with different connection IDs in the same datagram. URL was the result of a discussion where I was convinced that looser constraints here weren't harmful. The requirements would identify connections, not connection IDs. Originally, my inclination was to fix the requirement on the sender to use connection IDs, which would remove some linkability. But then I was reminded that linkability is already defeated for several reasons. You can't coalesce outside of the handshake and we require a stable path for the handshake AND long headers include a source connection ID that can be used for linkability. So the linkability concern is basically lost. We also learned that some implementations were generating packets while processing incoming packets. Later they would coalesce these into a datagram that might then have different connection IDs on the packets it contains. There has been some opposition to the proposed resolution in PR 3870. Apparently, for some, having multiple connection IDs in the same datagram complicates processing. I don't understand this objection. It seems to me more difficult to retain state across packets than it is to process each atomically. I was hoping that Christian or Nick can explain more about how this affects them. Marten also indicated that there was a reason not to allow this, but if I understand that, this is based on some potential for future optimization, which could be conditional on having consistent connection IDs. If we moved to a scheme that required consistent connection IDs, anyone using that scheme could be required to avoid coalescing different connection IDs. So this seems more of a theoretical concern. And - in anticipation of maybe choosing to make senders use a consistent connection ID - if you might be generating coalesced datagrams with different connection IDs in packets, how hard would it be to ensure that these are consistent? Lars and Ian, I think both of you indicated that you might do this.

Receivers MAY route based on the information in the first packet contained in a UDP datagram.  Senders MUST NOT coalesce QUIC packets  <ins> with different connection IDs into a single UDP datagram.  Receivers </ins> SHOULD ignore any subsequent packets with a different Destination Connection ID than the first packet in the datagram.


MUST NOT consider these streams to have any meaning upon receipt. The payload and length of the stream are selected in any manner the <del> implementation chooses.  Implementations MAY terminate these streams cleanly, or MAY abruptly terminate them.  When terminating abruptly, the error code H3_NO_ERROR or a reserved error code (http-error- codes) SHOULD be used. </del>  7.

I totally misread this passage, so let's make it more emphatic which side of the stream we're talking about.
In Section 6.2.3, we recommend the following about the reserved unidirectional stream types: However, this has a fatal flaw -- it recommends special handling for the grease types, which is precisely the behavior we're trying to disincent with grease. Instead, I think we should move the recommendation to use reserved error codes to the guidance on what to do with unknown stream types, which is currently: This has the same net effect on the grease streams. The change in behavior is on actual unknown-but-real stream types, which will now get grease error codes in response.
+1 lets do this (as an individual)
Actually, scratch that -- it's a misreading. This is a statement about how the sender of the stream should close them, not about behavior on receipt at all. I'll take this as editorial to emphasize that this is purely about how to send them.

MUST NOT consider these streams to have any meaning upon receipt. The payload and length of the stream are selected in any manner the  <ins> sending implementation chooses.  When sending a reserved stream type, the implementation MAY either terminate the stream cleanly or reset it.  When resetting the stream, either the H3_NO_ERROR error code or a reserved error code (http-error-codes) SHOULD be used. </ins> 7.


2.4. <del> There are certain operations that an application MUST be able to perform when interacting with QUIC streams.  This document does not specify an API, but any implementation of this version of QUIC MUST expose the ability to perform the operations described in this section on a QUIC stream. </del>  <del> On the sending part of a stream, application protocols need to be able to: </del>  write data, understanding when stream flow control credit (data- flow-control) has successfully been reserved to send the written frame (frame-reset-stream) if the stream was not already in a terminal state. <del> On the receiving part of a stream, application protocols need to be able to: </del>  read data; and abort reading of the stream and request closure, possibly resulting in a STOP_SENDING frame (frame-stop-sending). <del> Applications also need to be informed of state changes on streams, including when the peer has opened or reset a stream, when a peer aborts reading on a stream, when new data is available, and when data can or cannot be written to the stream due to flow control. </del>  3. 5.3. <del> There are certain operations that an application MUST be able to perform when interacting with the QUIC transport.  This document does not specify an API, but any implementation of this version of QUIC MUST expose the ability to perform the operations described in this section on a QUIC connection. </del>  <del> When implementing the client role, applications need to be able to: </del>  open a connection, which begins the exchange described in handshake; be informed when Early Data has been accepted or rejected by a server. <del> When implementing the server role, applications need to be able to: </del>  listen for incoming connections, which prepares for the exchange described in handshake; from the client's resumption ticket and enable rejecting Early Data based on that information. <del> In either role, applications need to be able to: </del>  configure minimum values for the initial number of permitted streams of each type, as communicated in the transport parameters

The strong requirements here were wrong. What is necessary from these sections is defining operations in QUIC that application protocols can depend on being present. Levying the requirement on implementations was incorrect. As some application protocols don't use certain QUIC features, it is not necessary to require implementations to support features if they are not general purpose. This rephrases these requirements without normative language, instead concentrating on what services the application protocol can depend on. This translates into requirements on implementations, but only via the application protocols they intend to support.
S 5.4 requires that the application be able to control resource allocation of various types, including flow control and the number of permitted streams of each type; This seems like a good idea, but it seems like one could have a reasonable QUIC stack that did not do this.
I would prefer to leave this. Noting that the level of control is not specified. This also suggests that the stack provide a keep-alive control, but there are likely cases (I'm thinking here of DNS over QUIC) where that is unlikely to be a useful feature. The goal is to list things that the specification believes a generic stack will provide. Maybe we could avoid the implication that such an implementation is incomplete by saying that specialized implementations might provide less than a complete API.
NAME willing to fight for this one?
After discussing with editors, we think that it would be better to remove the MUST in this section and say something like: "A general purpose QUIC implementation provides the following functions:" and add "A specialized implementation might provide a subset of these." The same applies to the stream operations.
I think that is a positive change. Borderline design issue by the sound of it. It would be good to see the PR once its available.

2.4.  <ins> This document does not define an API for QUIC, but instead defines a set of functions on streams that application protocols can rely upon. An application protocol can assume that an implementation of QUIC provides an interface that includes the operations described in this section.  An implementation designed for use with a specific application protocol might provide only those operations that are used by that protocol. </ins>  <ins> On the sending part of a stream, an application protocol can: </ins> write data, understanding when stream flow control credit (data- flow-control) has successfully been reserved to send the written frame (frame-reset-stream) if the stream was not already in a terminal state.  <ins> On the receiving part of a stream, an application protocol can: </ins> read data; and abort reading of the stream and request closure, possibly resulting in a STOP_SENDING frame (frame-stop-sending).  <ins> An application protocol can also request to be informed of state changes on streams, including when the peer has opened or reset a stream, when a peer aborts reading on a stream, when new data is available, and when data can or cannot be written to the stream due to flow control. </ins> 3. 5.3.  <ins> This document does not define an API for QUIC, but instead defines a set of functions for QUIC connections that application protocols can rely upon.  An application protocol can assume that an implementation of QUIC provides an interface that includes the operations described in this section.  An implementation designed for use with a specific application protocol might provide only those operations that are used by that protocol. </ins>  <ins> When implementing the client role, an application protocol can: </ins> open a connection, which begins the exchange described in handshake; be informed when Early Data has been accepted or rejected by a server.  <ins> When implementing the server role, an application protocol can: </ins> listen for incoming connections, which prepares for the exchange described in handshake; from the client's resumption ticket and enable rejecting Early Data based on that information.  <ins> In either role, an application protocol can: </ins> configure minimum values for the initial number of permitted streams of each type, as communicated in the transport parameters


7.9. <del> This document does not specify a pacer, but it is RECOMMENDED that a sender pace sending of all in-flight packets based on input from the congestion controller.  Sending multiple packets into the network without any delay between them creates a packet burst that might cause short-term congestion and losses.  Implementations MUST either use pacing or another method to limit such bursts to the initial congestion window; see initial-cwnd. </del>  An implementation should take care to architect its congestion controller to work well with a pacer.  For instance, a pacer might

Also does a bit of cleanup.
NAME : Can you provide some text? Also, shall we make this part of the consensus call?
NAME : I don't think we are missing it. The PR says that you SHOULD pace or limit the burst size to IW at all times. Isn't that what you want -- to limit bursts to IW?
"Implementations MUST either use pacing or another method to limit such bursts to the initial congestion window; see Section 7.2." This precludes use of segmentation offload of sizes greater than IW. Hardware does not pace TSO for TCP or USO for UDP. In datacenters we routinely send bursts that are higher without causing loss. Both Linux and Windows TCP send bursts larger than IW (up to 64k) at higher bandwidths. Higher bursts are required for achieving high data rates. This text should be changed to a SHOULD.
NAME DC requirements are likely to run into more than just this problem -- for instance, the initial window itself might be too small. Since the recovery document was really meant for Internet use, not for DC, I would suggest leaving out DC requirements. Perhaps there's a separate document in order for how to tune QUIC for DC.
As an individual, I agree with Jana. We're not going to be able to ship a specification that is able to accommodate all of the spectrum of network characteristics. The documents should allow implementers to build a QUIC implementation that is safe to deploy on the Internet. As someone less familiar with DCTCP and more general DC considerations, the nuance of making a MUST a SHOULD is likely to be lost on me. A document that takes a more broad look as QUIC in the DC would be super helpful.
Also being discussed
Based on the discussion on the mailing list, I'm proposing that we change the MUST to a SHOULD. See .
Do folks agree with this proposed resolution?
NAME I think it's the right direction, but I'd like to do some more work on the PR. We don't do this, but there is a plan. URL shows the plan that we haven't implemented. The idea is that the TLS stack exports an encryption primitive, which then performs nonce generation within the PKCS module according to the TLS rules. PKCS has the necessary interfaces. Decryption can't work that way and so doesn't: the caller has to provide the number. I don't know if FIPS insists on anti-replay tracking within the module, but we don't do that (and likely can't).

7.9.  <ins> A sender SHOULD pace sending of all in-flight packets based on input from the congestion controller. Sending multiple packets into the network without any delay between them creates a packet burst that might cause short-term congestion and losses.  Senders MUST either use pacing or limit such bursts. Senders SHOULD limit bursts to the initial congestion window; see initial-cwnd.  A sender with knowledge that the network path to the receiver can absorb larger bursts MAY use a higher limit. </ins> An implementation should take care to architect its congestion controller to work well with a pacer.  For instance, a pacer might


On confirming a peer's ownership of its new address, an endpoint MUST immediately reset the congestion controller and round-trip time estimator for the new path to initial values (see Appendices A.3 and <del> B.3 in QUIC-RECOVERY) unless it has knowledge that a previous send rate or round-trip time estimate is valid for the new path.  For instance, an endpoint might infer that a change in only the client's port number is indicative of a NAT rebinding, meaning that the new path is likely to have similar bandwidth and round-trip time. However, this determination will be imperfect.  If the determination is incorrect, the congestion controller and the RTT estimator are expected to adapt to the new path.  Generally, implementations are advised to be cautious when using previous values on a new path. </del>  There may be apparent reordering at the receiver when an endpoint sends data and probes from/to multiple addresses during the migration

This LGTM and would address my concern.
I think this is good to go.
immediately reset the congestion controller and round-trip time estimator for the new path to initial values (see Sections A.3 and B.3 in {{QUIC-RECOVERY}}) unless it has knowledge that a previous send rate or round-trip time estimate is valid for the new path. For instance, an endpoint might infer that a change in only the client's port number is indicative of a NAT rebinding, meaning that the new path is likely to have similar bandwidth and round-trip time. This is kind of an odd formulation. What knowledge does the endpoint have in this case that we don't already have? Why not just make an explicit exception for port-only changes?
...because that would create an explicit endorsement of the practice. This text was somewhat carefully negotiated to avoid creating that sort of endorsement. That said, I expect people to ignore port. But I also expect some servers to be able to recognize the presence of a NAT that uses a pool of addresses and to treat all of those addresses as equivalent also. And in IPv6 there are many cases where parts of the address are not relevant (/64 per host means that you might as well not bother with the last half in a lot of cases now). So there is a risk in narrowly permitting port variations that certain types of address validation shortcuts are implicitly excluded.
I see what you mean, but I think that this kind of hinting at the user without explicitly stating it does a disservice to the implementor, who then needs to figure it out for themselves. IMO we should explicitly state the exceptions that the WG believes are permissible and then add this escape hatch text.
I'm not sure I have a better suggestion than the existing text for NAME concern, but I noticed a different small possible issue. The process of validating the path is very likely to give you a RTT measurement for the path, so it might be worth tweaking the MUST to say "MUST ... and discard any round trip estimates associated with the old path" or just "MUST not use round trip time measurements from the old path on the new one."? One could keep the estimates for the old path, as we've discussed, so I think saying you must reset or discard the information is a bit strong.
This isn't meant to be a disservice to the implementer -- to an unaware implementer, the requirement is clear: reset the RTT and the congestion controller. This is meant to provide an escape hatch for those experimenting or for those who have other sources of information. For example, it might be possible that address changes within a /24 are also NAT rebinding (and there's evidence of this), so we want to allow endpoints to use this knowledge, but we don't want to specify that behavior. I think a "MUST ... unless" captures that.
I agree that we should leave room for the implementor do do more. However, what I am saying is that we already know that port changes (and perhaps lower-64 changes for v6) are likely to be rebinding and so we should tell people that rather than asking them to figure it out for themselves.
I would be weary about a blanket statement that if a port (or address) change is merely rebinding, then transmission should go on with the previously estimated rate and window. The port or address change is a network event indicating that something changed. It used to be that NAT rebinding were strongly correlated with transmission hiatus -- the NAT would recycle bindings if it did not observe traffic for some time, typically about 30 seconds. It may also well be that the rebinding indicates a routing change on the path to the NAT. In both cases, the network condition did change and merely reusing the old estimates is a mistake.
I'm not sure I agree with this analysis, but if that's the group consensus then we should prohibit this behavior, not have a MUST NOT with an unclear exception. My point is that what goes in this document should be what the WG thinks that generic internet servers ought to do.
I agree resetting congestion state is better in this case. Since NAT rebinding only occurs on idle, resetting state seems in spirit of past recommendations around resetting congestion window on idle. I think we should remove the exception.
NAME it would be nice if congestion response allowed better choices than "continue as nothing happened" and "go back to start as if nothing was known about the path". I think the endpoints have some knowledge about the past of the path, and also an indication that this knowledge could be wrong. How wrong might depend on timing, e.g., time since past measurements. I could see applying some Bayesian logic there. But this is clearly research.
I'd like to point out that this is unilateral endpoint behavior, and removing the exception does not mean that deployments won't implement different things here. This is at a minimum something that implementations will play around with, and the exception allows room for it because we believe it's safe to allow some discretion here.
NAME we might debate how safe it is to keep sending as if nothing changed. There is some risk of creating a spike of congestion on the new path if the assessment was wrong. On the other hand, doing that when "just the port changed" will work most of the time, especially if the congestion controller already shrinks the window after the link goes idle, which many do. So in the hand I agree that we are probably better off just keeping the existing text.
I'm still happy with the existing text, FWIW. I think it captures the important points.
NAME pointed me to this text, which could add confustion on this point. URL My current reading is that you don't have to reset the congestion controller if you're pretty sure it's a NAT rebind, but you still need to perform path validation. Is that correct?
Follow-up Q: Do you need to wait for a PATH_RESPONSE before sending on the new path(which delays application data 1RTT), or can you concurrently validate the path while also starting to use it?
There is a security risk if a client could convince a server to send data to an arbitrary address. The challenge mitigates that risk.
Could an amplification attack limit be imposed to allow some usage of the new path before validating it?
We're in the process of implementing this, and as currently specified the draft adds a round trip delay compared to what we did in GoogleQUIC. Using a middle ground with an amplification limit as suggested by NAME might be required to ensure reasonable performance.
The amplification limit can probably be used, but it's likely going to not be good enough, since you probably just received a request after a long pause and not a full sized packet. NAME Does GQUIC have this amplification protection at the moment?
NAME currently Connection Migration on gQUIC does not use an amplification limit nor path validation. We haven't seen that kind of attack at scale. I think we should add those protections when we implement the IETF take on Connection Migration, but we'll need to make sure the performance hit isn't too high. If we go with the amplification factor then we can have the client send a bunch of padding to make it work.
It'd be great if clients could send a full-sized datagram after quiescence. But regardless, I'm not too worried about servers going above 3x in this case, because only an on-path attacker can use this behavior to mount a reflection attack.
In section 9.3 "Responding to Connection MIgration", we allow sending the initial window per estimated round-trip time. There's no mention of amplification limit I can see. URL An endpoint MAY send data to an unvalidated peer address, but it MUST protect against potential attacks as described in Section 9.3.1 and Section 9.3.2. In 9.3.1: Until a peer's address is deemed valid, an endpoint MUST limit the rate at which it sends data to this address. The endpoint MUST NOT send more than a minimum congestion window's worth of data per estimated round-trip time (kMinimumWindow, as defined in [QUIC-RECOVERY]). .... If an endpoint skips validation of a peer address as described in Section 9.3, it does not need to limit its sending rate.
Ah, yes, thank you for finding that text. I think we did that because of what Kazuho said -- that only an on-path attacker can mount this reflection attack. NAME NAME Given the text in 9.3, is there a problem anymore?
NAME we'll have to run experiments to confirm that. Since kMinimumWindow is defined as 10 packets, that might be enough for most requests. If it doesn't turn out to be enough, we may need to change the limit to - but we'll collect data before we propose a change like that
Proposed resolution is to close with no action, consensus to be confirmed
I think I'm ok with close with no action from a design perspective, but I find having this text in multiple locations and having similar, but different statements quite confusing. I'd rather you label this editorial and we try to make the text a bit clearer and/or better organized.
NAME and my read of the discussion was that a solution that was going to address the issue would have to be a design change. Hence it's a design issue, and the resolution is "no change". We can definitely spin out some related editorial changes into another issue though.
NAME -- what text organization do you think might help?
OK, I'll label this as ready, and you can spin a new editorial issue for any such changes.
NAME a state transition diagram would be very nice I think, similar to streams. And maybe some reorg, but I need to re-read the relevant sections at once to understand how that could work.
More discussion
FYI, I've written up Ted's suggestion from the mailing list as (with some word-smithing)
Due to the discussion during the consensus call, we'll run this through the Late-stage process again.
We should mark it proposal-ready though, right?
For clarity sake, the proposed resolution is .
Discussion on-list continues, but if this is the direction we choose... believe that the current text is appropriate. As I said, I don't have a strong opinion about the outcome, but we shouldn't just hint to people that they should ignore the port. -Ekr

On confirming a peer's ownership of its new address, an endpoint MUST immediately reset the congestion controller and round-trip time estimator for the new path to initial values (see Appendices A.3 and  <ins> B.3 in QUIC-RECOVERY) unless the only change in the peer's address is its port number.  Because port-only changes are commonly the result of NAT rebinding or other middlebox activity, the endpoint MAY instead retain its congestion control state and round-trip estimate in those cases instead of reverting to initial values.  In cases where congestion control state retained from an old path is used on a new path with substantially different characteristics, a sender may transmit too aggressively until the congestion controller and the RTT estimator have adapted.  Generally, implementations are advised to be cautious when using previous values on a new path. </ins> There may be apparent reordering at the receiver when an endpoint sends data and probes from/to multiple addresses during the migration


A variable-length integer error code which indicates the reason for closing this connection.  A CONNECTION_CLOSE frame of type <del> 0x1c uses codes from the space defined in error-codes.  A CONNECTION_CLOSE frame of type 0x1d uses codes from the </del>  application protocol error code space; see app-error-codes. A variable-length integer encoding the type of frame that 20. <del> QUIC error codes are 62-bit unsigned integers. </del>  This section lists the defined QUIC transport error codes that may be <del> used in a CONNECTION_CLOSE frame.  These errors apply to the entire connection. </del>  An endpoint uses this with CONNECTION_CLOSE to signal that the connection is being closed abruptly in the absence of any error. Finally, generic errors are provided for conditions where implementations are unable or unwilling to use more specific codes. <del> 20.1. </del>  <del> Application protocol error codes are 62-bit unsigned integers, but the management of application error codes is left to application </del>  protocols.  Application protocol error codes are used for the RESET_STREAM frame (frame-reset-stream), the STOP_SENDING frame (frame-stop-sending), and the CONNECTION_CLOSE frame with a type of

The current structure is incorrect, because application error codes is a subsection of transport error codes, despite application error codes being disjoint from transport error codes.
Wonderful. Thank you.

A variable-length integer error code which indicates the reason for closing this connection.  A CONNECTION_CLOSE frame of type  <ins> 0x1c uses codes from the space defined in transport-error-codes. A CONNECTION_CLOSE frame of type 0x1d uses codes from the </ins> application protocol error code space; see app-error-codes. A variable-length integer encoding the type of frame that 20.  <ins> QUIC transport error codes and application error codes are 62-bit unsigned integers. 20.1. </ins> This section lists the defined QUIC transport error codes that may be  <ins> used in a CONNECTION_CLOSE frame with a type of 0x1c.  These errors apply to the entire connection. </ins> An endpoint uses this with CONNECTION_CLOSE to signal that the connection is being closed abruptly in the absence of any error. Finally, generic errors are provided for conditions where implementations are unable or unwilling to use more specific codes.  <ins> 20.2. </ins>  <ins> The management of application error codes is left to application </ins> protocols.  Application protocol error codes are used for the RESET_STREAM frame (frame-reset-stream), the STOP_SENDING frame (frame-stop-sending), and the CONNECTION_CLOSE frame with a type of


8. QUIC allows the application to abruptly terminate (reset) individual <del> streams or the entire connection when an error is encountered.  These are referred to as "stream errors" or "connection errors" and are described in more detail in QUIC-TRANSPORT. </del>  An endpoint MAY choose to treat a stream error as a connection error under certain circumstances.  Implementations need to consider the

The application may reset a stream, for example, when a browser tab is closed or when the pushed resource is found in cache. It might close the connection when the application is closed. These are not errors per se, even though the term "stream error" and "connection error" is used for these events, and they have an associated "error code".

8. QUIC allows the application to abruptly terminate (reset) individual  <ins> streams or the entire connection; see Sections 2.4 and 5.3 of QUIC- TRANSPORT.  These are referred to as "stream errors" or "connection errors" (see Section 11 of QUIC-TRANSPORT) and have associated error codes, but do not necessarily indicate a problem with the connection or either implementation.  For example, a stream can be reset if the requested resource is no longer needed. </ins> An endpoint MAY choose to treat a stream error as a connection error under certain circumstances.  Implementations need to consider the


ack-eliciting packet is sent or acknowledged, when the handshake is confirmed, or when Initial or Handshake keys are discarded.  This ensures the PTO is always set based on the latest RTT information and <del> for the last sent packet in the correct packet number space. </del>  When ack-eliciting packets in multiple packet number spaces are in flight, the timer MUST be set for the packet number space with the 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>  6.2.3.

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

ack-eliciting packet is sent or acknowledged, when the handshake is confirmed, or when Initial or Handshake keys are discarded.  This ensures the PTO is always set based on the latest RTT information and  <ins> for the last sent ack-eliciting packet in the correct packet number space.  When the PTO expires and there are no ack-eliciting packets in flight, the PTO is set from that moment. </ins> When ack-eliciting packets in multiple packet number spaces are in flight, the timer MUST be set for the packet number space with the 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  <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.