Introducing Brollups

Burak
17 min readJun 21, 2024

--

Brollup is a Bitcoin-native rollup design that works with a native Bitcoin peg and does not require any changes to the Bitcoin protocol. The peg is enforceable on-chain and can be unilaterally redeemed at any time. Brollups are in a category of their own, neither optimistic, ZKP, nor sovereign.

Brollups use the Bitcoin block space as their data availability layer, similar to sovereign rollups. Unlike sovereign rollups, however, Brollups are deeply baked into Bitcoin and work natively with Bitcoin as a payable construct.

Rollups Competitive Landscape

A Brollup is operated by an operator or a quorum of operators. Operators provide liquidity to the protocol and advance the rollup state by chaining Bitcoin transactions at regular intervals.

Brollups use the Bitcoin blockchain as the data availability layer, and run transactions on a custom VM known as the Bitcoin Virtual Machine.

Brollups build upon the concept of virtual UTXOs (VTXOs) and utilize them as the peg. However, VTXOs are not actually moved to a separate piece of chain, so-called the Brollup. Brollups simply enable virtual UTXOs to be used in smart contracts as payable constructs.

Brollups are, in short, coinswaps between VTXOs and calldatas.

Like how Ark Network swaps VTXOs for new VTXOs, Brollups swap VTXOs for calldata (plus new VTXOs).

VTXOs are always verifiable off-chain, and enforceable on-chain. Calldata, on the other hand, undergoes client-side validation; Bitcoin clients only see it as bytes, while Brollup clients read and interpret the bytes.

Since calldata is evaluated within the client-side validated context, and VTXOs are verifiable off-chain, a VTXO can be swapped for calldata that executes a payable condition in the smart contract.

Brollups combine calldata with VTXOs to execute payable contract conditions, where, for example, the contract can grant tokens to the msg.sender in exchange for paid Bitcoin, with the location of the payment VTXO derived from the calldata.

This covers >90% of DeFi use-cases, whether it’s listing an NFT for sale in exchange for Bitcoin, where the buyer pays with Bitcoin upon execution, or placing a token sell order on a decentralized exchange, where the filler pays with Bitcoin upon execution. All atomically executed, verifiable, scalable, and enforcable on Bitcoin.

Possible Brollup Applications

Simply put, Brollups extend upon VTXOs and utilize them as payables.

TPS Projections

TPS projections (assumes entire Bitcoin blockspace is used and others fields are ignored)

Brollups scale exceptionally well in terms of transactions executed per second (TPS). This efficiency is attributed to the effective use of data availability, which can be categorized into three key areas:

  1. Compact DA Encoding
    Brollups employ a compact DA encoding structure with bit-level commitments to efficiently store transactions.
  2. Signature Aggregation
    Brollups aggregate signatures by summing the nonces and commitments, rather than aggregating signatures using ZKPs.
  3. Virtual UTXOs
    Brollups work with virtual UTXOs instead of bare UTXOs. This means Brollups consume almost no on-chain footprint for payable referencing.

Brollups vs. PSBTs

An on-chain Ordinals PSBT trade consumes roughly ∼330 vBytes, whereas the entire process of trading an NFT on a Brollup marketplace, from listing to selling, consumes only 3.25 vBytes. This means that trading NFTs on Brollup is 100x times more scalable than trading with PSBTs.

Brollups vs. Omni

An on-chain Omni Layer token transfer consumes roughly ∼250 vBytes, whereas transferring tokens on Brollup consumes only 2.21 vBytes. This means transferring tokens on Brollup is 110x times more scalable than transferring on Omni Layer.

Contract Example— NFT Marketplace

This is an example NFT marketplace contract that can be run on the Bitcoin Virtual Machine, containing three callable methods:

  1. nft_auction_list
    msg.sender (Alice) calls this method to list her piece for sale, providing two arguments: her NFT contract address (Contract) and price tag (u32).
  2. nft_auction_buy
    msg.sender (Bob) calls this method to buy Alice’s piece, providing an argument and a payment: Alice’s NFT contract address (Contract) and the Bitcoin payment (Payable).
  3. nft_auction_cancel
    msg.sender (Alice) calls this method to cancel her listing if no one is interested in buying her piece, providing one argument: her NFT contract address.

