Skip to content

Commit

Permalink
Moving funds commitment implementation (#3748)
Browse files Browse the repository at this point in the history
#Refs: #3733.

This PR introduces functionalities that enable a wallet to submit a
moving funds commitment. These functionalities have been incorporated
into the `tbtcpg` package through the creation and execution of a
`MovingFundsTask`.
They were implemented in the same manner as other procedures such as
deposit sweeps and redemptions.

The task results in a moving funds proposal, which is needed for
generating the Bitcoin transaction that transfer funds between wallets.
Usually, this process will also involve submitting the moving funds
commitment transaction to the `Bridge`. This submission step may be
skipped if the commitment has already been submitted, which can occur if
a previously initiated moving funds procedure was interrupted
post-commitment submission and still needs completion.

Executing a moving funds task consists of several key steps:
1) Verifying the eligibility of the wallet for moving funds, involving
checks on the wallet's state and balance.
2) Preparing the target wallets, which includes either creating a new
list of wallets or retrieving an existing list from a previous,
interrupted moving funds procedure.
3) Submitting the moving funds transaction to the Bridge, unless it has
already been submitted.
4) Creating and validating the moving funds proposal, a process that
involves estimating the Bitcoin transaction fee.

Upon completion of the moving funds task, all necessary prerequisites
for the Bitcoin moving funds transaction should be satisfied.
  • Loading branch information
lukasz-zimnoch authored Feb 8, 2024
2 parents a78b3f3 + 4672795 commit cdda81b
Show file tree
Hide file tree
Showing 18 changed files with 2,507 additions and 71 deletions.
184 changes: 184 additions & 0 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,25 @@ func computeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte {
return mainUtxoHash
}

func (tc *TbtcChain) ComputeMovingFundsCommitmentHash(
targetWallets [][20]byte,
) [32]byte {
return computeMovingFundsCommitmentHash(targetWallets)
}

func computeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte {
packedWallets := []byte{}

for _, wallet := range targetWallets {
packedWallets = append(packedWallets, wallet[:]...)
// Each wallet hash must be padded with 12 zero bytes following the
// actual hash.
packedWallets = append(packedWallets, make([]byte, 12)...)
}

return crypto.Keccak256Hash(packedWallets)
}

func (tc *TbtcChain) BuildDepositKey(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
Expand Down Expand Up @@ -1463,6 +1482,81 @@ func (tc *TbtcChain) GetRedemptionParameters() (
return
}

func (tc *TbtcChain) GetWalletParameters() (
creationPeriod uint32,
creationMinBtcBalance uint64,
creationMaxBtcBalance uint64,
closureMinBtcBalance uint64,
maxAge uint32,
maxBtcTransfer uint64,
closingPeriod uint32,
err error,
) {
parameters, callErr := tc.bridge.WalletParameters()
if callErr != nil {
err = callErr
return
}

creationPeriod = parameters.WalletCreationPeriod
creationMinBtcBalance = parameters.WalletCreationMinBtcBalance
creationMaxBtcBalance = parameters.WalletCreationMaxBtcBalance
closureMinBtcBalance = parameters.WalletClosureMinBtcBalance
maxAge = parameters.WalletMaxAge
maxBtcTransfer = parameters.WalletMaxBtcTransfer
closingPeriod = parameters.WalletClosingPeriod

return
}

func (tc *TbtcChain) GetLiveWalletsCount() (uint32, error) {
return tc.bridge.LiveWalletsCount()
}

func (tc *TbtcChain) PastMovingFundsCommitmentSubmittedEvents(
filter *tbtc.MovingFundsCommitmentSubmittedEventFilter,
) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, error) {
var startBlock uint64
var endBlock *uint64
var walletPublicKeyHash [][20]byte

if filter != nil {
startBlock = filter.StartBlock
endBlock = filter.EndBlock
walletPublicKeyHash = filter.WalletPublicKeyHash
}

events, err := tc.bridge.PastMovingFundsCommitmentSubmittedEvents(
startBlock,
endBlock,
walletPublicKeyHash,
)
if err != nil {
return nil, err
}

convertedEvents := make([]*tbtc.MovingFundsCommitmentSubmittedEvent, 0)
for _, event := range events {
convertedEvent := &tbtc.MovingFundsCommitmentSubmittedEvent{
WalletPublicKeyHash: event.WalletPubKeyHash,
TargetWallets: event.TargetWallets,
Submitter: chain.Address(event.Submitter.Hex()),
BlockNumber: event.Raw.BlockNumber,
}

convertedEvents = append(convertedEvents, convertedEvent)
}

sort.SliceStable(
convertedEvents,
func(i, j int) bool {
return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
},
)

return convertedEvents, err
}

