Skip to content

Commit

Permalink
Expose revealDepositWithExtraData in the Bridge contract (#760)
Browse files Browse the repository at this point in the history
Refs: #749

Here we expose the `revealDepositWithExtraData` function in the `Bridge`
contract. This function allows revealing a deposit whose funding
transaction embeds arbitrary 32-byte extra data.

The new function opens a variety of use cases. An EOA depositor can
craft a Bitcoin deposit transaction that allows a third-party smart
contract to manage the deposit and provide additional services on top.
In this case, 32-byte extra data can be used to securely attribute the
deposit to the original EOA depositor.

### The `revealDepositWithExtraData` function

The `revealDepositWithExtraData` was added next to the existing
`revealDeposit` function to provide backward compatibility of the
`Bridge` contract API. The `Deposit` library that does the heavy lifting
was refactored to handle both regular deposits without extra data and
the new deposits with extra data. The common logic was extracted to the
`_revealDeposit` internal function to avoid duplication.

Worth noting, the extra data passed in the new flow has to be
externalized so tBTC wallets can reconstruct the deposit script and
sweep those deposits, just as the regular ones. To do so, we decided to
store the extra data in the `Bridge` storage (as part of the
`DepositRequest` structure). This approach is not ideal but has some
advantages:
- Negligible gas overhead for regular deposits without extra data (~2000
of gas)
- Easier integration as extra data can be easily fetched from storage by
tBTC wallets and third-party smart contracts
- Making the `Bridge` aware of the extra data may open some use cases in
the future

Alternative approaches rely on emitting the extra data using contract
events. However:
- Extending the existing `DepositReveal` event is not backward
compatible. The event signature would change and clients trying to fetch
past events that don't contain the extra data field would fail. This is
especially problematic for tBTC wallets, SPV maintainers, and block
explorers
- Using a new event just for emitting the extra data is clunky and makes
the integration experience significantly worse

### Changes in the `WalletProposalValidator` contract

The `WalletProposalValidator` contract is used by tBTC off-chain wallet
clients to validate incoming deposit sweep proposals. This contract
needs to reconstruct deposit scripts so must be aware of the new
alternative format with 32-byte extra data. As part of this pull
request, we are adjusting this contract to properly validate deposit
sweep proposals that include deposits with extra data.

### Impact on gas costs and contract size

Before the presented change, the gas costs of functions and size of
affected contract/libraries was as follows:

```
·------------------------------|---------------------------|--------------|-----------------------------·
|     Solc version: 0.8.17     ·  Optimizer enabled: true  ·  Runs: 1000  ·  Block limit: 30000000 gas  │
·······························|···························|··············|······························
|  Methods                                                                                              │
·············|·················|·············|·············|··············|···············|··············
|  Contract  ·  Method         ·  Min        ·  Max        ·  Avg         ·  # calls      ·  eur (avg)  │
·············|·················|·············|·············|··············|···············|··············
|  Bridge    ·  revealDeposit  ·  82767      ·  105463     ·  102057      ·  170          ·  -          │
·············|·················|·············|·············|··············|···············|··············
```
```
·-------------------------------|-------------|---------------·
|  Contract Name                ·  Size (KB)  ·  Change (KB)  │
································|·············|················
|  Bridge                       ·     21.264  ·               │
································|·············|················
|  Deposit                      ·      6.327  ·               │
································|·············|················
|  WalletProposalValidator      ·     11.266  ·               │
································|·············|················
```

After introducing `revealDepositWithExtraData`, those are as follows:

```
·------------------------------·············|---------------------------|--------------|-----------------------------·
|           Solc version: 0.8.17            ·  Optimizer enabled: true  ·  Runs: 1000  ·  Block limit: 30000000 gas  │
············································|···························|··············|······························
|  Methods                                                                                                           │
·············|······························|·············|·············|··············|···············|··············
|  Contract  ·  Method                      ·  Min        ·  Max        ·  Avg         ·  # calls      ·  eur (avg)  │
·············|······························|·············|·············|··············|···············|··············
|  Bridge    ·  revealDeposit               ·  85270      ·  107966     ·  104562      ·  170          ·  -          │
·············|······························|·············|·············|··············|···············|··············
|  Bridge    ·  revealDepositWithExtraData  ·  124176     ·  126773     ·  125872      ·  12           ·  -          │
·············|······························|·············|·············|··············|···············|··············
```
```
·-------------------------------|-------------|---------------·
|  Contract Name                ·  Size (KB)  ·  Change (KB)  │
································|·············|················
|  Bridge                       ·     21.563  ·               │
································|·············|················
|  Deposit                      ·      6.894  ·               │
································|·············|················
|  WalletProposalValidator      ·     11.520  ·               │
································|·············|················
```

### Next steps

This change is the most important one towards full support of deposits
with extra data. However, comprehensive support requires some additional
steps:
- Add support in the tBTC off-chain client
- Add support in the tBTC SDK
  • Loading branch information
nkuba committed Jan 9, 2024
2 parents 6709d4e + 165c474 commit 44d9fc8
Show file tree
Hide file tree
Showing 6 changed files with 1,285 additions and 95 deletions.
29 changes: 29 additions & 0 deletions solidity/contracts/bridge/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,35 @@ contract Bridge is
self.revealDeposit(fundingTx, reveal);
}

