Celestia Snark Accounts Design Spec

Celestia Snark Accounts

This is a design document on how Celestia might incorporate SNARK accounts into their base layer for enabling interoperability of TIA between Celestia and the rollups that utilize Celestia for data availability.

The core problem that leads to multiple proposals in this document, is that the SNARK account contract needs a form of replay protection, to prevent double withdrawing. This will lead to a few design choices that we explore below.

Background: Current Transaction Lifecycle on Celestia

In this section, we outline how a regular Celestia account and transaction lifecycle work for context before extending the design to SNARK accounts. Note that the code in this section in pseudocode and more intended as a “philosphical” overview rather than a 1-1 match with the Celestia codebase.

Account State

A account on the Celestia network has the following state:

struct Account {
    balance: u32
    pubkey: Pubkey
}

Transaction Lifecycle

A current “send” transaction on Celestia has the following fields:

struct Transaction {
    signature: Signature
    sender: Pubkey
    recipient: Pubkey
    amount: u32
}

To process this transaction, the following steps are executed (not including gas accounting):

fn process_transaction(tx: Transaction) {
    verify_signature(tx.signature, tx.sender)
    assert(balanceOf(tx.sender) >= tx.amount)
    tx.sender.balance -= tx.amount
    tx.recipient.balance += tx.amount
}

Celestia SNARK Accounts

In this design, each rollup on Celestia has a designated SNARK account that maintains the balance of TIA on the rollup. Users can deposit TIA on the rollup by sending money to the SNARK account (i.e. bridging to the rollup). Users can withdraw TIA from the rollup (withdrawing from the rollup) by providing a proof to the SNARK account that there was a valid withdrawal on the rollup corresponding to their claimed withdrawal on Celestia.

Deposit Transaction: From a Celestia Account to a Rollup SNARK Account

For users wanting to “deposit” money to a rollup, the deposit transaction works like any normal “send” on Celestia. The main difference is that this transaction has an additional “memory” field that can be used to specify additional information (like a recipient address on the rollup):

struct DepositTransaction {
    signature: Signature
    sender: Pubkey
    recipient: Pubkey // the SNARK account of the rollup
    amount: u32
    memo: bytes // memo field that can be used for arbitrary extra information
}
  • The memo field should be bytes to allow for additional extra information tied to a deposit. An example might be that a rollup has a address/public key scheme that might not be the same number of bytes as Celestia’s public key (true for EVM) that can be specified in this memo field.
  • When the transaction is processed on Celestia, the balance in the SNARK account address goes up by amount, and the SNARK account should emit an event with Deposit(sender, amount, rollup_recipient)
  • For sequenced rollups, the sequencer should monitor for Deposit events on their corresponding SNARK account, and then insert “system transactions” (i.e. transactions that only the sequencer can insert) that mint the corresponding amount of TIA to the recipient account on the rollup (can be specified in the memo field).
  • For based rollups, deposit transactions to the SNARK account are simply another type of transaction that are inputs to the STF that update the recipient account on the rollup with the deposit amount.

Withdrawal Transaction: From Rollup to the SNARK Account → Recipient account on Celestia

The withdrawal transaction from a rollup to Celestia involves 2 steps:

  1. A withdraw transaction on the rollup (to burn TIA tokens on the rollup to mark them as “withdrawn”)
    • Note that whether it is a sequenced or based rollup, withdrawing requires a signed user transaction on the rollup to mark the tokens as “withdrawn”
  2. Transaction on Celestia that prompts SNARK account to send withdrawn tokens to the withdrawal address after proper verification (and also keeps track of an accumulator to prevent double-spend).

Note that there are some designs where the above 2 steps can be combined, but they impose other tradeoffs that we discuss below. We start with assuming there’s a 2 step process for simplicity.

1. Withdrawal transaction on the rollup

To begin the withdrawal, the user sends a transaction to burn the tokens on the rollup (or otherwise mark them as “withdrawn”). This burn transaction includes:

  • The amount to be burnt as withdraw_amount
  • Address of the recipient account on Celestia as celestia_recipient (can be an arbitrary memo field as well)

2. Proof of STF to the SNARK account

There are several options with different tradeoffs for how the SNARK account can utilize a proof of the rollup’s STF and then process a user withdrawal on the rollup.

This has a design tradeoff because we must have a solution to prevent “double spends” for withdraws. We briefly sketch high level descriptions of the perceived options, before going into more elaborate detail.