func buildDepositKey(
fundingTxHash bitcoin.Hash,
fundingOutputIndex uint32,
Expand Down Expand Up @@ -1571,6 +1665,28 @@ func (tc *TbtcChain) GetDepositSweepMaxSize() (uint16, error) {
return tc.walletProposalValidator.DEPOSITSWEEPMAXSIZE()
}

func (tc *TbtcChain) SubmitMovingFundsCommitment(
walletPublicKeyHash [20]byte,
walletMainUTXO bitcoin.UnspentTransactionOutput,
walletMembersIDs []uint32,
walletMemberIndex uint32,
targetWallets [][20]byte,
) error {
mainUtxo := tbtcabi.BitcoinTxUTXO{
TxHash: walletMainUTXO.Outpoint.TransactionHash,
TxOutputIndex: walletMainUTXO.Outpoint.OutputIndex,
TxOutputValue: uint64(walletMainUTXO.Value),
}
_, err := tc.bridge.SubmitMovingFundsCommitment(
walletPublicKeyHash,
mainUtxo,
walletMembersIDs,
big.NewInt(int64(walletMemberIndex)),
targetWallets,
)
return err
}

func (tc *TbtcChain) ValidateRedemptionProposal(
walletPublicKeyHash [20]byte,
proposal *tbtc.RedemptionProposal,
Expand Down Expand Up @@ -1660,3 +1776,71 @@ func (tc *TbtcChain) ValidateHeartbeatProposal(

return nil
}

func (tc *TbtcChain) GetMovingFundsParameters() (
txMaxTotalFee uint64,
dustThreshold uint64,
timeoutResetDelay uint32,
timeout uint32,
timeoutSlashingAmount *big.Int,
timeoutNotifierRewardMultiplier uint32,
commitmentGasOffset uint16,
sweepTxMaxTotalFee uint64,
sweepTimeout uint32,
sweepTimeoutSlashingAmount *big.Int,
sweepTimeoutNotifierRewardMultiplier uint32,
err error,
) {
parameters, callErr := tc.bridge.MovingFundsParameters()
if callErr != nil {
err = callErr
return
}

txMaxTotalFee = parameters.MovingFundsTxMaxTotalFee
dustThreshold = parameters.MovingFundsDustThreshold
timeoutResetDelay = parameters.MovingFundsTimeoutResetDelay
timeout = parameters.MovingFundsTimeout
timeoutSlashingAmount = parameters.MovingFundsTimeoutSlashingAmount
timeoutNotifierRewardMultiplier = parameters.MovingFundsTimeoutNotifierRewardMultiplier
commitmentGasOffset = parameters.MovingFundsCommitmentGasOffset
sweepTxMaxTotalFee = parameters.MovedFundsSweepTxMaxTotalFee
sweepTimeout = parameters.MovedFundsSweepTimeout
sweepTimeoutSlashingAmount = parameters.MovedFundsSweepTimeoutSlashingAmount
sweepTimeoutNotifierRewardMultiplier = parameters.MovedFundsSweepTimeoutNotifierRewardMultiplier

return
}

func (tc *TbtcChain) ValidateMovingFundsProposal(
walletPublicKeyHash [20]byte,
mainUTXO *bitcoin.UnspentTransactionOutput,
proposal *tbtc.MovingFundsProposal,
) error {
abiProposal := tbtcabi.WalletProposalValidatorMovingFundsProposal{
WalletPubKeyHash: walletPublicKeyHash,
TargetWallets: proposal.TargetWallets,
MovingFundsTxFee: proposal.MovingFundsTxFee,
}
abiMainUTXO := tbtcabi.BitcoinTxUTXO3{
TxHash: mainUTXO.Outpoint.TransactionHash,
TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
TxOutputValue: uint64(mainUTXO.Value),
}

valid, err := tc.walletProposalValidator.ValidateMovingFundsProposal(
abiProposal,
abiMainUTXO,
)
if err != nil {
return fmt.Errorf("validation failed: [%v]", err)
}

// Should never happen because `validateMovingFundsProposal` returns true
// or reverts (returns an error) but do the check just in case.
if !valid {
return fmt.Errorf("unexpected validation result")
}

return nil
}
39 changes: 39 additions & 0 deletions pkg/chain/ethereum/tbtc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,45 @@ func TestComputeMainUtxoHash(t *testing.T) {
testutils.AssertBytesEqual(t, expectedMainUtxoHash, mainUtxoHash[:])
}

func TestComputeMovingFundsCommitmentHash(t *testing.T) {
toByte20 := func(s string) [20]byte {
bytes, err := hex.DecodeString(s)
if err != nil {
t.Fatal(err)
}

if len(bytes) != 20 {
t.Fatal("incorrect hexstring length")
}

var result [20]byte
copy(result[:], bytes[:])
return result
}

targetWallets := [][20]byte{
toByte20("4b440cb29c80c3f256212d8fdd4f2125366f3c91"),
toByte20("888f01315e0268bfa05d5e522f8d63f6824d9a96"),
toByte20("b2a89e53a4227dbe530a52a1c419040735fa636c"),
}

movingFundsCommitmentHash := computeMovingFundsCommitmentHash(
targetWallets,
)

expectedMovingFundsCommitmentHash, err := hex.DecodeString(
"8ba62d1d754a3429e2ff1fb4f523b5fad2b605c873a2968bb5985a625eb96202",
)
if err != nil {
t.Fatal(err)
}
testutils.AssertBytesEqual(
t,
expectedMovingFundsCommitmentHash,
movingFundsCommitmentHash[:],
)
}

// Test data based on: https://etherscan.io/tx/0x97c7a293127a604da77f7ef8daf4b19da2bf04327dd891b6d717eaef89bd8bca
func TestBuildDepositKey(t *testing.T) {
fundingTxHash, err := bitcoin.NewHashFromString(
Expand Down
24 changes: 24 additions & 0 deletions pkg/tbtc/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,15 @@ type WalletProposalValidatorChain interface {
walletPublicKeyHash [20]byte,
proposal *HeartbeatProposal,
) error

// ValidateMovingFundsProposal validates the given moving funds proposal
// against the chain. Returns an error if the proposal is not valid or
// nil otherwise.
ValidateMovingFundsProposal(
walletPublicKeyHash [20]byte,
mainUTXO *bitcoin.UnspentTransactionOutput,
proposal *MovingFundsProposal,
) error
}

// RedemptionRequestedEvent represents a redemption requested event.
Expand All @@ -376,6 +385,21 @@ type RedemptionRequestedEventFilter struct {
Redeemer []chain.Address
}

// MovingFundsCommitmentSubmittedEvent represents a moving funds commitment submitted event.
type MovingFundsCommitmentSubmittedEvent struct {
WalletPublicKeyHash [20]byte
TargetWallets [][20]byte
Submitter chain.Address
BlockNumber uint64
}

// MovingFundsCommitmentSubmittedEventFilter is a component allowing to filter MovingFundsCommitmentSubmittedEvent.
type MovingFundsCommitmentSubmittedEventFilter struct {
StartBlock uint64
EndBlock *uint64
WalletPublicKeyHash [][20]byte
}

// Chain represents the interface that the TBTC module expects to interact
// with the anchoring blockchain on.
type Chain interface {
Expand Down
21 changes: 21 additions & 0 deletions pkg/tbtc/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,19 @@ func (lc *localChain) operatorAddress() (chain.Address, error) {
return lc.Signing().PublicKeyToAddress(operatorPublicKey)
}

func (lc *localChain) GetWalletParameters() (
creationPeriod uint32,
creationMinBtcBalance uint64,
creationMaxBtcBalance uint64,
closureMinBtcBalance uint64,
maxAge uint32,
maxBtcTransfer uint64,
closingPeriod uint32,
err error,
) {
panic("unsupported")
}

func (lc *localChain) ValidateDepositSweepProposal(
walletPublicKeyHash [20]byte,
proposal *DepositSweepProposal,
Expand Down Expand Up @@ -891,6 +904,14 @@ func (lc *localChain) setHeartbeatProposalValidationResult(
lc.heartbeatProposalValidations[proposal.Message] = result
}

func (lc *localChain) ValidateMovingFundsProposal(
walletPublicKeyHash [20]byte,
mainUTXO *bitcoin.UnspentTransactionOutput,
proposal *MovingFundsProposal,
) error {
panic("unsupported")
}

// Connect sets up the local chain.
func Connect(blockTime ...time.Duration) *localChain {
operatorPrivateKey, _, err := operator.GenerateKeyPair(local_v1.DefaultCurve)
Expand Down
2 changes: 2 additions & 0 deletions pkg/tbtc/coordination.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ func (cf *coordinationFault) String() string {
type CoordinationProposalRequest struct {
WalletPublicKeyHash [20]byte
WalletOperators []chain.Address
ExecutingOperator chain.Address
ActionsChecklist []WalletActionType
}

Expand Down Expand Up @@ -581,6 +582,7 @@ func (ce *coordinationExecutor) executeLeaderRoutine(
&CoordinationProposalRequest{
WalletPublicKeyHash: walletPublicKeyHash,
WalletOperators: ce.coordinatedWallet.signingGroupOperators,
ExecutingOperator: ce.operatorAddress,
ActionsChecklist: actionsChecklist,
},
)
Expand Down
Loading

0 comments on commit cdda81b

Please sign in to comment.