packet; the client has to rely on the exchange that included the Retry packet for that property. <del> test-vectors-initial contains test vectors for the initial packet encryption. </del>  5.3.

Can someone please verify my work? I've written two implementations here and the values match, but I value independent validation.
NAME The value matches what quic-go is calculating. Will add this test vector as a unit test to my implementation.
Woo! I'm not as bad at this as I thought. Thanks NAME
I can also confirm that it verifies in Wireshark. (Likewise for the samples in Appendix A.)
The Retry integrity check is different than the protection for other packets, so it would make sense to include an example in the document. We need code for it in addition to the sample so that it can be updated more readily.
I also tested these vectors and it works fine. Thanks!

packet; the client has to rely on the exchange that included the Retry packet for that property.  <ins> test-vectors contains test vectors for packet encryption. </ins> 5.3.


contains an invalid Retry token, it knows the client will not accept another Retry token.  The server can discard such a packet and allow the client to time out to detect handshake failure, but that could <del> impose a significant latency penalty on the client.  A server MAY proceed with the connection without verifying the token, though the server MUST NOT consider the client address validated.  If a server chooses not to proceed with the handshake, it SHOULD immediately close (immediate-close) the connection with an INVALID_TOKEN error. Note that a server has not established any state for the connection at this point and so does not enter the closing period. </del>  A flow showing the use of a Retry packet is shown in fig-retry.

Fixes issue
This is right. I think that we got a bit of skew over time and the text you cut no longer makes sense.
( was on the mailing list.) PR introduces the following text: The problem with the "server MAY proceed" part is that there is no reasonable way for the server to do so. If ODCID cannot be extracted from the invalid Retry token, then the server cannot set the , which it MUST do. The recommendation that the "server MAY proceed" should come out.
I am not sure if I agree. IIUC, the sentence suggests that a server can process the Initial packet and proceed the handshake as if that Initial packet did not have a token attached. That's at least possible technically (we do that occasionally for NEW_TOKEN tokens, right?). Whether if people would want to do that is a different question, but I think this behavior makes sense for deployments in which you'd have a retry middlebox occasionally installed (and uninstalled).
The problem here is that something that is identifiable as a Retry token as distinct from a NEWTOKEN token will ultimately not be usable. There's a bit of hedging necessary though as junk that appears to be a Retry token is still ultimately just junk. To be concrete, let's say that you generate Retry tokens that start with "RETRY!!!" and NEWTOKEN tokens start with "~~~NEW_TOKEN" The difference between those two is such that it would be unlikely for corruption to cause one to be mistaken for the other, and you have a way to distinguish the two even when all the subsequent bits can't be interpreted after you lose keys or something like that.
The only case in which the server can proceed is when the server sees a junk token, but knows that the client does not consider it a Retry token. There's no mechanism for the client to tell the server what it thinks the token it has presented is, so the server doesn't know when it's in this state. I agree -- axe it.
As pointed out by NAME and NAME the client would reject the handshake even if the server ignores the token and proceeds with the handshake. This is because TP.originalconnectionid would not contain the expected value. I now agree that this MAY is awkward and that we can strike the sentence.
The resolution to send INVALID_TOKEN error could be used to attack tokens through timing attacks. It may be better to drop the packet or to require some attack mitigation measure. Hello, PR 3107 introduced the following text: If a server receives a client Initial that can be unprotected but contains an invalid Retry token, it knows the client will not accept another Retry token. The server can discard such a packet and allow the client to time out to detect handshake failure, but that could impose a significant latency penalty on the client. A server MAY proceed with the connection without verifying the token, though the server MUST NOT consider the client address validated. If a server chooses not to proceed with the handshake, it SHOULD immediately close (Section 10.3) the connection with an INVALIDTOKEN error. Note that a server has not established any state for the connection at this point and so does not enter the closing period. The problem with the "server MAY proceed" part is that the server's transport parameters MUST include originalconnection_id. If ODCID cannot be extracted from the invalid Retry token, then the server cannot let this connection proceed. Questions: The Retry token seems like the obvious place for the server to store ODCID. Is there an implementation that does something else (state on server)? Should we spell out this caveat in the draft? I.e. "server MAY proceed if it possesses ODCID." Thanks, Dmitri.

contains an invalid Retry token, it knows the client will not accept another Retry token.  The server can discard such a packet and allow the client to time out to detect handshake failure, but that could  <ins> impose a significant latency penalty on the client.  Instead, the server SHOULD immediately close (immediate-close) the connection with an INVALID_TOKEN error.  Note that a server has not established any state for the connection at this point and so does not enter the closing period. </ins> A flow showing the use of a Retry packet is shown in fig-retry.


17.2. Long headers are used for packets that are sent prior to the <del> establishment of 1-RTT keys.  Once both conditions are met, a sender </del>  switches to sending packets using the short header (short-header). The long form allows for special packets - such as the Version Negotiation packet - to be represented in this uniform fixed-length

URL What are these "both conditions"? Also, can there be made a clear statement such that: all packets prior to Application PN space use long headers, and all packets in application PN space uses short headers? And, the table of frames with section references, would it make sense to add PN space columns with an X where a frame is applicable?
Multiple issues listed here. Please consider opening separate issues rather than lumping them all together. (1) This is old language we agreed to in Chicago. The requirement to shift to short headers was 1-RTT key availability and version agreement, to accommodate versions where the final keys might be available immediately (i.e. out-of-band agreement) but version negotiation still had to complete. However, that's a principle for the invariants or such a version, not a requirement of this QUIC version -- and the version negotiation language this referenced is gone now. (2) What is "Application PN space"? 0-RTT and 1-RTT? If so, then no -- 0-RTT packets carry application data but are long-header packets. (3) Unrelated editorial suggestion -- please open a separate issue.
(3) see

17.2. Long headers are used for packets that are sent prior to the  <ins> establishment of 1-RTT keys.  Once 1-RTT keys are available, a sender </ins> switches to sending packets using the short header (short-header). The long form allows for special packets - such as the Version Negotiation packet - to be represented in this uniform fixed-length


which can allow for better stream prioritization. The sending part of a bidirectional stream initiated by a peer (type <del> 0 for a server, type 1 for a client) enters the "Ready" state then immediately transitions to the "Send" state if the receiving part enters the "Recv" state (stream-recv-states). </del>  In the "Send" state, an endpoint transmits - and retransmits as necessary - stream data in STREAM frames.  The endpoint respects the

The description here was a little over enthusiastic: it only enters the "Send" state when something is sent.
From -transport section 3.1: The event causing transition Ready->Send is the host initially having data to send on the stream, which is not the case when the receiving part has just been created in its initial Recv state. I think the send stream should stay in Ready until it has data. This also aligns better with similar text in 3.2. I suggest: p.s. see also , but which is more about being explicit about frames that cause stream creation... rather than this issue, which is about the state the new sending part starts in.
This issue can be closed -- see NAME commit above.
That commit hasn't been merged, and is sitting in a branch that doesn't appear to have an associated PR.
Oh -- sorry about that. I should have checked.
Wow, that sat there for ages. I made a PR.

which can allow for better stream prioritization. The sending part of a bidirectional stream initiated by a peer (type  <ins> 0 for a server, type 1 for a client) starts in the "Ready" state when the receiving part is created. </ins> In the "Send" state, an endpoint transmits - and retransmits as necessary - stream data in STREAM frames.  The endpoint respects the