Design decisions which potentially require the base layer to worst-case store state linear in # rollup blocks have been excluded from the tradeoff space.

  • Option 1: Withdrawal accumulator updated one at a time
  • Option 2: Require-ordered withdrawals, that must process all withdraws in a block range
  • Option 3: Blob submission requires proof to update state root
  • Option 4: The base layer provides replay protection for recent-history, user proves long-term
Name Pros Cons
Withdrawal accumulator updated one at a time * Similar to Ethereum-style rollups * Without working with the block builder, only 1 multi-withdraw tx possible per block.
Ordered Withdrawals * Ordered withdraws maybe a design choice

* Adversarial delay concerns with spamming either Base Layer or Rollup
* Ordered withdraws forced by the base layer, rather than rollup logic
Proof at blob submission time * Potential less overhead in what blobs need to be processed in ZKP of STF * Can only submit DA once proof is generated, dramatically increases rollup finality time
Base layer replay protection * Arbitrarily many withdraws per rollup can occur concurrently per block * Base layer maintains state for the last week of withdraw txs

Option 1: Withdrawal Accumulator updated one at a time

In this option, the SNARK account keeps track of a “state root” of the rollup and processes “State Update Transactions” that update the state root. There are separate “withdrawal transactions” that users submit that validate that withdrawals against this state root and update the withdrawal accumulator to prevent double-spend.

This option is most similar to how Ethereum-style rollups today do withdrawals from Ethereum. Note that this design works for both based and sequenced rollups.

Snark Account State

struct SnarkAccount {
    balance: u32
    vkey_stf: VerifyingKey
    vkey_withdrawal: VerifyingKey
    last_height: u32
    state_root: bytes32
    withdrawal_accumulator: bytes32
}

State update transaction

struct StateUpdateTransaction {
    proof: Proof
    snark_account: Account
    new_height: u32
    new_root: Bytes32
}

fn process_transaction(tx: StateUpdateTransaction) {
    require(tx.snark_account.last_height < tx.new_height);
    let vkey = tx.snark_account.vkey_stf;
    let trusted_header = self.header_at_height(tx.new_height);
    let public_inputs = [
          trusted_header
          tx.new_height,
          tx.snark_account.last_height,
          tx.new_root,
          tx.snark_account.state_root,
    ];
    verify_proof(public_inputs, tx.proof, vkey)
    // Update the account
    tx.snark_account.state_root = tx.new_root
    tx.snark_account.height = tx.new_height
}

In the proof, the statement that is being verified is the following:

const namespace = "my_namespace"
let namespace_blobs = []
let deposit_transactions = []
for height in last_height...new_height {
    let header = verify_header(trusted_header, header, height);
    let blobs = get_blobs(header, namespace);
    namespace_blobs.push(blobs);
    
    let deposit_tx = get_deposit_tx(header, snark_acct_address);
    deposit_transactions.push(deposit_tx);
}

verify_stf(state_root, new_root, namespace_blobs, deposit_transactions)

Note that in the above computation, the proof must process ALL blocks between heights last_height and new_height to ensure that it is including ALL blobs for the relevant namespace when verifying the STF. This is done to ensure that the prover is not excluding any rollup blobs submitted to Celestia. Note that we must include all deposit transactions on Celestia to the SNARK account as this is an input for the STF for the rollup.

Cost Analysis (Circuit):

Note that even if a block has no blobs corresponding to a namespace, it requires 1 opening proofs for the NMT for proof of non-inclusion, which is non-zero cost across all the intermediate blocks.

In a SNARK, the cost for the above computation is:
(new_height - last_height) * (parent_header_opening + NMT_opening_proof) + STF_verification + B * NMT_opening_proof

if there are B shares between (last_height, new_height) for the rollup’s namespace.

The parent_header_opening proof from our Tendermint implementation is ~6 SHA hashes and the NMT opening proof might be around 30 SHA hashes, for a total of H * 34 sha hashes for procesing H celestia blocks, and a total cost of (34H + 30B) SHA_cost + STF_verification_cost in terms of circuit complexity.

Withdrawal processing transaction

struct WithdrawalTransaction {
    proof: Proof
    snark_account: Account
    new_withdrawal_accumulator: Bytes32
    withdrawal_account: Account
    amount: u32
    withdrawal_id: Bytes32
}

