Skip to content

Commit

Permalink
Sweep deposits with extra data
Browse files Browse the repository at this point in the history
The `Bridge` contract supports a new shape of the deposit script that allows
embedding arbitrary 32-byte extra data. Here we introduce necessary adjustments
in the wallet client to make it able to sweep those deposits.
  • Loading branch information
lukasz-zimnoch committed Jan 22, 2024
1 parent 748468a commit 639c5be
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 38 deletions.
6 changes: 6 additions & 0 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1111,13 +1111,19 @@ func (tc *TbtcChain) GetDepositRequest(
vault = &v
}

var extraData *[32]byte
if depositRequest.ExtraData != [32]byte{} {
extraData = &depositRequest.ExtraData
}

return &tbtc.DepositChainRequest{
Depositor: chain.Address(depositRequest.Depositor.Hex()),
Amount: depositRequest.Amount,
RevealedAt: time.Unix(int64(depositRequest.RevealedAt), 0),
Vault: vault,
TreasuryFee: depositRequest.TreasuryFee,
SweptAt: time.Unix(int64(depositRequest.SweptAt), 0),
ExtraData: extraData,
}, true, nil
}

Expand Down
4 changes: 3 additions & 1 deletion pkg/tbtc/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ type DepositRevealedEvent struct {
BlockNumber uint64
}

