Bitcoin Scaling Proof of Concept: Implementing a Bridge Contract on Bitcoin with OP_CAT Support

This article is machine translated
Show original

Original text: Implementing a Bridge Covenant on OP_CAT-Enabled Bitcoin: A Proof of Concept

Translation and proofreading: Starknet Chinese Community

📑 Please indicate the source for reprinting🕹️

Quick Facts

  • A deep dive into building a demo bridge contract on Bitcoin, laying the foundation for a production-grade bridge on Starknet

  • Implement four smart contracts: deposit and withdrawal aggregator, bridge and withdrawal extender

  • Leverage recursive contracts and Merkle trees to efficiently batch deposit and withdrawal requests while maintaining the integrity and security of user accounts

introduction

In this article, we took a deep dive into how sCrypt built a demo bridge contract on Bitcoin. This proof-of-concept implementation aims to lay the foundation for a production-grade bridge for the Starknet Layer 2 (L2) network. The bridge is designed to allow multiple deposit or withdrawal request transactions to be merged into a single root transaction and incorporated into the main bridge contract, updating its state, which consists of a set of accounts organized in a Merkle tree.

Since the bridge contract script is very complex, we used sCrypt's domain-specific language (DSL) to write its implementation in sCrypt.

Overview

The bridge consists of a recursive contract Bitcoin script. Here, “contract” means that the locking script is able to impose conditions on spending transactions, and “recursive” means that the above rules are strong enough to implement persistent logic and state on-chain (a basic requirement for any on-chain smart contract).

The script exists in a chain of transactions, each of which imposes constraints on the structure of subsequent transactions, which unlock the outputs of the current transaction. Every time a new transaction is added to the chain, it represents an update to the bridge state. Therefore, the end of the chain stores the current bridge state.

The contract state — specifically, its hash — is stored in a non-spendable OP_RETURN output. Although we will not spend this UTXO, its data can be inspected when executing the contract script. Specifically, the state holds the root hash of the Merkle tree containing the account data, as shown below:

The Merkle tree stores data for a fixed set of account slots. Leaf nodes contain hash values of their respective account data, including addresses and balances. To represent empty account slots, these slots are marked with zero bytes.

Each update of the bridge results in a change to the account tree. To facilitate this update, we rely on Merkle proofs, which are very efficient to verify in Bitcoin Script. The update consists of two main steps. First, we verify a Merkle proof to prove that the Merkle tree contains the current state of a specific account. Then, after calculating the new state of the account, we use the same auxiliary nodes in the aforementioned Merkle proof to derive the new root hash.

Updates can be either deposits or withdrawals. The bridge can perform batches of these updates in a single transaction.

deposit

Our goal is to enable users to submit deposit or withdrawal requests independently. To do this, users create separate transactions that pay to the deposit or withdrawal aggregation contract respectively. The contract aggregates these requests into a Merkle tree. The root hash of this tree can be merged into the main bridge contract, which then processes each deposit or withdrawal.

In the deposit transaction, in addition to hashing the deposit data and building the Merkle tree, the contract also ensures that the deposit satoshis locked in the contract output are accumulated to the root node of the tree in the correct way. The aggregation contract ensures that only the correct on-chain smart contract can use these funds. (Of course, in a production environment, we will also allow users to cancel their deposit transactions).

This tree structure is designed from the constraints of contract script construction, which does not allow transactions with too many inputs and outputs. The tree structure allows us to scale to potentially arbitrary throughput.

Withdrawal Request

Aggregation of withdrawal requests is similar to deposits, but with a few differences. First, we need a method of authentication so that users can withdraw funds from their accounts. This is different from deposits, where anyone can deposit money into any account, similar to how Bitcoin addresses are used. Authentication is done at the leaf node level of the aggregation tree. The withdrawal request aggregation contract checks that the withdrawal address matches the P2WPKH address of the first input in the leaf transaction.

