From ea40b9f85ff303e927cd07d62b100d9533052f35 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Tue, 19 Nov 2024 21:00:20 -0300 Subject: [PATCH] feat: add cli to create babylon create btc delegation from staking tx --- cmd/stakercli/daemon/daemoncommands.go | 57 +++++++----------- staker/babylontypes.go | 1 + staker/stakerapp.go | 83 ++++++++++++++++++++------ stakerservice/client/rpcclient.go | 21 +++++++ stakerservice/service.go | 61 +++++++++++++++---- stakerservice/stakerdresponses.go | 2 + walletcontroller/client.go | 13 ++-- walletcontroller/interface.go | 3 +- 8 files changed, 167 insertions(+), 74 deletions(-) diff --git a/cmd/stakercli/daemon/daemoncommands.go b/cmd/stakercli/daemon/daemoncommands.go index 696cb50..40150d8 100644 --- a/cmd/stakercli/daemon/daemoncommands.go +++ b/cmd/stakercli/daemon/daemoncommands.go @@ -3,11 +3,14 @@ package daemon import ( "context" "errors" + "fmt" "strconv" "github.com/babylonlabs-io/btc-staker/cmd/stakercli/helpers" scfg "github.com/babylonlabs-io/btc-staker/stakercfg" dc "github.com/babylonlabs-io/btc-staker/stakerservice/client" + "github.com/babylonlabs-io/networks/parameters/parser" + "github.com/cometbft/cometbft/libs/os" "github.com/urfave/cli" ) @@ -358,7 +361,7 @@ func stake(ctx *cli.Context) error { func stakeFromPhase1TxBTC(ctx *cli.Context) error { daemonAddress := ctx.String(stakingDaemonAddressFlag) - client, err := dc.NewStakerServiceJsonRpcClient(daemonAddress) + client, err := dc.NewStakerServiceJSONRPCClient(daemonAddress) if err != nil { return err } @@ -369,44 +372,28 @@ func stakeFromPhase1TxBTC(ctx *cli.Context) error { if len(stakingTransactionHash) == 0 { return errors.New("staking tx hash hex is empty") } - // staking details is not good, because it loads from db, not BTC - // stakingTx, err := client.StakingDetails(sctx, stakingTransactionHash) - - // stakingTx. - - // sig, err := app.wc.SignBip322NativeSegwit(babylonAddrHash, stakerAddress) - - // if err != nil { - // return nil, err - // } - - // pop, err := cl.NewBabylonBip322Pop( - // babylonAddrHash, - // sig, - // stakerAddress, - // ) - - // if err := app.txTracker.AddTransactionSentToBTC( - // stakingTx, - // stakingOutputIdx, - // cmd.stakingTime, - // cmd.fpBtcPks, - // babylonPopToDbPop(cmd.pop), - // cmd.stakerAddress, - // ); err != nil { - // return nil, err - // } - // results, err := client.Stake(sctx, stakerAddress, stakingAmount, fpPks, stakingTimeBlocks, sendToBabylonFirst) - // if err != nil { - // return err - // } + inputGlobalParamsFilePath := ctx.Args().First() + if len(inputGlobalParamsFilePath) == 0 { + return errors.New("json file input is empty") + } - // client.WatchStaking() + if !os.FileExists(inputGlobalParamsFilePath) { + return fmt.Errorf("json file input %s does not exist", inputGlobalParamsFilePath) + } - // helpers.PrintRespJSON(results) + // QUEST: should the params be loaded from the chain? + // maybe it is good to still use the global as input as this is actually + // a phase1 tx being transitioned, so the user would already have the global + // params in hand to create the BTC staking tx + globalParams, err := parser.NewParsedGlobalParamsFromFile(inputGlobalParamsFilePath) + if err != nil { + return fmt.Errorf("error parsing file %s: %w", inputGlobalParamsFilePath, err) + } - return nil + stakerAddress := ctx.String(stakerAddressFlag) + _, err = client.BtcDelegationFromBtcStakingTx(sctx, stakerAddress, stakingTransactionHash, globalParams) + return err } func unstake(ctx *cli.Context) error { diff --git a/staker/babylontypes.go b/staker/babylontypes.go index 09579e6..f4ee61f 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -169,6 +169,7 @@ func (app *App) buildDelegation( ) return dg, nil } + return app.buildOwnedDelegation( req, stakerAddress, diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 48b7c22..843a624 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -9,6 +9,7 @@ import ( "time" "github.com/avast/retry-go/v4" + "github.com/babylonlabs-io/babylon/btcstaking" staking "github.com/babylonlabs-io/babylon/btcstaking" cl "github.com/babylonlabs-io/btc-staker/babylonclient" "github.com/babylonlabs-io/btc-staker/metrics" @@ -22,7 +23,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -507,6 +507,33 @@ func (app *App) mustSetTxSpentOnBtc(hash *chainhash.Hash) { } } +// AddBTCTransactionToDbAndCheckStatus receives a BTC staking transaction that should be +// already included in the BTC network +func (app *App) AddBTCTransactionToDbAndCheckStatus( + stakerAddr btcutil.Address, + stkTx *wire.MsgTx, + parsedStakingTx *btcstaking.ParsedV0StakingTx, +) error { + pop, err := app.createPop(stakerAddr) + if err != nil { + return err + } + + if err := app.addTransactionSentToBTC( + stkTx, + uint32(parsedStakingTx.StakingOutputIdx), + parsedStakingTx.OpReturnData.StakingTime, + []*btcec.PublicKey{parsedStakingTx.OpReturnData.FinalityProviderPublicKey.PubKey}, + babylonPopToDBPop(pop), + stakerAddr, + ); err != nil { + return err + } + + // not fun to call check transaction, because there is no request and way to get the tx result + return app.checkTransactionsStatus() +} + // TODO: We should also handle case when btc node or babylon node lost data and start from scratch // i.e keep track what is last known block height on both chains and detect if after restart // for some reason they are behind staker @@ -1826,25 +1853,11 @@ func (app *App) StakeFunds( // build proof of possession, no point moving forward if staker do not have all // the necessary keys stakerPubKey, err := app.wc.AddressPublicKey(stakerAddress) - if err != nil { return nil, err } - babylonAddrHash := tmhash.Sum(app.babylonClient.GetKeyAddress().Bytes()) - - sig, err := app.wc.SignBip322NativeSegwit(babylonAddrHash, stakerAddress) - - if err != nil { - return nil, err - } - - pop, err := cl.NewBabylonBip322Pop( - babylonAddrHash, - sig, - stakerAddress, - ) - + pop, err := app.createPop(stakerAddress) if err != nil { return nil, err } @@ -2208,6 +2221,42 @@ func (app *App) UnbondStaking( return &unbondingTxHash, nil } -func (app *StakerApp) TxDetailsBTC(stakingTxHash *chainhash.Hash) (*btcjson.GetTransactionResult, *btcutil.Tx, error) { +func (app *App) addTransactionSentToBTC( + stakingTx *wire.MsgTx, + stakingOutputIdx uint32, + stakingTime uint16, + fpPubKeys []*btcec.PublicKey, + pop *stakerdb.ProofOfPossession, + btcStakerAddr btcutil.Address, +) error { + return app.txTracker.AddTransactionSentToBTC( + stakingTx, + stakingOutputIdx, + stakingTime, + fpPubKeys, + pop, + btcStakerAddr, + ) +} + +func (app *App) BtcParams() *chaincfg.Params { + return app.network +} + +func (app *App) TxDetailsBTC(stakingTxHash *chainhash.Hash) (*btcutil.Tx, error) { return app.wc.Tx(stakingTxHash) } + +func (app *App) createPop(stakerAddress btcutil.Address) (*cl.BabylonPop, error) { + babylonAddrHash := tmhash.Sum(app.babylonClient.GetKeyAddress().Bytes()) + sig, err := app.wc.SignBip322NativeSegwit(babylonAddrHash, stakerAddress) + if err != nil { + return nil, err + } + + return cl.NewBabylonBip322Pop( + babylonAddrHash, + sig, + stakerAddress, + ) +} diff --git a/stakerservice/client/rpcclient.go b/stakerservice/client/rpcclient.go index 48070cc..a23db07 100644 --- a/stakerservice/client/rpcclient.go +++ b/stakerservice/client/rpcclient.go @@ -4,6 +4,7 @@ import ( "context" service "github.com/babylonlabs-io/btc-staker/stakerservice" + "github.com/babylonlabs-io/networks/parameters/parser" jsonrpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" ) @@ -85,6 +86,26 @@ func (c *StakerServiceJSONRPCClient) Stake( return result, nil } +func (c *StakerServiceJSONRPCClient) BtcDelegationFromBtcStakingTx( + ctx context.Context, + stakerAddress string, + btcStkTxHash string, + globalParams *parser.ParsedGlobalParams, +) (*service.ResultBtcDelegationFromBtcStakingTx, error) { + result := new(service.ResultBtcDelegationFromBtcStakingTx) + + params := make(map[string]interface{}) + params["stakerAddress"] = stakerAddress + params["btcStkTxHash"] = btcStkTxHash + params["globalParams"] = globalParams + + _, err := c.client.Call(ctx, "btc_delegation_from_btc_staking_tx", params, result) + if err != nil { + return nil, err + } + return result, nil +} + func (c *StakerServiceJSONRPCClient) ListStakingTransactions(ctx context.Context, offset *int, limit *int) (*service.ListStakingTransactionsResponse, error) { result := new(service.ListStakingTransactionsResponse) diff --git a/stakerservice/service.go b/stakerservice/service.go index d3fe671..3109bc7 100644 --- a/stakerservice/service.go +++ b/stakerservice/service.go @@ -12,13 +12,16 @@ import ( "strings" "sync/atomic" + "github.com/babylonlabs-io/babylon/btcstaking" "github.com/babylonlabs-io/btc-staker/babylonclient" str "github.com/babylonlabs-io/btc-staker/staker" scfg "github.com/babylonlabs-io/btc-staker/stakercfg" "github.com/babylonlabs-io/btc-staker/stakerdb" + "github.com/babylonlabs-io/networks/parameters/parser" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/cometbft/cometbft/libs/log" @@ -125,20 +128,56 @@ func (s *StakerService) stake(_ *rpctypes.Context, }, nil } -func (s *StakerService) bbnStakeFromBTCStakingTx(_ *rpctypes.Context, +func (s *StakerService) btcDelegationFromBtcStakingTx( + _ *rpctypes.Context, + stakerAddress string, btcStkTxHash string, -) (*struct{}, error) { + globalParams *parser.ParsedGlobalParams, +) (*ResultBtcDelegationFromBtcStakingTx, error) { stkTxHash, err := chainhash.NewHashFromStr(btcStkTxHash) if err != nil { return nil, err } - tx, _, err := s.staker.TxDetailsBTC(stkTxHash) + stkTx, err := s.staker.TxDetailsBTC(stkTxHash) + if err != nil { + return nil, err + } + + stakerAddr, err := btcutil.DecodeAddress(stakerAddress, &s.config.ActiveNetParams) + if err != nil { + return nil, err + } + + wireStkTx := stkTx.MsgTx() + parsedStakingTx, err := parseV0StakingTx(globalParams, s.staker.BtcParams(), wireStkTx) if err != nil { return nil, err } - return nil, nil + if err := s.staker.AddBTCTransactionToDbAndCheckStatus(stakerAddr, wireStkTx, parsedStakingTx); err != nil { + return nil, err + } + + return &ResultBtcDelegationFromBtcStakingTx{}, nil +} + +func parseV0StakingTx(globalParams *parser.ParsedGlobalParams, btcParams *chaincfg.Params, wireStkTx *wire.MsgTx) (*btcstaking.ParsedV0StakingTx, error) { + for i := len(globalParams.Versions) - 1; i >= 0; i-- { + params := globalParams.Versions[i] + parsedStakingTx, err := btcstaking.ParseV0StakingTx( + wireStkTx, + params.Tag, + params.CovenantPks, + params.CovenantQuorum, + btcParams, + ) + if err != nil { + continue + } + return parsedStakingTx, nil + } + return nil, fmt.Errorf("failed to parse BTC staking tx %s using the global params %+v", wireStkTx.TxHash().String(), globalParams) } func (s *StakerService) stakingDetails(_ *rpctypes.Context, @@ -562,13 +601,13 @@ func (s *StakerService) GetRoutes() RoutesMap { // info AP "health": rpc.NewRPCFunc(s.health, ""), // staking API - "stake": rpc.NewRPCFunc(s.stake, "stakerAddress,stakingAmount,fpBtcPks,stakingTimeBlocks,sendToBabylonFirst"), - "bbnStakeFromBTCStakingTx": rpc.NewRPCFunc(s.bbnStakeFromBTCStakingTx, "btcStkTxHash"), - "staking_details": rpc.NewRPCFunc(s.stakingDetails, "stakingTxHash"), - "spend_stake": rpc.NewRPCFunc(s.spendStake, "stakingTxHash"), - "list_staking_transactions": rpc.NewRPCFunc(s.listStakingTransactions, "offset,limit"), - "unbond_staking": rpc.NewRPCFunc(s.unbondStaking, "stakingTxHash"), - "withdrawable_transactions": rpc.NewRPCFunc(s.withdrawableTransactions, "offset,limit"), + "stake": rpc.NewRPCFunc(s.stake, "stakerAddress,stakingAmount,fpBtcPks,stakingTimeBlocks,sendToBabylonFirst"), + "btc_delegation_from_btc_staking_tx": rpc.NewRPCFunc(s.btcDelegationFromBtcStakingTx, "stakerAddress,btcStkTxHash,globalParams"), + "staking_details": rpc.NewRPCFunc(s.stakingDetails, "stakingTxHash"), + "spend_stake": rpc.NewRPCFunc(s.spendStake, "stakingTxHash"), + "list_staking_transactions": rpc.NewRPCFunc(s.listStakingTransactions, "offset,limit"), + "unbond_staking": rpc.NewRPCFunc(s.unbondStaking, "stakingTxHash"), + "withdrawable_transactions": rpc.NewRPCFunc(s.withdrawableTransactions, "offset,limit"), // watch api "watch_staking_tx": rpc.NewRPCFunc(s.watchStaking, "stakingTx,stakingTime,stakingValue,stakerBtcPk,fpBtcPks,slashingTx,slashingTxSig,stakerBabylonAddr,stakerAddress,stakerBtcSig,unbondingTx,slashUnbondingTx,slashUnbondingTxSig,unbondingTime,popType"), diff --git a/stakerservice/stakerdresponses.go b/stakerservice/stakerdresponses.go index 3a76474..286b136 100644 --- a/stakerservice/stakerdresponses.go +++ b/stakerservice/stakerdresponses.go @@ -2,6 +2,8 @@ package stakerservice type ResultHealth struct{} +type ResultBtcDelegationFromBtcStakingTx struct{} + type ResultStake struct { TxHash string `json:"tx_hash"` } diff --git a/walletcontroller/client.go b/walletcontroller/client.go index dd1efef..8dc0833 100644 --- a/walletcontroller/client.go +++ b/walletcontroller/client.go @@ -268,19 +268,14 @@ func (w *RPCWalletController) getTxDetails(req notifier.ConfRequest, msg string) return res, nofitierStateToWalletState(state), nil } -// Tx implements WalletController. -func (w *RpcWalletController) Tx(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, *btcutil.Tx, error) { - tx, err := w.Client.GetTransaction(txHash) - if err != nil { - return nil, nil, err - } - +// Tx returns the raw transaction based on the transaction hash +func (w *RPCWalletController) Tx(txHash *chainhash.Hash) (*btcutil.Tx, error) { rawTx, err := w.Client.GetRawTransaction(txHash) if err != nil { - return nil, nil, err + return nil, err } - return tx, rawTx, nil + return rawTx, nil } // Fetch info about transaction from mempool or blockchain, requires node to have enabled transaction index diff --git a/walletcontroller/interface.go b/walletcontroller/interface.go index 66eab7a..c3ecd23 100644 --- a/walletcontroller/interface.go +++ b/walletcontroller/interface.go @@ -3,7 +3,6 @@ package walletcontroller import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" @@ -67,7 +66,7 @@ type WalletController interface { SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) ListOutputs(onlySpendable bool) ([]Utxo, error) TxDetails(txHash *chainhash.Hash, pkScript []byte) (*notifier.TxConfirmation, TxStatus, error) - Tx(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, *btcutil.Tx, error) + Tx(txHash *chainhash.Hash) (*btcutil.Tx, error) SignBip322NativeSegwit(msg []byte, address btcutil.Address) (wire.TxWitness, error) // SignOneInputTaprootSpendingTransaction signs transactions with one taproot input that // uses script spending path.