Hackathon Highlights: Bitcoin Bedrock Data Availability Layer

05/17/2314 min read

Mantleby Mantle





Hackathon Highlights: Bitcoin Bedrock Data Availability Layer

Recently at ETHDenver (one of the largest and most prestigious global web3 hackathons), members of the Mantle team managed to fight their way into the finals of the “infrastructure” track with a tongue-in-cheek submission named “Bitcoin Vitaliks Vision”.

This technical blog post will herein outline the journey the team took to carry out this process.

Bitcoin has recently entered the modular L2 spotlight for its potential to provide high security data availability in the form of the Ordinal-inspired SegWit technique. The Mantle-powered hackathon team forked and modified OpStacks Bedrock codebase (specifically the batching process) to instead rely on Bitcoin for data availability.

Graph: https://twitter.com/MessariCrypto/status/1641813305290530816/photo/1

The Mantle team already had experience changing this flow when we had modified an earlier Optimism version stack to use Mantle DA as powered by EigenDA technology instead of the CTC contract.

Flexibility of the Bedrock Design

The primary modification to the OpStack was to the batcher. The batch submitter, also referred to as the batcher, is the entity submitting the L2 sequencer data to L1, to make it available for verifiers.

Taking a step back: The familiar sequencer accepts L2 transactions from users. It is responsible for building blocks out of these. For each such block, it also creates a corresponding sequencer batch. It is also responsible for submitting each batch to a data availability provider (e.g. Ethereum calldata), which it does via its batcher component.

The L2 chain can then be derived from this data. The modifications made do not include updating the L2OutputRoot commitment contract.

The destination address is currently an EOA on mainnet (when Bedrock enters mainnet production — see the equivalent on Goerli for now)

Ordinals: Using Bitcoin’s SegWit for Data Blobs

Initially we thought OP_RETURN would work (limited to 80 bytes; fun fact: used by Tether for “lazy rollup”).

Ordinals developer Casey Rodarmor found a way to essentially create the equivalent of “calldata” on Bitcoin script thanks to the Taproot upgrade. Additionally, this “calldata” can be as large as the Bitcoin block size limit (4MB) — benefits from the SegWit discount — making “blobspace” cheaper and more abundant than on Ethereum as of Feb. 20, 2023.


One of taproots restrictions is that the maximum size of an object being pushed to the stack is 520 bytes. As calldata is typically larger than that, we had to chunk it in order to construct an acceptable tapscript.

We then constructed a high level Bitcoin script as follows:


We decided to implement our modifications in Golang, in which the majority of the Bedrock modular services are written, which meant using the BTCD modules extensively. We also leaned on the Bitcoin Core debugging tools and guides found here.

Starting by replicating the existing typical Transaction Manager entity, we created a Bitcoin Transaction Manager that has the following functionality:

  • Connects to a Bitcoin JSON RPC endpoint (using password and URL)
  • Gets a Bitcoin block header for a particular block number (getBTCBlockHeaderForHeight)
  • Gets a Bitcoin block header for a particular block hash (getBTCBlockHeaderForHash)
  • Manages transaction state
  • Waiting for a particular number of confirmations
  • Retries if transaction was not included

The real magic happens in the op-batcher main loop and carries out the following steps:

  1. It decodes a hardcoded private key and creates a private key object and a corresponding public key object using the btcec.PrivKeyFromBytes function. These keys are used later in the function to sign the transaction.
  2. It splits the input data into chunks and creates a Taproot script using the CreateBitcoinScript function. This script includes the hash of the Merkle root of the chunks. This is where the OP_FALSE and OP_TRUE from above get pushed into the script.
  3. It creates a base Tapleaf object for the Taproot script using the txscript.NewBaseTapLeaf function and assembles a Taproot script tree using the txscript.AssembleTaprootScriptTree function. This tree includes the Taproot script, the Taproot leaf node, and the Merkle tree root.
  4. It creates a control block for the Taproot script using the ToControlBlock function of the txscript.LeafMerkleProof object and computes the Taproot script tree root hash using the TapHash function of the txscript.TaprootScriptTree object.
  5. It computes a tweaked public key for the Taproot output using the txscript.ComputeTaprootOutputKey function and creates a Taproot address using the btcutil.NewAddressTaproot function.
  6. It sends coins to the generated Taproot address using the SendToAddress function of the Bitcoin client and waits for the transaction to be confirmed on the blockchain using the waitForTransactionConfirmation function.
  7. It creates a Pay-to-Taproot (P2TR) script using the PayToTaprootScript function and verifies the Taproot leaf commitment using the txscript.VerifyTaprootLeafCommitment function.
  8. It creates a new Bitcoin transaction object using the wire.NewMsgTx function and creates a Tapscript signature using the txscript.RawTxInTapscriptSignature function.
  9. It validates the transaction using the txscript.NewEngine and vm.Execute functions, adjusts the transaction fee, and broadcasts the transaction to the Bitcoin network using the SendRawTransaction function of the Bitcoin client. Finally, it waits for the transaction to be confirmed on the blockchain using the waitForTransactionConfirmation function.

Inclusion Verification

To ensure these finely crafted transactions were included on Bitcoin (and not sitting permanently/dropped from the mempool), we decided to verify the inclusion using a Bitcoin SPV light client that runs on our modified L2.

The existing SPV light clients were not suited for our task:

So our team member (and Huff wizard) Phillip implemented an assembly written light client verifier, which verifies both the header and the inclusion of our data blob transaction in the Merkle tree included in the header. We tested this with headers but it remains to fully integrate this in the Bedrock codebase.


Bedrock was nice and modular, and relatively straightforward to modify, so this project was achievable over the course of a few coffee-fueled days (and nights). The separation of services (a.k.a. modularity) such as the batcher separated from the sequencer code helped this process significantly.

Although the concepts were familiar, the team was new to Bitcoin-based tooling and struggled to find documentation for carrying out this level of programmability on Bitcoin. In particular running a Bitcoin regtest node (restarting this with different flags approximately 100k times — shoutout @LibEvm) and knowing which versions of btcgo to use (we chose to write the integration code in Go, keeping with the existing Batcher implementation).

The Bedrock Repo has some nice built-in Make commands to spin up entire end to end testnets locally (these can be found in the top level Makefile — our favorites included devnet-up-deploy and devnet-clean).


  • Post state root on Bitcoin too
  • Prove the retrieval of data from Bitcoin (this will be required for the fraud proofs)
  • Validate Bitcoin headers of interest (mentioned in Inclusion Verification section above)
  • Optimise batch posting (make batches as large as possible, 10 mins block mainnet)
  • More compression


To Optimism for producing Bedrock, their modular layer-2 stack.

There were several other teams working on similar ideas. In particular the Celestia-associated Rollkit team produced a similar project providing a code for any ABCI to use Bitcoin as a data availability layer (https://github.com/rollkit/bitcoin-da).

Join the Community