Trusted Setup

SNARK-based ZKP rollups are built upon a fundamental reliance on a trusted setup. This setup involves the creation of initial cryptographic parameters, where at least one participant must act honestly to ensure the security and authenticity of these parameters. Optimistic rollups operate under a similar assumption as well; necessitating the presence of at least one honest participant to maintain the accuracy of state transitions.

The necessity of minimal trust in most rollup solutions is a fundamental characteristic that we cannot escape from. Whether discussing ZKP rollups or optimistic rollups, both rely on the principle that the involvement of at least one honest member is crucial for safeguarding the system’s reliability and preventing malicious activities. The same principle applies to Brollups, which also depend on the at least one honest member premise.

While we can’t employ proper covenants on Bitcoin today, we can emulate covenants in a manner akin to a trusted setup; we can create a pseudo-covenant known as key deletion covenants. In a key deletion covenant setup, a group of members generates new keys, collaboratively signs some state transition with these keys, and promises to delete their keys afterward. As long as at least one member of the setup is honest, the covenant remains valid, mirroring the same trust assumption as in trusted setups.

Essentially, key deletion covenants represent a variant of trusted setup that is feasible to deploy on Bitcoin today, relying on the same assumptions, but on Bitcoin.

Brollups use key deletion covenants to constrain payable VTXOs to a shared utxo, a concept already implemented in the clArk project for vanilla VTXOs.

The Covenant

Since we can’t implement proper covenants on Bitcoin today, we can use aggregate keys with strong trust assumptions to constrain virtual UTXOs (VTXOs). In this regard, Brollups use three keys to construct the covenant:

  1. Operator Key
    The Brollup operator, whether it’s a single-owned entity or a FROSTy quorum, signs with their key to constrain virtual UTXOs.
  2. msg.senders[] Aggregate Key
    Accounts that transact on the Brollup (msg.senders) each sign with their key to constrain virtual UTXOs. All individual msg.sender keys are aggregated into a single key in coordination with the operator.
  3. kdc.signers[] Aggregate Key
    The top 16,384 accounts by their rank (kdc.signers) each sign with their key to constrain virtual UTXOs. Individual kdc.signer keys who opted in are aggregated into a single key in coordination with the operator.

Respective signatures of each key is stored in the annex of the first prevout, consuming a total discounted space of 48 vBytes.

To break the covenant, all three signatures must be forged. This means msg.senders, kdc.signers, and the operator must all collude together;

  • If all msg.senders and all kdc.signers collude, except the operator, the covenant holds true.
  • If the operator, all kdc.signers, and all msg.senders collude except for one msg.sender, the covenant holds still true. This provides strong assurance, especially considering that senders frequently send coins to themselves to refresh their coins (every week). It’s shooting oneself in the foot for a sender to break a covenant involving their own coins.
  • If the operator, all msg.senders, and all kdc.signers collude except for one kdc.signer, the covenant holds still true. This provides strong assurance given that there are 16,384 kdc.signers in total. If at least one of the 16,384 accounts is honest, the covenant cannot be broken.

Finality

When payable coins are granted to an account whose wallet is offline, these coins are considered in an intermediate state. Coins held in this state depend on at least one trusted setup participant (kdc.singers) to prevent double-spending of the covenant.

This can be likened to optimistic rollups, where finality can take weeks. In the Brollup context, however, finality for a recipient occurs when they come online and refresh their VTXOs. Refreshing involves sending coins back to oneself, with the recipient being part of the msg.senders[] Aggregate Key.

Since the covenant is bound by each msg.sender, a refresher msg.sender will never violate a covenant involving their own coins, thereby establishing a trustless nature for the covenant they are participating in. VTXOs ossify when msg.senders refresh them.

Any attempt to double-spend an intermediate-state VTXO on-chain is detected by Brollup clients, resulting in the permanent banning of the operator from participating in future rounds. Similarly, any on-chain double-spend attempt by a kdc.signer or a msg.sender results in their account being permanently banned from transacting ever again.