encoded in network byte order.  The CID Length field contains the length of the Connection ID field. <del> The maximum number of connection IDs from the peer that an endpoint is willing to store.  This value includes the connection ID received during the handshake, that received in the preferred_address transport parameter, and those received in NEW_CONNECTION_ID frames.  Unless a zero-length connection ID is being used, the value of the active_connection_id_limit parameter MUST be no less than 2.  If this transport parameter is absent, a default of 2 is assumed.  When a zero-length connection ID is being used, the active_connection_id_limit parameter MUST NOT be sent. </del>  If present, transport parameters that set initial flow control limits (initial_max_stream_data_bidi_local,

Unlike the other transport parameter definitions, there is no type specified for the parameter. I will assume that the type is an integer (like the other numeric-like transport parameters) and submit a PR to adjust the wording so that it is in line with the other transport parameter descriptions, if that's ok? Thanks for all the great work that you are all doing on getting QUIC specified!
Good catch! Looks like all the other values have a type either implicitly or explicitly stated. is indeed intended to be an integer, a PR to that effect would be great.
Thanks NAME

encoded in network byte order.  The CID Length field contains the length of the Connection ID field.  <ins> The active connection ID limit is an integer value specifying the maximum number of connection IDs from the peer that an endpoint is willing to store.  This value includes the connection ID received during the handshake, that received in the preferred_address transport parameter, and those received in NEW_CONNECTION_ID frames.  Unless a zero-length connection ID is being used, the value of the active_connection_id_limit parameter MUST be no less than 2.  If this transport parameter is absent, a default of 2 is assumed.  When a zero-length connection ID is being used, the active_connection_id_limit parameter MUST NOT be sent. </ins> If present, transport parameters that set initial flow control limits (initial_max_stream_data_bidi_local,


6.4. <del> Recovery is a period of time beginning with detection of a lost packet or an increase in the ECN-CE counter.  Because QUIC does not retransmit packets, it defines the end of recovery as a packet sent after the start of recovery being acknowledged.  This is slightly different from TCP's definition of recovery, which ends when the lost packet that started recovery is acknowledged. </del>  The recovery period limits congestion window reduction to once per round trip.  During recovery, the congestion window remains unchanged

I'm happy to merge this as is, but I'll give it 24hrs in case others have comments.
This is weird in that it assumes some context that isn't provided: Perhaps instead: This "Because QUIC does not retransmit packets" thing is unnecessary. I'll open a PR.

6.4.  <ins> A recovery period is entered when loss or ECN-CE marking of a packet is detected.  A recovery period ends when a packet sent during the recovery period is acknowledged.  This is slightly different from TCP's definition of recovery, which ends when the lost packet that started recovery is acknowledged. </ins> The recovery period limits congestion window reduction to once per round trip.  During recovery, the congestion window remains unchanged


HTTP/3 can either rely on transport-layer padding or employ the reserved frame and stream types discussed in frame-reserved and stream-grease.  These methods of padding produce different results in <del> terms of the granularity of padding, the effect of packet loss and recovery, and how an implementation might control padding. </del>  10.2.

This text was pretty good and more or less covered the problem already, but I think that this might be slightly better. NAME if you don't like this, close this AND .
From a discussion on : We should probably have a conversation about the trade-offs with padding. Transport-layer padding is in some ways superior, like in how it can be granular to a single byte, which reserved streams and frames cannot do. But PADDING frames at the transport aren't retransmitted when lost, which could lead to some interesting attacks if packets can be suppressed by an attacker. If retransmissions aren't padded, boom. Originally posted by NAME in URL

HTTP/3 can either rely on transport-layer padding or employ the reserved frame and stream types discussed in frame-reserved and stream-grease.  These methods of padding produce different results in  <ins> terms of the granularity of padding, how padding is arranged in relation to the information that is being protected, whether padding is applied in the case of packet loss, and how an implementation might control padding. </ins> 10.2.


Initial packets and Handshake packets could be never acknowledged, but they are removed from bytes in flight when the Initial and <del> Handshake keys are discarded. </del>  5.3.1.

Pseudocode added, PTAL
I'm fine with requiring it, given it's already a design issue.
should be reset to 0 when a packet number space is dropped. I'd argue that this is the consistent thing to do, because when we drop a packet number space, we implicitely acknowledge all outstanding packets in that space (and remove them from ).
I think this is alright. It should happen anyway, since receiving an ACK should be the common case.
I also tend to think this is Ok. Importantly, if you don't get any ACKs, you also have no RTT samples, and in that case, continuing to backoff the PTO is very possibly the right thing to do.
Maybe I'm confused, but shouldn't be per-PN-space, now that PTO is per-PN-space ()?
I believe the simplest way to ensure QUIC isn't overly aggressive during the handshake and to encourage packet coalescing is to specify ptocount on a connection scope, similar to the rtt variables. I considered making ptocount per-PN space, but that doesn't ensure quite the same exponential backoff properties and I don't believe it's necessary.
I think we agree that PTO during handshake should be as aggressive as after the handshake. However, I am starting to wonder if the current spec. actually works that way. Consider the case when the server responds to ClientHello. It would be sending Initial, Handshake, and 1-RTT packet at the same time. Then, when those QUIC packets gets lost, the loss timer would be invoked three times, resulting in ptocount set to 3. Is that the intended behavior? I think in such case, ptocount should become one, after the PTO timers of all those epochs are triggered, once each. Therefore, to me, the correct logic seems to be: have ptocount for each epoch every time the per-epoch PTO timer is triggered, increment the per-epoch ptocount by 1 when an ACK is being received on any* epoch, reset pto_count of all the epochs PS. I acknowledge that the issue I'm discussing in this comment might be orthogonal to the original issue, and that it might deserve a separate issue.
NAME you make a good point. I don't think this is orthogonal to the original issue, I think it's very much related. In addition to what you note, you would also need to use the per-epoch counter for determining how long to set the timer. This is going to be imperfect or ugly or both. If we use a single ptocount, we'll have duplicate counts. If we use separate ptocounts, we'll have to reconcile different epochs having varying counts. I think the behavior we're looking for is (i) use a separate ptocount per epoch, (ii) use the max of all ptocounts when setting the timer for any PN space, (iii) reset all pto_counts when an ACK is received for any epoch. This isn't great, and it is imperfect -- you can end up being more aggressive than with a single count. But I think it's good enough.
NAME The logic you describe sounds fine to me (apparently I missed (ii)), it would make the PTO behavior during handshake as aggressive as after the handshake. I cannot see when it would be more aggressive than that, and I think it's better than "imperfect but good enough."
NAME : In theory, if we use the max of counts when setting the timer, it is possible for a single pto_count to increase without increasing the max. Which means that the sender can effectively seem to not back off sometimes, causing it to be potentially more aggressive than after the handshake. I might have been a bit harsh earlier -- I think this is fine.
My plan is: 1) Keep a single ptocount. 2) Reset it when a packet number space is dropped, since that is an indication of forward progress. 3) Re-arm the loss detection timer when dropping a packet number space in case it was armed for the space being dropped. 4) Add some more explanatory text about how ptocount and multiple packet number spaces interact.
Discussed in ZRH. Proposed resolution is in , which should include corresponding updates to the pseudocode.

Initial packets and Handshake packets could be never acknowledged, but they are removed from bytes in flight when the Initial and  <ins> Handshake keys are discarded.  When Initial or Handshake keys are discarded, the PTO and loss detection timers MUST be reset, because discarding keys indicates forward progress and the loss detection timer may have been set for a now discarded packet number space. </ins> 5.3.1.


receive one or more extension frame types with the one transport parameter.  Extension frames MUST be congestion controlled and MUST cause an ACK frame to be sent.  The exception is extension frames that replace or supplement the ACK frame.  Extension frames are not included in flow

