The Republic: A Flexible, Optional Governance Proposal with Self-Governed Portals

Limited by the technology of my time, here is the last characters.

Closing thoughts

Due to my separation of governance into rollup and portal governance, I’m extending the spectrum.

I believe this proposal to be fairly flexible, supporting instances (1, 2, 3, 4 and more) spread across this spectrum, however some more likely than others.

As seen from the diagram multiple options are outlined:

  1. With most portals implemented as immutable contracts the governance is effectively out of power, and behaves as if there were no governance;
  2. Opposite, if most portals are following rollup governance, we are in the same case as a canonical rollup bridge controlled by governance;
  3. If portals are all governed by their own governance implementations (not immutable) the setup is still governed, though not by the same set (assumed) as for 2. This might resemble multi-sig world of today depending on portal governance;
  4. The combination of portal governance that i think is most likely. Many portals wanting simply to work as a canonical bridge, with some using own governance and few completely without governance.

Overall, the proposal strives to support the ability to REJECT upgrades and instead migrate for maximum immutability, but acknowledges that migrating the chain state at every upgrade is impractical. Therefore, portals are able to decide what instance is the real rollup on their own.

Since portals can decide on their own, we introduce a “guide” in the form of the Senate which can be used by portals that don’t wish to be immutable but simply follow what “token-governance” deems the real rollup. Remember that the opinion of the Senate can be discarded.

References

Appendix Part A

Adapting Messages

As described in the Aztec Docs | L1 ↔ L2 communication message boxes are used to move a message from L1 to L2 and opposite.

Every message have a sender and receipient along with an identifier for the underlying chains. When instructed to consume a message, a series of events happens:

  • For a portal contract, the portal implementation must ensure that the sender matches the expected L2 contract (simple access control on L1) and that the version comes from the rollup that it believe to be real. If these checks are not performed, the portal might be insecure.
  • For a L2 contract, the rollup circuits (not contract specific) ensures that the sender and recipient matches what is stored in the contract-leaf and that the identifiers (chainid and version) matches the rollup in which it is executed (as specified in the state transitioner contract).

Note
Note that these checks are automatically enforced on L2, but must be enforced by the portal implementation on L1.

When we are specifying a precise version in the message we have some undesired properties for messages that are yet to be consumed (“pending” messages). A message can be pending under different circumstances:

  • Bridging funds into L2, funds are locked on L1 while \mathcal{R}_c = \mathcal{R}_1 and:
    • the message is consumed by the state transition contract. But the upgrade happens BEFORE the message is consumed by the user on L2! When the user want to consume it on \mathcal{R}_c there will be issue since \mathcal{R}_c \neq \mathcal{R}_1 so the message and current chain don’t match and it would revert.
    • the upgrade happens before messages is consumed by the state transitioner. The user either gotta hope that i) the developer of the portal supports cancellation or ii) deposit it into an older rollup rollup where \mathcal{R}_c \neq \mathcal{R}_i and then exit from there (which might require him to also run a sequencer if no-one else wants to).
  • L2 → L1:
    • A user wants to exit a rollup, he creates an exit transaction and it is broadcasted to L1. BEFORE this messages is consumed, the upgrade happens, and the portal (following rollup governance) will not let him consume the message, since \mathcal{R}_c \neq \mathcal{R}_1

Supporting Pending Messages

To support consumption of pending messages, we have a couple of tricks up our sleeve.

Supporting pending L1 → L2

Abuse that if a message is moved along in an upgrade it have been inserted before the upgrade happened. Frist, include L1 block-number in the messages when they are inserted on L1. Second, let snapshots be available inside the circuits (add the snapshot tree root in the public inputs). It is then possible to allow consumption of messages that where moved along if it matches an earlier snapshot :sunglasses:. This supports the case where the pending message had left L1 but not yet executed on L2 as it is already in the state that was copied.

To support the second case, we must allow the current rollup to consume message that were not directly intended for it to be consumed by it. However, if it can consume any message, it can really mess up a fork. Also, we need a design that can distinguish between forks without relying on governance. To work around this, we use the address of the state transitioner as the “version” and have the max value, (type(uint160).max) mean that the message should be consumable by the current rollup \mathcal{R}_c (which the circuit can check from the snapshot).