This ensures that the owner of the address approves the withdrawal, since they have signed the transaction requesting the withdrawal. Another subtle difference compared to deposit aggregation is that we also hash the intermediate accumulated amounts and pass them up the tree. This is because we will need this data when scaling withdrawals, more on that later.

Astute readers may notice a potential problem with this withdrawal request authentication model. What if the operator decides to cheat and create a root transaction of an aggregation tree whose data is forged locally through unauthenticated fake withdrawal requests? We need an effective way to verify that the root transaction is derived from a valid leaf transaction.

To solve this problem, we perform what is called a "genesis check". Essentially, we have the aggregate contract check its previous transaction and the previous two transactions, its "ancestor transactions". The contract verifies that these transactions contain the same contract script and perform the same checks. In this way, we implement an inductive transaction history check. Since the previous two transactions perform the same checks as the current contract, we can confirm that the "ancestors" of these transactions also performed the same checks, all the way back to the leaf node (the genesis transaction).

Of course, we perform validation on both branches of the tree. Therefore, each aggregation node transaction checks a maximum of six transactions in total.

Withdrawal Extension

Now let's move on to the final part of the solution: withdrawal extensions. After processing a batch of withdrawal requests, the main bridge contract enforces an output that pays the total withdrawal amount to the extension contract. We can think of this contract as doing the reverse of what the withdrawal request aggregation contract does. It starts at the root node of the withdrawal tree and expands it into two branches, each containing the corresponding withdrawal amount that should be paid to that branch. This process continues all the way to the leaf nodes of the withdrawal tree. The leaf transaction enforces a simple payment output that pays the account owner's address the amount they requested to withdraw.

Implementation

To implement our bridge contract, we developed four sCrypt smart contracts, each handling a different aspect of the system. In this section, we briefly outline the functionality of each contract.

Deposit Aggregator Contract

