Summary of the design:
- Tries are stored as immutable, copy-on-write trees
- All trie data is stored in the `TrieData` segment. Since it's immutable, data is never modified/deleted, new versions are just appended at the end.
- In order to support reverts, each context stores a pointer to the initial state trie version, plus the initial version of each storage trie.
One variation which may be worth considering is storing the whole state trie as one big trie (with sub-tries for storage). Reverts would then be simpler - we'd replace a single pointer. I thought that approach might make hashing the trie a bit more complex, as the node associated with an account would be a special type of node, rather than just another leaf. Either approach seems reasonable though.
- `GlobalMetadata` - offsets for global kernel variables in memory
- `ContextMetadata` - offsets for context-specific kernel variables in memory
- `GAS_CONSTANTS`, based on the yellowpaper
Also move constants to a separate module since `aggregator` was getting long.
Ultimately they're encoded as `[F; 8]`s in the table, but I don't anticipate that we'll have any use cases where we want to store more than 256 bits. Might as well store `U256` until we actually build the table since they're more compact.
It's a bit more type-safe (can't mix up segment with context or virtual addr), and this way uniqueness of ordinals is enforced, partially addressing a concern raised in #591.
To avoid making `Segment` public (which I don't think would be appropriate), I had to make some other visibility changes, and had to move `generate_random_memory_ops` into the test module.
This should improve cache locality - since we generally access several values at a time in a given row, we want themt to be close together in memory.
There are a few steps that make more sense column-wise, though, such as generating the `COUNTER` column. I put those after the transpose.
This adds padding rows which satisfy the ordering checks. To ensure that they also satisfy the value consistency checks, I just copied the address and value from the last operation.
I think this method of padding feels more natural, though it is a bit more code since we need to calculate the max range check in a different way. But on the plus side, the constraints are a bit smaller and simpler.
Also added a few constraints that I think we need for soundness:
- Each `is_channel` flag is bool.
- Sum of `is_channel` flags is bool.
- Dummy operations must be reads (otherwise the prover could put writes in the memory table which aren't in the CPU table).
By no longer storing unsorted operations; they are effectively stored in the CPU table already.
I ran into some issues with sorting, since the existing sort method didn't include `is_channel` columns. Rather than update the existing method, I removed it and added a sort on the `MemoryOp`s, which I think seems cleaner.