This helps us cover both cases it is up to the user to decide on who can move his message from L1 to L2. The user has the power to decide if her messages can be moved by \mathcal{R}_c or only a specific version.

Supporting pending L2 → L1

To support this case, we can similarly to above include the L1 block number when messages are hitting the outbox. At the portal contract, it is possible to use the “age” of the message to deduce if it was there before or after upgrading. As before, it is still up to the portal to honor this, but it can be made easier with libraries and standards.

The portal contract must in its logic handle check that the sender is as expected, e.g., from the desired contract on the desired rollup. This can be more or less tricky depending on how nicely it want to support pending messages (follow standard or not).

Note that new messages from an older rollup would still NOT be accepted by the portal because the age would show that it was done after upgrade.

New message structure

The above changes mean that we get the following message structure, instead of the one defined in Aztec Docs | L1 ↔ L2 communication.

struct L1Actor {
    address: actorAddress,
    uint256: chainid,
}

struct L2Actor {
    bytes32: actorAddress,
    address: stateTransitioner,
}

struct L1L2CrossMsg {
    L1Actor: sender,
    L2Actor: recipient,
    bytes32: content,
    bytes32: secretHash,
    uint256: l1BlockNumber,
}

struct InnerL2L1CrossMsg {
    L2Actor: sender,
    L1Actor: recipient,
    bytes32: content,
}

struct L2L1CrossMsg {
    InnerL2L1CrossMsg: inner,
    uint256: l1BlockNumber,
}

DANGER
If governance changes the messages boxes to use different logic, it might influence the ability to fork after that upgrade! Users must stay vigilant.

Efficient Mass Migrations for Immutable Portals

The goal is simple; support migrations from rollup A to rollup B for ungovernable portals in a way that has little financial overhead for the user.

At a high level, the idea can be though of as generating a merkle-tree of “migration balances” on L2, and then enqueue a transaction that will send the root along with a target and amount to the portal which transfers the funds to the target and updates a root value it in. The root can be ingested to a new L2 contract that allow “claims” of the migrated funds by the original sender.

Let say that we have the portal, \mathcal{P}_1, on L1 ands its matching L2 contract \mathcal{C}_1 that lives on the current rollup \mathcal{R}_1, the process is then as follows:

  1. Someone deploys a new portal \mathcal{P}_2 on L1 and its L2 contract \mathcal{C}_2 on the new rollup \mathcal{R}_2
  2. User initiate “migrate” with an L2 transaction on \mathcal{R}_1 that is inserting into a merkle tree in a public function on \mathcal{C}_1.
  3. At some point, a lot of people have initiated “migrations” and there is “critial” mass (amount of funds to migrate)
  4. Anyone can initiate a L2 → L1 message that sends the tuple (target, amount, merkle_root) to \mathcal{P}_1
  5. Anyone may then consume the message on L1 at \mathcal{P}_1, which sends amount and adds merkle_root into the target \mathcal{P}_2 which then sends a L1 → L2 message with the (amount, merkle_root) to \mathcal{C}_2.
  6. The new message is consumed \mathcal{C}_2 and set as a claim root.
  7. Users can execute a transaction on \mathcal{C}_2 that “claims” their funds on the new rollup

INFO
You could use an array of roots to support multiple migrations, or give users X time to do a large single one, depends on the usecase and users really.

Happy path

A new state transitioner is deployed, and a lot of users want to move to it instead of this old deployment. They are following the scheme, and made two L2 transactions to move their funds (1 to deposit from \mathcal{R}_1 and 1 to claim on \mathcal{R}_2). The user needed to be somewhat active to make sure that he performed his migration initiation while there was still plenty of activity such that he could benefit from the shared costs.

Sad path

If the migration is to happen because sequencers are censoring heavily, they might censor all migration tries, which will degrade the UX since it requires the user to use L1 for forced inclusion. The inclusion is still cheaper than solo exiting, as the final migrate transfer is shared, but will have a faily high cost for the user as it is now a L1 transaction and a L2 transcation (L2 tx as we assume the new sequencer are not censoring on the fork).

69 Likes