Forfeiting

To call a payable smart contract and pay transaction fees on Brollups, msg.senders atomically swap their VTXOs for new VTXOs through a process known as forfeiting.

To forfeit a VTXO, the owner (msg.sender) signs the VTXO outpoint together with the connector provided by the operator. The outpoint reference of the provided connector commits to three areas:

  1. Calldata
    Forfeited VTXOs commit to calldata, encoded in the witness of the calldata output (the payload). This ensures the atomicity of the committed data whether its a contract call or a contract deployment.
  2. Payable VTXOs
    Forfeited VTXOs commit to new VTXOs, similar to vanilla Ark payments. In the Brollup context, however, these VTXOs serve as payable constructs in smart contracts, as they are verifiable off-chain without additional DA overhead (the location is derived from calldata).
  3. Transaction fees
    Forfeited VTXOs commit to transaction fees that are distributed among miners, the operator, and kdc.signers, in accordance with feeconomics.

Calldata outputs are utilized as the change output for the operator, revealing the payload they contain in the subsequent transaction. The calldata output contains the following script:

// Store calldata in the witness
OP_FALSE
OP_IF

OP_PUSHDATA
<CALLDATA>

OP_ENDIF

OP_IF

// The operator uses this as the change output for the next round.
<operator key>
OP_CHECKSIG

OP_ELSE

// The otherwise this becomes anyone-can-spend.
// This encourages the operator to keep its operations running without interruption.
<24 hours>
OP_CHECKSEQUENCEVERIFY

OP_ENDIF

Account Model

Brollups operate on an account-based model, where Nostr public keys function as the designated accounts. Much like Ethereum addresses, Nostr keys serve as the dedicated identifiers through which users interact and transact on the network.

To facilitate efficient transactions and minimize blockspace usage, accounts are indexed and identified by their index numbers, similar to the data encoding scheme used in optimistic rollups.

Initially, a large list of existing Nostr keys will be indexed and hardcoded into the npub directory of Brollup clients. This allows Brollup clients to access and reference accounts using their compact index numbers.

As new users transact, their accounts are automatically appended to the npub directory for efficient referencing later. This efficient referencing occurs during the bit-encoding process of the payload.

Compact Payload Encoding (CPE)

Transactions on Brollups are referred to as entries. An entry either calls a smart contract or deploys a new contract.

Brollups employ a compact payload encoding structure with bit-level commitments to efficiently store entries. Entries are sequentially encoded within the payload, followed by the aggregate signature.

Brollups aggregate signatures by summing the signature nonces and commitments, similar to how ZK rollups aggregate signatures using ZKPs. This aggregation occurs during the entry signing session in coordination with the operator.

The payload is split into 520-byte chunks and stored directly in the witness;

OP_FALSE
OP_IF

OP_PUSHDATA
<chunk_1>

OP_PUSHDATA
<chunk_2>

...

OP_PUSHDATA
<chunk_n>

OP_ENDIF

Entries contain the following fields in the payload encoding:

  1. account index
  2. contact index
  3. method call
  4. calldata

Account Index

Accounts are indexed by their rank, which is determined by how frequently they transact. First two bits of the account index maps to four different index tiers;

00 -> this an unregistered account, followed by the 256-bits x-only npub.
01 -> this a registered frequent account, top-65025 on the rank. followed by the 16 bits index number.
10 -> this a registered non-frequent account, between 65025–16646400 on the rank. followed by the 24 bits index locator number.
11 -> this a registered non-frequent account, between 16646400–4244897025 on the rank. followed by the 32 bits index locator number.

account index is followed by the contract index;

Contract Index

Contracts are indexed by their rank, which is determined by how frequently they are called. First two bits of the contract index maps to four different index tiers;

00 -> this is a contract deployment, followed by the contract bytecode starting with the varint prefix.
01
-> this is a frequent contract, top-64 on the rank. followed by the 6-bit index number.
10 -> this is a non-frequent contract, between 64–65089 on the rank. followed by the 16 bits index locator number.
11 -> this is a non-frequent contract, between 65089–1061208000 on the rank. followed by the 30 bits index locator number.