The SHOULD is guidance for future extensions; if we'd like to downgrade that to non-normative, that's also fine.
I'm going to merge this.
Resolution of was to have no design change, but consider a "here be dragons" warning in the extension section paralleling what was added to HTTP/3 in . (The reasons, and therefore the guidance, are different at the transport layer -- we can't directly steal the text.)

receive one or more extension frame types with the one transport parameter. <ins> Extensions that modify or replace core protocol functionality (including frame types) will be difficult to combine with other extensions which modify or replace the same functionality unless the behavior of the combination is explicitly defined.  Such extensions SHOULD define their interaction with previously-defined extensions modifying the same protocol components. </ins> Extension frames MUST be congestion controlled and MUST cause an ACK frame to be sent.  The exception is extension frames that replace or supplement the ACK frame.  Extension frames are not included in flow


Even though a sender is encouraged to assemble frames containing up- to-date information every time it sends a packet, it is not forbidden <del> to retransmit copies of frames from lost packets.  A receiver MUST accept packets containing an outdated frame, such as a MAX_DATA frame carrying a smaller maximum data than one found in an older packet. </del>  Upon detecting losses, a sender MUST take appropriate congestion control action.  The details of loss detection and congestion control

And this could make retransmitting frames complex.
We're currently not specifying which DCID is supposed to be used on retransmissions of Handshake packets. A client might receive a NEWCONNECTIONID frame in the first couple of 1-RTT packets it receives, before receiving either a HANDSHAKEDONE or an acknowledgement for a 1-RTT packet is sent itself. Therefore, it's still running loss recovery on the Handshake packets it sent out earlier. If there's an acknowledgement outstanding for one of those Handshake packets, the client will have to retransmit that packet. Which DCID is it supposed to use on the retransmission? Is it the DCID that was used on the original packet? Or is it the CID provided in the NEWCONNECTIONID frame? Note that the NEWCONNECTION_ID frame might already have requested the retiring of the CID used during the Handshake.
As I can see, there are three options: a) let the receiver of NCID to send Handshake packets using the new CIDs b) require (or recommend) receiver of NCID to continue using the original DCID until the handshake is confirmed c) require (or recommend) the sender to withhold the emission NCID (with a non-zero RTP) to until it sees an ACK for handshake confirmation I prefer (c) (or maybe (b)), because (a) has corner cases even though it might seem easy. Consider the case where a server sends TP.preferredaddress then as the first packet, sends the NCID frame with RTP set to 2. If we adopt (a), this would mean that the client would have only one CID that it can use (the one being embedded in the NCID frame) even though it needs two (one for the original path, and another for the path specified by TP.preferredaddress). As you can see, the design of the NCID has been based on the assumption that providing one new CID alongside a retirement request guarantees that the receiver would have enough active CIDs. But in this particular case, that is not the case. Based on this observation, and based on my assumption that a non-zero RTP would be sent by only some of the endpoints (the necessity of CID retirement is questionable for HTTP/3, the scope of QUIC v1, as the HTTP connections are typcially short-lived and can most likely be gracefully terminated to induce a new connection when retirement of CIDs is necessary), my preference goes to (c): an endpoint that uses RTP should cover the cost.
NAME (I think you mean RPT, not RTP) Thank you for the analysis. I prefer (c) as well, except that I do not want to condition it on non-zero RPT, to keep things simpler. Otherwise we're still left with what to do at the receiver. Ensuring that the handshake confirmation is acked means that there are no HS packets pending.
I'll also note that this issue was found through a resilience test we've built for the interop runner -- high loss handshake test. We're excited to see how everyone's handshake machinery performs under 50% loss :-)
At the time of this writing, ngtcp2 does (b).
NAME for you problem with (a) isn't that just a general problem with having multiple paths and then getting a NEWCONNECTIONID with RPT for all previous CIDs? You end up in a scenario where you only have 1 CID (the new one) and multiple paths that just had all their CIDs retired. If you can get away with using that one CID for the active path, you can still make forward progress. If not, the connection dies.
I believe we specify (b) already: "A client MUST only change the value it sends in the Destination Connection ID in response to the first packet of each type it receives from the server (Retry or Initial); a server MUST set its value based on the Initial packet. Any additional changes are not permitted; if subsequent packets of those types include a different Source Connection ID, they MUST be discarded. This avoids problems that might arise from stateless processing of multiple Initial packets producing different connection IDs." URL
NAME that is just indicating when a client should change DCID in response to receiving a Initial/Retry packet with a new CID. I don't think anything restricts either side from sending NEWCONNECTIONID frames in their first 1-RTT packets and their peers immediately using them for any packets (including handshake) they send. Personally, I don't have a problem with (a). I think that no matter what receivers will have to handle the multiple paths but single CID scenario, so I would prefer not to special case the handshake. I also don't think we have handshake confirmation problems because an endpoint should only send a NEWCONNECTIONID out when it's able to accept it. Then, if the peer can decrypt the packet with the new CID, I see no reason why it shouldn't be allowed to use it immediately.
NAME my reading of the text I quoted is that you cannot use any DCIDs in long header packets that weren't either the original client DCID/SCID or from a server Initial or Retry.
NAME There's a fourth option: d) require that the sender withholds the NCID frame until it has confirmed the handshake. As soon as an endpoint confirms the handshake, it drops the Handshake keys (I'm assuming here). As a result, the endpoint obviously doesn't care about Handshake packets any longer (and especially doesn't care which DCID was used to send the packet). Therefore, there's no problem if the receiver of the NCID frame immediately retires the connection ID used during the handshake, even if the the NCID and the HANDSHAKE_DONE frame were reordered.
NAME The part about what to do with DCID changes is underspecified. From the part of the spec you quote -- "Any additional changes are not permitted; if subsequent packets of those types include a different Source Connection ID, they MUST be discarded." -- it's not clear what these additional changes are limited to. I think the intent was for all long-form packets, but that's certainly not clear here. In any case, the problem with that approach is that the client will have to carefully construct HS packets with the old DCID while sending everything else with the new DCID.
NAME : (d) works too, but an endpoint still has to decide which DCID to use for sending HS packets in the case that the HANDSHAKE_DONE frame is lost. I'd rather not leave that unspecified.
"of those types" => Retry or Initial, since that's what it was talking about. That's trying to avoid the case where the server statelessly generates a new Retry (or Initial, in the rare case that would be stateless) with a second SCID upon receiving a retransmission or duplication of the client's Initial. But it doesn't say anything about the server issuing new CIDs via an NCID frame in 1-RTT before the handshake is confirmed. I think (a) or (b) would be fine; the server is clearly intending to accept the other CID if it issued it, but could conceivably use separate logic for long-header versus short-header routing.
Can someone explain why using the new connection ID is a problem?
NAME I think that that is a good way at looking into the issue. I agree that the endpoints can start using the new CIDs during handshake, if the peer sends NCID frames. The only problem that I can think of is related to retirement (see URL). When the server provides TP.preferred_address, and also decides to retire the CIDs immediately after sending all the handshake transcript, there's going to be a very small time window in which it has to send two CIDs (one for the current path and another for the path using the preferred address). Unless the QUIC stack allows bundling of two NCID frames when building a QUIC packet, the client might end up not having sufficient amount of CIDs, in which case it might terminate the connection. It is my view that this is very different from the ordinary case of retiring CIDs, in which case there would be enough time window to guarantee that a handful number of new generation CIDs are delivered before the older ones are delivered (cc NAME That said, I now tend to think that the most we might have to do is the following two: clarify that CIDs received using NCID frames can be used for Handshake packets have a cautionary text stating that a server needs to make sure that the client has enough number of new generation CIDs when it retires an older generation of CIDs
NAME Wouldn't we be able to solve this by using my proposed option d?
Artificial constraints on when the frame can be sent would be terrible to enforce and would introduce a performance cost for using NEWCONNECTIONID. In a great many cases, the 0.5RTT from the server is idle, which makes it a great way to get things like NEWCONNECTIONID sent without taking capacity from real work. Asking the server to defer sending would mean that NEWCONNECTIONID could compete with HTTP responses and the like. I see no reason not to allow use of connection IDs when they are available. They apply to the connection as a whole. You will note that we allow changing them with the Initial/Retry mini-protocol in ways that don't correlate with other connection-level events, so this isn't any different in that regard. Forcing the connection ID state to synchronize with other state changes seems more likely to complicate things than help. The caution NAME mentions is always true. Whenever an endpoint is forced to retire connection IDs, a short supply is always a risk to the connection.
I think QUIC allows more than 1 NCID frames in a single QUIC packet. Now my preference is (a). If server utilizes retire-prior-to, it should know the consequences and should provide sufficient backup connection IDs. Otherwise, it would be misconfiguration of server software.
NAME The problem is that the client might end up receiving only some of the NCID frames, unless the server always sends (and retransmits) the set of NCID frames as a whole. But anyways, I agree that (a) is probably fine. That is because it is trivial to design a server that never requests CID retirement during the handshake. All you need to do is start issuing the new generation of CIDs at least N seconds before sending NCID frames that asks for the retirement of the previous generation, where N is your handshake timeout. Knowing that this design is why I initially preferred (c), not realizing that it works equally well with (a). But know that I understand that, I prefer (a) as it is simpler.
Given a server has to sent NCID to have the CID change, I'm fine with (a), but I thought it was not what we'd previously agreed to and specified, so I believed (b) was the status quo.
I don't think this is a serious problem, because if the server doesn't want the long header DCID to change, it can wait before sending NEWCONNECTIONID. That said, we do have an issue here in that different readers of the draft had different interpretations of what was allowed. All that to say I don't much care if this is allowed or prohibited, but we should at least make an editorial change to clarify things.
I agree with NAME here. Any alternative is ok, but the draft should say whether servers can expect that the client might switch DCID and whether the clients can expect the server to send NCID w/ RPT>0 before the handshake is complete.
Let's keep it simple: no migration before Handshake Done, period. That would include prohibit NCID in 0-RTT Also state that implementations MAY drop any packet received with NCID different from original before Handshake Done.
NAME The problem is not that simple. Consider the case where the server receives ClientFinished and sends two 1-RTT packets, the first packet containing HANDSHAKE_DONE, and the second packet containing an NCID with RPT set to 1. If the first 1-RTT packet gets lost, the client receives a request to retire the connection ID before it confirms the handshake.
I think I'd prefer a design that meets the two requirements stated below: A server should not be required to withhold NCID frames until it receives an ACK for HANDSHAKEDONE. A client should be allowed to use the original CID until the handshake is confirmed. The intention is to minimize additional complexity to existing (and future) QUIC stacks. One solution that meets these requirements is to state that a NCID frame confirms the handshake, exactly the same way as HANDSHAKEDONE frame does. That would be a trivial change to any of the QUIC stacks, and there would be no ambiguity regarding the state changes.
Discussed in ZRH. Proposed resolution is to clarify that issuing a CID means being prepared to seeing it used at any time. Additional Editorial issue on packetization issues.
Editorial PR on packetization challenges with retransmitted frames is .
Ugh, editorial changes for a design issue. Reopened for consensus calling.
This was fixed by . is related to this issue, but that simply clarifies things. To keep things sane, let's just close this when is merged.
Wôt Eric sèd tü.Looking great! One minor wording suggestion.

Even though a sender is encouraged to assemble frames containing up- to-date information every time it sends a packet, it is not forbidden  <ins> to retransmit copies of frames from lost packets.  A sender that retransmits copies of frames needs to handle decreases in available payload size due to change in packet number length, connection ID length, and path MTU.  A receiver MUST accept packets containing an outdated frame, such as a MAX_DATA frame carrying a smaller maximum data than one found in an older packet. </ins> Upon detecting losses, a sender MUST take appropriate congestion control action.  The details of loss detection and congestion control


A client need not store a transport parameter it cannot process. A client MUST NOT use remembered values for the following parameters: <del> original_connection_id, preferred_address, stateless_reset_token, ack_delay_exponent and active_connection_id_limit.  The client MUST use the server's new values in the handshake instead, and absent new values from the server, the default value. </del>  A client that attempts to send 0-RTT data MUST remember all other transport parameters used by the server.  The server can remember initial_max_streams_uni  Omitting or setting a zero value for certain transport parameters can result in 0-RTT data being enabled, but not usable.  The applicable subset of transport parameters that permit sending of application

Simple. It appears as though I missed this when NAME added this transport parameter. We shouldn't make more exceptions for 0-RTT.
This is a bigger design change than it might look like from the diff. Our design principle was to only remember things for 0-RTT that are actually needed to make 0-RTT work. This means that we need the flow control limits as well as the stream number limits, because obviously the client needs to be able to send application data in 0-RTT. This saves state on the client side (because the client only needs to remember those 6 parameters) as well as in the session ticket (because that’s how the server stores that state). A reasonable implementation strategy on the server is to reject 0-RTT if those parameters changed / were reduced, so keeping the number of things to remember small increases the probability that 0-RTT is not rejected.
"needed to make 0-RTT work" has slightly fuzzy boundaries, though. A more concrete rendering of the principle is that the client needs to remember any limit which it might exceed prior to receiving the actual limit in the ServerHello. The possible solutions for any given TP, then, are to structurally ensure that the client can't exceed it in 0-RTT or to remember it.
From comment URL How is the TP to be remembered when it is to be ignored when 0 length CID is received?
After looking back at the notes(linked on the issue), I see we had fairly strong agreement on this at the time, so I'm willing to let that stand, even if it's not what I think is the optimal choice.
This value is not remembered for 0-RTT. A client therefore cannot send NCID frames in 0-RTT without risking to violate the server’s limit.
The alternative would be to add it to the list of things to remember, but yes, we should fix it.
NAME That would mean that the server needs to remember / save in the session ticket, and can’t reduce in the resumed connection. My understand was that we only remember those values that are absolutely necessary to make 0-RTT make sense, i.e. flow control and stream number limits.
Discussed in ZRH. NAME to write up a PR for his preferred option.
Post-discussion and having re-read, I believe the current text covers this for NEWCONNECTIONID: The client has already issued one CID to the server during the handshake, which might be zero-length Sending NEWCONNECTIONID is forbidden if you are using zero-length CIDs; receipt of one is a protocol violation The default and minimum value of applies until the server's value is received, which is two if the client is using non-zero-length CIDs Exceeding the limit is a protocol violation So the client can send no more than one NEWCONNECTIONID frame in 0-RTT, or it's a protocol violation. If it sends exactly one, that never exceeds the server's limit, provided that it has a non-zero-length Source Connection ID. It also covers it for RETIRECONNECTIONID: The client has exactly one CID for the server during 0-RTT It is a protocol violation for RETIRECONNECTIONID to retire the CID used by the containing packet
Over supper, NAME wanted to know why a client would include a new connection ID in its 0-RTT packets. The answer is that the client's handshake CID has no associated SRT. If the client wants the ability to send stateless resets, it might very well put NCID(N=1, RPT=1) in its 0-RTT flight. The server will retire the handshake connection ID in 0.5-RTT and begin using the CID with an associated token.
Discussed in ZRH. Proposed resolution is written up in .
The meeting notes record that there was a 12 to 1 vote for saving the value instead of using the default of 2. URL I believe I was convinced by the argument that a server could just always use the default and never change it, so it doesn't really matter for servers.
I agree with NAME Although a client could decide to not remember the activeconnectionidlimit (and would then not not be able to send more than one CID in 0-RTT packets), the server needs to store this value in order to decide whether to accept or to reject 0-RTT. This seems like a bit of a hassle for something that doesn't provide any real-world benefits.Besides the motivation of minimizing exceptions in 0-RTT, I'm not sure why we're making this change? Remembering more TPs makes server upgrades and config changes potentially more disruptive, as has been discussed, and there's no critical use case for the client to send NEWCONNECTION_ID in 0-RTT.