fn process_transaction(tx: WithdrawalTransaction) {
    let vkey = tx.snark_account.vkey_withdrawal;
    let state_root = snark_account.state_root;
    let withdrawal_accumulator = snark_account.withdrawal_accumulator
    let public_inputs = [
          state_root,
          withdrawal_accumulator,
          tx.new_withdrawal_accumulator,
          tx.withdrawal_account,
          tx.amount,
          tx.withdrawal_id
    ];
    verify_proof(public_inputs, tx.proof, vkey)
    // Update the account
    tx.snark_account.withdrawal_accumulator = tx.new_withdrawal_accumulator

    // send the amount to the withdrawal account
    tx.withdrawal_account.balance += tx.amount
    tx.snark_account.balance -= tx.amount
}

In the proof, the statement that is being verified is the following:

let withdrawal = (withdrawal_id, withdrawal_account, amount)
verify_withdrawal_exists(state_root, withdrawal)
verify_unspent(withdrawal_id, withdrawal_accumulator)
verify_accumulator_update(withdrawal_id, withdrawal_accumulator, new_withdrawal_accumulator)

When the verification is complete, the SNARK account sends the withdrawal amount to the recipient and updates its accumulator.

Cost Analysis (Circuit)

Verifying existing of a withdrawal against the state root of the rollup is 1 merkle proof of the rollup state (can potentially be quite cheap if the rollup is using SNARK-friendly hash function for its state committment).

Verifying that the withdrawal is not in the current accumulator and updating the accumulator properly with the withdrawal id can be similarly cheap if the accumulator is constructed with a snark-friendly hash function.

Tradeoffs:

The above has the following drawbacks, with potential fixes or thoughts suggested in subbullets.

  • The biggest tradeoff with this design is that because the user must know the current withdrawal accumulator to create a withdrawal transaction, without external coordination with the block builder, there is only 1 withdrawal per block.
  • Requires 2 vkeys
    • It would be easy to get rid of this if we combined the udpate_state_root and process_withdrawal circuits into 1, where a withdrawal transaction would also update the state root (if needed).
  • Keeps around a withdrawal accumulator and state root
    • Seems hard to get rid of a withdrawal accumulator, since we need some way to keep track of the processed withdrawals, unless we want process all of the withdrawals sequentially
    • Seems possible to get rid of state root with recursive snarks
  • Keeps around last processed height from Celestia
    • Seems possible to get rid of last processed height with recursive snarks
  • When processing state root updating transaction, it requires Celestia to provide the header root of a block height arbitrarily far in the past.
    • Seems hard to get rid of this requirement because the proof has to be generated with respect to a particular Celestia block hash that may not be the tip of the chain
  • The “update state root” circuit might be very expensive to prove because it requires a lot of SHAs for NMT opening proofs to get all of the share/blobs.

Option 2: Processing withdrawals sequentially

In this option, we only have a STF snark that also has as public input allpending withdrawals that occured between the last state root and the current state root, and as part of that transaction process all of the withdrawals and send them to the corresponding recipient accounts. Most of the other details are the same around the computation that the STF snark verifies, but the SNARK account would have less state:

Snark Account State

struct SnarkAccount {
    balance: u32
    vkey_stf: VerifyingKey
    last_height: u32
    state_root: bytes32
}
struct StateUpdateTransaction {
    proof: Proof
    snark_account: Account
    new_height: u32
    new_root: Bytes32
    all_withdrawals: Withdrawal[]
}

fn process_transaction(tx: StateUpdateTransaction) {
    require(tx.snark_account.last_height < tx.new_height);
    let vkey = tx.snark_account.vkey_stf;
    let trusted_header = self.header_at_height(tx.new_height);
    let public_inputs = [
          trusted_header
          tx.new_height,
          tx.snark_account.last_height,
          tx.new_root,
          tx.snark_account.state_root,
          tx.all_withdrawals
    ];
    verify_proof(public_inputs, tx.proof, vkey)
    // Update the account
    tx.snark_account.state_root = tx.new_root
    tx.snark_account.height = tx.new_height
}
  • The verify_proof would verify that all withdrawals included in the public inputs are the only withdrawals between the previous rollup state root and the current rollup state root (can be done by checking the nonce of a withdrawal contract and ensure that it is always incremented by 1 or something similar).
  • This would remove the need for the “withdrawal accumulator” field because it’s guaranteed all withdrawals are processed in order (and exactly once) and also remove the need for the withdrawal verification key. It’s also possible to remove the state_root field with recursive snarks as described above.
  • One con of this approach is there is a “free-rider” problem where the person submitting a proof for their particular withdrawal also has to pay the gas costs associated with all the withdrawals prior to them that have not been processed. But it is perhaps more elegant.

