The DA layer is responsible for several tasks:
- Ensuring the availability of rollup data
- Rate limiting and prioritizing the inbound batches that a rollup is forced to process
- Providing tamper-proof attestations to the sender of batches (as long as rate-limiting is enforced, this is optional)
- Providing a backstop for censorship resistance
- Providing a total ordering of batches within and between rollups on the chain
Let's go through each of these tasks in detail:
Rollups are designed to allow arbitrary state machines to inherit security guarantees of an underlying L1 blockchain. In order for these guarantees to hold, the rollup's data must be "available". Otherwise, an adversary can freeze the rollup by publishing a new state root that incorporates a valid but secret state update. Since no one else knows the new state of the system, honest participants will be unable to post new blocks.
In the Sovereign SDK, we assume that data published on an underlying L1 is available, and simply check in-proof that the data in question really is included in the L1's history. It is the responsibility of the rollup developer to choose a chain that provides suitable data availability guarantees for their application.
No chain has infinite capacity. So, in order to ensure robust operation, chains need to ensure that transactions can be prioritized. Since transactions on the rollup create useful economic activity, we assume that, over time, honest sequencers, including "real" transactions, should be able to generate revenue. They can use this revenue to bid for blockspace on the L1. By contrast, dishonest sequencers sending only "spam" transactions do not generate revenue. For DOS resistance, it's vital that these dishonest sequencers not be able to "crowd out" honest sequencers permanently. So, sending transactions on the L1 must be costly. In addition, the fee paid on the L1 should be proportional to the demand, so that the cost of crowding out honest rollup transactions rises with the value of those transactions.
One underappreciated strength of layer 1 blockchains is their ability to prune out invalid transactions before they get included in blocks. Since most academic work on blockchains abstracts the peer-to-peer layer as a simple gossip network, it has not (to our knowledge) been pointed out that L1s "hyperscale" in their ability to weed out invalid transactions. In other words, the ability of a typical L1 to withstand a DOS attack based on invalid transactions scales linearly with the number of full nodes.
One simple DOS attack on a blockchain is to submit a large number of plausible-looking transactions into the mempool, all of which have invalid signatures. Since the signatures are invalid, nobody can be charged on-chain for the spam. But, since the transactions look plausible, full nodes have to do the work of checking the signatures. Since it's much cheaper to pick some random bytes that look like a signature than it is to check the signature's validity, a resource constrained attacker can launch a fairly effective spam attack with this method.
But, L1s aren't vulnerable. Why not? Because full nodes refuse to gossip invalid transactions, disconnect from any nodes that do, and only open new connections at a limited rate. This prevents the attacker from either overwhelming individual peers or - even worse - getting his spam transactions included in the final ledger.
Because Sovereign SDK chains are designed to operate over a "lazy" ledger which contains invalid transactions, they don't inherit this property by default. To compensate, the Sovereign SDK uses a simple trick: we force sequencers to register on the L2 chain by bonding some (L2) tokens and claiming ownerships of an L1 address. Using this trick, we can offload the sequencer signature checks to the L1. At the rollup level, we only process batches that have been sent by bonded sequencers (who we can slash to disincentivize spam), and we use the fact that the L1 is enforcing signature checks to offload work to the L1 consensus network. So, rather than making an expensive signature check for each batch in zk, we use a much cheaper lookup to check if the user is a registered sequencer.
Censorship resistance is the whole point. If your rollup doesn't have it, it's not an L2, just a sparkling database. But, a rollup can't provide censorship resistance on its own - after all, the underlying L1 could always censor bundles containing unpopular transactions. So, the L1 needs to be censorship resistant.
We allow Sovereign SDK chains to specify any state model of their choosing. So, the underlying DA layer must provide a total ordering over rollup batches. For purposes of bridging, it also needs to provide an ordering across batches on different rollups. (This is only an issue if the underlying chain uses a DAG model). This requirement may be relaxed in the future.
TODO: 2-way trust-minimized bridge with rollup
This interface defines the types shared between the DaService
and DaVerifier
traits. It has no associated
functions.
Name | Type | Description |
---|---|---|
sender |
Address |
The address which sent this transaction |
data |
bytes |
Data intended for rollup. Can be a proof, a transaction list, and etc. |
A proof showing that each item in an associated vector is included in some state commitment. For example, this could be a list of merkle siblings.
A proof showing that an associated vector does not omit any "relevant" transactions. For example, this could be a merkle proof of the items immediately preceding and following a particular Celestia namespace. This type may be the unit struct if no completeness proof is required.
Must include a prev_hash
field. Must provide a function to compute its canonical hash.
Name | Type | Description |
---|---|---|
prev_hash |
SlotHash |
the hash of the previous (L1) block |
The hash of a DA layer block. May be any type, but must provide access to a (canonical) byte string uniquely representing this hash.
Name | Type | Description |
---|---|---|
inner |
bytes |
The raw bytes of the hash |
Code
Expressed in Rust, the DA Spec interface is a trait
. You can find the trait implementation here.
The DaVerifier trait is part the rollup's state machine - meaning that it has to be proven in zk. Its job is to ensure that the DA layer's consensus translates into rollup state.
-
Usage:
- An adaptation of the
get_relevant_txs
method designed for use by verifiers. This method returns aResult
containing the unit type on success, and an error message on failure. An invocation succeeds if and only if the provided set oftxs
was included on the L1 (as demonstrated byinclusion_proof
), and is complete (as demonstrated bycompleteness_proof
). The list of blob transactions verified by this trait will be processed by rollups in order - which means that the verification must be careful to enforce a deterministic ordering to prevent consensus failures on the rollup. Except for the "Error" response, all of the types required by this function are defined in theDaSpec
trait.
- An adaptation of the
-
Arguments:
Name | Type | Description |
---|---|---|
block_header |
Blockheader |
The header of the DA layer block including the relevant transactions |
txs |
iterable<BlobTransaction> |
A list of L1 transactions ("data blobs"), with their senders |
inclusion_proof |
InclusionMultiproof |
A witness showing that each transaction was included in the DA layer block |
completeness_proof |
CompletenessProof |
A witness showing that the returned list of transactions is complete |
- Response:
Name | Type | Description |
---|---|---|
Ok |
_ |
No response |
Err |
Error |
An error message |
- Note: This response is a
Result
type - only one of Ok or Err will be populated
Code
Expressed in Rust, the DA Verifier interface is a trait
. You can find the trait implementation here.
The DA Service interface allows the Sovereign SDK to interact with a DA layer. It's responsible for communicating with the DA layer's peer network (likely via JSON-RPC requests to a local light node), and for converting information about the DA layer into a form which can be efficiently verified in the SNARK. The DA service exists outside of the rollup's state machine, so it will not be proven in-circuit. For this reason, implementers are encouraged to prioritize readability and maintainability over efficiency.
-
Usage:
- The core of the DA service. Fetches all "relevant" transactions from a given DA layer block. The exact criteria that make transactions "relevant" are up to the implementer, but must be defined without reference to the current state of the rollup. For example, a rollup on Celestia might define "relevant" to mean, "occurring in namespace 'foo'".
-
Arguments:
Name | Type | Description |
---|---|---|
block |
FilteredBlock |
The relevant subset of data from a DA layer block |
- Response:
Name | Type | Description |
---|---|---|
txs |
iterable<BlobTransaction> |
A list of L1 transactions ("data blobs"), with their senders |
-
Usage:
- An adaptation of the
extract_relevant_blobs
method designed for use by provers. This method returns the same list of transactions that would be returned byextract_relevant_blobs
, in addition to a witness proving the inclusion of these transactions in the DA layer block, and a witness showing the completeness of the provided list. The output of this function is intended to be passed to theDaVerifier
.
- An adaptation of the
-
Arguments:
Name | Type | Description |
---|---|---|
block |
FilteredBlock |
The relevant subset of data from a DA layer block |
- Response:
Name | Type | Description |
---|---|---|
txs |
iterable<BlobTransaction> |
A list of L1 transactions ("data blobs"), with their senders |
inclusion_proof |
InclusionMultiproof |
A witness showing that each transaction was included in the DA layer block |
completeness_proof |
CompletenessProof |
A witness showing that the returned list of transactions is complete |
-
Usage:
- Fetch the (relevant portion of the) finalized block at the given height. If
height
has not been finalized, block until it is.
- Fetch the (relevant portion of the) finalized block at the given height. If
-
Arguments:
Name | Type | Description |
---|---|---|
height |
u64 |
The height of the block to return |
- Response:
Name | Type | Description |
---|---|---|
block |
FilteredBlock |
The relevant subset of data from a DA layer block |
-
Usage:
- Fetch the (relevant portion of the) block at the given height. If
height
has not yet been reached, block until it is. A response may be returned before it is "finalized" by the underlying consensus.
- Fetch the (relevant portion of the) block at the given height. If
-
Arguments:
Name | Type | Description |
---|---|---|
height |
u64 |
The height of the block to return |
- Response:
Name | Type | Description |
---|---|---|
block |
FilteredBlock |
The relevant subset of data from a DA layer block |
-
Usage:
- Post the provided blob of bytes onto the data availability layer
-
Arguments:
Name | Type | Description |
---|---|---|
blob |
bytes` | The data to post on the DA layer |
- Response:
Name | Type | Description |
---|---|---|
Ok |
_ |
No response |
Err |
Error |
An error message |
A struct containing whatever runtime configuration is necessary to initialize this DaService
. For example, this
struct could contain the IP address and port of the remote RPC node that this DaService
should connect to.
The relevant subset of data from the DA layer block. This type must contain all data which will be processed by the rollup
and enough auxiliary data to allow its block hash to be recomputed by the DaVerifier
.
Code
Expressed in Rust, the DA Verifier interface is a trait
. You can find the trait implementation here.