Skip to content

Commit

Permalink
Add witness verification on Bridge contract (#352)
Browse files Browse the repository at this point in the history
* Add witness verification on Bridge

* Use segwit txn as test data in Bridge contract test

* Change deposit parameters to a struct in Bridge

* Change L1BlockHashList address in Bridge

* Remove block header from Bridge contract

* Fix witness utils bugs

* Witness inclusion in Bridge

* Further tests for Bridge

* Rename signature count in Bridge
  • Loading branch information
okkothejawa authored Apr 19, 2024
1 parent 2eeb369 commit 8b6fd82
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 96 deletions.
134 changes: 134 additions & 0 deletions crates/evm/src/evm/system_contracts/lib/WitnessUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity ^0.8.4;

/** @title WitnessUtils */
/** @author Citrea, modified from Bitcoin-SPV */

import {BytesLib} from "bitcoin-spv/solidity/contracts/BytesLib.sol";
import {SafeMath} from "bitcoin-spv/solidity/contracts/SafeMath.sol";
import "bitcoin-spv/solidity/contracts/BTCUtils.sol";

library WitnessUtils {
using BytesLib for bytes;
using BTCUtils for bytes;
using SafeMath for uint256;

function calculateWtxId(
bytes4 version,
bytes2 flag,
bytes calldata vin,
bytes calldata vout,
bytes calldata witness,
bytes4 locktime
) internal view returns (bytes32) {
return abi.encodePacked(version, flag, vin, vout, witness, locktime).hash256View();
}

/// @notice Checks that the witness passed up is properly formatted
/// @param _witness Raw bytes length-prefixed witness vector
/// @param _nWits The number of witnesses expected, sourced from number of inputs
/// @return True if it represents a validly formatted witness
function validateWitness(bytes memory _witness, uint256 _nWits) internal pure returns (bool) {
// Not valid if it says there are no witnesses
if (_nWits == 0) {
return false;
}

uint256 _offset = 0;

for (uint256 i = 0; i < _nWits; i++) {
// If we're at the end, but still expect more
if (_offset >= _witness.length) {
return false;
}

// Grab the next input and determine its length.
uint256 _nextLen = determineWitnessLengthAt(_witness, _offset);
if (_nextLen == BTCUtils.ERR_BAD_ARG) {
return false;
}

// Increase the offset by that much
_offset += _nextLen;
}

// Returns false if we're not exactly at the end
return _offset == _witness.length;
}

/// @notice Determines the length of a witness,
/// starting at the specified position
/// @param _witness The byte array containing the witness vector
/// @param _at The position of the witness in the array
/// @return The length of the witness in bytes
function determineWitnessLengthAt(bytes memory _witness, uint256 _at) internal pure returns (uint256) {
uint256 _varIntDataLen;
uint256 _nWits;

(_varIntDataLen, _nWits) = BTCUtils.parseVarIntAt(_witness, _at);
if (_varIntDataLen == BTCUtils.ERR_BAD_ARG) {
return BTCUtils.ERR_BAD_ARG;
}

uint256 _itemLen;
uint256 _offset = 1 + _varIntDataLen;

for (uint256 i = 0; i < _nWits; i++) {
(_varIntDataLen, _itemLen) = BTCUtils.parseVarIntAt(_witness, _offset);
if (_itemLen == BTCUtils.ERR_BAD_ARG) {
return BTCUtils.ERR_BAD_ARG;
}

_offset += 1 + _varIntDataLen + _itemLen;
}

return _offset;
}

/// @notice Extracts the witness at a given index in the witnesses vector
/// @dev Iterates over the witness. If you need to extract multiple, write a custom function
/// @param _witness The witness vector to extract from
/// @param _index The 0-indexed location of the witness to extract
/// @return The specified witness
function extractWitnessAtIndex(bytes memory _witness, uint256 _index) internal pure returns (bytes memory) {
uint256 _len = 0;
uint256 _offset = 0;

for (uint256 _i = 0; _i < _index; _i ++) {
_len = determineWitnessLengthAt(_witness, _offset);
require(_len != BTCUtils.ERR_BAD_ARG, "Bad VarInt in witness");
_offset += _len;
}

_len = determineWitnessLengthAt(_witness, _offset);
require(_len != BTCUtils.ERR_BAD_ARG, "Bad VarInt in witness");
return _witness.slice(_offset, _len);
}

/// @notice Extracts the item at a given index in the witness
/// @dev Iterates over the items. If you need to extract multiple, write a custom function
/// @param _witness The witness to extract from
/// @param _index The 0-indexed location of the item to extract
/// @return The specified item
function extractItemFromWitness(bytes memory _witness, uint256 _index) internal pure returns (bytes memory) {
uint256 _varIntDataLen;
uint256 _nItems;

(_varIntDataLen, _nItems) = BTCUtils.parseVarInt(_witness);
require(_varIntDataLen != BTCUtils.ERR_BAD_ARG, "Read overrun during VarInt parsing");
require(_index < _nItems, "Vin read overrun");

uint256 _itemLen = 0;
uint256 _offset = 1 + _varIntDataLen;

for (uint256 i = 0; i < _index; i++) {
(_varIntDataLen, _itemLen) = BTCUtils.parseVarIntAt(_witness, _offset);
require(_itemLen != BTCUtils.ERR_BAD_ARG, "Bad VarInt in item");
_offset += 1 + _varIntDataLen + _itemLen;
}

(_varIntDataLen, _itemLen) = BTCUtils.parseVarIntAt(_witness, _offset);
require(_itemLen != BTCUtils.ERR_BAD_ARG, "Bad VarInt in item");
return _witness.slice(_offset, _itemLen + _varIntDataLen + 1);
}
}
126 changes: 73 additions & 53 deletions crates/evm/src/evm/system_contracts/src/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ pragma solidity ^0.8.13;

import "bitcoin-spv/solidity/contracts/ValidateSPV.sol";
import "bitcoin-spv/solidity/contracts/BTCUtils.sol";
import "../lib/WitnessUtils.sol";
import "../lib/Ownable.sol";


import "./MerkleTree.sol";
import "./L1BlockHashList.sol";
Expand All @@ -11,19 +14,34 @@ import "./L1BlockHashList.sol";
/// @author Citrea

contract Bridge is MerkleTree, Ownable {
// TODO: Update this to be the actual address of the L1BlockHashList contract
L1BlockHashList public constant BLOCK_HASH_LIST = L1BlockHashList(address(0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD));
using BTCUtils for bytes;
using BytesLib for bytes;

struct DepositParams {
bytes4 version;
bytes2 flag;
bytes vin;
bytes vout;
bytes witness;
bytes4 locktime;
bytes intermediate_nodes;
uint256 block_height;
uint256 index;
}

L1BlockHashList public constant BLOCK_HASH_LIST = L1BlockHashList(address(0x3100000000000000000000000000000000000001));

bytes public DEPOSIT_TXOUT_0 = hex"c2ddf50500000000225120fc6eb6fa4fd4ed1e8519a7edfa171eddcedfbd0e0be49b5e531ef36e7e66eb05";
bytes public depositScript;
bytes public scriptSuffix;
uint256 public constant DEPOSIT_AMOUNT = 1 ether;
address public operator;
mapping(bytes32 => bool) public blockHashes;
mapping(bytes32 => bool) public spentTxIds;
mapping(bytes32 => bool) public spentWtxIds;
uint256 public requiredSigsCount;

event Deposit(bytes32 txId, uint256 timestamp);
event Deposit(bytes32 wtxId, uint256 timestamp);
event Withdrawal(bytes32 bitcoin_address, uint32 indexed leafIndex, uint256 timestamp);
event DepositTxOutUpdate(bytes oldExpectedVout0, bytes newExpectedVout0);
event BlockHashAdded(bytes32 block_hash);
event DepositScriptUpdate(bytes depositScript, bytes scriptSuffix, uint256 requiredSigsCount);
event OperatorUpdated(address oldOperator, address newOperator);

modifier onlyOperator() {
Expand All @@ -33,55 +51,58 @@ contract Bridge is MerkleTree, Ownable {

constructor(uint32 _levels) MerkleTree(_levels) {}

/// @notice Sets the expected first transaction output of a deposit transaction on Bitcoin, which signifies the multisig address on Bitcoin
/// @dev TxOut0 is derived from the multisig on Bitcoin so it stays constant as long as the multisig doesn't change
/// @param _depositTxOut0 The new expected first transaction output of a deposit transaction on Bitcoin
function setDepositTxOut0(bytes calldata _depositTxOut0) external onlyOwner {
bytes memory oldDepositTxOut0 = DEPOSIT_TXOUT_0;
DEPOSIT_TXOUT_0 = _depositTxOut0;
emit DepositTxOutUpdate(oldDepositTxOut0, DEPOSIT_TXOUT_0);
/// @notice Sets the expected deposit script of the deposit transaction on Bitcoin, contained in the witness
/// @dev Deposit script contains a fixed script that checks signatures of verifiers and pushes EVM address of the receiver
/// @param _depositScript The new deposit script
/// @param _scriptSuffix The part of the deposit script that succeeds the receiver address
/// @param _requiredSigsCount The number of signatures that are needed for deposit transaction
function setDepositScript(bytes calldata _depositScript, bytes calldata _scriptSuffix, uint256 _requiredSigsCount) external onlyOwner {
require(_requiredSigsCount != 0, "Verifier count cannot be 0");
require(_depositScript.length != 0, "Deposit script cannot be empty");

depositScript = _depositScript;
scriptSuffix = _scriptSuffix;
requiredSigsCount = _requiredSigsCount;

emit DepositScriptUpdate(_depositScript, _scriptSuffix, _requiredSigsCount);
}

/// @notice Checks if funds 1 BTC is sent to the bridge multisig on Bitcoin, and if so, sends 1 cBTC to the receiver
/// @param version The version of the Bitcoin transaction
/// @param vin The transaction inputs
/// @param vout The transaction outputs
/// @param locktime Locktime of the Bitcoin transaction
/// @param intermediate_nodes -
/// @param block_header Block header of the Bitcoin block that the deposit transaction is in
/// @param index Index of the transaction in the block
/// @param p The deposit parameters that contains the info of the deposit transaction on Bitcoin
function deposit(
bytes4 version,
bytes calldata vin,
bytes calldata vout,
bytes4 locktime,
bytes calldata intermediate_nodes,
bytes calldata block_header,
uint256 block_height,
uint index
DepositParams calldata p
) external onlyOperator {
bytes32 block_hash = BTCUtils.hash256(block_header);
require(BLOCK_HASH_LIST.getBlockHash(block_height) == block_hash, "Incorrect block hash");
require(requiredSigsCount != 0, "Contract is not initialized");

bytes32 wtxId = WitnessUtils.calculateWtxId(p.version, p.flag, p.vin, p.vout, p.witness, p.locktime);
require(!spentWtxIds[wtxId], "wtxId already spent");
spentWtxIds[wtxId] = true;

require(BTCUtils.validateVin(p.vin), "Vin is not properly formatted");
require(BTCUtils.validateVout(p.vout), "Vout is not properly formatted");

(, uint256 _nIns) = BTCUtils.parseVarInt(p.vin);
require(_nIns == 1, "Only one input allowed");
// Number of inputs == number of witnesses
require(WitnessUtils.validateWitness(p.witness, _nIns), "Witness is not properly formatted");

bytes32 extracted_merkle_root = BTCUtils.extractMerkleRootLE(block_header);
bytes32 txId = ValidateSPV.calculateTxId(version, vin, vout, locktime);
require(!spentTxIds[txId], "txId already spent");
spentTxIds[txId] = true;
require(BLOCK_HASH_LIST.verifyInclusion(p.block_height, wtxId, p.intermediate_nodes, p.index), "Transaction is not in block");

bool result = ValidateSPV.prove(txId, extracted_merkle_root, intermediate_nodes, index);
require(result, "SPV Verification failed.");
bytes memory witness0 = WitnessUtils.extractWitnessAtIndex(p.witness, 0);
(, uint256 _nItems) = BTCUtils.parseVarInt(witness0);
require(_nItems == requiredSigsCount + 2, "Invalid witness items"); // verifier sigs + deposit script + witness script

// First output is always the bridge utxo, so it should be constant
bytes memory output1 = BTCUtils.extractOutputAtIndex(vout, 0);
require(isBytesEqual(output1, DEPOSIT_TXOUT_0), "Incorrect Deposit TxOut");
bytes memory script = WitnessUtils.extractItemFromWitness(witness0, requiredSigsCount);
uint256 _len = depositScript.length;
bytes memory _depositScript = script.slice(0, _len);
require(isBytesEqual(_depositScript, depositScript), "Invalid deposit script");
bytes memory _suffix = script.slice(_len + 20, script.length - (_len + 20)); // 20 bytes for address
require(isBytesEqual(_suffix, scriptSuffix), "Invalid script suffix");

// Second output is the receiver of tokens
bytes memory output2 = BTCUtils.extractOutputAtIndex(vout, 1);
bytes memory output2_ext = BTCUtils.extractOpReturnData(output2);
address receiver = address(bytes20(output2_ext));
require(receiver != address(0), "Invalid receiver address");
address receiver = extractRecipientAddress(script);

emit Deposit(wtxId, block.timestamp);

emit Deposit(txId, block.timestamp);
(bool success, ) = receiver.call{value: DEPOSIT_AMOUNT}("");
require(success, "Transfer failed");
}
Expand All @@ -104,13 +125,6 @@ contract Bridge is MerkleTree, Ownable {
emit Withdrawal(bitcoin_addresses[i], nextIndex, block.timestamp);
}
}

/// @notice Adds a block hash to the list of block hashes
/// @param block_hash The block hash to be added
function addBlockHash(bytes32 block_hash) external onlyOwner {
blockHashes[block_hash] = true;
emit BlockHashAdded(block_hash);
}

/// @notice Sets the operator address that can process user deposits
/// @param _operator Address of the privileged operator
Expand All @@ -136,4 +150,10 @@ contract Bridge is MerkleTree, Ownable {
}
result = true;
}

function extractRecipientAddress(bytes memory _script) internal view returns (address) {
uint256 offset = depositScript.length;
bytes20 _addr = bytes20(_script.slice(offset, 20));
return address(uint160(_addr));
}
}
Loading

0 comments on commit 8b6fd82

Please sign in to comment.