Option 3: Blob submission requires proof to update state root

One problem with the above design is that someone can spam a namespace with a bunch of garbage data that does not have valid transactions, but it might be very expensive to process this garbage data within a SNARK (for example if the data is very large). One method of preventing this type of attack is to require a proof alongside blob submission to a particular namespace that updates the STF alongside blob submission.

The SNARK account state would still be similar to the design above, but to post a transaction to a namespace with a snark account enabled would require that there is a valid proof to update the STF of the SNARK account with the new state root with the corresponding blob.

In this design, the snark account state could also have last_height removed as we are guaranteed all submitted blobs are processed.

Snark Account State

struct SnarkAccount {
    balance: u32
    vkey_stf: VerifyingKey
    vkey_withdrawal: VerifyingKey
    state_root: bytes32
    withdrawal_accumulator: bytes32
}

The withdrawal process against the state root would look very similar to Option 1.

Tradeoffs

  • The main drawback of this design (that likely makes it untenable) is that then the rollup’s finality is bottlenecked by proof generation time because posting DA is coupled with a valid proof of the STF. The main advantage of Celestia right now is with the 12 second fast-finality rollups can post to the DA layer and have finality within 12 seconds, and proof generation can be slower but doesn’t impact time to finality. This design would break that.

Option 4: The base layer provides replay protection for recent-history, user proves long-term

The high level idea is that user withdraws have a TTL of at most TTL_LENGTH into the future. The base layer stores all succesful withdrawals for a period WITHDRAW_STORAGE_PERIOD > TTL_LENGTH.

Every withdraw has an ID containing:

  • a nonce
  • A timestamp for the rollup’s block time when the withdrawal was created

Snark Account State

struct SnarkAccount {
    balance: u32
    vkey_stf: VerifyingKey
    vkey_withdrawal: VerifyingKey
    latest_state_root: bytes32
    latest_withdrawal_accumulator: bytes32
    // Timestamp -> (state root, with draw accumulator)
    storage_period_headers: Map<Timestamp, (bytes32, bytes32)>
    storage_period_withdraws: Map<bytes32, Timestamp>
}

When a withdraw request gets to the base layer, it consists of:

  • ZKP Public input:
    • Withdrawal ID (nonce, timestamp)
    • Header to verify fields
      • Timestamp, State root, withdraw accumulator

Additional base layer logic:

  • Check that current block time is less than withdrawal.timestamp + TTL_LENGTH
  • Check that withdrawal.nonce is not in snarkaccount.storage_period_withdraws.
  • Set storage_period_withdraws[withdrawal_id.Nonce] = block time

Maintain state for every withdrawal ID in the last week. This is done with two state entries, one in the account one global across celestia (TBD how Celestia wants to deal with this, e.g. future withdraws have to prune 2 expired txs?):

  • List of withdrawal ID’s to prune at different heights.

Possible tx flows

Withdraw attempt on rollup occurs → Withdraw succeeds on Celestia

Withdraw attempt on rollup occurs → Fails to get onto Celestia in TTL window. Show this on rollup and get funds returned.

Header updates

Todo import logic thats described for STF verification, to allow adding more STF’s that aren’t just the latest STF.

State pruning options

  • Every withdraw has to delete at least 2 ‘expired txs’
  • Some global celestia logic to delete expired state

Other questions:

  1. What ZK scheme should we use?
  2. Is adding extra state beneficial to rollups? If there’s a special tx type to update the state of the rollups onchain, then they won’t have to recursively prove previous proofs
  3. Anyone can post to a specific namespace - how do we avoid spam and filter for specific transactions? - solvable with intra-namespace blob prioritization, where the rollup only process eg the 100 highest priority blobs (e.g. according to their gas paid). Based vs Sequenced rollups.
  4. NMTs in Celestia use SHA256 - which are quite inefficient with ZK (although this can be optimized)
  5. Comparison to ZK IBC, Bitcoin Opcodes, Mina rollups? Benefits of ZK IBC
  6. How does gas work for these sorts of transactions.
  7. Do we need a deposit accumulator? One Celestia tx could include multiple deposit txs to various rollup accounts. Events are emitted on Celestia and committed to block headers - similar to Ethereum
  8. Proof size?