A client need not store a transport parameter it cannot process. A client MUST NOT use remembered values for the following parameters:  <ins> original_connection_id, preferred_address, stateless_reset_token, and ack_delay_exponent.  The client MUST use the server's new values in the handshake instead, and absent new values from the server, the default value. </ins> A client that attempts to send 0-RTT data MUST remember all other transport parameters used by the server.  The server can remember initial_max_streams_uni <ins> active_connection_id_limit </ins> Omitting or setting a zero value for certain transport parameters can result in 0-RTT data being enabled, but not usable.  The applicable subset of transport parameters that permit sending of application


endpoint is willing to store.  This value includes the connection ID received during the handshake, that received in the preferred_address transport parameter, and those received in <del> NEW_CONNECTION_ID frames.  Unless a zero-length connection ID is being used, the value of the active_connection_id_limit parameter MUST be no less than 2.  If this transport parameter is absent, a default of 2 is assumed.  When a zero-length connection ID is being used, the active_connection_id_limit parameter MUST NOT be sent. </del>  If present, transport parameters that set initial flow control limits (initial_max_stream_data_bidi_local,

Purely editorial nit: This text has a lot of line breaks that won't render as such in the output. If you want to have separate paragraphs in the definition, you need a new for each new paragraph.
In the description of activeconnectionidlimit, we require that the server make the value of the transport parameter conditional on the choice of connection ID from the client. In the past, we have had the principle that either endpoint not condition its choice of transport parameter values based on anything from its peer. We should maintain this principle throughout. This means changing the definition of activeconnectionidlimit; NAME had a sense that there was another.
I'd like to understand what your argument is here. Is it that you must not condition your response, or merely that it is not necessary to. For instance, you suppose we have a set of extensions that exist and that those extensions need to be jointly agreed on whether they will be done. The client supports some set Ec and the set of extensions Es. The client clearly sends Ec. Is your argument that the server must send Es and not Ec cap Es and the client must compute the intersection? Or that it might send either?
My understanding of the argument is that we shouldn't force servers to condition their TPs based on the client's Initial packet (possibly including TPs). E.g. the server currently needs to condition its activeconnectionidlimit TP based on whether the client's Initial had a zero-len SCID; PR relaxes that constraint. Assuming we have some future sets of TPs for extensions (and these TPs are advertised values, not negotiated values) Ec on the client and Es on the server: The server could always send Es (so it doesn't have to condition its response), or the server could send Ec cap Es, and in either case the client will ignore values sent by the server not in E_c. For future extensions that have negotiated transport params, the server will have to condition those values based on what the client sent.
MAY send a fixed set (so MAY NOT condition the response I guess) or that it is not necessary to. So managing intersection of incompatible extensions is one case where conditioning might seem to be necessary, but that can be resolved by establishing precedence for instance.
I looked through the existing transport parameters for what currently conflicts with a fixed profile of transport parameters. I've only found one (apart from the already discussed activeconnectionidlimit): originalconnection_id. This field is necessarily dependent on what the client sends. The only way to avoid this is to not send Retry packets so you don't need to send this TP. The rest of the transport parameters appear to be independent of what the peer sent.
What about statelessresettoken? I doubt many servers would send a static value (or not send it at all).
NAME Where did we use this principle in the past? I can't remember that we said anything like that. Since we're using transport parameters to negotiate extensions (and extensions might use the value of the TP to negotiate something), I don't think this is a viable principle to follow.
NAME IIRC the most recent occasion that we discussed this principle is when we changed the name of to . The reason we changed the name was specifically due to this principle. Each endpoint declares its maximum, instead of conditioning its value on the others. That's why it's now called max-idle-timeout, and why we require each endpoint to calculate the minimum of the two.
My understanding is that the principle was you shouldn't be required to condition TPs based on data from the peer, not that one is precluded from doing so.
I guess that "fixed" is the wrong term. More to the point that the transport parameters don't necessarily depend on what a peer does. That accounts for the stateless reset case, because that only "depends" on local choices.
I think we need to see more (supportive) discussion in order to justify taking this on.
I don't know that there's much here to "take on" beyond clarifying what our design principles are, and maybe writing those down somewhere for designers of future transport parameters. The principle here is that TPs are independent declarations by each side, not negotiations. I've noticed as we get further and further into this process, we have a number of principles which haven't been written down which have guided our design -- and we keep revisiting them because only the resulting design is explicit. I'm wondering if those principles belong... somewhere. An appendix, a published draft, etc. even if not enshrined in an RFC.
I agree with Mike. I noticed we already state this design principle in section 7.3: "These declarations are made unilaterally by each endpoint." Taking on/resolving this issue brings transport parameters back in line with the stated property of them being declarations, not negotiated. I think this is a worthwhile change.
The current language defining the activeconnectionid_limit transport parameter refers to "a zero-length connection ID is being used". It's unclear whether the zero-length CID is the CID of the sender of the TP or the receiver of the TP. Since this TP limits how many CIDs the peer can send, the references to using a zero-length CID only make sense if it's the peer using a zero-length CID, but it would be useful to clean up that language. I've created PR to clarify this language, including that checking whether the peer is using a zero-length CID can only be done by the server.
Chairs, the fix here turned out to be more complex. Let's get wider review for this.
LGTMLGTM, modulo one suggestion.

endpoint is willing to store.  This value includes the connection ID received during the handshake, that received in the preferred_address transport parameter, and those received in  <ins> NEW_CONNECTION_ID frames.  The value of the active_connection_id_limit parameter MUST be at least 2.  An endpoint that receives a value less than 2 MUST close the connection with an error of type TRANSPORT_PARAMETER_ERROR.  If this transport parameter is absent, a default of 2 is assumed.  If an endpoint issues a zero-length connection ID, it will never send a NEW_CONNECTION_ID frame and therefore ignores the active_connection_id_limit value received from its peer. </ins> If present, transport parameters that set initial flow control limits (initial_max_stream_data_bidi_local,


An HTTP/3 implementation can immediately close the QUIC connection at any time.  This results in sending a QUIC CONNECTION_CLOSE frame to <del> the peer; the error code in this frame indicates to the peer why the connection is being closed.  See errors for error codes which can be used when closing a connection. </del>  Before closing the connection, a GOAWAY MAY be sent to allow the client to retry some requests.  Including the GOAWAY frame in the

This goes most of the way to solving my perceived problem. I do wonder if we should also add a little bit more text to section 8 in this paragraph under certain circumstances. Implementations need to consider the impact on outstanding requests before making this choice. Perhaps just a reference e.g. "Implementations need to consider the impact on outstanding requests before making this choice, see {immediate-application-closure}."
But that section doesn't discuss the impact on outstanding requests, and it seems irrelevant to this topic. Can you expand on the connection you see?
to me, the choice to promote a stream error to a connection error is the choice to promote it to an immediate application close. Therefore the present statement about "impact on outstanding requests" is really about how to do/decide on graceful close with GOAWAY. If that is not the case, then I don't understand what other consierations my endpoint should make
That text was added as a compromise, basically reminding implementations that choosing to blow up their connection will lose any requests in flight, so you should have a good reason.
NAME I'm going to go ahead and merge this, since it addresses the original issue and stands by itself. If you think we need additional text elsewhere, can you open a separate issue?
Sure thing, I'll review again
Draft 25 section 5.3 says: any time. This results in sending a QUIC CONNECTIONCLOSE frame to the peer; the error code in this frame indicates to the peer why the connection is being closed. This doesn't specifically tie down H3-initiated closure to only use the application variant of CONNECTIONCLOSE (0x1d). This gets a bit strange when combined with later guidance to use H3 error codes or treat unknown error codes as

An HTTP/3 implementation can immediately close the QUIC connection at any time.  This results in sending a QUIC CONNECTION_CLOSE frame to  <ins> the peer indicating that the application layer has terminated the connection.  The application error code in this frame indicates to the peer why the connection is being closed.  See errors for error codes which can be used when closing a connection in HTTP/3. </ins> Before closing the connection, a GOAWAY MAY be sent to allow the client to retry some requests.  Including the GOAWAY frame in the


packet number 0, and contains a CRYPTO frame carrying the ClientHello. <del> Note that multiple QUIC packets - even of different encryption levels - may be coalesced into a single UDP datagram (see packet-coalesce), </del>  and so this handshake may consist of as few as 4 UDP datagrams, or any number more.  For instance, the server's first flight contains <del> packets from the Initial encryption level (obfuscation), the Handshake level, and "0.5-RTT data" from the server at the 1-RTT encryption level. </del>  tls-0rtt-handshake shows an example of a connection with a 0-RTT handshake and a single packet of 0-RTT data.  Note that as described <del> in packet-numbers, the server acknowledges 0-RTT data at the 1-RTT encryption level, and the client sends 1-RTT packets in the same packet number space. </del>  7.2. Section 4.1.2 of QUIC-TLS), an endpoint MUST send any CONNECTION_CLOSE frames in a 1-RTT packet.  However, prior to confirming the handshake, it is possible that more advanced packet <del> protection keys are not available to the peer, so the frame MAY be replicated in a packet that uses a lower packet protection level. A client will always know whether the server has Handshake keys (see discard-initial), but it is possible that a server does not know whether the client has Handshake keys.  Under these circumstances, a server SHOULD send a CONNECTION_CLOSE frame in both Handshake and Initial packets to ensure that at least one of them is processable by the client.  Similarly, a peer might be unable to read 1-RTT packets, so an endpoint SHOULD send CONNECTION_CLOSE in Handshake and 1-RTT packets prior to confirming the handshake.  These packets can be </del>  coalesced into a single UDP datagram; see packet-coalesce. 10.4. All other packets are protected with keys derived from the cryptographic handshake.  The type of the packet from the long header or key phase from the short header are used to identify which <del> encryption level - and therefore the keys - that are used.  Packets protected with 0-RTT and 1-RTT keys are expected to have confidentiality and data origin authentication; the cryptographic handshake ensures that only the communicating endpoints receive the corresponding keys. </del>  The packet number field contains a packet number, which has additional confidentiality protection that is applied after packet packets. Coalescing packets in order of increasing encryption levels (Initial, <del> 0-RTT, Handshake, 1-RTT) makes it more likely the receiver will be able to process all the packets in a single pass.  A packet with a short header does not include a length, so it can only be the last packet included in a UDP datagram.  An endpoint SHOULD NOT coalesce multiple packets at the same encryption level. </del>  Senders MUST NOT coalesce QUIC packets for different connections into a single UDP datagram.  Receivers SHOULD ignore any subsequent The "Packets" column in frame-types does not form part of the IANA registry (see iana-frames).  This column lists the types of packets <del> that each frame type can appear in, indicated by the following </del>  characters: Initial (packet-initial) 1-RTT (short-header) <del> A CONNECTION_CLOSE frame of type 0x1c can appear in Initial, Handshake, and 1-RTT packets, whereas a CONNECTION_CLOSE of type 0x1d can only appear in a 1-RTT packet. </del>  Section 4 of QUIC-TLS provides more detail about these restrictions. Note that all frames can appear in 1-RTT packets. Data sent in CRYPTO frames is retransmitted according to the rules in QUIC-RECOVERY, until all data has been acknowledged.  Data in CRYPTO frames for Initial and Handshake packets is discarded when <del> keys for the corresponding encryption level are discarded. </del>  Application data sent in STREAM frames is retransmitted in new STREAM frames unless the endpoint has sent a RESET_STREAM for that CONNECTION_CLOSE frames.  Endpoints MUST treat receipt of Handshake packets with other frames as a connection error. <del> Like Initial packets (see discard-initial), data in CRYPTO frames at the Handshake encryption level is discarded - and no longer retransmitted - when Handshake protection keys are discarded. </del>  17.2.5. RFC3629. The application-specific variant of CONNECTION_CLOSE (type 0x1d) can <del> only be sent using an 1-RTT packet (QUIC-TLS, Section 4).  When an application wishes to abandon a connection during the handshake, an endpoint can send a CONNECTION_CLOSE frame (type 0x1c) with an error code of 0x15a ("user_canceled" alert; see TLS13) in an Initial or a Handshake packet. </del>  19.20. A server received a Retry Token in a client Initial that is invalid.  An endpoint has received more data in CRYPTO frames than it can buffer.

This allows us to avoid treating 0-RTT specially more generally. The advice on where to send CONNECTIONCLOSE seems ornate, but it reduces to "send CONNECTIONCLOSE in every space you have keys for". The trick is that we don't want to allow application error codes to leak in Initial (edit: and Handshake) packets, so this defines an error code in the transport space that says "the application decided to close the connection". I think that this is better than relying on the proxied TLS alert code, so I changed the recommendation to use 0x15a to refer to this new APPLICATION_ERROR error code.
I've updated this one to fix the other issue as well. And yes NAME that means multiple error codes. That's a little ugly, but otherwise we have cases where CONNECTIONCLOSE is initiated by the application in a way that the peer can't consume it. We either let the connection die without CONNECTIONCLOSE, do some mapping, or we leak application state into "cleartext". I like the explicit signal.
NAME can you check the recent changes here?
In fixing , I realized that it was tricky to send an application-level close in 0-RTT because of the need to potentially send a close in Initial or Handshake packets. However, the same applies to 0.5-RTT. The client might not have 1-RTT keys when the server sends a CONNECTION_CLOSE in a short header packet. Rather than try to remap application errors onto transport errors to solve this, just let the application signal an error any time. Sure, both 0-RTT and 0.5-RTT both have in common the lack of input from a peer, so signaling an error seems unnecessary, but the restriction we have only exists because we aren't sure that the condition is valid. But it might be. Imagine that an application error condition could be triggered by TLS extensions or transport parameters. Or maybe the endpoint just doesn't want to continue. Lifting the restriction should simplify a lot of things.
I am not sure if that is the correct thing to do. IIRC, the reason we have limited use of application CONNECTION_CLOSE to 1-RTT is because the error code is part of the application protocol, and because the role of QUIC as a transport protocol is to provide "encryption" to the application protocol. That's what we agreed late last year, see . The flip side of allowing application close to be sent using Initial packets is that every application protocol using QUIC would be required to specify the error codes that can be sent using Initial packets or not, because the error code sent using an Initial packet would be observable. I would argue that that would be a complexity.
That is an entirely reasonable objection. The question then becomes: what do we do when we have a desire to signal an application-level error, but we aren't sure that the peer can receive a packet that is amply protected? For instance, if a server accepts 0-RTT only to find that the contents are gibberish, it might signal in a 1-RTT packet with the appropriate application CONNECTIONCLOSE, but it has no guarantee that this will be readable to a client. I suspect that the right answer is to switch to a transport-level CONNECTIONCLOSE with a different error code in that case. Allowing application CONNECTION_CLOSE in 0-RTT would still make sense then, if only to keep the API surface consistent.
Agreed. FWIW, we state the following, which I think is the right advice. When an application wishes to abandon a connection during the handshake, an endpoint can send a CONNECTIONCLOSE frame (type 0x1c) with an error code of 0x15a ("usercanceled" alert; see ) No objection from me.
I agree with NAME Just use the CONNECTIONCLOSE frame with the generic usercanceled TLS alert.
The exception for CONNECTIONCLOSE in 0-RTT is just odd. We currently exclude things from 0-RTT only because they are impossible. As NAME says, "feel free to send me a PROTOCOLVIOLATION when I send CONNECTION_CLOSE in 0-RTT".
IIRC, the rationale behind forbidding use of CONNECTIONCLOSE in a 0-RTT packet was that the client should send CONNECTIONCLOSE in an Initial or Handshake packet. I am fine with allowing use of CONNECTION_CLOSE in 0-RTT, as long as that advice is kept (or stated).
After sending CONNECTIONCLOSE, the client is supposed to enter the closing state and stops responding other than to repeat the connection close. However, the server might reject 0-RTT, in which case the server doesn't know the connection has been closed. I see the principle for including it, but I agree with NAME that we should advise clients that it's kinder to send the CONNECTIONCLOSE at other encryption levels as well / instead to ensure the server can read it.
There's already text about sending connection close in the highest encryption level the sender knows the peer can process. When 0-RTT is sent, the client doesn't know whether the server can process the packets. See text starting with "When sending CONNECTION_CLOSE, the goal is to ensure that the peer will process the frame. Generally, this means sending the frame in a packet with the highest level of packet protection to avoid the packet being discarded." in URL I think the existing text advises clients not do this, but making it illegal is silly.
The testing of that feature is going to be interesting. Besides the case of servers that just don't negotiate 0-RTT because they don't feel like it today, there is also the case of 0-RTT packets being lost. The loss is not guaranteed to be detected before the TLS handshake complete. A bad faith server could just ignore any 0-RTT packet that carries a connection close request, and do as if it was not received. I get the desire of treating 0-RTT just like 1-RTT, but the number of corner cases is going to be fun.
Let's be clear: we have a problem of this shape already. A server can sent CONNECTION_CLOSE prior to confirming that the client has the keys necessary to interpret it. It might even have good cause to do so. Ignoring signals is always possible; these signals exist largely as a courtesy, so that endpoints don't dedicate resources to connections their peer no longer wants.
To add to what NAME has pointed out, the server possibly ignoring frames contained in 0-RTT packet is not limited to CONNECTION_CLOSE. While I have a mixed feeling about allowing most if not all 1-RTT frames in 0-RTT too, it is my understanding that that has been the principle we have agreed. I would very much prefer applying the principle everywhere rather than making decisions on a case-by-case basis.
In TLS draft, we state that all other frame types MUST only be sent in the 0-RTT and 1-RTT levels, then state that it is impossible to send some of those frames in 0-RTT (HANDSHAKEDONE, NEWTOKEN, PATHRESPONSE, RETIRECONNECTION_ID). As it seems that we think use of these frames in 0-RTT to be forbidden, I think we should be clear about that.
See also .
It seemed to me that the principle MT was espousing was that we don't forbid any frames in 0-RTT and just note in the draft the frames where some other rule made it impossible to send a conformant frame in 0-RTT.
It's not forbidden, it's impossible -- or more precisely, it's a protocol violation to send them for other reasons. HANDSHAKEDONE and NEWTOKEN frames are sent only by servers, and servers don't send 0-RTT. A client sending them at any time is forbidden. PATHRESPONSE is sent only in response to a PATHCHALLENGE; PATHCHALLENGE can only be sent in 0-RTT or 1-RTT, and servers don't send 0-RTT. Therefore, the client can't be responding to a PATHCHALLENGE while it's still sending 0-RTT. RETIRECONNECTIONID can't retire the CID of its own packet. Until the transport parameters have been parsed, the client only has one CID for the server, so it's illegal to retire it. Once the transport parameters have been parsed, in the "simple" handshake flow, the client shouldn't be sending 0-RTT. If the handshake requires an extra round-trip, is it possible to keep sending 0-RTT after having processed the Server Hello? Should this update the table in 12.4?
NAME yes, I think that is what I was saying.
Does that mean I can't send a FRAMEPARSING error? That would complicate things quite a bit, since I'd have to build extra tracking logic e.g. for a HANDSHAKEDONE that is not sent in 1-RTT.
Why bother? The client can't send HANDSHAKE_DONE, so you only need to check the role of the endpoint when receiving it.
I think you need both pieces of logic anyway. If a peer sends HANDSHAKEDONE in a Handshake frame, that's an error regardless of role. You need to check the packet type on receipt. If a client sends HANDSHAKEDONE, that's an error regardless of packet type. You need to check the peer's role on receipt. The assertion here is that we don't need to expand the packet type check to overlap with the role check. Then we get into discussion about which of multiple overlapping errors you should use. We've done that already.
FWIW, I think is the way to go. Though to avoid confusion, I think it might be helpful to clarify what th "Packets" column means in of the transport draft. At the moment, the column is being explained as the types of packets that each frame type can appear in (URL), but we could specifically say that it reflects the rules defined in plus prohibitions stated elsewhere.
Modulo clearing the reason phrase, this looks good.

packet number 0, and contains a CRYPTO frame carrying the ClientHello.  <ins> Note that multiple QUIC packets - even of different packet types - can be coalesced into a single UDP datagram (see packet-coalesce), </ins> and so this handshake may consist of as few as 4 UDP datagrams, or any number more.  For instance, the server's first flight contains  <ins> Initial packets, Handshake packets, and "0.5-RTT data" in 1-RTT packets with a short header. </ins> tls-0rtt-handshake shows an example of a connection with a 0-RTT handshake and a single packet of 0-RTT data.  Note that as described  <ins> in packet-numbers, the server acknowledges 0-RTT data in 1-RTT packets, and the client sends 1-RTT packets in the same packet number space. </ins> 7.2. Section 4.1.2 of QUIC-TLS), an endpoint MUST send any CONNECTION_CLOSE frames in a 1-RTT packet.  However, prior to confirming the handshake, it is possible that more advanced packet  <ins> protection keys are not available to the peer, so another CONNECTION_CLOSE frame MAY be sent in a packet that uses a lower packet protection level.  More specifically: A client will always know whether the server has Handshake keys (see discard-initial), but it is possible that a server does not know whether the client has Handshake keys.  Under these circumstances, a server SHOULD send a CONNECTION_CLOSE frame in both Handshake and Initial packets to ensure that at least one of them is processable by the client. A client that sends CONNECTION_CLOSE in a 0-RTT packet cannot be assured of the server has accepted 0-RTT and so sending a CONNECTION_CLOSE frame in an Initial packet makes it more likely that the server can receive the close signal, even if the application error code might not be received. Prior to confirming the handshake, a peer might be unable to process 1-RTT packets, so an endpoint SHOULD send CONNECTION_CLOSE in both Handshake and 1-RTT packets.  A server SHOULD also send CONNECTION_CLOSE in an Initial packet. Sending a CONNECTION_CLOSE of type 0x1d in an Initial or Handshake packet could expose application state or be used to alter application state.  A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE of type 0x1c when sending the frame in Initial or Handshake packets.  Otherwise, information about the application state might be revealed.  Endpoints MUST clear the value of the Reason Phrase field and SHOULD use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE of type 0x1c. CONNECTION_CLOSE frames sent in multiple packet types can be </ins> coalesced into a single UDP datagram; see packet-coalesce. 10.4. All other packets are protected with keys derived from the cryptographic handshake.  The type of the packet from the long header or key phase from the short header are used to identify which  <ins> encryption keys are used.  Packets protected with 0-RTT and 1-RTT keys are expected to have confidentiality and data origin authentication; the cryptographic handshake ensures that only the communicating endpoints receive the corresponding keys. </ins> The packet number field contains a packet number, which has additional confidentiality protection that is applied after packet packets. Coalescing packets in order of increasing encryption levels (Initial,  <ins> 0-RTT, Handshake, 1-RTT; see Section 4.1.4 of QUIC-TLS) makes it more likely the receiver will be able to process all the packets in a single pass.  A packet with a short header does not include a length, so it can only be the last packet included in a UDP datagram.  An endpoint SHOULD NOT coalesce multiple packets at the same encryption level. </ins> Senders MUST NOT coalesce QUIC packets for different connections into a single UDP datagram.  Receivers SHOULD ignore any subsequent The "Packets" column in frame-types does not form part of the IANA registry (see iana-frames).  This column lists the types of packets  <ins> that each frame type could appear in, indicated by the following </ins> characters: Initial (packet-initial) 1-RTT (short-header)  <ins> A CONNECTION_CLOSE frame of type 0x1d cannot appear in Initial or Handshake packets. </ins> Section 4 of QUIC-TLS provides more detail about these restrictions. Note that all frames can appear in 1-RTT packets. Data sent in CRYPTO frames is retransmitted according to the rules in QUIC-RECOVERY, until all data has been acknowledged.  Data in CRYPTO frames for Initial and Handshake packets is discarded when  <ins> keys for the corresponding packet number space are discarded. </ins> Application data sent in STREAM frames is retransmitted in new STREAM frames unless the endpoint has sent a RESET_STREAM for that CONNECTION_CLOSE frames.  Endpoints MUST treat receipt of Handshake packets with other frames as a connection error.  <ins> Like Initial packets (see discard-initial), data in CRYPTO frames for Handshake packets is discarded - and no longer retransmitted - when Handshake protection keys are discarded. </ins> 17.2.5. RFC3629. The application-specific variant of CONNECTION_CLOSE (type 0x1d) can  <ins> only be sent using 0-RTT or 1-RTT packets (QUIC-TLS, Section 4). When an application wishes to abandon a connection during the handshake, an endpoint can send a CONNECTION_CLOSE frame (type 0x1c) with an error code of APPLICATION_ERROR in an Initial or a Handshake packet. </ins> 19.20. A server received a Retry Token in a client Initial that is invalid. <ins> The application or application protocol caused the connection to be closed. </ins> An endpoint has received more data in CRYPTO frames than it can buffer.


a peer to maintain a more accurate round-trip time estimate (see Section 13.2 of QUIC-TRANSPORT).  4. At a high level, an endpoint measures the time from when a packet was

and by documenting the core difference between TCP's RTO and QUIC's PTO.
QUIC: TCP:
Discussed in ZRH. Difference to be explained as an editorial comment.
Specifically, we'll add some text that describes how PTO in QUIC is different than RTO and TLP in TCP.
"When an ACK frame is received that establishes loss of all in-flight packets sent over a long enough period of time, the network is considered to be experiencing persistent congestion." QUIC declares persistent congestion only upon receipt of an ACK for a packet sent after entering loss recovery. This means that the cwnd will remain large and usable even if there is prolonged network silence with packets outstanding. If the state of the transport was application limited, this can cause a large burst on the network. In TCP, an RTO causes immediate drop of the cwnd to min_cwnd. Ideally QUIC should drop the cwnd in the same timescale as TCP would. Typically minrto for the Internet is 200 msec or 300 msec (and larger if the RTT is larger).
I do not want to change the existing algorithm, because the app-limited case is the case when the sender is strictly less aggressive than the congestion controller limits, so I believe it's less likely to cause issues for the network. I can write text pointing out this difference.
Discussed in ZRH. Proposed resolution is to close with no action. Editorial issue to explain the difference to TCP and its safety.
NAME this was in the latest batch of consensus calls, but you didn't mark it has having consensus. Can you review please?
You are correct, I missed this one somehow sorry so moving to has consensus
LGTM

a peer to maintain a more accurate round-trip time estimate (see Section 13.2 of QUIC-TRANSPORT). <ins> 3.1.7. 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 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 congestion window upon expiry of an RTO.  Instead of collapsing the congestion window and declaring everything in-flight lost, QUIC allows probe packets to temporarily exceed the congestion window whenever the timer expires. In doing this, QUIC avoids unnecessary congestion window reductions, obviating the need for correcting mechanisms such as F-RTO RFC5682. Since QUIC does not collapse the congestion window on a PTO expiration, a QUIC sender is not limited from sending more in-flight packets after a PTO expiration if it still has available congestion window.  This occurs when a sender is application-limited and the PTO timer expires.  This is more aggressive than TCP's RTO mechanism when application-limited, but identical when not application-limited. A single packet loss at the tail does not indicate persistent congestion, so QUIC specifies a time-based definition to ensure one or more packets are sent prior to a dramatic decrease in congestion window; see persistent-congestion. </ins> 4. At a high level, an endpoint measures the time from when a packet was


round trip.  During recovery, the congestion window remains unchanged irrespective of new losses or increases in the ECN-CE counter.  6.6. During the handshake, some packet protection keys might not be

Similar to TCP as described in Section 5 of RFC 6675
Should we restrict this to retransmittable frames that were lost in the packet? If it was a small packet, can I pack to the MTU? What about pure ACKs that I've declared lost?
Those are great questions. There are two goals in my mind. The first is to ensure loss recovery is not unnecessarily slower than TCP. The second is to ensure this behavior isn't significantly more aggressive than TCP. But within those bounds, I'm inclined to not be too restrictive, if that makes sense? Suggestions welcome if you think they'd be an improvement.
Well the pedantic answer is that TCP would only detect a loss if there was something in the bytestream. the QUIC equivalents would then be any packet that contained STREAM, CRYPTO, MAXSTREAMDATA, etc, plus any packet that flipped the key update bit. But not ACK, MAX_DATA, and other things that are in the TCP header. That is obviously a dumb way to approach things. I don't actually have a strong opinion on this one way of the other, but if I read the draft correctly, the loss of an ACK-only packet will affect cwnd in QUIC but not TCP (granted, a cwnd-constrained endpoint sending pure ACKs is a corner case). So I guess I'd err on the side of permitting the packet in response to loss of an ACK, as we're already quite a bit less aggressive than TCP in that case. Anyway, do we want any normative language against, say, using this permission to send high-priority new data instead of the lost data?
Adding language against sending new data or data besides data that's been lost couples the "can I send" with "what should I send" questions more than I would like unless it's absolutely critical. FYI, to be declared lost, it must be in-flight: URL "A packet is declared lost if it meets all the following conditions: The packet is unacknowledged, in-flight, and was sent prior to an acknowledged packet."
Ah, I think we're fine then.
TCP recommends that a sender retransmit a packet immediately upon detecting the first loss in a recovery window (see Section 3.2, second para, of RFC 5681; Section 5 of RFC 6675 seems to follow the same idea). QUIC should at least allow the same. Not doing this can delay loss recovery by 0.5 RTT, which is not great. This replaces ; this is a more correct description of the problem, which is a design issue.
Discussed in ZRH. Proposed resolution is to get a PR done.

round trip.  During recovery, the congestion window remains unchanged irrespective of new losses or increases in the ECN-CE counter. <ins> When entering recovery, a single packet MAY be sent even if bytes in flight now exceeds the recently reduced congestion window.  This speeds up loss recovery if the data in the lost packet is retransmitted and is similar to TCP as described in Section 5 of RFC6675.  If further packets are lost while the sender is in recovery, sending any packets in response MUST obey the congestion window limit. </ins> 6.6. During the handshake, some packet protection keys might not be


An endpoint restarts its idle timer when a packet from its peer is received and processed successfully.  The idle timer is also <del> restarted when sending an ack-eliciting packet (see QUIC-RECOVERY), but only if no other ack-eliciting packets have been sent since last receiving a packet.  Restarting when sending packets ensures that connections do not prematurely time out when initiating new activity. An endpoint might need to send packets to avoid an idle timeout if it is unable to send application data due to being blocked on flow control limits; see flow-control. </del>  An endpoint that sends packets near the end of the idle timeout period risks having those packets discarded if its peer enters the draining state before the packets arrive.  If a peer could time out within a Probe Timeout (PTO; see Section 6.6 of QUIC-RECOVERY), it is <del> advisable to test for liveness before sending any data that cannot be retried safely.  Note that it is likely that only applications or application protocols will know what information can be retried. </del>  10.3.

From NAME and NAME comments on
In I received a number of comments on the existing text from NAME and NAME . This is an issue to track improving that text in a separate PR. One suggestion was to introduce an ITO(Idle Timeout) acronym.
NAME thinks cognitive load is increased by ITO and it's not that much shorter, so let's not do that.

An endpoint restarts its idle timer when a packet from its peer is received and processed successfully.  The idle timer is also  <ins> restarted when sending the first ack-eliciting packet (see QUIC- RECOVERY) after receiving a packet.  Restarting when sending packets ensures that connections do not prematurely time out when initiating new activity.  Only restarting when sending after receipt of a packet ensures the idle timeout is not excessively lengthened past the time the peer's timeout has expired.  An endpoint might need to send packets to avoid an idle timeout if it is unable to send application data due to being blocked on flow control limits; see flow-control. </ins> An endpoint that sends packets near the end of the idle timeout period risks having those packets discarded if its peer enters the draining state before the packets arrive.  If a peer could time out within a Probe Timeout (PTO; see Section 6.6 of QUIC-RECOVERY), it is  <ins> advisable to probe the path with an ack-eliciting packet to ensure the connection is still responsive before sending any data that cannot be retried safely.  Note that it is likely that only applications or application protocols will know what information can be retried. </ins> 10.3.


this document, the chosen controller MUST conform to the congestion control guidelines specified in Section 3.1 of RFC8085.  The algorithm in this document specifies and uses the controller's congestion window in bytes.

Clarifies that losing ACK-only packets is detectable in QUIC, but this draft specifies no response to their loss. Possibly this isn't the right spot for this text. If so, please suggest an alternate location.
NAME the statement that ACK only packets should be included in ACKs is in transport: URL "Every packet SHOULD be acknowledged at least once, and ack-eliciting packets MUST be acknowledged at least once within the maximum ack delay."
After some discussion, the "in-flight" condition here leads to an odd requirement. This says that lost ACK-only packets cannot be declared lost. This buries a potential signal that might drive a congestion controller. This comes from TCP. In TCP, detecting loss of an ACK is impossible, but in QUIC, we can. But this requirement essentially forbids that. Implementations don't seem to be consistent on this point. NAME says that if we change this, we should also mandate the acknowledgment of ACK-only packets when sending an ACK for other ACK-eliciting packets. We could simply say that this is good and recognize that this requirement is consistent with not requiring ACKs for these. Or we could take the signal and declare them lost.
I do not think we need to allow ACK-only packets to be declared lost in the specification. That's something an implementation might do. At the same time, I'd point out that QUIC is designed on the premise that "acknowledgment of ACK-only packets when sending an ACK for other ACK-eliciting packets." The most prominent example is ; the entire section is based on the assumption that an endpoint behaves that way. Therefore, we should be clear about the requirement, if we aren't already clear about that.
We should at least recognize that ACK-only packets can be declared lost. One interpretation of the current text is that ACK-only packets MUST NOT be declared lost. The reason that you might not want to declare these as lost that is that the requirements for acknowledging ACK-only packets are (necessarily) more loose than ACK-eliciting packets. But I believe that it is simpler to allow code to declare them lost. Correct me if I am wrong, but I believe that you have one fewer condition if you ignore whether a packet is in flight or not when doing loss detection. It is also likely better to mark lost ACKs as lost as it means that you don't ignore a potential congestion signal.
The problem is that a loss event has impact on CC, which applies to the amount of data the endpoint sends. IIUC, the basis of CC is that an endpoint throttles the bandwidth it uses based on the loss rate of data it sent. However, generation of ACKs is not controlled by the endpoint. It's controlled by the peer. Consider the case of in-flight wifi that uses a satellite link. The client (web browser) could be sending far more ACK-only packets than ACK-eliciting packets. In such case, I am concerned about the impact of taking loss of ACK-only packets into calculation.
I don't follow that argument. There's a simpler one: if sending packets causes congestion such that loss (or ECN-CE marking) occurs, then ignoring that signal means that you will likely create more congestion. I don't see how the relationship to inbound packets is relevant here. There are many ways in which the rate of outbound packets might be determined by inbound packets. Servers answers requests, after all. And if ACK-only packets are the dominant use of a link, then deliberately ignoring signals from them likely only drives send rates up to unsustainable levels; and that's on top of not counting those packets toward limits (something that is necessary).
Let my paraphrase. Are you arguing that an endpoint SHOULD throttle the rate in which it sends ACK-only packets, when some of those ACK-only packets are being lost? I would assume that your answer is no, and that is because send-rate of ACKs are not controlled by the endpoint. It is controlled by the peer that is sending data. Up until now, CC is per-direction. An endpoint sends data at a rate controlled by ACKs sent by peer in response. The other side does the same thing, individually. While I do not dispute the fact that ideally we should take the loss of ACK-only packets into consideration, my argument is that we are not at that point (yet), because we do not control the send-rate of ACK-only packets. Using the loss of ACK-only packets without controlling the send-rate of ACK-only packets based on their loss essentially means that we are going to prioritize emission of ACKs above data. This would be a departure from the per-direction CC design that we have now. As I stated in my previous comment, I am concerned about the negative impacts that the change in design might cause.
Can't find the text right now, but I thought that ack-only packets are not blocked by the congestion controller. At least that's what I implemented in quic-go.
NAME is right. You don't allow ACK-only packets to be limited by the congestion controller. I believe that don't is "can't" in practice. And we should prioritize acknowledgements over data. There's a point where you can't do anything but ACK, but that's the case no matter what we decide here. All I'm saying is that loss of an ACK-only packet is a signal that we are being encouraged to ignore right now, for no good reason.
Agreed. Yeah. I might agree that there is "no good reason". But my argument is that simply saying that an endpoint MAY take loss of ACK-only packets into consideration does not fix the problem. Please correct me if I am wrong, but CC is designed as a feedback loop between send-rate adjustment and loss observation. Including loss of ACK-only packets to the latter, without adjusting the send-rate of ACK-only packets is going to change the equation. Let's consider an example. Consider the case of a satellite link that has 1000ms RTT, 1Mb/s downstream bandwidth. When the downstream path is fully utilized, there would be ~100 downstream packets per second (or per RTT), which means that there would be ~50 ACKs being sent to upstream. Let's assume that the loss rate of packets sent to upstream is 2%, regardless of the packet size. Under the status quo, CWND to upstream would be ~8 packets, because on average one loss would be observed per every 50 ACK-eliciting packets being sent. However, if we are to take into consideration the loss of ACK-only packets, the outcome would be that packet loss would be observed at a probability of 70% for each RTT. I think that would lead us to a CWND size of 2 packets or below. One might argue that the loss rate is actually proportional to the size of packets, rather than the number of packets. So let's assume that the size of an ACK-only packet is typically 100 bytes. That does move a needle a bit, but I think the problem still exists. Assume that MTU is 1400 bytes, loss rate being 10% / MTU, ACK-eliciting packet are always full-sized, and that ACK-only packets are 50 bytes long. In status quo, I think that CWND would be somewhere between 2 MTU to 3 MTU (because the loss rate is 10% / MTU). But once we take the loss of ACK-only packets into consideration, the observed loss rate per RTT becomes 1.5x~2x, because the amount of data being sent per RTT increases by 50*50=2500 bytes. That would lead to a noticeable reduction of CWND size.
I'd prefer to keep this as is, because I think ensuring everything works correctly with an updated design is going to be a bit complex and potentially error prone, and I don't see any value in the change. For example, when an ACK is lost, if the CWND is reduced, that doesn't reduce the rate of ACKs, it just reduces the rate of sending future data. Because ACKs do not count towards bytes in flight, the congestion window would never increase if a peer is only sending ACKs, so we'd be putting peers in a situation that the CWND could decrease, but never increase. That alone seems sub-optimal. FWIW, I do believe we discussed this at length a few years ago and ended up where we did because of (at least) three points: 1) ACKs are not required to be acknowledged, even once. 2) This is very similar to what TCP does. 3) Congestion controlling ACKs is a research topic. NAME We track lost packets for a while in case they were declared lost too early, so the existing design is quite natural and a bit simpler. If in QUICv2/etc we think it's worth making ACK-only packets congestion controlled and count towards bytes in flight, I think that would be a more holistically appealing design. I also think it's quite a bit of work and research to get right. But I do think we should make this point a bit clearer in recovery and articulate why this choice was made.
I agree with Martin, there is no reason to ignore loss of ACKs as a signal. What you do with this information is a different question. There have been proposals for "ACK congestion control" in the past which basically means that you may adjust your ACK rate (e.g. if you send an ACK for every second packet you could easily reduce that rate if possible by your indicated max ack delay). I'm not saying we should specify any "ACK congestion control" but we should definitely make it possible to enable future experimentation on this.
I'd be happy to add text that says: "You MAY detect the loss of ACK-only packets and use that information to adjust the congestion controller or the rate of ACK-only packets being sent, but this document does not describe a mechanism for doing so."
I wasn't suggesting that we do this bit. Just the other piece.
Thank you, NAME for raising a point that I had completely forgotten! I remember now why I had not wanted ACK loss to drive the congestion controller. I think NAME and NAME are saying similar things, but the short of it is this: the congestion controller assumes that cwnd amount of data is in flight when it reacts to loss, reducing the cwnd. However, the cwnd has nothing to do with the amount of ACKs that are in flight, and as a result, the reduction of the cwnd in response makes no sense in this case. (It does make an increasing amount of sense to use as the traffic in this direction is increasingly dominated by ack-eliciting data, but that also makes it increasingly likely that a congestive loss will be of an ack-eliciting packet.) We should not change the design here. I'm fine with NAME proposed addition, but I don't think we should use the loss of ACKs as a congestion signal that affects the congestion window.
Are there any interop issues with this? Implementations keep tab of what is going on in all kind of ways. Many use "ack of ack" mechanisms to notice which ack have been acknowledged by the peer and which ack ranges can be safely pruned. Some do what Mirja described. Some don't care. Please do not try to overspecify this.
The text is slightly ambiguous on the topic of whether pure-ACK packets are to be acknowledged. Section "13.2. Generating Acknowledgements" implies that they are ACKed later: Section "13.2.1. Sending ACK Frames" implies that they SHOULD but not MUST be ACKed: and It seems strange NOT to acknowledge all packets when sending an ACK frame. What benefit is there to NOT doing so? And if there is no benefit, why not require the protocol implementations to be more predictable? What specific response a specific congestion controller has as a reaction to a lost / EC-marked pure-ACK packet is an implementation matter, of course.
NAME I'm not sure there is a contradiction here. You want to acknowledge all packets eventually because as you say, it is odd to have to bypass an ACK frame. But you don't want certain frames to actively initiate ACKS. I think this is covered in the three sentences that you quote. The odd special case is when the connection terminates. What if there are pending ACKs that you don't want to acknowledge?
NAME The reason it's a SHOULD is both because it's not designed to be required for QUIC to function correctly and because of the case when no ACK frame is sent prior to a connection close. The only advantages of not requiring it I can think of would arise in extremely simple implementations, such as those which only track 1 ack range. In that case, a receiver which received an ACK-only packet and then an ack-eliciting packet with a gap between the two would want to only acknowledge the ack-eliciting packet.
NAME NAME Thanks, the SHOULD in "Section "13.2.1. Sending ACK Frames" being a SHOULD due to connection termination makes sense. It would be nice if it were explicit that the only case when packets may never get ACKed is due to the connection termination. NAME Some implementations might not care about loss of ACK-only packets, while others may care about such losses. For the latter ones, knowing of such losses is as required for correct functioning as knowing of loss of packets with DATAGRAMs (what bytes count toward a congestion window is not the only implementation concern). I do not think that there is any burden on even the simplest implementation. If the simplest implementation detects a gap, it can send a packet with an ACK. If that gap was detected due to a receipt of an non-ack-eliciting packet, a PING frame could be included with the resulting ACK (to satisfy "An endpoint MUST NOT send a non-ack-eliciting packet in response to a non-ack-eliciting packet, even if there are packet gaps which precede the received packet." The bottom line is that building in a "loss ambiguity" into the protocol without a great reason is a mistake. TCP has a retransmission ambiguity, and we know it is a problem.
Your solution very much goes against what I intended for that text, so I filed to clarify what I meant and add some more text about what can be expected of ACKs. I think it makes sense to continue discussion on that issue, if there is further discussion, because this issue is about whether ACK-only packets can be declared lost, not about acknowledgement reliability. In particular, acknowledgements are not guaranteed to be received. Almost all packets get acknowledged, but a sender can never rely on all packets being acknowledged, even of ACK-eliciting packets.
Happy to discuss acknowledgement reliability, though I want to point out that the two issues are related. If it is decided that non-ACK-eliciting packets will be acknowledged eventually (except for special circumstances, such as connection termination or packet number space issues), then it informs the "considered lost" discussion.
The difference between MUST with caveats(ie: connection-close/etc) and SHOULD is small to zero in practice I believe, since in both cases you may never receive an acknowledgement for the packet. It's also an unenforceable MUST, which I hate adding unless it's critical to protocol correctness.
:+1: Thank you for the PR. LGTM, and to me it seems that the add text is at the right location. Using the loss of ACK-only packets is one way of deviating from CC that is described in this document, and therefore it seems correct to me to have that stated right after the general requirements regarding CC when one chooses a design that is different from what is being specified in quic-recovery.ICLWT.That's fine. I don't know whether we want to mention ack-of-ack and ack-range pruning here. Probably not. But it should be very clear that while ACK only packets do not trigger acks, the packet numbers should be included in the ack ranges.

