Change:
6.1.4. <del> As described in server-behavior, the server MAY either accept ECH and use ClientHelloInner or reject it and use ClientHelloOuter.  In handling the server's response, the client's first step is to determine which value was used. </del> <ins> As described in server-behavior, the server may either accept ECH and use ClientHelloInner or reject it and use ClientHelloOuter.  This is determined by the server's initial message. If the message does not negotiate TLS 1.3 or higher, the server has rejected ECH.  Otherwise, it is either a ServerHello or HelloRetryRequest. </ins> <del> If the server replied with a HelloRetryRequest, then the client proceeds as described in client-hrr.  Otherwise, if the server replied with a ServerHello, then the client checks if the last 8 bytes of "ServerHello.random" are equal to "accept_confirmation" as defined in backend-server.  If so, then it presumes acceptance. Otherwise, the client presumes rejection. </del> <ins> If the message is a ServerHello, the client computes "accept_confirmation" as described in backend-server.  If this value matches the last 8 bytes of "ServerHello.random", the server has accepted ECH.  Otherwise, it has rejected ECH. </ins> <del> 6.1.4.1. </del> <ins> If the message is a HelloRetryRequest, the client checks for the "encrypted_client_hello" extension.  If none is found, the server has rejected ECH.  Otherwise, if it has a length other than 8, the client aborts the handshake with a "decode_error" alert.  Otherwise, the client computes "hrr_accept_confirmation" as described in backend- server-hrr.  If this value matches the extension payload, the server has accept ECH.  Otherwise, it has rejected ECH. [[OPEN ISSUE: Depending on what we do for issue#450, it may be appropriate to change the client behavior if the HRR extension is present but with the wrong value.]] </ins> <del> If the server used ClientHelloInner, the client proceeds with the connection as usual, authenticating the connection for the true server name. </del> <ins> If the server accepts ECH, the client handshakes with ClientHelloInner as described in accepted-ech.  Otherwise, the client handshakes with ClientHelloOuter as described in rejected-ech. </ins> <del> 6.1.4.2. </del> <ins> 6.1.5. </ins> <del> If the server used ClientHelloOuter, the client proceeds with the handshake, authenticating for ECHConfig.contents.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application. It MUST NOT use the retry configurations. </del> <ins> If the server accepts ECH, the client proceeds with the connection as in RFC8446, with the following modifications: The client behaves as if it had sent ClientHelloInner as the ClientHello.  That is, it evaluates the handshake using the ClientHelloInner's preferences, and, when computing the transcript hash (Section 4.4.1 of RFC8446), it uses ClientHelloInner as the first ClientHello. If the server responds with a HelloRetryRequest, the client computes the updated ClientHello message as follows: It computes a second ClientHelloInner based on the first ClientHelloInner, as in Section 4.1.4 of RFC8446.  The ClientHelloInner's "encrypted_client_hello" extension is left unmodified. It constructs EncodedClientHelloInner as described in encoding- inner. It constructs a second partial ClientHelloOuterAAD message.  This message MUST be syntactically valid.  The extensions MAY be copied from the original ClientHelloOuter unmodified, or omitted.  If not sensitive, the client MAY copy updated extensions from the second ClientHelloInner for compression. It encrypts EncodedClientHelloInner as described in encrypting- clienthello, using the second partial ClientHelloOuterAAD, to obtain a second ClientHelloOuter.  It reuses the original HPKE encryption context computed in real-ech and uses the empty string for "enc". The HPKE context maintains a sequence number, so this operation internally uses a fresh nonce for each AEAD operation.  Reusing the HPKE context avoids an attack described in flow-hrr-hijack. The client then sends the second ClientHelloOuter to the server. However, as above, it uses the second ClientHelloInner for preferences, and both the ClientHelloInner messages for the transcript hash.  Additionally, it checks the resulting ServerHello for ECH acceptance as in determining-ech-acceptance.  If the ServerHello does not also indicate ECH acceptance, the client MUST terminate the connection with an "illegal_parameter" alert. 6.1.6. If the server rejects ECH, the client proceeds with the handshake, authenticating for ECHConfig.contents.public_name as described in auth-public-name.  If authentication or the handshake fails, the client MUST return a failure to the calling application.  It MUST NOT use the retry configurations. </ins> Otherwise, if both authentication and the handshake complete successfully, the client MUST abort the connection with an

