Change:
ClientHelloOuter. The client serializes this structure to construct the <del> ClientHelloOuterAAD.  It then computes the payload as: </del> <ins> ClientHelloOuterAAD.  It then computes the final payload as: </ins> Finally, the client replaces "payload" with "final_payload" to obtain ClientHelloOuter.  The two values have the same length, so it is not

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 :)