Skip to content

Commit

Permalink
CLI command to submit a deposit sweep proposal (#3549)
Browse files Browse the repository at this point in the history
We added a CLI command to submit a deposit sweep proposal. The command
verifies the proposal with the same rules as nodes will do and submits
the proposal to the chain.

To submit a deposit sweep proposal run:
```
keep-client coordinator propose-deposits-sweep --wallet <wallet_address> --fee <fee> <deposit_1> <deposit_2> ...
```

Following flags can be used with the command:
- `--wallet <address>` - wallet to sweep
- `--fee` - fee to use for the sweep transaction

The command requires `ethereum` and `bitcoin.electrum` node details to
be provided in the config.

### Deposits

The command requires the deposits details to be provided as variadic
arguments in a specific format:
`<unprefixed bitcoin transaction hash>:<bitcoin transaction output
index>:<ethereum reveal block number>`
e.g. 

`bd99d1d0a61fd104925d9b7ac997958aa8af570418b3fde091f7bfc561608865:1:8392394`

## Sample command

```
keep-client --config ./configs/forked/config.toml \
  --goerli \
  coordinator \
  propose-deposits-sweep \
  --wallet 0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e \
  --fee 12 \
  da3ec4f5620f686d5013c37d49a411fa49daac5077c98809402572f481c6ea0:0:8502238 \
  214a85db0871b537d4cd649a8d74aecdbee112919983ad22fb85cfa2704a3593:0:8502224 \
  3704d01b48212ddacc77e613be0491978a8c19ad11af6b8536eacf9e9953eb13:0:8502070
```

## Testing

~To test this feature `WalletCoordinator` contract is required to be
deployed. It is not yet available on Goerli or Mainnet. To have the
contract available you can run a hardhat node as Goerli fork
(https://github.com/nkuba/hardhat-forked-node) and deploy the
`WalletCoordinator` contract there.~

Depends on: #3545
  • Loading branch information
lukasz-zimnoch authored May 9, 2023
2 parents c510ba4 + b98cbbf commit 6c6c6d0
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 31 deletions.
109 changes: 99 additions & 10 deletions cmd/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var (
sortByAmountFlagName = "sort-amount"
headFlagName = "head"
tailFlagName = "tail"
feeFlagName = "fee"
dryRunFlagName = "dry-run"
)

// CoordinatorCommand contains the definition of tBTC Wallet Coordinator tools.
Expand All @@ -36,8 +38,8 @@ var CoordinatorCommand = &cobra.Command{
},
}

var depositsCommand = cobra.Command{
Use: "deposits",
var listDepositsCommand = cobra.Command{
Use: "list-deposits",
Short: "get list of deposits",
Long: "Gets tBTC deposits details from the chain and prints them.",
TraverseChildren: true,
Expand Down Expand Up @@ -94,6 +96,69 @@ var depositsCommand = cobra.Command{
},
}

var proposeDepositsSweepCommand = cobra.Command{
Use: "propose-deposits-sweep",
Short: "propose deposits sweep",
Long: proposeDepositsSweepCommandDescription,
TraverseChildren: true,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
return err
}

for i, arg := range args {
if err := coordinator.ValidateDepositString(arg); err != nil {
return fmt.Errorf(
"argument [%d] failed validation: %v",
i,
err,
)
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

wallet, err := cmd.Flags().GetString(walletFlagName)
if err != nil {
return fmt.Errorf("failed to find wallet flag: %v", err)
}

fee, err := cmd.Flags().GetInt64(feeFlagName)
if err != nil {
return fmt.Errorf("failed to find fee flag: %v", err)
}

dryRun, err := cmd.Flags().GetBool(dryRunFlagName)
if err != nil {
return fmt.Errorf("failed to find fee flag: %v", err)
}

_, tbtcChain, _, _, _, err := ethereum.Connect(cmd.Context(), clientConfig.Ethereum)
if err != nil {
return fmt.Errorf(
"could not connect to Ethereum chain: [%v]",
err,
)
}

btcChain, err := electrum.Connect(ctx, clientConfig.Bitcoin.Electrum)
if err != nil {
return fmt.Errorf("could not connect to Electrum chain: [%v]", err)
}

return coordinator.ProposeDepositsSweep(tbtcChain, btcChain, wallet, fee, args, dryRun)
},
}

var proposeDepositsSweepCommandDescription = `Submits a deposits sweep proposal to
the chain.
Expects --wallet and --fee flags along with deposits to sweep provided
as arguments.
` + coordinator.DepositsFormatDescription

func init() {
initFlags(
CoordinatorCommand,
Expand All @@ -102,37 +167,61 @@ func init() {
config.General, config.Ethereum, config.BitcoinElectrum,
)

// Coordinator Command
CoordinatorCommand.PersistentFlags().String(
// Deposits Subcommand
listDepositsCommand.Flags().String(
walletFlagName,
"",
"wallet public key hash",
)

// Deposits Subcommand
depositsCommand.Flags().Bool(
listDepositsCommand.Flags().Bool(
hideSweptFlagName,
false,
"hide swept deposits",
)

depositsCommand.Flags().Bool(
listDepositsCommand.Flags().Bool(
sortByAmountFlagName,
false,
"sort by deposit amount",
)

depositsCommand.Flags().Int(
listDepositsCommand.Flags().Int(
headFlagName,
0,
"get head of deposits",
)

depositsCommand.Flags().Int(
listDepositsCommand.Flags().Int(
tailFlagName,
0,
"get tail of deposits",
)

CoordinatorCommand.AddCommand(&depositsCommand)
CoordinatorCommand.AddCommand(&listDepositsCommand)

// Propose Deposits Sweep Subcommand
proposeDepositsSweepCommand.Flags().String(
walletFlagName,
"",
"wallet public key hash",
)

if err := proposeDepositsSweepCommand.MarkFlagRequired(walletFlagName); err != nil {
logger.Panicf("failed to mark wallet flag as required: %v", err)
}

proposeDepositsSweepCommand.Flags().Int64(
feeFlagName,
0,
"fee for the entire bitcoin transaction (satoshi)",
)

proposeDepositsSweepCommand.Flags().Bool(
dryRunFlagName,
false,
"don't submit a proposal to the chain",
)

CoordinatorCommand.AddCommand(&proposeDepositsSweepCommand)
}
11 changes: 11 additions & 0 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ func (tc *TbtcChain) PastDepositRevealedEvents(
RefundPublicKeyHash: event.RefundPubKeyHash,
RefundLocktime: event.RefundLocktime,
Vault: vault,
BlockNumber: event.Raw.BlockNumber,
}

convertedEvents = append(convertedEvents, convertedEvent)
Expand Down Expand Up @@ -1348,3 +1349,13 @@ func (tc *TbtcChain) ValidateDepositSweepProposal(

return nil
}

func (tc *TbtcChain) SubmitDepositSweepProposal(
proposal *tbtc.DepositSweepProposal,
) error {
_, err := tc.walletCoordinator.SubmitDepositSweepProposal(
convertDepositSweepProposalToAbiType(proposal),
)

return err
}
5 changes: 3 additions & 2 deletions pkg/coordinator/deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func getDeposits(

func printTable(deposits []depositEntry) error {
w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, ' ', tabwriter.AlignRight)
fmt.Fprintf(w, "index\twallet\tvalue (BTC)\tdeposit key\tfunding transaction\tconfirmations\tswept\t\n")
fmt.Fprintf(w, "index\twallet\tvalue (BTC)\tdeposit key\trevealed deposit data\tconfirmations\tswept\t\n")

for i, deposit := range deposits {
fmt.Fprintf(w, "%d\t%s\t%.5f\t%s\t%s\t%d\t%t\t\n",
Expand All @@ -188,9 +188,10 @@ func printTable(deposits []depositEntry) error {
deposit.amountBtc,
deposit.depositKey,
fmt.Sprintf(
"%s:%d",
"%s:%d:%d",
deposit.fundingTransactionHash.Hex(bitcoin.ReversedByteOrder),
deposit.fundingTransactionOutputIndex,
deposit.revealBlock,
),
deposit.confirmations,
deposit.isSwept,
Expand Down
129 changes: 129 additions & 0 deletions pkg/coordinator/sweep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package coordinator

import (
"fmt"
"math/big"
"regexp"
"strconv"

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

type btcTransaction = struct {
FundingTxHash bitcoin.Hash
FundingOutputIndex uint32
}

const requiredFundingTxConfirmations = uint(6)

var (
DepositsFormatDescription = `Deposits details should be provided as strings containing:
- bitcoin transaction hash (unprefixed bitcoin transaction hash in reverse (RPC) order),
- bitcoin transaction output index,
- ethereum block number when the deposit was revealed to the chain.
The properties should be separated by semicolons, in the following format:
` + depositsFormatPattern + `
e.g. bd99d1d0a61fd104925d9b7ac997958aa8af570418b3fde091f7bfc561608865:1:8392394
`
depositsFormatPattern = "<unprefixed bitcoin transaction hash>:<bitcoin transaction output index>:<ethereum reveal block number>"
depositsFormatRegexp = regexp.MustCompile(`^([[:xdigit:]]+):(\d+):(\d+)$`)
)

// ProposeDepositsSweep handles deposit sweep proposal request submission.
func ProposeDepositsSweep(
tbtcChain tbtc.Chain,
btcChain bitcoin.Chain,
walletStr string,
fee int64,
depositsString []string,
dryRun bool,
) error {
walletPublicKeyHash, err := hexToWalletPublicKeyHash(walletStr)
if err != nil {
return fmt.Errorf("failed extract wallet public key hash: %v", err)
}

btcTransactions, depositsRevealBlocks, err := parseDeposits(depositsString)
if err != nil {
return fmt.Errorf("failed to parse arguments: %w", err)
}

proposal := &tbtc.DepositSweepProposal{
WalletPublicKeyHash: walletPublicKeyHash,
DepositsKeys: btcTransactions,
SweepTxFee: big.NewInt(fee),
DepositsRevealBlocks: depositsRevealBlocks,
}

logger.Infof("validating the proposal...")
if _, err := tbtc.ValidateDepositSweepProposal(
logger,
proposal,
requiredFundingTxConfirmations,
tbtcChain,
btcChain,
); err != nil {
return fmt.Errorf("failed to verify deposit sweep proposal: %v", err)
}

if !dryRun {
logger.Infof("submitting the proposal...")
if err := tbtcChain.SubmitDepositSweepProposal(proposal); err != nil {
return fmt.Errorf("failed to submit deposit sweep proposal: %v", err)
}
}

return nil
}

func parseDeposits(depositsStrings []string) ([]btcTransaction, []*big.Int, error) {
depositsKeys := make([]btcTransaction, len(depositsStrings))
depositsRevealBlocks := make([]*big.Int, len(depositsStrings))

for i, depositString := range depositsStrings {
matched := depositsFormatRegexp.FindStringSubmatch(depositString)
// Check if number of resolved entries match expected number of groups
// for the given regexp.
if len(matched) != 4 {
return nil, nil, fmt.Errorf("failed to parse deposit: [%s]", depositString)
}

txHash, err := bitcoin.NewHashFromString(matched[1], bitcoin.ReversedByteOrder)
if err != nil {
return nil, nil, fmt.Errorf("invalid bitcoin transaction hash [%s]: %v", matched[1], err)

}

outputIndex, err := strconv.ParseInt(matched[2], 10, 32)
if err != nil {
return nil, nil, fmt.Errorf("invalid bitcoin transaction output index [%s]: %v", matched[2], err)
}

revealBlock, err := strconv.ParseInt(matched[3], 10, 32)
if err != nil {
return nil, nil, fmt.Errorf("invalid reveal block number [%s]: %v", matched[3], err)
}

depositsKeys[i] = btcTransaction{
FundingTxHash: txHash,
FundingOutputIndex: uint32(outputIndex),
}

depositsRevealBlocks[i] = big.NewInt(revealBlock)
}

return depositsKeys, depositsRevealBlocks, nil
}

// ValidateDepositString validates format of the string containing deposit details.
func ValidateDepositString(depositString string) error {
if !depositsFormatRegexp.MatchString(depositString) {
return fmt.Errorf(
"[%s] doesn't match pattern: %s",
depositString,
depositsFormatPattern,
)
}
return nil
}
8 changes: 4 additions & 4 deletions pkg/internal/hexutils/hexutils_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package hexutils

import (
"bytes"
"fmt"
"reflect"
"testing"

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

type unmarshalTest struct {
Expand Down Expand Up @@ -60,9 +61,8 @@ func TestDecode(t *testing.T) {
if !reflect.DeepEqual(err, test.wantErr) {
t.Fatalf("unexpected error\nexpected: %v\nactual: %v", test.wantErr, err)
}
if !bytes.Equal(test.want, dec) {
t.Errorf("unexpected result\nexpected: %v\nactual: %v", test.want, dec)
}

testutils.AssertBytesEqual(t, test.want, dec)
})
}
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/tbtc/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ type WalletCoordinatorChain interface {
FundingTx *bitcoin.Transaction
},
) error

// SubmitDepositSweepProposal submits a deposit sweep proposal to the chain.
SubmitDepositSweepProposal(proposal *DepositSweepProposal) error
}

// HeartbeatRequestSubmittedEvent represents a wallet heartbeat request
Expand Down
4 changes: 4 additions & 0 deletions pkg/tbtc/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ func buildDepositSweepProposalValidationKey(
return sha256.Sum256(buffer.Bytes()), nil
}

func (lc *localChain) SubmitDepositSweepProposal(proposal *DepositSweepProposal) error {
panic("unsupported")
}

// Connect sets up the local chain.
func Connect() *localChain {
operatorPrivateKey, _, err := operator.GenerateKeyPair(local_v1.DefaultCurve)
Expand Down
Loading

0 comments on commit 6c6c6d0

Please sign in to comment.