The DepositAggregator contract aggregates individual deposits into a Merkle tree, which is then merged into the main bridge contract. This aggregation enables batch deposit processing, reducing the number of transactions that need to be processed individually by the bridge. In addition, it allows users to submit deposits independently and be processed by the operator later.

 class DepositAggregator extends SmartContract { @prop() operator: PubKey @prop() bridgeSPK: ByteString /** * Covenant used for the aggregation of deposits. * * @param operator - Public key of bridge operator. * @param bridgeSPK - P2TR script of the bridge state covenant. Includes length prefix! */ constructor(operator: PubKey, bridgeSPK: ByteString) { super(...arguments) this.operator = operator this.bridgeSPK = bridgeSPK } @method() public aggregate( shPreimage: SHPreimage, isPrevTxLeaf:boolean, sigOperator: Sig, prevTx0: AggregatorTransaction, prevTx1: AggregatorTransaction, // Additional parameters... ) { // Validation steps... } @method() public finalize( shPreimage: SHPreimage, sigOperator: Sig, prevTx: AggregatorTransaction, ancestorTx0: AggregatorTransaction, ancestorTx1: AggregatorTransaction, bridgeTxId: Sha256, fundingPrevout: ByteString ) { // Finalization steps... } }

The contract construction function has two parameters:

  • operator: The public key of the bridge operator who has the authority to aggregate deposits.

  • bridgeSPK: The script public key (SPK) of the main bridge contract, ensuring that aggregated deposits are merged correctly.

The core functionality of the deposit aggregator is encapsulated in the "aggregate" method. This method performs the following steps:

Verify sighash preimage and operator signature : Ensures that the transaction is authorized by the bridge operator and that the sighash preimage is well-formed and belongs to the transaction being executed. Learn more about sighash preimage verification in this article .

 // Check sighash preimage. const s = SigHashUtils.checkSHPreimage(shPreimage) assert(this.checkSig(s, SigHashUtils.Gx)) // Check operator signature. assert(this.checkSig(sigOperator, this.operator))

Construct and validate predecessor transaction IDs : Check that the aggregated predecessor transaction is valid and correctly referenced.

 // Construct previous transaction ID. const prevTxId = AggregatorUtils.getTxId(prevTx, false) // Verify that the transaction unlocks the specified outputs. const hashPrevouts = AggregatorUtils.getHashPrevouts( bridgeTxId, prevTxId, fundingPrevout ) assert(hashPrevouts == shPreimage.hashPrevouts)

Merkle tree aggregation : Verify that the deposit data passed as a witness hash matches the state stored in the preceding transaction.

 const hashData0 = DepositAggregator.hashDepositData(depositData0) const hashData1 = DepositAggregator.hashDepositData(depositData1) assert(hashData0 == prevTx0.hashData) assert(hashData1 == prevTx1.hashData)

Amount Validation : Confirms that the amount in the previous output matches the specified deposit amount, ensuring that the funds are correctly calculated in the aggregation.

 // Check that the prev outputs actually carry the specified amount // of satoshis. The amount values can also carry aggregated amounts, // in case we're not aggregating leaves anymore. assert( GeneralUtils.padAmt(depositData0.amount) == prevTx0.outputContractAmt ) assert( GeneralUtils.padAmt(depositData1.amount) == prevTx1.outputContractAmt )

State update : Calculate a new hash by concatenating the hash of the previous transaction and update the state in the OP_RETURN output.

 // Concatinate hashes from previous aggregation txns (or leaves) // and compute new hash. Store this new hash in the state OP_RETURN // output. const newHash = hash256(prevTx0.hashData + prevTx1.hashData) const stateOut = GeneralUtils.getStateOutput(newHash)

Reentrancy prevention : Enforce strict output scripts and amounts to prevent unauthorized modifications or double spending.

 // Sum up aggregated amounts and construct contract output. const contractOut = GeneralUtils.getContractOutput( depositData0.amount + depositData1.amount, prevTx0.outputContractSPK ) // Recurse. Send to aggregator with updated hash. const outputs = contractOut + stateOut assert( sha256(outputs) == shPreimage.hashOutputs )

Once deposits are aggregated, they must be merged into the main bridge contract. This process is handled by the "finalize" method, which includes the following steps:

  • Verify predecessor transactions : Similar to the “aggregate” method, verify predecessor transactions to ensure the integrity of the merged data.

  • Integration with the bridge contract : Check that the aggregated deposit is correctly merged into the main bridge contract by referencing the bridge’s transaction ID and script public key.

The full source code of the deposit aggregation contract is available on GitHub .

Withdrawal Aggregator Contract

The WithdrawalAggregator contract is designed to aggregate individual withdrawal requests into a Merkle tree, similar to how the Deposit Aggregator handles deposits. However, withdrawal operations require additional authentication to ensure that only legitimate account owners can withdraw funds from their accounts.

 class WithdrawalAggregator extends SmartContract { @prop() operator: PubKey @prop() bridgeSPK: ByteString /** * Covenant used for the aggregation of withdrawal requests. * * @param operator - Public key of bridge operator. * @param bridgeSPK - P2TR script of the bridge state covenant. Includes length prefix! */ constructor(operator: PubKey, bridgeSPK: ByteString) { super(...arguments) this.operator = operator this.bridgeSPK = bridgeSPK } @method() public aggregate( shPreimage: SHPreimage, isPrevTxLeaf:boolean, sigOperator: Sig, prevTx0: AggregatorTransaction, prevTx1: AggregatorTransaction, // Additional parameters... ) { // Validation and aggregation logic... } @method() public finalize( shPreimage: SHPreimage, sigOperator: Sig, prevTx: AggregatorTransaction, ancestorTx0: AggregatorTransaction, ancestorTx1: AggregatorTransaction, bridgeTxId: Sha256, fundingPrevout: ByteString ) { // Validation logic... } }

The core functionality of the withdrawal aggregator is encapsulated in the "aggregate" method, which performs the following steps:

 // Check sighash preimage. const s = SigHashUtils.checkSHPreimage(shPreimage) assert(this.checkSig(s, SigHashUtils.Gx)) // Check operator signature. assert(this.checkSig(sigOperator, this.operator))

Construct and validate predecessor transaction IDs : This process verifies that the aggregated predecessor transactions are valid and referenced correctly.

 // Construct previous transaction IDs. const prevTxId0 = AggregatorUtils.getTxId(prevTx0, isPrevTxLeaf) const prevTxId1 = AggregatorUtils.getTxId(prevTx1, isPrevTxLeaf) // Verify that the previous transactions are unlocked by the current transaction. const hashPrevouts = AggregatorUtils.getHashPrevouts( prevTxId0, prevTxId1, fundingPrevout ) assert(hashPrevouts == shPreimage.hashPrevouts)

Proof of Ownership Verification : Verifying Proof of Ownership transactions ensures that only the rightful owner can withdraw funds from the account.

  • Proof of Ownership Transaction : A transaction that proves control of a withdrawal address. The contract checks if the address in the withdrawal request matches the address in the Proof of Ownership transaction.
 if (isPrevTxLeaf) { // Construct ownership proof transaction IDs. const ownershipProofTxId0 = WithdrawalAggregator.getOwnershipProofTxId(ownProofTx0) const ownershipProofTxId1 = WithdrawalAggregator.getOwnershipProofTxId(ownProofTx1) // Check that the leaf transactions unlock the ownership proof transactions. assert(ownershipProofTxId0 + toByteString('0000000000ffffffff') == prevTx0.inputContract0) assert(ownershipProofTxId1 + toByteString('0000000000ffffffff') == prevTx1.inputContract0) // Verify that the withdrawal addresses match the addresses in the ownership proof transactions. assert(withdrawalData0.address == ownProofTx0.outputAddrP2WPKH) assert(withdrawalData1.address == ownProofTx1.outputAddrP2WPKH) }

Genesis checks via ancestor transactions : Similar to the deposit aggregator, the contract performs inductive checks by verifying ancestor transactions. This ensures the integrity of the transaction history and prevents operators from inserting unauthorized withdrawal requests.

 if (!isPrevTxLeaf) { // Construct ancestor transaction IDs. const ancestorTxId0 = AggregatorUtils.getTxId(ancestorTx0, isAncestorLeaf) const ancestorTxId1 = AggregatorUtils.getTxId(ancestorTx1, isAncestorLeaf) const ancestorTxId2 = AggregatorUtils.getTxId(ancestorTx2, isAncestorLeaf) const ancestorTxId3 = AggregatorUtils.getTxId(ancestorTx3, isAncestorLeaf) // Verify that previous transactions unlock the ancestor transactions. assert(prevTx0.inputContract0 == ancestorTxId0 + toByteString('0000000000ffffffff')) assert(prevTx0.inputContract1 == ancestorTxId1 + toByteString('0000000000ffffffff')) assert(prevTx1.inputContract0 == ancestorTxId2 + toByteString('0000000000ffffffff')) assert(prevTx1.inputContract1 == ancestorTxId3 + toByteString('0000000000ffffffff')) // Ensure that the ancestor transactions have the same contract SPK. assert(prevTx0.outputContractSPK == ancestorTx0.outputContractSPK) assert(prevTx0.outputContractSPK == ancestorTx1.outputContractSPK) assert(prevTx0.outputContractSPK == ancestorTx2.outputContractSPK) assert(prevTx0.outputContractSPK == ancestorTx3.outputContractSPK) }

Amount verification and total amount calculation : This method calculates the total amount to be withdrawn by adding the amounts of the withdrawal requests or previous aggregations.

 let sumAmt = 0n if (isPrevTxLeaf) { sumAmt = withdrawalData0.amount + withdrawalData1.amount } else { sumAmt = aggregationData0.sumAmt + aggregationData1.sumAmt }

State update : Calculate a new hash value containing the sum of the hash value of the previous transaction and the withdrawal amount. This hash value is stored in the OP_RETURN output to update the state.

 // Create new aggregation data. const newAggregationData: AggregationData = { prevH0: prevTx0.hashData, prevH1: prevTx1.hashData, sumAmt } const newHash = WithdrawalAggregator.hashAggregationData(newAggregationData) const stateOut = GeneralUtils.getStateOutput(newHash)

Reentrancy prevention and output enforcement : Ensure that outputs are strictly defined to prevent unauthorized modification or reentrancy attacks.

 // Construct contract output with the minimum dust amount. const contractOut = GeneralUtils.getContractOutput( 546n, prevTx0.outputContractSPK ) // Ensure outputs match the expected format. const outputs = contractOut + stateOut assert( sha256(outputs) == shPreimage.hashOutputs, )

The full source code of the withdrawal aggregation contract is available on GitHub .

Bridge Contract

The Bridge contract is the core component of our system and is the main contract that maintains the state of the bridge, including accounts and their balances organized in a Merkle tree. It handles deposit and withdrawal operations by integrating with the aggregator contract we discussed earlier.

 class Bridge extends SmartContract { @prop() operator: PubKey @prop() expanderSPK: ByteString constructor( operator: PubKey, expanderSPK: ByteString ) { super(...arguments) this.operator = operator this.expanderSPK = expanderSPK } @method() public deposit( shPreimage: SHPreimage, sigOperator: Sig, prevTx: BridgeTransaction, // Previous bridge update transaction. aggregatorTx: AggregatorTransaction, // Root aggregator transaction. fundingPrevout: ByteString, deposits: FixedArray<DepositData, typeof MAX_NODES_AGGREGATED>, accounts: FixedArray<AccountData, typeof MAX_NODES_AGGREGATED>, depositProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED>, accountProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED> ) { // Method implementation... } @method() public withdrawal( shPreimage: SHPreimage, sigOperator: Sig, prevTx: BridgeTransaction, // Previous bridge update transaction. aggregatorTx: AggregatorTransaction, // Root aggregator transaction. fundingPrevout: ByteString, withdrawals: FixedArray<WithdrawalData, typeof MAX_NODES_AGGREGATED>, accounts: FixedArray<AccountData, typeof MAX_NODES_AGGREGATED>, intermediateSumsArr: FixedArray<IntermediateValues, typeof MAX_NODES_AGGREGATED>, withdrawalProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED>, accountProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED> ) { // Method implementation... } }

The contract construction function has two parameters:

  • *operator: *The public key of the bridge operator who has the authority to update the bridge state.

  • expanderSPK *: The script public key (SPK) of the WithdrawalExpander* contract , used in the withdrawal process.

The deposit method is responsible for processing the aggregated deposit transaction and updating the account balance accordingly.

 @method() public deposit( shPreimage: SHPreimage, sigOperator: Sig, prevTx: BridgeTransaction, // Previous bridge update transaction. aggregatorTx: AggregatorTransaction, // Root aggregator transaction. fundingPrevout: ByteString, deposits: FixedArray<DepositData, typeof MAX_NODES_AGGREGATED>, accounts: FixedArray<AccountData, typeof MAX_NODES_AGGREGATED>, depositProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED>, accountProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED> ) { // Common validation steps... // (Same as in previous contracts: sighash preimage check, operator signature verification, prevouts verification) // Ensure this method is called from the first input. assert(shPreimage.inputNumber == toByteString('00000000')) // Verify that the second input unlocks the correct aggregator script. assert(prevTx.depositAggregatorSPK == aggregatorTx.outputContractSPK) // Process deposits and update accounts. let accountsRootNew: Sha256 = prevTx.accountsRoot let totalAmtDeposited = 0n for (let i = 0; i < MAX_NODES_AGGREGATED; i++) { const deposit = deposits[i] if (deposit.address != toByteString('')) { accountsRootNew = this.applyDeposit( deposits[i], depositProofs[i], aggregatorTx.hashData, accounts[i], accountProofs[i], accountsRootNew ) } totalAmtDeposited += deposit.amount } // Update the bridge state and outputs. // (Compute new state hash, construct contract output, enforce outputs) }

The steps performed by the deposit method include:

Process the deposit and update the account :

  • Iterate over the deposits and apply each deposit to the corresponding account using the applyDeposit method.

Update bridge status and outputs :

  • After a deposit is processed, a new account Merkle root is calculated.

  • Create a new state hash value, representing the updated bridge state.

  • Construct the contract output to add the total deposit amount to the bridge balance.

  • Ensure output conforms to expected format to maintain data integrity.

The withdraw method handles the aggregated withdrawal transaction, updates the account balance, and prepares the funds for distribution via the withdraw extender.

 @method() public withdrawal( shPreimage: SHPreimage, sigOperator: Sig, prevTx: BridgeTransaction, // Previous bridge update transaction. aggregatorTx: AggregatorTransaction, // Root aggregator transaction. fundingPrevout: ByteString, withdrawals: FixedArray<WithdrawalData, typeof MAX_NODES_AGGREGATED>, accounts: FixedArray<AccountData, typeof MAX_NODES_AGGREGATED>, intermediateSumsArr: FixedArray<IntermediateValues, typeof MAX_NODES_AGGREGATED>, withdrawalProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED>, accountProofs: FixedArray<MerkleProof, typeof MAX_NODES_AGGREGATED> ) { // Common validation steps... // (Same as in previous contracts: sighash preimage check, operator signature verification, prevouts verification) // Ensure this method is called from the first input. assert(shPreimage.inputNumber == toByteString('00000000')) // Verify that the second input unlocks the correct aggregator script. assert(prevTx.withdrawalAggregatorSPK == aggregatorTx.outputContractSPK) // Process withdrawals and update accounts. let accountsRootNew: Sha256 = prevTx.accountsRoot let totalAmtWithdrawn = 0n for (let i = 0; i < MAX_NODES_AGGREGATED; i++) { const withdrawal = withdrawals[i] if (withdrawal.address != toByteString('')) { accountsRootNew = this.applyWithdrawal( withdrawal, withdrawalProofs[i], intermediateSumsArr[i], aggregatorTx.hashData, accounts[i], accountProofs[i], accountsRootNew ) } totalAmtWithdrawn += withdrawal.amount } // Update the bridge state and outputs. // (Compute new state hash, construct contract output, create expander output, enforce outputs) }

The steps performed by the withdrawal method include:

Process withdrawal request and update account :

  • Iterate over the withdrawal requests and apply each withdrawal to the corresponding account using the applyDeposit method.

Update bridge status and outputs :

  • After processing a withdrawal, a new account Merkle root is calculated.

  • Create a new state hash value, representing the updated bridge state.

  • Construct the contract output to deduct the total withdrawal amount from the bridge balance.

  • Create an extension output for the withdrawal extender contract containing the total withdrawal amount.

  • Ensure output conforms to expected format to maintain data integrity.

The full source code is available on GitHub .

Withdrawal Extender Contract

WithdrawalExpander is the final component of our bridge system, responsible for distributing the aggregated withdrawal amounts back to individual users based on their withdrawal requests. It reverses the aggregation process performed by the Withdrawal Aggregator, expanding the aggregated withdrawal data back to individual users' payments.

 class WithdrawalExpander extends SmartContract { @prop() operator: PubKey constructor( operator: PubKey ) { super(...arguments) this.operator = operator } @method() public expand( shPreimage: SHPreimage, sigOperator: Sig, // Additional parameters... ) { // Expansion logic... } }

The core functionality of the withdrawal expander is encapsulated in the "expand" method. This method accepts the aggregated withdrawal data and recursively expands it into individual withdrawal transactions, paying the corresponding amount to the user.

Expanding to a leaf node : If the method expands to a leaf node (single withdrawal), it verifies the withdrawal data and constructs an output that pays directly to the user's address.

 if (isExpandingLeaves) { // If expanding to leaves, verify the withdrawal data. if (isExpandingPrevTxFirstOutput) { const hashWithdrawalData = WithdrawalAggregator.hashWithdrawalData(withdrawalData0) assert(hashWithdrawalData == prevAggregationData.prevH0) hashOutputs = sha256( WithdrawalExpander.getP2WPKHOut( GeneralUtils.padAmt(withdrawalData0.amount), withdrawalData0.address ) ) } else { const hashWithdrawalData = WithdrawalAggregator.hashWithdrawalData(withdrawalData1) assert(hashWithdrawalData == prevAggregationData.prevH1) hashOutputs = sha256( WithdrawalExpander.getP2WPKHOut( GeneralUtils.padAmt(withdrawalData1.amount), withdrawalData1.address ) ) } }

Further expansion : If the method has not yet reached the leaf node layer, it will continue to expand, split the aggregated data into two branches, and create outputs for consumption by further expanded transactions.

 else { // Verify current aggregation data matches previous aggregation data. const hashCurrentAggregationData = WithdrawalAggregator.hashAggregationData(currentAggregationData) if (isPrevTxBridge) { assert(hashCurrentAggregationData == prevTxBridge.expanderRoot) } else if (isExpandingPrevTxFirstOutput) { assert(hashCurrentAggregationData == prevAggregationData.prevH0) } else { assert(hashCurrentAggregationData == prevAggregationData.prevH1) } // Prepare outputs for the next level of expansion. let outAmt0 = 0n let outAmt1 = 0n if (isLastAggregationLevel) { const hashWithdrawalData0 = WithdrawalAggregator.hashWithdrawalData(withdrawalData0) const hashWithdrawalData1 = WithdrawalAggregator.hashWithdrawalData(withdrawalData1) assert(hashWithdrawalData0 == currentAggregationData.prevH0) assert(hashWithdrawalData1 == currentAggregationData.prevH1) outAmt0 = withdrawalData0.amount outAmt1 = withdrawalData1.amount } else { const hashNextAggregationData0 = WithdrawalAggregator.hashAggregationData(nextAggregationData0) const hashNextAggregationData1 = WithdrawalAggregator.hashAggregationData(nextAggregationData1) assert(hashNextAggregationData0 == currentAggregationData.prevH0) assert(hashNextAggregationData1 == currentAggregationData.prevH1) outAmt0 = nextAggregationData0.sumAmt outAmt1 = nextAggregationData1.sumAmt } // Construct outputs for further expansion. let expanderSPK = prevTxExpander.contractSPK if (isPrevTxBridge) { expanderSPK = prevTxBridge.expanderSPK } hashOutputs = sha256( GeneralUtils.getContractOutput(outAmt0, expanderSPK) + GeneralUtils.getContractOutput(outAmt1, expanderSPK) + GeneralUtils.getStateOutput(hashCurrentAggregationData) ) }

in conclusion

In this proof-of-concept implementation, we developed a bridge contract based on Bitcoin with OP_CAT support using the sCrypt embedded domain -specific language (DSL). The bridge leverages recursive contracts and Merkle trees to efficiently batch deposit and withdrawal requests while maintaining the integrity and security of user accounts. By designing and implementing four smart contracts, DepositAggregator, WithdrawalAggregator , Bridge , and WithdrawalExpander , we provide a way to manage stateful interactions on Bitcoin, facilitating interoperability with second-layer networks like Starknet. This work provides a technical foundation for building production-grade bridges, potentially enhancing scalability and functionality in the Bitcoin ecosystem.

All code implementations and end-to-end tests are available on GitHub .

Mirror
Disclaimer: The content above is only the author's opinion which does not represent any position of Followin, and is not intended as, and shall not be understood or construed as, investment advice from Followin.
Like
Add to Favorites
Comments
Followin logo