contract index is followed by the method call, if it is not 0b00.

Method Call

Method calls are indexed based on their written order when the IDE compiles the contract into bytecode. First bit of the method call maps to two different index tiers;

0 -> index of the called method is #4 or less, followed by the 2-bit index number.
1 -> index of the called method is between #4 — #20, followed by the 4-bit index locator number.

method call is followed by the calldata.

Sessions

A Brollup state transition involves four subsequent sessions;

  1. Entry Signing Session;
    This session sums individual signatures from msg.senders of the round and results in an aggregate signature to authorize the payload.
  2. KDC Session
    This session sums individual signatures from the top 16,384 accounts by their rank and results in an aggregate signature to constrain payable VTXOs to the shared output.
  3. Covenant Signing Session
    This session sums individual signatures from msg.senders of the round and results in an aggregate signature to constrain payable VTXOs to the shared output.
  4. Forfeiting Session
    This session forfeits VTXOs from msg.senders of the round to cover transaction fees and payable VTXOs.

Entry Signing Session

Accounts that are initiating calls or deploying contracts initially participate in this session to aggregate their signatures in coordination with the operator. This session employs a straightforward, single-round aggregation scheme where each msg.sender signs their own version, with the signature committing to H(m) rather than H(R || P || m). This allows the operator to simply sum all signatures in a single round. The resulting signature is a 64-byte Schnorr signature, which is batch-verified within the Brollup context.

  +--------------+                                      +--------------+
| |--(1)--- entryCtx ----->| |
| |<-(2)--- entryAck ------| |
| | | |
| msg.sender | // wait until the operator | operator |
| | // aggregates all entries | |
| | | |
| |<-(3)--- payloadCtx ------| |
+--------------+ +--------------+

(1) msg.sender creates a entryCtx, and requests to join the round;

let entryCtx = (prevHash, npub, entry, sig), where;

  • prevHash is the hash of previous Brollup state
  • nsec is secret key of the msg.sender
  • npub is the public key of the msg.sender; nsec • G
  • entry is the non-compact bytecode of the entry that msg.sender intends to transact;
account id (32 bytes) || contract id (32 bytes) || method call (1 byte) || calldata (varsize)
  • let message = TaggedHash(bytes(prevHash || entry), “SighashEntry”)
  • let nonce = TaggedHash(bytes(message || nsec), “DeterministicNonce”)
  • let sig = (nonce • G, (nonce + message • nsec) mod n)

(2) operator responds with an acknowledge message, entryAck.

(3) The operator gathers all entries, sums the signatures, and responds with payloadCtx;

  • let aggpub = nsec1 • G + nsec2 • G .. nsecN • G
  • let aggmsg = m1 + m2 .. mN
  • let aggnonce = 𝑘1 • G + 𝑘2 • G .. 𝑘N • G
  • let aggcmt = (𝑘1 + m1 • nsec1) + (𝑘2 + m2 • nsec2) … (𝑘N + mN • nsecN)
  • let aggsig = (aggnonce, aggcmt)
  • let payloadCtx = (prevHash, npubs[], entries[], fee, aggsig)

At the end of the round, based on the list of entries[] and the fee provided, msg.senders can deterministically see the outcome of their execution and determine the precise amount they need to pay for the execution.

If the outcome of the execution results in an asserted failure in the contract logic, the msg.sender is removed from the round. This prevents failed transactions from being included in blocks, thereby saving space and fees.

KDC Session

A Key-Deletion-Covenant session, or KDC session for short, gathers signatures from the top 16,384 accounts by their rank and results in an aggregate signature to constrain payable VTXOs to the shared output. If at least one of the 16,384 accounts is honest, the covenant holds true. The information about which of the top 16,384 accounts participated in the KDC session is encoded in a compact field called bitmap.

Bitmap