Discussion:
This PR cleans up the client text, in hopes of making it more precise and organized somewhat more chronologically. It then uses this cleanup to clarify bits we forgot to write down in the accept/reject/HRR machinery: Handling the Server Response is now Determining ECH Acceptance and only discusses the initial accept/reject decision. The portion of Handling HelloRetryRequest discussing the accept/reject decision is moved to Determining ECH Acceptence. Determining ECH Acceptance is tidied up to discuss to what happens if you see an older TLS version (it's a reject). In particular, in DTLS 1.2, the first server message might be HelloVerifyRequest and not ServerHello or HelloRetryRequest. Accepted/Rejected ECH are now Handshaking with ClientHelloInner/ClientHelloOuter to fit with the other section names (lots of VERBing). They are unindented from (1) because that section only talks about the accept/reject decision. This gives us a place to discuss all changes to the handshake. We describe what it actually means to handshake with ClientHelloInner, including implications to the transcript. And... URL portion of Handling HelloRetryRequest that discussed constructing the second ClientHello never applied to the ECH Reject case anyway, only ECH Accept. Move this to (4). Also expand on it so it's a bit more clearly defined. I've also moved HPKE setup out of Encrypting the ClientHello, as that's only relevant to one of the two "callers" anyway. I'm not thrilled with this notion of "partial ClientHelloOuterAAD", but the text used in Offering ECH and Encrypting the ClientHello sections otherwise doesn't actually match. Before this PR, both think they are responsible for constructing ClientHelloOuter. With this and (1), Handling HelloRetryRequest is now empty and removed. We forgot to write down the client half of URL There is now a place for it in Handshaking with ClientHelloInner. We say that the ECH reject case ignores the URL extension, so ECH GREASE says the same thing, to keep them aligned. The Handshaking with ClientHelloOuter section could also do with an editorial pass, but I've omitted it from this PR because it was getting a bit large as it is.

I suspect this is editorial, but it would be helpful to clarify exactly how the client computes its transcript when it generates its keys, based on the permutations of who gets what. IIUC: If the server authenticates the inner SNI, the client must construct the client hello as seen by the back-end server, including any OuterExtensions compression, and uses it in the transcript. If the server authenticates the public SNI, the client uses the entire client hello in the hash as-is, including the (unreadable) encryptedclienthello extension. A server that combines the client-facing and back-end functions also has to duplicate this logic, since it can see everything but maybe shouldn't use it all, and has to go through the motions of formally constructing a proper client hello to a back-end server. I found this all quite subtle to work out and asked some questions of one of the authors to fully understand. I'll leave it to the editors as to whether the draft needs some text to make this clearer. [Aside: is it computationally feasible to add some sort of "checksum" that would neutralize the difference in the hashes? That would make this a little less tricky, IMO]
+1 and test vectors would be good too
Yes, agree. This is really tricky: "The client presumes acceptance if the last 8 bytes of URL are equal to the first 8 bytes of accept_confirmation as defined in Section 7.2. Otherwise, it presumes rejection."
Here's what I ended up writing after I solved this: "This issue is that the ECH transcript needs to be calculated from an unsent inner ClientHello message, which differs in a few ways from the EncodedClientHelloInner used in HPKE. Upon ECH acceptance, the transcript from the ClientHelloOuter is discarded, then substituted with one calculated from the unsent inner ClientHello message, and the client random is changed to the inner random value. After that, the ServerHello message is added to the transcript as normal. The transcript used for ECH acceptance is separate from the normal transcript."
I think the way to think about this is that there are two possible handshakes here, joined at the first message. Either we handshake with ClientHelloOuter, or we handshake with ClientHelloInner.

Whichever, we handshake with, we follow the usual RFC8446 rules, including incorporating the chosen ClientHello into the transcript. Indeed on the server, the protocol even supports a client-facing vs. backend server split where the connection is actually rewritten to use ClientHelloInner. EncodedClientHelloInner, per the name, is just an encoding. We don't want to send two copies of things like key shares. The actual ClientHello being conveyed is still ClientHelloInner. (This is important since the transcript needs to reflect all inputs to TLS parameter negotiation.) But, yes, on the client this gets a little fun because the client needs to offer these two somewhat related ClientHellos, learn which was to used a message later, and then complete the handshake accordingly.
Perhaps there's missing text in "6.1.4.1. Accepted ECH", where client transcript handling could be addressed. My feedback is that it was possible to construct a conformant EncodedClientHelloInner and enclosing ClientHelloOuter from reading the spec as a non-author, but I had to test with other implementations to discover what I was supposed to do with the ClientHelloInner. If you get this part wrong, then you just hit a decryption error after ServerHello. I did understand that I had to update the transcript, but I initially thought I should do it with an EncodedClientHelloInner added to the ClientHelloOuter transcript. Once I discovered that was not the case, I had to construct a ClientHelloInner that still had the fields that are compressed in EncodedClientHelloInner. That doesn't cross anything in the text, but it wasn't obvious to me.
I think we're in agreement here, but is the encoding of ClientHelloInner deterministic enough that the client-facing server can't possibly express the expanded ClientHelloInner in a way that breaks the transcript?
It's decode, not encode, that needs to be deterministic. And yes, it is deterministic.
Here's what I ended up writing to get this working. I don't have HRR support done yet, so I expect these comments will change a little bit:
Which transcript is used should be better clarified now that landed. Closing as such. (If we want to track test vectors for ECH, let's open a new issue.)
Thanks for capping the amount of changes in this PR :)