16 Likes

Looking forward to the first ever proposal on Celestia, @puma314 !

How did you decide on one week for option 4?

2 Likes

Great Proposal.

Idea Option 5: Celestia accounts with withdrawal nonces

Pattern: We could add the transaction on Celestia itself as input on the ZKP or any account details we want to fetch from the state.

We could add a withdrawal nonce per Celestia account. When you withdraw on the Rollup, you must specify a Celestia withdrawal nonce while withdrawing, which you can prove from the stateroot afterward.

When you submit a withdrawal on Celestia, you must also provide the account-specific withdrawal nonce as input to the ZKP. After successful withdrawal, Celestia increases the nonce by one, so you cannot replay a withdrawal.

This enables many withdrawals per rollup per account in the same Celestia height, with the downside of adding another nonce to the state of Celestia.

Edge Case: The user uses the same nonce for multiple rollup withdrawals. With that, the next withdrawal cannot be completed, and the funds are already burned on the rollup. To “unburn” the tokens, you would prove on the Rollup that you used the nonce for another snark account on Celestia.

Another con is that if you are withdrawing from 10 Rollups at the same time and the first Rollups stateroot does not get updated on Celestia because of a liveness failure, then your other Rollups funds are stuck as well. That seems like a UX issue that can be solved by “burning” the nonce with another withdrawal. This could be, for example, through another rollup, a dummy snark account that you control, or an integrated function on Celestia.

2 Likes

I’m going to propose an isomorphism of @nashqueue’s proposal.

Rather than adding a separate withdrawal nonce to each Celestia account, the existing nonce can be re-used. Rather than specifying a withdrawal nonce on the rollup, the withdrawing account can simply specify its Celestia nonce. The rest of the scheme proceeds as above, with either manual withdrawals (which consume the Celestia nonce with a Celestia transaction) or automatic withdrawals (which consume the Celestia nonce automatically as part of the begin block/end block transition).

Just as above, a withdrawal can be cancelled or reverted, or stuck withdrawal can be bypassed by consuming the nonce with an arbitrary Celestia transaction.

2 Likes

From the zk-wg call regarding Option 5:

  • Client Integration and Standardization Issues: Ensuring different rollups agree on a nonce management standard could be difficult and require significant effort from client integration teams. This complexity may hinder interoperability and user experience.
  • The idea to overcome this was using multiple nonces, let’s say 16 per account. The rebuttal was that while introducing multiple nonces or “sequence lanes” might offer a solution, it could also introduce arbitrary limits on parallelism. Future applications or use cases might encounter these constraints, leading to unforeseen complications or user friction.

New Option 6: Combine option 3 with option 4.

The main way to withdraw is with option 4, but you also have the option of posting a zkp alongside the blob to do it in the same block.

This pattern naturally leads to posting a zkp alongside any transaction type where the verification of the zkp is necessary before the transaction is being accepted into the block.

You could see currently designed withdraws as a transfer function being gated by a ZKP. Now instead of calling it withdraws it could be stakig, bridging or blob posting. What other applications this could unlock I go into detail in this post. That post also covers how to deal with the woods attack, trying to solve one of OP’s open questions.

1 Like

One thought I had about advantages of SNARK accounts vs. ZK IBC (perhaps this can be a good topic to discuss in the next call) is that a SNARK account can be made fully general to support arbitrary actions on Celestia (like Celestia restaking, etc.), whereas ZK IBC likely cannot. While I think ZK IBC might be more simple for just interoperability at the base layer, I think SNARK accounts’ big advantage is that they are more extensible and can provide a foundation basically for arbitrary code logic attached to accounts in a way that ZK IBC cannot since it is a protocol tailor-made to interoperability. Just something that we should also discuss as I know some working group calls have lightly touched on ZK IBC as well.

Aditya (ibc-go) and I have written up some more detailed thoughts on what it would look like to use IBC for this, specifically addressing what the IBC client would look like and how to prune state since current IBC token transfers result in a lot of state bloat.

See the following hackmd, and please feel free to comment: IBC Snark Account - HackMD

I had some questions while writing up the above hackmd.

When the design talks about sequencers adding “System Transactions”, what happens if the sequencer lies and adds a System transaction that doesn’t exist. Or refuses to add a System transaction that should exist?

