Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Censorship Resistance & Executing Messages #520

Open
tynes opened this issue Jan 17, 2025 · 1 comment
Open

Censorship Resistance & Executing Messages #520

tynes opened this issue Jan 17, 2025 · 1 comment

Comments

@tynes
Copy link
Contributor

tynes commented Jan 17, 2025

Allowing deposits to trigger executing messages was descoped from the interop devnet. There is a desire to include this feature into scope for the production release. Without this feature, it is possible for a sequencer to censor a cross chain transaction, effectively burning the user’s funds. Not good. How to solve this?

There is an open question around the latency at which using a deposit to trigger an executing message may happen. The specific problem is that a force include deposit transaction may reference an invalid executing message. The main invariant of interop is that all executing messages must be valid - meaning that the identifier references the initiating message. If a deposit transaction is allowed to trigger an executing message, what are the tradeoffs between waiting for finality versus not waiting?

Not waiting for finality adds complexity around deposit-only blocks and requires synchrony assumptions. We wouldn't know if a reorg might occur for a deposit only block until we observe finality from remote chains. While technically feasible, it introduces complexity and ties preconfirmations deeply to the system's correct functioning. Since some organizations may not want to rely on pre-confirmation security, we shouldn't make it a consensus requirement. This is why waiting for finality is the better approach.

We generally need some sort of “finality oracle”. One approach of a finality oracle is waiting for the sequencing window. Going any faster than that would need some sort of proof that the data was posted and derives into the outputs that contain the required logs.

Solution: Preregistration

This design space involves preregistering the executing message before executing it. The preregistration stage needs to be easy to validate and needs to be possible via a deposit transaction.

Constraints & Design Goals

  • Derivation should not need to execute the deposit
    • It is technically possible to “optimistically execute” the deposits using a forking EVM and then filtering based the standard interop invariants but strictly enforcing 12h on top of the initiating message. This would essentially double the deposit cost in the system
  • Separate registration from execution
    • Allows one proof to be consumed many times
  • Reduce single points of failure
    • If the diff is bigger in the smart contracts, it better be worth it

Proposal

Add a new mapping named registered to the CrossL2Inbox that can authenticate executing messages.

Invariants:

  • Only deposits can populate the registered mapping
  • The call to populate the registered mapping must be a top level call, meaning the data must be in the data field of the transaction. This enables static analysis
  • The timestamp on the identifier must be older than the sequencing window

Instead of reverting when the call to validateMessage is in the deposit context, instead require that the hash of the identifier and message hash is in the registered mapping.

Should the invariants be enforced onchain or offchain?

  • They have to be enforced offchain, there is no way to enforce “only EOA” effectively

The derivation pipeline is updated to enforce the above invariants. Derivation maps TransactionDeposited events into DepositTx transactions. We add a special rule in derivation that matches on the TransactionDeposited event. If the target is the CrossL2Inbox and the calldata is exactly the right number of bytes and has a prefix of the 4byte selector of the register function, then it maps the from into the DEPOSITOR_ACCOUNT rather than from the msg.sender in the context of the OptimismPortal. If the executing message is invalid, then the deposit transaction will revert in its call to the CrossL2Inbox

Pseudocode for this logic is below:

deposits = []
receipts = eth.get_receipts(block.hash)
for receipt in receipts:
  for log in receipt.logs:
    if is_transaction_deposited(log):
      deposit_tx = map_log(log)
      if is_register_deposit(deposit_tx):
        id, hash = abi.decode(deposit_tx.data[0:4], (Identifier, bytes32))
        client = clients[id.chain_id]
        receipts = client.get_receipts_by_blocknumber(id.block_number)
        log = filter_log_by_index(receipts, id.log_index)
        serialized = serialize_log(log)
        is_correct_hash = hash == keccak(serialized)
        is_before_expiry = id.timestamp > next_l2_timestamp - EXPIRY_WINDOW
        is_after_sequencer_window = id.timestamp + next_l2_timestamp < SEQUENCER_WINDOW
        if is_correct_hash and is_before_expiry and is_after_sequencer_window:
          deposit_tx.from = DEPOSITOR_ACCOUNT
      deposits.append(deposit_tx)
    
def is_transaction_deposited(log):
  # check correct number of topics
  # check topic[0] is correct
  # check log.data is abi encoded correctly

def is_register_deposit(tx):
  is_correct_target = tx.to == CROSS_L2_INBOX
  is_correct_calldata_size = len(tx.calldata) == REGISTER_SIZE
  is_correct_calldata_selector = tx.calldata[0:4] == REGISTER_SELECTOR
  is_correct_encoding = abi.decode(tx.calldata, (REGISTER_ABI))
  return is_correct_target && is_correct_calldata_size && is_correct_calldata_selector && is_correct_encoding

We know that it is impossible to create a subcall from the DEPOSITOR_ACCOUNT into the register function, therefore we guarantee the invariant that the calldata can always be statically analyzed in the derivation pipeline.

Pseudocode for the register function is below:

function register(Identifier memory _id, bytes32 _hash) {
  require(msg.sender == DEPOSITOR_ACCOUNT);
  
  bytes32 hash = keccak(_id, _hash);
  registered[hash] = true;
}
@ajsutton
Copy link
Contributor

One possible variant here is that the register deposit transaction is still required to be statically identifiable but the check for executing message validity does not happen synchronously - the register transaction is allowed to proceed regardless and the mapping of registered initiating messages updated on chain.

Nodes then have a 12 hour window to asynchronously check the validity of the registered message. When the second deposit transaction arrives to actually execute:

  • if there is no pre-registration the transaction reverts (but is included as-is)
  • if the pre-registration exists and is valid, the transaction executes (ideally we'd remove the pre-registration here but then it would need to be deposit tx specific)
  • if the pre-registration exists and is invalid, the deposit transaction is replaced with one that removes the invalid pre-registration

This allows the cross-chain dependency check to be executed asynchronously (even nodes that have snap sync'd could read the list of pre-registrations and begin checking them). The executing deposit message is still only executed once - it may be invalid and need to be reverted but the invalidity can be cached and the rollback should be cheap. Potentially you could even relax the restriction on the pre-registration message being statically analysable since it is now unconditionally included and nodes could simply look for the event emitted when a pre-registration occurs.

The major downside I see is the message validity check would now occur in the execution client for deposit transactions whereas I think previously all validity checks would have been in the L2 consensus client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants