Skip to content

Commit ec00a89

Browse files
committed
Adjusted safety margin calculations when moving funds
1 parent 8c2fc66 commit ec00a89

File tree

5 files changed

+247
-28
lines changed

5 files changed

+247
-28
lines changed

pkg/tbtc/chain.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,32 @@ type BridgeChain interface {
223223
movingFundsTxHash bitcoin.Hash,
224224
movingFundsTxOutpointIndex uint32,
225225
) (*MovedFundsSweepRequest, bool, error)
226+
227+
// GetMovingFundsParameters gets the current value of parameters relevant
228+
// for the moving funds process.
229+
GetMovingFundsParameters() (
230+
txMaxTotalFee uint64,
231+
dustThreshold uint64,
232+
timeoutResetDelay uint32,
233+
timeout uint32,
234+
timeoutSlashingAmount *big.Int,
235+
timeoutNotifierRewardMultiplier uint32,
236+
commitmentGasOffset uint16,
237+
sweepTxMaxTotalFee uint64,
238+
sweepTimeout uint32,
239+
sweepTimeoutSlashingAmount *big.Int,
240+
sweepTimeoutNotifierRewardMultiplier uint32,
241+
err error,
242+
)
243+
244+
// PastMovingFundsCommitmentSubmittedEvents fetches past moving funds
245+
// commitment submitted events according to the provided filter or
246+
// unfiltered if the filter is nil. Returned events are sorted by the block
247+
// number in the ascending order, i.e. the latest event is at the end of the
248+
// slice.
249+
PastMovingFundsCommitmentSubmittedEvents(
250+
filter *MovingFundsCommitmentSubmittedEventFilter,
251+
) ([]*MovingFundsCommitmentSubmittedEvent, error)
226252
}
227253

228254
// NewWalletRegisteredEvent represents a new wallet registered event.

pkg/tbtc/chain_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,29 @@ func buildMovedFundsSweepProposalValidationKey(
10991099
return sha256.Sum256(buffer.Bytes()), nil
11001100
}
11011101

1102+
func (lc *localChain) GetMovingFundsParameters() (
1103+
txMaxTotalFee uint64,
1104+
dustThreshold uint64,
1105+
timeoutResetDelay uint32,
1106+
timeout uint32,
1107+
timeoutSlashingAmount *big.Int,
1108+
timeoutNotifierRewardMultiplier uint32,
1109+
commitmentGasOffset uint16,
1110+
sweepTxMaxTotalFee uint64,
1111+
sweepTimeout uint32,
1112+
sweepTimeoutSlashingAmount *big.Int,
1113+
sweepTimeoutNotifierRewardMultiplier uint32,
1114+
err error,
1115+
) {
1116+
panic("unsupported")
1117+
}
1118+
1119+
func (lc *localChain) PastMovingFundsCommitmentSubmittedEvents(
1120+
filter *MovingFundsCommitmentSubmittedEventFilter,
1121+
) ([]*MovingFundsCommitmentSubmittedEvent, error) {
1122+
panic("unsupported")
1123+
}
1124+
11021125
// Connect sets up the local chain.
11031126
func Connect(blockTime ...time.Duration) *localChain {
11041127
operatorPrivateKey, _, err := operator.GenerateKeyPair(local_v1.DefaultCurve)

pkg/tbtc/moving_funds.go

Lines changed: 197 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/keep-network/keep-common/pkg/chain/ethereum"
1111
"github.com/keep-network/keep-core/pkg/bitcoin"
12+
"github.com/keep-network/keep-core/pkg/chain"
1213
"go.uber.org/zap"
1314
)
1415

@@ -53,6 +54,11 @@ const (
5354
movingFundsCommitmentConfirmationBlocks = 32
5455
)
5556

57+
// MovingFundsCommitmentLookBackBlocks is the look-back period in blocks used
58+
// when searching for submitted moving funds commitment events. It's equal to
59+
// 30 days assuming 12 seconds per block.
60+
const MovingFundsCommitmentLookBackBlocks = uint64(216000)
61+
5662
// MovingFundsProposal represents a moving funds proposal issued by a wallet's
5763
// coordination leader.
5864
type MovingFundsProposal struct {
@@ -312,20 +318,33 @@ func ValidateMovingFundsProposal(
312318
proposal *MovingFundsProposal,
313319
) error
314320

321+
BlockCounter() (chain.BlockCounter, error)
322+
315323
GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error)
324+
325+
GetMovingFundsParameters() (
326+
txMaxTotalFee uint64,
327+
dustThreshold uint64,
328+
timeoutResetDelay uint32,
329+
timeout uint32,
330+
timeoutSlashingAmount *big.Int,
331+
timeoutNotifierRewardMultiplier uint32,
332+
commitmentGasOffset uint16,
333+
sweepTxMaxTotalFee uint64,
334+
sweepTimeout uint32,
335+
sweepTimeoutSlashingAmount *big.Int,
336+
sweepTimeoutNotifierRewardMultiplier uint32,
337+
err error,
338+
)
339+
340+
PastMovingFundsCommitmentSubmittedEvents(
341+
filter *MovingFundsCommitmentSubmittedEventFilter,
342+
) ([]*MovingFundsCommitmentSubmittedEvent, error)
316343
},
317344
) error {
318345
validateProposalLogger.Infof("calling chain for proposal validation")
319346

320-
walletChainData, err := chain.GetWallet(walletPublicKeyHash)
321-
if err != nil {
322-
return fmt.Errorf(
323-
"cannot get wallet's chain data: [%w]",
324-
err,
325-
)
326-
}
327-
328-
err = ValidateMovingFundsSafetyMargin(walletChainData)
347+
err := ValidateMovingFundsSafetyMargin(walletPublicKeyHash, chain)
329348
if err != nil {
330349
return fmt.Errorf("moving funds proposal is invalid: [%v]", err)
331350
}
@@ -354,10 +373,87 @@ func ValidateMovingFundsProposal(
354373
// deposits so, it makes sense to preserve a safety margin before moving
355374
// funds to give the last minute deposits a chance to become eligible for
356375
// deposit sweep.
376+
//
377+
// Similarly, wallets that just entered the MovingFunds state may have become
378+
// target wallets for another moving funds wallets. It makes sense to preserve
379+
// a safety margin to allow the wallet to merge the moved funds from another
380+
// wallets. In this case a longer safety margin should be used.
357381
func ValidateMovingFundsSafetyMargin(
358-
walletChainData *WalletChainData,
382+
walletPublicKeyHash [20]byte,
383+
chain interface {
384+
BlockCounter() (chain.BlockCounter, error)
385+
386+
GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error)
387+
388+
GetMovingFundsParameters() (
389+
txMaxTotalFee uint64,
390+
dustThreshold uint64,
391+
timeoutResetDelay uint32,
392+
timeout uint32,
393+
timeoutSlashingAmount *big.Int,
394+
timeoutNotifierRewardMultiplier uint32,
395+
commitmentGasOffset uint16,
396+
sweepTxMaxTotalFee uint64,
397+
sweepTimeout uint32,
398+
sweepTimeoutSlashingAmount *big.Int,
399+
sweepTimeoutNotifierRewardMultiplier uint32,
400+
err error,
401+
)
402+
403+
PastMovingFundsCommitmentSubmittedEvents(
404+
filter *MovingFundsCommitmentSubmittedEventFilter,
405+
) ([]*MovingFundsCommitmentSubmittedEvent, error)
406+
},
359407
) error {
408+
// In most cases the safety margin of 24 hours should be enough. It will
409+
// allow the wallet to sweep the last deposits that were made before the
410+
// wallet entered the moving funds state.
360411
safetyMargin := time.Duration(24) * time.Hour
412+
413+
// It is possible that our wallet is the target wallet in another pending
414+
// moving funds procedure. If this is the case we must apply a longer
415+
// 14-day safety margin. This will ensure the funds moved from another
416+
// wallet can be merged with our wallet's main UTXO before moving funds.
417+
isMovingFundsTarget, err := isWalletPendingMovingFundsTarget(
418+
walletPublicKeyHash,
419+
chain,
420+
)
421+
if err != nil {
422+
return fmt.Errorf(
423+
"cannot check if wallet is pending moving funds target: [%w]",
424+
err,
425+
)
426+
}
427+
428+
if isMovingFundsTarget {
429+
safetyMargin = time.Duration(24) * 14 * time.Hour
430+
}
431+
432+
// As the moving funds procedure is time constrained, we must ensure the
433+
// safety margin does not exceed half of the moving funds timeout parameter.
434+
// This should give the wallet enough time to complete moving funds.
435+
_, _, _, movingFundsTimeout, _, _, _, _, _, _, _, err :=
436+
chain.GetMovingFundsParameters()
437+
if err != nil {
438+
return fmt.Errorf("cannot get moving funds parameters: [%w]", err)
439+
}
440+
441+
maxAllowedSafetyMargin := time.Duration(
442+
float64(movingFundsTimeout) * 0.5 * float64(time.Second),
443+
)
444+
445+
if safetyMargin > maxAllowedSafetyMargin {
446+
safetyMargin = maxAllowedSafetyMargin
447+
}
448+
449+
walletChainData, err := chain.GetWallet(walletPublicKeyHash)
450+
if err != nil {
451+
return fmt.Errorf(
452+
"cannot get wallet's chain data: [%w]",
453+
err,
454+
)
455+
}
456+
361457
safetyMarginExpiresAt := walletChainData.MovingFundsRequestedAt.Add(safetyMargin)
362458

363459
if time.Now().Before(safetyMarginExpiresAt) {
@@ -375,6 +471,97 @@ func (mfa *movingFundsAction) actionType() WalletActionType {
375471
return ActionMovingFunds
376472
}
377473

474+
func isWalletPendingMovingFundsTarget(
475+
walletPublicKeyHash [20]byte,
476+
477+
chain interface {
478+
BlockCounter() (chain.BlockCounter, error)
479+
480+
GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error)
481+
482+
GetMovingFundsParameters() (
483+
txMaxTotalFee uint64,
484+
dustThreshold uint64,
485+
timeoutResetDelay uint32,
486+
timeout uint32,
487+
timeoutSlashingAmount *big.Int,
488+
timeoutNotifierRewardMultiplier uint32,
489+
commitmentGasOffset uint16,
490+
sweepTxMaxTotalFee uint64,
491+
sweepTimeout uint32,
492+
sweepTimeoutSlashingAmount *big.Int,
493+
sweepTimeoutNotifierRewardMultiplier uint32,
494+
err error,
495+
)
496+
497+
PastMovingFundsCommitmentSubmittedEvents(
498+
filter *MovingFundsCommitmentSubmittedEventFilter,
499+
) ([]*MovingFundsCommitmentSubmittedEvent, error)
500+
},
501+
) (bool, error) {
502+
blockCounter, err := chain.BlockCounter()
503+
if err != nil {
504+
return false, fmt.Errorf("failed to get block counter: [%w]", err)
505+
}
506+
507+
currentBlockNumber, err := blockCounter.CurrentBlock()
508+
if err != nil {
509+
return false, fmt.Errorf(
510+
"failed to get current block number: [%w]",
511+
err,
512+
)
513+
}
514+
515+
filterStartBlock := uint64(0)
516+
if currentBlockNumber > MovingFundsCommitmentLookBackBlocks {
517+
filterStartBlock = currentBlockNumber - MovingFundsCommitmentLookBackBlocks
518+
}
519+
520+
// Get all the recent moving funds commitment submitted events.
521+
filter := &MovingFundsCommitmentSubmittedEventFilter{
522+
StartBlock: filterStartBlock,
523+
}
524+
525+
events, err := chain.PastMovingFundsCommitmentSubmittedEvents(filter)
526+
if err != nil {
527+
return false, fmt.Errorf(
528+
"failed to get past moving funds commitment submitted events: [%w]",
529+
err,
530+
)
531+
}
532+
533+
isWalletTarget := func(event *MovingFundsCommitmentSubmittedEvent) bool {
534+
for _, targetWallet := range event.TargetWallets {
535+
if walletPublicKeyHash == targetWallet {
536+
return true
537+
}
538+
}
539+
return false
540+
}
541+
542+
for _, event := range events {
543+
if !isWalletTarget(event) {
544+
continue
545+
}
546+
547+
// Our wallet is on the list of target wallets. If the state is moving
548+
// funds, there is probably moving funds to our wallet in the process.
549+
walletChainData, err := chain.GetWallet(walletPublicKeyHash)
550+
if err != nil {
551+
return false, fmt.Errorf(
552+
"cannot get wallet's chain data: [%w]",
553+
err,
554+
)
555+
}
556+
557+
if walletChainData.State == StateMovingFunds {
558+
return true, nil
559+
}
560+
}
561+
562+
return false, nil
563+
}
564+
378565
func assembleMovingFundsTransaction(
379566
bitcoinChain bitcoin.Chain,
380567
walletMainUtxo *bitcoin.UnspentTransactionOutput,

pkg/tbtcpg/chain.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -131,23 +131,6 @@ type Chain interface {
131131
proposal *tbtc.HeartbeatProposal,
132132
) error
133133

134-
// GetMovingFundsParameters gets the current value of parameters relevant
135-
// for the moving funds process.
136-
GetMovingFundsParameters() (
137-
txMaxTotalFee uint64,
138-
dustThreshold uint64,
139-
timeoutResetDelay uint32,
140-
timeout uint32,
141-
timeoutSlashingAmount *big.Int,
142-
timeoutNotifierRewardMultiplier uint32,
143-
commitmentGasOffset uint16,
144-
sweepTxMaxTotalFee uint64,
145-
sweepTimeout uint32,
146-
sweepTimeoutSlashingAmount *big.Int,
147-
sweepTimeoutNotifierRewardMultiplier uint32,
148-
err error,
149-
)
150-
151134
// PastMovingFundsCommitmentSubmittedEvents fetches past moving funds
152135
// commitment submitted events according to the provided filter or
153136
// unfiltered if the filter is nil. Returned events are sorted by the block

pkg/tbtcpg/moving_funds.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) (
9595

9696
// Check the safety margin for moving funds early. This will prevent
9797
// commitment submission if the wallet is not safe to move funds.
98-
err = tbtc.ValidateMovingFundsSafetyMargin(walletChainData)
98+
err = tbtc.ValidateMovingFundsSafetyMargin(walletPublicKeyHash, mft.chain)
9999
if err != nil {
100100
taskLogger.Infof("source wallet moving funds safety margin validation failed: [%v]", err)
101101
return nil, false, nil

0 commit comments

Comments
 (0)