func (dre *DepositRevealedEvent) unpack() *Deposit {
func (dre *DepositRevealedEvent) unpack(extraData *[32]byte) *Deposit {
return &Deposit{
Utxo: &bitcoin.UnspentTransactionOutput{
Outpoint: &bitcoin.TransactionOutpoint{
Expand All @@ -276,6 +276,7 @@ func (dre *DepositRevealedEvent) unpack() *Deposit {
RefundPublicKeyHash: dre.RefundPublicKeyHash,
RefundLocktime: dre.RefundLocktime,
Vault: dre.Vault,
ExtraData: extraData,
}
}

Expand Down Expand Up @@ -303,6 +304,7 @@ type DepositChainRequest struct {
Vault *chain.Address
TreasuryFee uint64
SweptAt time.Time
ExtraData *[32]byte
}

// WalletChainData represents wallet data stored on-chain.
Expand Down
51 changes: 44 additions & 7 deletions pkg/tbtc/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ type localChain struct {
heartbeatProposalValidationsMutex sync.Mutex
heartbeatProposalValidations map[[16]byte]bool

depositRequestsMutex sync.Mutex
depositRequests map[[32]byte]*DepositChainRequest

blockCounter chain.BlockCounter
operatorPrivateKey *operator.PrivateKey
}
Expand Down Expand Up @@ -582,13 +585,6 @@ func (lc *localChain) GetPendingRedemptionRequest(
return request, true, nil
}

func (lc *localChain) GetDepositRequest(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
) (*DepositChainRequest, bool, error) {
panic("not supported")
}

func (lc *localChain) setPendingRedemptionRequest(
walletPublicKeyHash [20]byte,
request *RedemptionRequest,
Expand All @@ -611,6 +607,46 @@ func buildRedemptionRequestKey(
return sha256.Sum256(append(walletPublicKeyHash[:], redeemerOutputScript...))
}

func (lc *localChain) GetDepositRequest(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
) (*DepositChainRequest, bool, error) {
lc.depositRequestsMutex.Lock()
defer lc.depositRequestsMutex.Unlock()

requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex)

request, ok := lc.depositRequests[requestKey]
if !ok {
return nil, false, nil
}

return request, true, nil
}

func (lc *localChain) setDepositRequest(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
request *DepositChainRequest,
) {
lc.depositRequestsMutex.Lock()
defer lc.depositRequestsMutex.Unlock()

requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex)

lc.depositRequests[requestKey] = request
}

func buildDepositRequestKey(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
) [32]byte {
buffer := make([]byte, 4)
binary.BigEndian.PutUint32(buffer[:], fundingOutputIndex)

return sha256.Sum256(append(fundingTxHash[:], buffer...))
}

func (lc *localChain) GetWallet(walletPublicKeyHash [20]byte) (
*WalletChainData,
error,
Expand Down Expand Up @@ -891,6 +927,7 @@ func ConnectWithKey(
pendingRedemptionRequests: make(map[[32]byte]*RedemptionRequest),
redemptionProposalValidations: make(map[[32]byte]bool),
heartbeatProposalValidations: make(map[[16]byte]bool),
depositRequests: make(map[[32]byte]*DepositChainRequest),
blockCounter: blockCounter,
operatorPrivateKey: operatorPrivateKey,
}
Expand Down
45 changes: 35 additions & 10 deletions pkg/tbtc/deposit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,25 @@ package tbtc
import (
"encoding/hex"
"fmt"
"strings"

"github.com/keep-network/keep-core/pkg/bitcoin"
"github.com/keep-network/keep-core/pkg/chain"
"strings"
)

// depositScriptFormat is the format of the deposit P2(W)SH Bitcoin script
// The specific placeholders are: depositor, blindingFactor, walletPublicKeyHash,
// refundPublicKeyHash and refundLocktime. For reference see:
// https://github.com/keep-network/tbtc-v2/blob/83310bdc9ed934e286bc9ea5091cc16979950134/solidity/contracts/bridge/Deposit.sol#L172
// https://github.com/keep-network/tbtc-v2/blob/4b6143974b43297e69a45191f0e2b6a25561e72b/solidity/contracts/bridge/Deposit.sol#L217
const depositScriptFormat = "14%v7508%v7576a914%v8763ac6776a914%v8804%vb175ac68"

// depositWithExtraDataScriptFormat is the format of the deposit P2(W)SH Bitcoin
// script with optional 32-byte extra data included. The specific placeholders
// are: depositor, extraData, blindingFactor, walletPublicKeyHash, refundPublicKeyHash,
// and refundLocktime. For reference see:
// https://github.com/keep-network/tbtc-v2/blob/4b6143974b43297e69a45191f0e2b6a25561e72b/solidity/contracts/bridge/Deposit.sol#L246
const depositWithExtraDataScriptFormat = "14%v7520%v7508%v7576a914%v8763ac6776a914%v8804%vb175ac68"

// Deposit represents a tBTC deposit.
type Deposit struct {
// Utxo is the unspent output of the deposit funding transaction that
Expand All @@ -33,6 +41,9 @@ type Deposit struct {
// Vault is an optional field that holds the host chain address of the
// target vault.
Vault *chain.Address
// ExtraData is an optional field that holds 32 bytes of extra data
// embedded in the deposit script.
ExtraData *[32]byte
}

// Script constructs the deposit P2(W)SH Bitcoin script. This function
Expand All @@ -48,14 +59,28 @@ func (d *Deposit) Script() ([]byte, error) {
return nil, fmt.Errorf("wrong byte length of depositor field")
}

script := fmt.Sprintf(
depositScriptFormat,
hex.EncodeToString(depositorBytes),
hex.EncodeToString(d.BlindingFactor[:]),
hex.EncodeToString(d.WalletPublicKeyHash[:]),
hex.EncodeToString(d.RefundPublicKeyHash[:]),
hex.EncodeToString(d.RefundLocktime[:]),
)
var script string

if d.ExtraData != nil {
script = fmt.Sprintf(
depositWithExtraDataScriptFormat,
hex.EncodeToString(depositorBytes),
hex.EncodeToString(d.ExtraData[:]),
hex.EncodeToString(d.BlindingFactor[:]),
hex.EncodeToString(d.WalletPublicKeyHash[:]),
hex.EncodeToString(d.RefundPublicKeyHash[:]),
hex.EncodeToString(d.RefundLocktime[:]),
)
} else {
script = fmt.Sprintf(
depositScriptFormat,
hex.EncodeToString(depositorBytes),
hex.EncodeToString(d.BlindingFactor[:]),
hex.EncodeToString(d.WalletPublicKeyHash[:]),
hex.EncodeToString(d.RefundPublicKeyHash[:]),
hex.EncodeToString(d.RefundLocktime[:]),
)
}

return hex.DecodeString(script)
}
34 changes: 33 additions & 1 deletion pkg/tbtc/deposit_sweep.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,20 @@ func ValidateDepositSweepProposal(
FundingTx *bitcoin.Transaction
},
) error

// GetDepositRequest gets the on-chain deposit request for the given
// funding transaction hash and output index.The returned values represent:
// - deposit request which is non-nil only when the deposit request was
// found,
// - boolean value which is true if the deposit request was found, false
// otherwise,
// - error which is non-nil only when the function execution failed. It will
// be nil if the deposit request was not found, but the function execution
// succeeded.
GetDepositRequest(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
) (*DepositChainRequest, bool, error)
},
btcChain bitcoin.Chain,
) ([]*Deposit, error) {
Expand Down Expand Up @@ -352,11 +366,29 @@ func ValidateDepositSweepProposal(
)
}

depositRequest, found, err := chain.GetDepositRequest(
depositKey.FundingTxHash,
depositKey.FundingOutputIndex,
)
if err != nil {
return nil, fmt.Errorf(
"cannot get request data for deposit [%v]: [%v]",
depositDisplayIndex,
err,
)
}
if !found {
return nil, fmt.Errorf(
"request data not found for deposit [%v]",
depositDisplayIndex,
)
}

depositExtraInfo[i] = struct {
*Deposit
FundingTx *bitcoin.Transaction
}{
Deposit: matchingEvent.unpack(),
Deposit: matchingEvent.unpack(depositRequest.ExtraData),
FundingTx: fundingTx,
}
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/tbtc/deposit_sweep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ func TestDepositSweepAction_Execute(t *testing.T) {
if err != nil {
t.Fatal(err)
}

hostChain.setDepositRequest(
fundingTxHash,
fundingOutputIndex,
&DepositChainRequest{
// Set only relevant fields.
Depositor: deposit.Depositor,
Amount: uint64(deposit.Utxo.Value),
Vault: deposit.Vault,
ExtraData: deposit.ExtraData,
},
)
}

// Build the sweep proposal based on the scenario data.
Expand Down Expand Up @@ -252,6 +264,7 @@ func TestAssembleDepositSweepTransaction(t *testing.T) {
RefundPublicKeyHash: d.RefundPublicKeyHash,
RefundLocktime: d.RefundLocktime,
Vault: d.Vault,
ExtraData: d.ExtraData,
}
}

Expand Down
79 changes: 62 additions & 17 deletions pkg/tbtc/deposit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/hex"
"testing"

"github.com/keep-network/keep-core/pkg/chain"

"github.com/keep-network/keep-core/internal/testutils"
)

Expand All @@ -16,24 +18,67 @@ func TestDeposit_Script(t *testing.T) {
return bytes
}

// Fill only the fields relevant for script computation.
d := new(Deposit)
d.Depositor = "934b98637ca318a4d6e7ca6ffd1690b8e77df637"
copy(d.BlindingFactor[:], hexToSlice("f9f0c90d00039523"))
copy(d.WalletPublicKeyHash[:], hexToSlice("8db50eb52063ea9d98b3eac91489a90f738986f6"))
copy(d.RefundPublicKeyHash[:], hexToSlice("28e081f285138ccbe389c1eb8985716230129f89"))
copy(d.RefundLocktime[:], hexToSlice("60bcea61"))

script, err := d.Script()
if err != nil {
t.Fatal(err)
var tests = map[string]struct {
depositor string
blindingFactor string
walletPublicKeyHash string
refundPublicKeyHash string
refundLocktime string
extraData string
expectedScript string
}{
"no extra data": {
depositor: "934b98637ca318a4d6e7ca6ffd1690b8e77df637",
blindingFactor: "f9f0c90d00039523",
walletPublicKeyHash: "8db50eb52063ea9d98b3eac91489a90f738986f6",
refundPublicKeyHash: "28e081f285138ccbe389c1eb8985716230129f89",
refundLocktime: "60bcea61",
extraData: "",
expectedScript: "14934b98637ca318a4d6e7ca6ffd1690b8e77df637750" +
"8f9f0c90d000395237576a9148db50eb52063ea9d98b3eac91489a90f" +
"738986f68763ac6776a91428e081f285138ccbe389c1eb89857162301" +
"29f89880460bcea61b175ac68",
},
"with extra data": {
depositor: "934b98637ca318a4d6e7ca6ffd1690b8e77df637",
blindingFactor: "f9f0c90d00039523",
walletPublicKeyHash: "8db50eb52063ea9d98b3eac91489a90f738986f6",
refundPublicKeyHash: "28e081f285138ccbe389c1eb8985716230129f89",
refundLocktime: "60bcea61",
extraData: "a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55" +
"196396b03d9bda",
expectedScript: "14934b98637ca318a4d6e7ca6ffd1690b8e77df637752" +
"0a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55196396" +
"b03d9bda7508f9f0c90d000395237576a9148db50eb52063ea9d98b3e" +
"ac91489a90f738986f68763ac6776a91428e081f285138ccbe389c1eb" +
"8985716230129f89880460bcea61b175ac68",
},
}

expectedScript := hexToSlice(
"14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d0003" +
"95237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a" +
"91428e081f285138ccbe389c1eb8985716230129f89880460bcea61b175ac68",
)
for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
// Fill only the fields relevant for script computation.
d := new(Deposit)
d.Depositor = chain.Address(test.depositor)
copy(d.BlindingFactor[:], hexToSlice(test.blindingFactor))
copy(d.WalletPublicKeyHash[:], hexToSlice(test.walletPublicKeyHash))
copy(d.RefundPublicKeyHash[:], hexToSlice(test.refundPublicKeyHash))
copy(d.RefundLocktime[:], hexToSlice(test.refundLocktime))

if len(test.extraData) > 0 {
var extraData [32]byte
copy(extraData[:], hexToSlice(test.extraData))
d.ExtraData = &extraData
}

script, err := d.Script()
if err != nil {
t.Fatal(err)
}

testutils.AssertBytesEqual(t, expectedScript, script)
expectedScript := hexToSlice(test.expectedScript)

testutils.AssertBytesEqual(t, expectedScript, script)
})
}
}
7 changes: 7 additions & 0 deletions pkg/tbtc/internal/test/marshaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (dsts *DepositSweepTestScenario) UnmarshalJSON(data []byte) error {
RefundPublicKeyHash string
RefundLocktime string
Vault string
ExtraData string
}
InputTransactions []string
Fee int64
Expand Down Expand Up @@ -87,6 +88,12 @@ func (dsts *DepositSweepTestScenario) UnmarshalJSON(data []byte) error {
}
d.Vault = vault

if len(deposit.ExtraData) > 0 {
var extraData [32]byte
copy(extraData[:], hexToSlice(deposit.ExtraData))
d.ExtraData = &extraData
}

dsts.Deposits = append(dsts.Deposits, d)
}

Expand Down
Loading

0 comments on commit 639c5be

Please sign in to comment.