Note the following three lines in XEdDSA:
$\newcommand{\opn}{\operatorname}$
- $r=\opn{hash}(a\parallel M\parallel Z)\bmod q$
- $R=rB$
- $h=\opn{hash}(R\parallel A\parallel M)\bmod q$
As you can see, the hash input of the third instruction depends on the hash output of the first instruction. And even worse you can't "just append" said output, it ahs to come first.
Now assume that you have a large message (like one gigabyte in size). While you could stream-process the first hash easily, you would have to buffer the full message so that you can feed it into the second hash invocation, requiring you to either a) have a stupid large statically-allocated buffer or b) significantly limit message size or c) use dynamic allocation (which is really slow).
So how can we fix this? Pre-Hashing. Assume that the "$M$" for the signing algorithm isn't actually the 1GB M, but rather the fixed-size $\opn{hash}(M)$. Now we can use a small, constant-sized buffer to store the "$M$" and we can just stream-compute $\opn{hash}(M)$, meaning we don't need unnecessarily large buffers there either. However the obvious downside of doing pre-hashing is that a collision on the hash easily translates into a forgery attack on the signature scheme (as $\opn{hash}(M)$ and $\opn{hash}(M')$ have the same hash, they are essentially the same message for the signature algorithm even though $M\neq M'$).
I said "stream-process" here multiple times, so I'll give you a quick understanding what it means. In actual implementations of hash functions you usually find the following three functions:
hash_init(context*)
hash_update(context*,byte* data)
hash_final(context*,byte* digest)
So you normally initialize some context for the hash, then you update the context with data a few times and finally extract the hash value (the digest) out of the context. Most hash functions are designed to support this style of implementation and allow context
to be a compile-time-sized structure so that if you get a stream of data, you can continually call hash_update
and then discard the message part's buffer (if you have no further use for it, that is). So in this case, you end up with a small context structure, a small message-part buffer as opposed to one giant full-message-buffer.