And similarly in the based rollup case: how do we ensure that the rollup full nodes are adding these inputs at the same rollup height? If different nodes add these deposit transactions at different heights, they will arrive at different app hashes.

I’m assuming that the deposit transactions are happening at the Celestia app layer, and they need to somehow be added into the rollup blockchain which is in the namespaced blob layer.

I ask because the design for IBC clients in the document makes use of these “System Transactions” to send authenticated IBC packet messages from Celestia to the rollup without the use of an independent relayer or even a light client of Celestia on the rollup. However, verifying the correctness of this approach requires more understanding of how these transactions get included into the rollup blockchain.

The proposal itself is described in the L1 to L2 state change description of the hackmd if you want to read and verify that I’m using the mechanism correctly.

Effectively, I’m replacing the “Deposit Tx” described here with IBC transactions sent from L1 → L2, which should hopefully make sense given their analogous function

1 Like

I also wanted to add a clarification from the discussion on yesterdays call about the design space here that I hope will be useful.

It’s worth distinguishing between two kinds of snark accounts depending on the level of coordination between users of a single account. For lack of better terminology, we could call these “end user snark accounts” (e-snac) and “rollup snark accounts” (r-snac), with the difference being that users of an e-snac have high coordination ability and users of an r-snac don’t. This matters for the UX of replay protection, but in my understand we actually want to support both.

End User Snark Account (e-snac)

The e-snac can basically be identical to the existing account structure, using the existing sequence numbers for replay protection, but just replacing the secp256k1 pubkey with a zk verification key for an arbitrary circuit. The assumption is that users of a single e-snac can coordinate so they can set the right sequence number when they send a tx. This requires no new state to be added to the blockchain (besides handling the potentially much larger verification keys) and in principle could/should be added to the Cosmos-SDK as a new “signature scheme” that all SDK chains could benefit from. I think this is basically what @musalbas originally proposed.

If users are not coordinated, and thus can’t coordinate on picking the right sequence number in the right order, they won’t know which seq number to pick and will race each other, leading to lots of failed txs. In principle we could set up some kind of off-chain “sequencer” service that users transact through to get the right sequence number but we can’t guarantee they use it and it introduces a new central point of failure. Solving this requires more protocol design.

Rollup Snark Account (r-snac)

Hence the r-snac, which the options in the OP address, to support replay protection across uncoordinated users. This adds more complexity to the design and is why we need more state beyond just the base account type, though some of the options have significant drawbacks. What I’ve gathered from the calls / comments / my own view of the original options is that:

  • Option 1 - only allows one withdrawal per block (since users need to know which withdraw id is being added to the merkle tree and what the new merkle tree root is)
  • Option 2 - allows users to grief eachother by spamming the withdrawls to arbitrarily delay another users withdrawal
  • Option 3 - mostly orthogonal to the other options in my view (just a question of when the state transition update proof is verified)
  • Option 4 - seemingly the most favoured option, temporarily adds more state but otherwise addresses the problems of Option 1 and 2
  • Option 5 - limits parallelism across rollups

IBC for r-snac

At the end of the day, designing an account that supports replay protection across uncoordinated users is exactly what unordered channels in IBC were designed for (ICS-04), and so the preferred Option 4 above is something like a partial implementation of this piece of IBC, with the core difference that it supports pruning where IBC currently does not. But as we show in our writeup, pruning can also be easily added to IBC. Additionally, there may be some concern that IBC is a bit bulkier than a custom stripped down solution, which might be true, though there is also some ongoing effort to simplify the IBC core to make it easier to integrate in more places. In the meantime, IBC seems to be the primary way that the Celestia state machine is expanding (e.g. see CIP-9, CIP-12, CIP-14, all proposed in the upcoming lemongrass upgrade)

In the model of using IBC for an r-snac, instead of a “snark account” what we really have is a snark based IBC client - this is the object in the Celestia state that verifies the state transitions of the rollup. On top of this IBC client we can layer any of the IBC application protocols, including token transfer, interchain accounts, nft transfer, etc. It’s at this application layer that we’d have an actual account (eg. every IBC token transfer channel is really a single account that holds the token balances) which in turn would be controlled by the underlying IBC client (in this case, a snark-based one).

The nice thing about this is that with interchain accounts (ICA), a rollup would be able to send any Celestia tx, and so rollups could stake TIA, participate in gov, send IBC transfers to others, etc.

Hope that is a useful/clarifying summary!

3 Likes