this document, the chosen controller MUST conform to the congestion control guidelines specified in Section 3.1 of RFC8085. <ins> Similar to TCP, packets containing only ACK frames do not count towards bytes in flight and are not congestion controlled.  Unlike TCP, QUIC can detect the loss of these packets and MAY use that information to adjust the congestion controller or the rate of ACK- only packets being sent, but this document does not describe a mechanism for doing so. </ins> The algorithm in this document specifies and uses the controller's congestion window in bytes.


2.1.4.  The Known Received Count is the total number of dynamic table insertions and duplications acknowledged by the decoder.  The encoder tracks the Known Received Count in order to identify which dynamic

Much belated, this attempts to address the concerns about deadlocking. Basically, this says that you should avoid instructions that don't have flow control credit. It doesn't say how an encoder might learn what limits are, but we've seen a range of tactics being used in implementations and I don't want to get into transport API debates. This doesn't cover the memory exhaustion attack that NAME suggests on the issue; I think that is better covered more generally by the connection-level flow control limits.
I debated adding another paragraph:
Thanks for adding this!

2.1.4. <ins> Writing instructions on streams that are limited by flow control can produce deadlocks. A decoder might stop issuing flow control credit on the stream that carries a header block until the necessary updates are received on the encoder stream.  If the granting of flow control credit on the encoder stream (or the connection as a whole) depends on the consumption and release of data on the stream carrying the header block, a deadlock might result. More generally, a stream containing a large instruction can become deadlocked if the decoder withholds flow control credit until the instruction is completely received. To avoid these deadlocks, an encoder SHOULD avoid writing an instruction unless sufficient stream and connection flow control credit is available for the entire instruction. 2.1.5. </ins> The Known Received Count is the total number of dynamic table insertions and duplications acknowledged by the decoder.  The encoder tracks the Known Received Count in order to identify which dynamic


estimate (4*rttvar), and max_ack_delay, to account for the maximum time by which a receiver might delay sending an acknowledgement. When the PTO is armed for Initial or Handshake packet number spaces, <del> the max_ack_delay is 0, as specified in 13.2.5 of QUIC-TRANSPORT. </del>  The PTO value MUST be set to at least kGranularity, to avoid the timer expiring immediately.

In recovery for maxackdelay of 0.
Section 5.2.1 of -recovery-25 says: However, Section 13.2.5 of -transport-25 doesn't actually say that (anymore?) Section 13.2.1 says so I guess at least the reference is incorrect. But since Section 19.3 only has a SHOULD for encoding zero and the pseudo code simply uses the encoded ACK delay, if the ACK sender doesn't encode zero, the ACK receiver will compute its RTT based on the encoded ACK delay.
The intent is to use 0 as a peer maxackdelay for the Initial and Handshake packet number spaces. The pseudocode says: ackdelay = min(Ack Delay in ACK Frame, maxack_delay) So for Initial and Handshake, min(Ack Delay, 0) = 0 The text in the "Estimating smoothed RTT and RTTVar" section is fairly clear I think? I fixed the section reference in
I understood the in the to be the value of the transport parameter?
Not for Initial and Handshake PN spaces. Would it be helpful to have a comment above the min() in the pseudocode?
That would at least help me (pointing out it is NOT the value of the TP, although the name is the same.)
I noticed the pseudocode before UpdateRtt() already makes this fairly clear. I can fold the pn_space check into UpdateRtt if you think it would read more clearly?
Ah, so we are not limiting , we are overwriting .

estimate (4*rttvar), and max_ack_delay, to account for the maximum time by which a receiver might delay sending an acknowledgement. When the PTO is armed for Initial or Handshake packet number spaces,  <ins> the max_ack_delay is 0, as specified in 13.2.1 of QUIC-TRANSPORT. </ins> The PTO value MUST be set to at least kGranularity, to avoid the timer expiring immediately.