/// @notice Sibling of the `revealDeposit` function. This function allows
/// to reveal a P2(W)SH Bitcoin deposit with 32-byte extra data
/// embedded in the deposit script. The extra data allows to
/// attach additional context to the deposit. For example,
/// it allows a third-party smart contract to reveal the
/// deposit on behalf of the original depositor and provide
/// additional services once the deposit is handled. In this
/// case, the address of the original depositor can be encoded
/// as extra data.
/// @param fundingTx Bitcoin funding transaction data, see `BitcoinTx.Info`.
/// @param reveal Deposit reveal data, see `RevealInfo struct.
/// @param extraData 32-byte deposit extra data.
/// @dev Requirements:
/// - All requirements from `revealDeposit` function must be met,
/// - `extraData` must not be bytes32(0),
/// - `extraData` must be the actual extra data used in the P2(W)SH
/// BTC deposit transaction.
///
/// If any of these requirements is not met, the wallet _must_ refuse
/// to sweep the deposit and the depositor has to wait until the
/// deposit script unlocks to receive their BTC back.
function revealDepositWithExtraData(
BitcoinTx.Info calldata fundingTx,
Deposit.DepositRevealInfo calldata reveal,
bytes32 extraData
) external {
self.revealDepositWithExtraData(fundingTx, reveal, extraData);
}

