Here's my take @formal lantern.
I agree with @sharp stirrup and @sleek sigil that achieving racing-free and decentralized Merkle tree is complicated. But definitely doable IMO.
The approach that I would try to take is to either use Protokit, or, if you want to do it on the L1:
- Put new Merkle leaves on chain in the form of actions/events (this guarantees native L1 data availability)
- In the reducer, perform updates to the onchain Merkle root. Whoever runs the reducer needs to know the Merkle tree (it can always be rebuilt from actions/events which can be pulled from an archive node.)
- In the reducer circuit, you pull in your
MerkleWitness in a Provable.witness() block. The Merkle tree itself should never be part of the circuit, only witnesses.
I've implemented those ideas here: https://github.com/mitschabaude/o1js-offchain-state/blob/main/contracts/src/Offchain.ts (Beware: this isn't production code. It's just storing the Merkle tree in memory. But the circuit code should provide a good example)
To overcome the static size of actions a circuit can process, my example only processes a fixed batch of actions, and then uses a recursive proof which connects the last processed action to the current onchain action state. The recursive proof is very simple and can handle 100s of actions per proof. And it's very generic (doesn't have to be rewritten per project)
However, the more efficient solution would probably be to put the entire reducer logic (including Merkle updates) in a recursive proof. It will still be heavy though, and a downside is that the recursive part becomes application specific.