Bitmap is a compact 16,384-bit-long bitstring that maps to the participants of the KDC session. Each bit indicates whether the nth account by rank participated in the session. Bitmap is stored in the annex of the first prevout, consuming a discounted space of 256 vBytes in each state transition.

1st bit: 0 -> Account #1 by rank did not participate in the KDC
2nd bit: 1 -> Account #2 by rank did participate in the KDC
3rd bit: 1 -> Account #3 by rank did participate in the KDC
4th bit: 0 -> Account #4 by rank did not participate in the KDC
5th bit: 1 -> Account #5 by rank did participate in the KDC
.
.
.
16,384th bit: 1 -> Account #16384 by rank did participate in the KDC

It is possible to map from bitmap to an array of signers kdc.signers[] using a function bitmapToSigners();

  • let bitmapToSigners(bitmap) => kdc.signers[]
    For each bit in the bitmap, output the array of signers kdc.signers[];
    1
    -> kdc.signers.push( npub_directory[ iterator++ ] )
    0 -> iterator++

It is possible to compute aggregate KDC key aggkey by summing the npubs of kdc.signers[];

  • let keyaggCtx = KeyAgg( kdc.signers[] ) => (Q, gacc, tacc)
  • Let (aggkey, _, _) = keyaggCtx
  +--------------+                                      +--------------+
| |--(1)--- covenantCtx ----->| |
| |<-(2)--- pubnonce ------| |
| | | |
| | // wait until the operator | |
| | // aggregates all pubnonces | |
| | | |
| operator |--(3)--- kdcCtx ----->| kdc.signer |
| |<-(4)--- partialsig ------| |
| | | |
| | // wait until all signers | |
| | // respond with partial sigs | |
| | | |
| |--(5)--- aggsig ----->| |
+--------------+ +--------------+

(1) operator prepares and responds with covenantCtx;

let covenantCtx = (outpoint_self, spks[], values[]), where;

  • outpoint_self is the prevout that contains the aggregate key, from which msg.senders aim to produce an aggregate signature.
  • spks[] is an array of scriptPubKeys containing the payable VTXOs
  • values[] is an array nValues containing the payable VTXO amounts.

(2) kdc.signer prepares and responds with pubnonce;

  • let (secnonce, pubnonce) = NonceGen(nsec, npub)

(3) operator responds with kdcCtx, containing the aggregate nonce aggnonce and compact list of signers bitmap;

  • let kdcCtx = (aggnonce, bitmap)
  • let aggnonce = NonceAgg(pubnonce1 .. pubnonceN) => aggnonce

(4) kdc.signer signs and responds with partial signature partialsig;

  • let kdc.signers[] = bitmapToSigners(bitmap) -> kdc.signers[]
  • let sighash = ReturnSighashCov(covenantCtx)
  • let sessionCtx = (aggnonce, kdc.signers[], sighash)
  • let partialsig = Sign(secnonce, nsec, sessionCtx) => partialsig

(5) operator responds with aggregate signature aggsig;

  • let aggsig = PartialSigAgg(psig1 .. psigN, sessionCtx) => aggsig

Covenant Signing Session

Accounts that pass the entry signing session move into this session to constrain payable VTXOs to the shared output. This session employs a Musig2-based two-round aggregation scheme, where msg.senders aim to produce an aggregate signature from the aggregate key. The resulting signature is a 64-byte valid BIP-340 Schnorr signature.

  +--------------+                                      +--------------+
| |--(1)--- signerCtx ----->| |
| |<-(2)--- covenantCtx ------| |
| |--(3)--- pubnonce ----->| |
| | | |
| | // wait until the operator | |
| | // aggregates all pubnonces | |
| msg.sender | | operator |
| |<-(4)--- aggnonce ------| |
| |--(5)--- partialsig ----->| |
| | | |
| | // wait until all signers | |
| | // respond with partial sigs | |
| | | |
| |<-(6)--- aggsig ------| |
+--------------+ +--------------+

(0) Given payloadCtx from the earlier session, msg.sender computes the aggregate key aggkey;

  • let (prevHash, msg.senders[], _, _, _) = payloadCtx
  • let keyaggCtx = KeyAgg(msg.senders[]) => (Q, gacc, tacc)
  • Let (aggkey, _, _) = keyaggCtx