/// @notice Used by the wallet to prove the BTC deposit sweep transaction
/// and to update Bank balances accordingly. Sweep is only accepted
/// if it satisfies SPV proof.
Expand Down
183 changes: 156 additions & 27 deletions solidity/contracts/bridge/Deposit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ import "./Wallets.sol";
/// Since each depositor has their own Ethereum address and their own
/// blinding factor, each depositor’s script is unique, and the hash
/// of each depositor’s script is unique.
///
/// This library also supports another variant of the deposit script
/// allowing to embed 32-byte extra data. The extra data allows to attach
/// additional context to the deposit. The script with 32-byte extra data
/// looks like this:
///
/// ```
/// <depositorAddress> DROP
/// <extraData> DROP
/// <blindingFactor> DROP
/// DUP HASH160 <walletPubKeyHash> EQUAL
/// IF
/// CHECKSIG
/// ELSE
/// DUP HASH160 <refundPubkeyHash> EQUALVERIFY
/// <refundLocktime> CHECKLOCKTIMEVERIFY DROP
/// CHECKSIG
/// ENDIF
/// ```
library Deposit {
using BTCUtils for bytes;
using BytesLib for bytes;
Expand Down Expand Up @@ -96,6 +115,8 @@ library Deposit {
// the time when the sweep proof was delivered to the Ethereum chain.
// XXX: Unsigned 32-bit int unix seconds, will break February 7th 2106.
uint32 sweptAt;
// The 32-byte deposit extra data. Optional, can be bytes32(0).
bytes32 extraData;
// This struct doesn't contain `__gap` property as the structure is stored
// in a mapping, mappings store values in different slots and they are
// not contiguous with other values.
Expand Down Expand Up @@ -152,6 +173,28 @@ library Deposit {
BitcoinTx.Info calldata fundingTx,
DepositRevealInfo calldata reveal
) external {
_revealDeposit(self, fundingTx, reveal, bytes32(0));
}

/// @notice Internal function encapsulating the core logic of the deposit
/// reveal process. Handles both regular deposits without extra data
/// as well as deposits with 32-byte extra data embedded in the
/// deposit script. The behavior is controlled by the `extraData`
/// parameter. If `extraData` is bytes32(0), the function triggers
/// the flow for regular deposits. If `extraData` is not bytes32(0),
/// the function triggers the flow for deposits with 32-byte
/// extra data.
/// @param fundingTx Bitcoin funding transaction data, see `BitcoinTx.Info`.
/// @param reveal Deposit reveal data, see `RevealInfo struct.
/// @param extraData 32-byte deposit extra data. Can be bytes32(0).
/// @dev Requirements are described in the docstrings of `revealDeposit` and
/// `revealDepositWithExtraData` external functions.
function _revealDeposit(
BridgeState.Storage storage self,
BitcoinTx.Info calldata fundingTx,
DepositRevealInfo calldata reveal,
bytes32 extraData
) internal {
require(
self.registeredWallets[reveal.walletPubKeyHash].state ==
Wallets.WalletState.Live,
Expand All @@ -167,33 +210,70 @@ library Deposit {
validateDepositRefundLocktime(self, reveal.refundLocktime);
}

bytes memory expectedScript = abi.encodePacked(
hex"14", // Byte length of depositor Ethereum address.
msg.sender,
hex"75", // OP_DROP
hex"08", // Byte length of blinding factor value.
reveal.blindingFactor,
hex"75", // OP_DROP
hex"76", // OP_DUP
hex"a9", // OP_HASH160
hex"14", // Byte length of a compressed Bitcoin public key hash.
reveal.walletPubKeyHash,
hex"87", // OP_EQUAL
hex"63", // OP_IF
hex"ac", // OP_CHECKSIG
hex"67", // OP_ELSE
hex"76", // OP_DUP
hex"a9", // OP_HASH160
hex"14", // Byte length of a compressed Bitcoin public key hash.
reveal.refundPubKeyHash,
hex"88", // OP_EQUALVERIFY
hex"04", // Byte length of refund locktime value.
reveal.refundLocktime,
hex"b1", // OP_CHECKLOCKTIMEVERIFY
hex"75", // OP_DROP
hex"ac", // OP_CHECKSIG
hex"68" // OP_ENDIF
);
bytes memory expectedScript;

if (extraData == bytes32(0)) {
// Regular deposit without 32-byte extra data.
expectedScript = abi.encodePacked(
hex"14", // Byte length of depositor Ethereum address.
msg.sender,
hex"75", // OP_DROP
hex"08", // Byte length of blinding factor value.
reveal.blindingFactor,
hex"75", // OP_DROP
hex"76", // OP_DUP
hex"a9", // OP_HASH160
hex"14", // Byte length of a compressed Bitcoin public key hash.
reveal.walletPubKeyHash,
hex"87", // OP_EQUAL
hex"63", // OP_IF
hex"ac", // OP_CHECKSIG
hex"67", // OP_ELSE
hex"76", // OP_DUP
hex"a9", // OP_HASH160
hex"14", // Byte length of a compressed Bitcoin public key hash.
reveal.refundPubKeyHash,
hex"88", // OP_EQUALVERIFY
hex"04", // Byte length of refund locktime value.
reveal.refundLocktime,
hex"b1", // OP_CHECKLOCKTIMEVERIFY
hex"75", // OP_DROP
hex"ac", // OP_CHECKSIG
hex"68" // OP_ENDIF
);
} else {
// Deposit with 32-byte extra data.
expectedScript = abi.encodePacked(
hex"14", // Byte length of depositor Ethereum address.
msg.sender,
hex"75", // OP_DROP
hex"20", // Byte length of extra data.
extraData,
hex"75", // OP_DROP
hex"08", // Byte length of blinding factor value.
reveal.blindingFactor,
hex"75", // OP_DROP
hex"76", // OP_DUP
hex"a9", // OP_HASH160
hex"14", // Byte length of a compressed Bitcoin public key hash.
reveal.walletPubKeyHash,
hex"87", // OP_EQUAL
hex"63", // OP_IF
hex"ac", // OP_CHECKSIG
hex"67", // OP_ELSE
hex"76", // OP_DUP
hex"a9", // OP_HASH160
hex"14", // Byte length of a compressed Bitcoin public key hash.
reveal.refundPubKeyHash,
hex"88", // OP_EQUALVERIFY
hex"04", // Byte length of refund locktime value.
reveal.refundLocktime,
hex"b1", // OP_CHECKLOCKTIMEVERIFY
hex"75", // OP_DROP
hex"ac", // OP_CHECKSIG
hex"68" // OP_ENDIF
);
}

bytes memory fundingOutput = fundingTx
.outputVector
Expand Down Expand Up @@ -258,6 +338,21 @@ library Deposit {
deposit.treasuryFee = self.depositTreasuryFeeDivisor > 0
? fundingOutputAmount / self.depositTreasuryFeeDivisor
: 0;
deposit.extraData = extraData;

_emitDepositRevealedEvent(fundingTxHash, fundingOutputAmount, reveal);
}

/// @notice Emits the `DepositRevealed` event.
/// @param fundingTxHash The funding transaction hash.
/// @param fundingOutputAmount The funding output amount in satoshi.
/// @param reveal Deposit reveal data, see `RevealInfo struct.
/// @dev This function is extracted to overcome the stack too deep error.
function _emitDepositRevealedEvent(
bytes32 fundingTxHash,
uint64 fundingOutputAmount,
DepositRevealInfo calldata reveal
) internal {
// slither-disable-next-line reentrancy-events
emit DepositRevealed(
fundingTxHash,
Expand All @@ -272,6 +367,40 @@ library Deposit {
);
}

/// @notice Sibling of the `revealDeposit` function. This function allows
/// to reveal a P2(W)SH Bitcoin deposit with 32-byte extra data
/// embedded in the deposit script. The extra data allows to
/// attach additional context to the deposit. For example,
/// it allows a third-party smart contract to reveal the
/// deposit on behalf of the original depositor and provide
/// additional services once the deposit is handled. In this
/// case, the address of the original depositor can be encoded
/// as extra data.
/// @param fundingTx Bitcoin funding transaction data, see `BitcoinTx.Info`.
/// @param reveal Deposit reveal data, see `RevealInfo struct.
/// @param extraData 32-byte deposit extra data.
/// @dev Requirements:
/// - All requirements from `revealDeposit` function must be met,
/// - `extraData` must not be bytes32(0),
/// - `extraData` must be the actual extra data used in the P2(W)SH
/// BTC deposit transaction.
///
/// If any of these requirements is not met, the wallet _must_ refuse
/// to sweep the deposit and the depositor has to wait until the
/// deposit script unlocks to receive their BTC back.
function revealDepositWithExtraData(
BridgeState.Storage storage self,
BitcoinTx.Info calldata fundingTx,
DepositRevealInfo calldata reveal,
bytes32 extraData
) external {
// Strong requirement in order to differentiate from the regular
// reveal flow and reduce potential attack surface.
require(extraData != bytes32(0), "Extra data must not be empty");

_revealDeposit(self, fundingTx, reveal, extraData);
}

/// @notice Validates the deposit refund locktime. The validation passes
/// successfully only if the deposit reveal is done respectively
/// earlier than the moment when the deposit refund locktime is
Expand Down
Loading

0 comments on commit 44d9fc8

Please sign in to comment.