AZIP-22: Fast Inbox#55
Conversation
|
|
||
| Note that circular storage introduces a hazard here: a bucket that has not yet been consumed could be overwritten once the ring wraps around, so an L2 outage longer than the ring covers would permanently destroy in-flight messages. The Inbox must either refuse inserts that would overwrite an unconsumed bucket, halting message sends for the remainder of the outage, or fall back to plain non-circular storage. | ||
|
|
||
| We lean towards the rollover solution: reverting on overflow makes message sending griefable, since anyone can cheaply fill a bucket to delay a victim's message, whereas rolling over keeps sends live and only delays consumption. |
There was a problem hiding this comment.
With the rolling over approach, is there a risk that an attacker could fill so many future L1 blocks' buckets with messages that L2 (with its limits on copying capacity) can't copy-over messages fast enough and falls further and further behind, such that latency of L1->L2 messages becomes hours or days?
There was a problem hiding this comment.
I think the solution for the old approach was to make L1 capacity larger than can possibly be filled in the timeframe, given the then-current block gas limits on ethereum. Since then, block gas limits have gone up and up, so it's not a very reliable approach.
There was a problem hiding this comment.
With the rolling over approach, is there a risk that an attacker could fill so many future L1 blocks' buckets with messages that L2 (with its limits on copying capacity) can't copy-over messages fast enough and falls further and further behind, such that latency of L1->L2 messages becomes hours or days?
Yes, but it's the same risk as we have today for that matter.
I think the solution for the old approach was to make L1 capacity larger than can possibly be filled in the timeframe, given the then-current block gas limits on ethereum. Since then, block gas limits have gone up and up, so it's not a very reliable approach.
We can certainly revisit the constants to adjust for new block gas limits.
| assert blocks[-1].stateref == checkpoint.stateref | ||
| ``` | ||
|
|
||
| This option is simpler in terms of changes to circuits, since the parity circuit today predicates over the L1-to-L2 messages of the entire checkpoint, not of each block. However, it means that it's possible to prove the validity of a checkpoint that contains blocks whose headers' `inHash` values do not correspond to the messages they include. A workaround to this could be to just _remove_ the `inHash` from block headers, and only use them in block proposals to signal the messages to be included. Again, the correctness of `inHash`es would still be enforced by the committee and the L2 network itself. |
There was a problem hiding this comment.
I suppose we could hash a "block number" into the L1 rolling hash when the first message is inserted into a new "bucket". With that information, the BlockRoot circuit could have enough information to know that a "whole bucket" is indeed being inserted.
There was a problem hiding this comment.
This option 3 (and the other options) is missing some impl details about how we ensure consistency between the msgs inserted in the BlockRoot circuits and the msgs used in the CheckpointRoot circuit to reconstruct the inHash.
My mind comes back to the idea of copying what we do with spongeblob for blobs in order to flatten the data (msgs) that the BlockRoot circuits saw into a sponge, and then recomputing that same sponge in CheckpointRoot.
Illustrative:
circuit BlockRoot
assert merkleInsert(lastBlock.stateref.l1ToL2, msgs) == block.stateref.l1ToL2.root
block.inHashPoseidonSponge = lastBlock.inHashPoseidonSponge.absorb(msgs);
circuit CheckpointRoot
assert sha256(lastCheckpoint.inHash, blocks.msgs) == checkpoint.inHash
assert blocks[-1].inHash == checkpoint.inHash
assert blocks[-1].stateref == checkpoint.stateref
inHashPoseidonSponge = lastCheckpoint.inHashPoseidonSponge.absorb(blocks.msgs)
assert(inHashPoseidonSponge == blocks[-1].inHashPoseidonSponge
Alternatively, and depending on the gate costs of sha256 vs recursive verification, we could use the sha256 rolling hash in place of the poseidonSponge, which you touch on in your Option2 pseudocode:
circuit BlockRoot
assert sha256(lastBlock.inHash, msgs) == block.inHash
assert merkleInsert(lastBlock.stateref.l1ToL2, msgs) == block.stateref.l1ToL2.root
circuit CheckpointRoot
assert blocks[-1].inHash == checkpoint.inHash
assert blocks[-1].stateref == checkpoint.stateref
This is quite elegant, but all those sha256 hashes in the BlockRoot might slow-down that layer of proving too much. At least if we put the sha256 hashing in the CheckpointRoot, there's an option of moving those hashes to a single Parity circuit (which is worthwhile if the time cost of the sha256 hashing exceeds the cost of a recursive snark verification).
There was a problem hiding this comment.
I suppose we could hash a "block number" into the L1 rolling hash when the first message is inserted into a new "bucket". With that information, the BlockRoot circuit could have enough information to know that a "whole bucket" is indeed being inserted.
But that would require each block root to run a SHA256, right? Which is what we wanted to avoid in this option.
There was a problem hiding this comment.
No, the block root could compute a rolling posiedon2 hash (sponge) and insert block numbers into that. Then the checkpoint root could make sure those block numbers were inserted into the same positions in both the poseidon2 rolling hash and the sha256 rolling hash.
There was a problem hiding this comment.
This option 3 (and the other options) is missing some impl details about how we ensure consistency between the msgs inserted in the BlockRoot circuits and the msgs used in the CheckpointRoot circuit to reconstruct the inHash.
Why the other options? The list of messages isn't fed into the checkpoint root in options 1 and 2, so this affects only option 3, right?
Good catch, btw!
There was a problem hiding this comment.
My mind comes back to the idea of copying what we do with spongeblob for blobs in order to flatten the data (msgs) that the BlockRoot circuits saw into a sponge, and then recomputing that same sponge in CheckpointRoot.
I like it, I've added it to the proposal.
There was a problem hiding this comment.
No, the block root could compute a rolling posiedon2 hash (sponge) and insert block numbers into that. Then the checkpoint root could make sure those block numbers were inserted into the same positions in both the poseidon2 rolling hash and the sha256 rolling hash.
I see! Sounds great, added it to options 2 and 3. I've named it a generic "marker", since pretty much anything can serve that purpose, including a constant magic value.
Today, all L1-to-L2 messages for a checkpoint are added at its start, yielding a message latency of between 24 and 108 seconds. This AZIP streams L1-to-L2 messages into L2 blocks as they are received by the Inbox, rather than waiting for an Inbox tree to be sealed in order to inject all its messages simultaneously at the beginning of the next checkpoint. The Inbox is redesigned to commit to received messages via a rolling hash snapshotted per L1 block, checkpoints reference the last message bucket they consume, and an
INBOX_LAG_SECONDSlag guards against shallow L1 reorgs. The result reduces message latency to between 12 and 30 seconds.