(1) msg.sender joins the round with signerCtx;

  • let signerCtx = (prevHash, npub)

(2) operator returns covenantCtx;

let covenantCtx = (outpoint_self, spks[], values[]), where;

  • outpoint_self is the prevout that contains the aggregate key, from which msg.senders aim to produce an aggregate signature.
  • spks[] is an array of scriptPubKeys containing the payable VTXOs
  • values[] is an array nValues containing the payable VTXO amounts.

(3) msg.sender prepares and responds with pubnonce;

  • let (secnonce, pubnonce) = NonceGen(nsec, npub)

(4) operator aggregares pubnonces, and responds with aggnonce;
NonceAgg(pubnonce1 .. pubnonceN) => aggnonce

(5) msg.sender signs and responds with partial signature partialsig;

  • let sighash = ReturnSighashCov(covenantCtx)
  • let sessionCtx = (aggnonce, aggkey, sighash)
  • let partialsig = Sign(secnonce, nsec, sessionCtx) => partialsig

(6) operator responds with aggregate signature aggsig;

  • let aggsig = PartialSigAgg(psig1 .. psigN, sessionCtx) => aggsig

Design Considerations

BVM vs. EVM comparison

Determinism

The Bitcoin Virtual Machine (BVM) aligns more heavily on p2p use-cases such as atomic trades between two parties. Smart contracts in this context act as facilitators of these interactions.

While it’s also feasible to construct more complex contracts such as AMMs for token-to-token swaps that operate out-of-band and aren’t atomic, BVM transactions are deterministic, meaning the transaction outcome is determined upon creation. In contrast, EVM transactions rely on their ordering, which exposes them to risks like MEV.

Even Brollup operators themselves cannot modify transaction ordering through additional fees; Brollups are designed with a rank-based ordering system where higher-ranked accounts are given priority. Every entry in a payload pays an equal fee for the same execution.

Although higher-ranked accounts are prioritized in the ordering, front-running is not feasible for them either, since there is no mempool to track transactions; operators gather and aggregate transactions themselves.

Gas & Failures

Given the deterministic nature of transaction outcomes, the msg.sender knows in advance whether the transaction will succeed or fail before entering the forfeiting session where fees are paid. Likewise, the msg.sender is aware in advance of the exact amount of transaction fees to be paid.

Internal State

Given the inherent design of operators chaining Bitcoin transactions together, each state-transition is interconnected. A msg.sender knows in advance the specific Brollup payload height at which their transaction will be included, eliminating the need for tracking internal state. Consequently, a nonce field is not required in the entry encoding.

DoS, Blaming & Blacklists

  • A kdc.signer can interrupt a KDC signing session. This leads to a redo of the earlier Entry signing session. kdc.senders, however, are each part of a well-known list of top accounts by their rank. Their behaviors can be further analyzed, and they can be temporarily or permanently blacklisted by the operator.
  • A msg.sender can interrupt a covenant signing session or a forfeiting session, resulting in a redo of the affected sessions. Their behaviors can be further analyzed, and they may be temporarily or permanently blacklisted by the operator. A blacklisted account might be required by the operator to place a refundable collateral bond in advance of participating in further sessions. The required bond amount varies according to the rank of the account.
  • A permanently blacklisted account can still transact without needing permission from the operator. This is because an account does not require an operator to transact on the Brollup; they can execute transactions by incorporating their own version of the on-chain transaction and placing calldata in the respective fields. A Brollup operator acts solely as an aggregator and liquidity provider, facilitating efficient transactions at scale.

Conclusion

Brollups are a Bitcoin-native rollup design that operates with a native Bitcoin peg and does not require protocol changes to Bitcoin. Brollups bring over 90% of DeFi use cases natively to Bitcoin and further scale Bitcoin usage.

Brollups are currently in the design phase, and the project is not associated with any issued tokens.

Interested in learning more or contributing? You should definitely join our TG community channel! Alternatively, you can reach out me on X or Nostr.

--

--