One of the things that doesn’t seem fully resolved to me is how to do zk IBC without forcing every rollup to implement Celestia client verification. I was suggesting if all users are expected to run light nodes that they simply throw out all transactions not within the Celestia fork choice, however I believe there were reservations about this.

Also, an interesting observation about the withdrawal queue in option 4 is this effectively becomes an enshrined shared sequencer that’s expressly for Rollup → Celestia or Rollup → Celestia → Rollup transaction flows.

One of the things that doesn’t seem fully resolved to me is how to do zk IBC without forcing every rollup to implement Celestia client verification. I was suggesting if all users are expected to run light nodes that they simply throw out all transactions not within the Celestia fork choice, however I believe there were reservations about this.

I think this is what Aditya was asking for clarification on above Celestia Snark Accounts Design Spec - #8 by AdityaSripal

But isn’t it the same problem for rollups whether using IBC or not? They need some clear way to securely include txs from the L1, whether those are special “DepositTxs” or IBC transfers

I’m aware of the wg calls spending a lot of time discussing which proving system to adopt (and whether to support multiple - i.e. be more flexible to what users want to use). How significant or long lasting is this decision? How difficult is it to migrate from one proving system to another?

Also is there any consideration required for when the state machine of a rollup changes? Will verifying the proofs still work in the same way before and after those changes?

So my take on the idea of using IBC to develop SNARK accounts is that ICS-002,ICS-003 and ICS-004 are in appropriate for the Snark account design and impose substantial overheads.

The only argument I can see for not designing SNARK account specific variants of these concepts would be if we want to rush to market and want to be compatible with the existing IBC relayer software.

So I think the ICS-02 state has a lot of additional complexity that can actually be embedded directly in the verification circuit of SNARK accounts.

I expect that ICS-03 is largely unnecessary because I expect that rollups that connect to SNARK accounts will mostly operate in an enshrined mode where mutual validation of a connection by the chains is superfluous.

I think the packet semantics in ICS-04 are also unecessary because of the shared data layer between the snark account and the rollup we can assume guaranteed delivery of packets and having Acknowledge packets just creates costs of the prover with no obvious benefit.

A topic that has not yet been covered by specs or discussion is whether we want to support upgrades and how to do them.

Here are possible upgrade mechanisms :

Mechanism 1:
Governance of the Snark account upgrades the Snark account. Governance is embedded into the Snark account itself. We still have only one verification key, and the Snark account can send an upgrade message to Celestia with some specified Celestia height when the upgrade should happen. Those messages have to be validated on the Snark account itself before.

The Snark account also defines the exit window to validate the correct height.
Celestia caches that height with the new verification key, and when the height approaches, it upgrades all snark accounts specified in that height.

Mechanism 2:

We replicate the system above, but we have a second verification key (governance) that can only upgrade the first one (logic) when valid. The benefit of separating these concerns is when there is a bug in the circuit of the snark account, and no valid proof can be generated anymore, the funds would be frozen in mechanism 1. Here, the governance circuit is much leaner and should probably have less surface area for fault, as both would have to be comprised. If only the governance circuit is broken then users still can migrate the funds to a new snark account. Another benefit is that one governance snark account can be used to upgrade many others. Imagine that there is one governing body that has the power to upgrade some cluster. This can be theoretically achieved with mechanism 1 but is made explicit here.

The downside is that we would add more state per Snark account (1 verification key)

Mechanism 3

Instead of upgrading an existing snark account, a new snark account can be created permissionlessly (forked). Now, governance, which is a second verification key as in Mechanism 2, can decide which account is canonical. After the governance decision, the user has some time to exit the zk account otherwise, the “powers” and TIA that the snark account had will be transferred to the new account. This seems like it is isomorphic to mechanism 2, but with the major difference that users who disagree with the fork and governance can keep on using the old fork; they just need to move their TIA deposits for a period of time back to Celestia. With governance deciding on a new fork, the old one should no longer be controlled by the same governance, so a new governance key has to be changed. The easiest solution would be to void the governance key and make the account immutable. Still, I can imagine a more powerful governance mechanism/rotation is possible that has to be verified through the verification key of the governance account. This makes the governance account itself self upgradable. Again, this will require Mechanism 1 to work.

Any thoughts are welcome as the design space is unexplored yet.

1 Like

Another option is to have to wrap the snark in another snark, such that the outer snark has governance logic to upgrade the inner snark.