From 35c690a22e9449f47697ed5fdcd51d6aa99d4303 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Fri, 29 Nov 2024 00:22:22 -0300 Subject: [PATCH] feat: new command phase1 stake to BTC delegation (#90) * chore: init command draft * chore: add get tx details from tx hash only * chore: add initial impl to btc staking from phase 1 * feat: add cli to create babylon create btc delegation from staking tx * chore: add #90 to changelog * chore: add stake command to CLI * fix: lint * chore: move cli and manager to separated file * chore: manager split btc data * chore: split func to generate covenant pk * chore: add staking of phase 1 test * fix: lint * trytest: stuck at including finality provider to running chain * chore: add last err to debug * pass json serializable params * chore: refactory send transaction to phase 1 * chore: fix comment * chore: address PR comments * chore: rollback rand * chore: use rand previous created * chore: add possible responde of BTC delegation to consumer * chore: try to refactory to receive BTC delegation tx hash * chore: moved to use command * chore: add todo * chore: fix lint and allow nonamedreturns * chore: remove go routine of mine empty blocks * chore: add new field to transaction tracked of tx hash of btc delegation * tryfix: get btc delegation tx hash * fix: lint removed unused func * chore: add goroutine to return the cmd request * chore: address pr comment and simplified the stking tx migration * chore: add btc del tx hash to delegation sent to babylon * fix: lint removed unused func * chore: removed txInclusionHeightFlag flag and getting the block height by querying the btc * chore: rename BtcDelegationTxHash to BabylonBTCDelegationTxHash * chore: update error msg * fix: lint * chore: add back tx-inclusion-height as optional, if not set queries the tx hash --------- Co-authored-by: KonradStaniec --- .golangci.yml | 2 +- CHANGELOG.md | 2 + babylonclient/babyloncontroller.go | 1 - cmd/stakercli/daemon/daemoncommands.go | 83 ++ .../transaction/transactions_test.go | 82 +- itest/bitcoind_node_setup.go | 3 +- itest/containers/containers.go | 16 +- itest/e2e_test.go | 1298 +++-------------- itest/manager.go | 1218 ++++++++++++++++ itest/testutil/appcli.go | 127 ++ proto/transaction.pb.go | 134 +- proto/transaction.proto | 1 + staker/babylontypes.go | 21 +- staker/commands.go | 27 + staker/events.go | 7 +- staker/stakerapp.go | 386 +++-- stakerdb/trackedtranactionstore.go | 26 +- stakerdb/trackedtransactionstore_test.go | 4 +- stakerservice/client/rpcclient.go | 55 + stakerservice/service.go | 119 +- stakerservice/stakerdresponses.go | 11 + walletcontroller/client.go | 15 + walletcontroller/interface.go | 35 + 23 files changed, 2238 insertions(+), 1435 deletions(-) create mode 100644 itest/manager.go create mode 100644 itest/testutil/appcli.go diff --git a/.golangci.yml b/.golangci.yml index 717f7ad..5872f4d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,7 +39,7 @@ linters: - nilerr # - nlreturn # Style wise I personally like this one, todo(lazar): unlax at somepoint, good practice - noctx - - nonamedreturns + # - nonamedreturns - nosprintfhostport - paralleltest - reassign diff --git a/CHANGELOG.md b/CHANGELOG.md index 1115d1e..4b771e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Improvements +* [#90](https://github.com/babylonlabs-io/btc-staker/pull/90) Add CLI to create +babylon BTC delegation from phase-1 BTC staking transaction. * [#99](https://github.com/babylonlabs-io/btc-staker/pull/99) Bump babylon version and adapt staker to the changes diff --git a/babylonclient/babyloncontroller.go b/babylonclient/babyloncontroller.go index 4a5ebc8..347f660 100644 --- a/babylonclient/babyloncontroller.go +++ b/babylonclient/babyloncontroller.go @@ -723,7 +723,6 @@ func (bc *BabylonController) RegisterFinalityProvider( Pop: pop, } - fpPrivKeyBBN.PubKey() relayerMsgs := bbnclient.ToProviderMsgs([]sdk.Msg{registerMsg}) _, err := bc.bbnClient.SendMessageWithSigner(context.Background(), fpAddr, fpPrivKeyBBN, relayerMsgs) diff --git a/cmd/stakercli/daemon/daemoncommands.go b/cmd/stakercli/daemon/daemoncommands.go index f62a38a..67e38e7 100644 --- a/cmd/stakercli/daemon/daemoncommands.go +++ b/cmd/stakercli/daemon/daemoncommands.go @@ -2,11 +2,15 @@ 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" ) @@ -26,6 +30,7 @@ var DaemonCommands = []cli.Command{ listStakingTransactionsCmd, withdrawableTransactionsCmd, unbondCmd, + stakeFromPhase1Cmd, }, }, } @@ -37,6 +42,7 @@ const ( fpPksFlag = "finality-providers-pks" stakingTransactionHashFlag = "staking-transaction-hash" stakerAddressFlag = "staker-address" + txInclusionHeightFlag = "tx-inclusion-height" ) var ( @@ -133,6 +139,36 @@ var stakeCmd = cli.Command{ Action: stake, } +var stakeFromPhase1Cmd = cli.Command{ + Name: "stake-from-phase1", + ShortName: "stfp1", + Usage: "\nstakercli daemon stake-from-phase1 [fullpath/to/global_parameters.json]" + + " --staking-transaction-hash [txHashHex] --staker-address [btcStakerAddrHex] --tx-inclusion-height [blockHeightTxInclusion]", + Description: "Creates a Babylon BTC delegation transaction from the Phase1 BTC staking transaction", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: stakingDaemonAddressFlag, + Usage: "full address of the staker daemon in format tcp:://:", + Value: defaultStakingDaemonAddress, + }, + cli.StringFlag{ + Name: stakingTransactionHashFlag, + Usage: "Hash of original staking transaction in bitcoin hex format", + Required: true, + }, + cli.StringFlag{ + Name: stakerAddressFlag, + Usage: "BTC address of the staker in hex", + Required: true, + }, + cli.Uint64Flag{ + Name: txInclusionHeightFlag, + Usage: "Expected BTC height at which transaction was included. This value is important to choose correct global parameters for transaction, if set doesn't query bitcoin to get the block height from txHash", + }, + }, + Action: stakeFromPhase1TxBTC, +} + var unstakeCmd = cli.Command{ Name: "unstake", ShortName: "ust", @@ -335,6 +371,53 @@ func stake(ctx *cli.Context) error { return nil } +func stakeFromPhase1TxBTC(ctx *cli.Context) error { + daemonAddress := ctx.String(stakingDaemonAddressFlag) + client, err := dc.NewStakerServiceJSONRPCClient(daemonAddress) + if err != nil { + return err + } + + sctx := context.Background() + stakingTransactionHash := ctx.String(stakingTransactionHashFlag) + if len(stakingTransactionHash) == 0 { + return errors.New("staking tx hash hex is empty") + } + + inputGlobalParamsFilePath := ctx.Args().First() + if len(inputGlobalParamsFilePath) == 0 { + return errors.New("json file input is empty") + } + + if !os.FileExists(inputGlobalParamsFilePath) { + return fmt.Errorf("json file input %s does not exist", inputGlobalParamsFilePath) + } + + globalParams, err := parser.NewParsedGlobalParamsFromFile(inputGlobalParamsFilePath) + if err != nil { + return fmt.Errorf("error parsing file %s: %w", inputGlobalParamsFilePath, err) + } + + blockHeighTxInclusion := ctx.Uint64(txInclusionHeightFlag) + if blockHeighTxInclusion == 0 { + resp, err := client.BtcTxDetails(sctx, stakingTransactionHash) + if err != nil { + return fmt.Errorf("error to get btc tx and block data from staking tx %s: %w", stakingTransactionHash, err) + } + + blockHeighTxInclusion = uint64(resp.Blk.Height) + } + + paramsForHeight := globalParams.GetVersionedGlobalParamsByHeight(blockHeighTxInclusion) + if paramsForHeight == nil { + return fmt.Errorf("error getting param version from global params %s with height %d", inputGlobalParamsFilePath, blockHeighTxInclusion) + } + + stakerAddress := ctx.String(stakerAddressFlag) + _, err = client.BtcDelegationFromBtcStakingTx(sctx, stakerAddress, stakingTransactionHash, paramsForHeight) + return err +} + func unstake(ctx *cli.Context) error { daemonAddress := ctx.String(stakingDaemonAddressFlag) client, err := dc.NewStakerServiceJSONRPCClient(daemonAddress) diff --git a/cmd/stakercli/transaction/transactions_test.go b/cmd/stakercli/transaction/transactions_test.go index 8208724..cd7c143 100644 --- a/cmd/stakercli/transaction/transactions_test.go +++ b/cmd/stakercli/transaction/transactions_test.go @@ -28,9 +28,8 @@ import ( "github.com/stretchr/testify/require" "github.com/urfave/cli" - cmdadmin "github.com/babylonlabs-io/btc-staker/cmd/stakercli/admin" - cmddaemon "github.com/babylonlabs-io/btc-staker/cmd/stakercli/daemon" "github.com/babylonlabs-io/btc-staker/cmd/stakercli/transaction" + "github.com/babylonlabs-io/btc-staker/itest/testutil" "github.com/babylonlabs-io/btc-staker/utils" ) @@ -71,9 +70,6 @@ var ( Versions: []*parser.VersionedGlobalParams{&defaultParam}, } - //nolint:errchkjson - paramsMarshalled, _ = json.Marshal(globalParams) - parsedGlobalParams, _ = parser.ParseGlobalParams(&globalParams) lastParams = parsedGlobalParams.Versions[len(parsedGlobalParams.Versions)-1] ) @@ -110,36 +106,12 @@ func FuzzFinalityProviderDeposit(f *testing.F) { fmt.Sprintf("--staking-time=%d", fpStakingTimeLock), } - app := testApp() - stakingTx := appRunCreatePhase1StakingTx(r, t, app, append(createTxCmdArgs, commonFlags...)) + app := testutil.TestApp() + stakingTx := testutil.AppRunCreatePhase1StakingTx(r, t, app, append(createTxCmdArgs, commonFlags...)) require.NotNil(t, stakingTx) }) } -func appRunCreatePhase1StakingTxWithParams(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1StakingTxResponse { - args := []string{"stakercli", "transaction", "create-phase1-staking-transaction-with-params"} - args = append(args, arguments...) - output := appRunWithOutput(r, t, app, args) - - var data transaction.CreatePhase1StakingTxResponse - err := json.Unmarshal([]byte(output), &data) - require.NoError(t, err) - - return data -} - -func appRunCreatePhase1StakingTx(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1StakingTxResponse { - args := []string{"stakercli", "transaction", "create-phase1-staking-transaction"} - args = append(args, arguments...) - output := appRunWithOutput(r, t, app, args) - - var data transaction.CreatePhase1StakingTxResponse - err := json.Unmarshal([]byte(output), &data) - require.NoError(t, err) - - return data -} - func appRunCheckPhase1StakingTxParams(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CheckPhase1StakingTxResponse { args := []string{"stakercli", "transaction", "check-phase1-staking-transaction-params"} args = append(args, arguments...) @@ -192,15 +164,6 @@ func readFromFile(t *testing.T, f *os.File) string { return buf.String() } -func testApp() *cli.App { - app := cli.NewApp() - app.Name = "stakercli" - app.Commands = append(app.Commands, cmddaemon.DaemonCommands...) - app.Commands = append(app.Commands, cmdadmin.AdminCommands...) - app.Commands = append(app.Commands, transaction.TransactionCommands...) - return app -} - func appRunCreatePhase1UnbondingTx(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1UnbondingTxResponse { args := []string{"stakercli", "transaction", "create-phase1-unbonding-transaction"} args = append(args, arguments...) @@ -223,19 +186,8 @@ func appRunCreatePhase1WithdrawalTx(r *rand.Rand, t *testing.T, app *cli.App, ar return data } -func randRange(_ *rand.Rand, min, max int) int { - return rand.Intn(max+1-min) + min -} - -func createTempFileWithParams(f *testing.F) string { - file, err := os.CreateTemp("", "tmpParams-*.json") - require.NoError(f, err) - defer file.Close() - _, err = file.Write(paramsMarshalled) - require.NoError(f, err) - info, err := file.Stat() - require.NoError(f, err) - return filepath.Join(os.TempDir(), info.Name()) +func randRange(r *rand.Rand, minV, maxV int) int { + return r.Intn(maxV+1-minV) + minV } type StakeParameters struct { @@ -280,7 +232,7 @@ func createCustomValidStakeParams( func TestCheckPhase1StakingTransactionCmd(t *testing.T) { t.Parallel() - app := testApp() + app := testutil.TestApp() stakerCliCheckP1StkTx := []string{ "stakercli", "transaction", "check-phase1-staking-transaction", "--covenant-quorum=1", @@ -386,12 +338,12 @@ func TestCheckPhase1StakingTransactionCmd(t *testing.T) { // Property: Every create should end without error for valid params func FuzzCreatPhase1Tx(f *testing.F) { - paramsFilePath := createTempFileWithParams(f) + paramsFilePath := testutil.CreateTempFileWithParams(f) datagen.AddRandomSeedsToFuzzer(f, 5) f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) - app := testApp() + app := testutil.TestApp() var args []string args = append(args, paramsFilePath) @@ -400,7 +352,7 @@ func FuzzCreatPhase1Tx(f *testing.F) { args = append(args, createArgs...) - resCreate := appRunCreatePhase1StakingTxWithParams( + resCreate := testutil.AppRunCreatePhase1StakingTxWithParams( r, t, app, args, ) require.NotNil(t, resCreate) @@ -412,12 +364,12 @@ func keyToSchnorrHex(key *btcec.PublicKey) string { } func FuzzCheckPhase1Tx(f *testing.F) { - paramsFilePath := createTempFileWithParams(f) + paramsFilePath := testutil.CreateTempFileWithParams(f) datagen.AddRandomSeedsToFuzzer(f, 5) f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) - app := testApp() + app := testutil.TestApp() stakerParams, _ := createCustomValidStakeParams(t, r, &globalParams, &chaincfg.RegressionNetParams) @@ -462,7 +414,7 @@ func FuzzCheckPhase1Tx(f *testing.F) { } func FuzzCreateUnbondingTx(f *testing.F) { - paramsFilePath := createTempFileWithParams(f) + paramsFilePath := testutil.CreateTempFileWithParams(f) datagen.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { @@ -495,7 +447,7 @@ func FuzzCreateUnbondingTx(f *testing.F) { fmt.Sprintf("--network=%s", chaincfg.RegressionNetParams.Name), } - app := testApp() + app := testutil.TestApp() unbondingTxResponse := appRunCreatePhase1UnbondingTx(r, t, app, createTxCmdArgs) require.NotNil(t, unbondingTxResponse) utx, _, err := bbn.NewBTCTxFromHex(unbondingTxResponse.UnbondingTxHex) @@ -512,7 +464,7 @@ func FuzzCreateUnbondingTx(f *testing.F) { } func FuzzCreateWithdrawalStaking(f *testing.F) { - paramsFilePath := createTempFileWithParams(f) + paramsFilePath := testutil.CreateTempFileWithParams(f) datagen.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { @@ -557,7 +509,7 @@ func FuzzCreateWithdrawalStaking(f *testing.F) { fmt.Sprintf("--network=%s", chaincfg.RegressionNetParams.Name), } - app := testApp() + app := testutil.TestApp() wr := appRunCreatePhase1WithdrawalTx(r, t, app, createTxCmdArgs) require.NotNil(t, wr) @@ -600,7 +552,7 @@ func FuzzCreateWithdrawalStaking(f *testing.F) { } func FuzzCreateWithdrawalUnbonding(f *testing.F) { - paramsFilePath := createTempFileWithParams(f) + paramsFilePath := testutil.CreateTempFileWithParams(f) datagen.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { @@ -668,7 +620,7 @@ func FuzzCreateWithdrawalUnbonding(f *testing.F) { fmt.Sprintf("--network=%s", chaincfg.RegressionNetParams.Name), } - app := testApp() + app := testutil.TestApp() wr := appRunCreatePhase1WithdrawalTx(r, t, app, createTxCmdArgs) require.NotNil(t, wr) diff --git a/itest/bitcoind_node_setup.go b/itest/bitcoind_node_setup.go index 30d9084..fb3b6ea 100644 --- a/itest/bitcoind_node_setup.go +++ b/itest/bitcoind_node_setup.go @@ -3,13 +3,14 @@ package e2etest import ( "encoding/json" "fmt" - "github.com/ory/dockertest/v3" "os" "strconv" "strings" "testing" "time" + "github.com/ory/dockertest/v3" + "github.com/babylonlabs-io/btc-staker/itest/containers" "github.com/stretchr/testify/require" ) diff --git a/itest/containers/containers.go b/itest/containers/containers.go index 0ad3776..31d498d 100644 --- a/itest/containers/containers.go +++ b/itest/containers/containers.go @@ -179,19 +179,11 @@ func (m *Manager) RunBabylondResource( coventantQuorum int, baseHeaderHex string, slashingPkScript string, - covenantPk1 *btcec.PublicKey, - covenantPk2 *btcec.PublicKey, - covenantPk3 *btcec.PublicKey, + covenantPks ...*btcec.PublicKey, ) (*dockertest.Resource, error) { - covenantPks := []*bbn.BIP340PubKey{ - bbn.NewBIP340PubKeyFromBTCPK(covenantPk1), - bbn.NewBIP340PubKeyFromBTCPK(covenantPk2), - bbn.NewBIP340PubKeyFromBTCPK(covenantPk3), - } - - var covenantPksStr []string - for _, pk := range covenantPks { - covenantPksStr = append(covenantPksStr, pk.MarshalHex()) + covenantPksStr := make([]string, len(covenantPks)) + for i, cvPk := range covenantPks { + covenantPksStr[i] = bbn.NewBIP340PubKeyFromBTCPK(cvPk).MarshalHex() } cmd := []string{ diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 11f0b48..7241cdb 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -7,1138 +7,36 @@ import ( "bytes" "context" "encoding/hex" - "errors" + "encoding/json" "fmt" - "math/rand" - "net" - "net/netip" - "os" - "path/filepath" - "strconv" - "sync" "testing" "time" "github.com/babylonlabs-io/btc-staker/itest/containers" "github.com/babylonlabs-io/btc-staker/itest/testutil" + "github.com/babylonlabs-io/networks/parameters/parser" + "github.com/babylonlabs-io/babylon/btcstaking" "github.com/babylonlabs-io/babylon/crypto/bip322" btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" - "github.com/cometbft/cometbft/crypto/tmhash" - staking "github.com/babylonlabs-io/babylon/btcstaking" - txformat "github.com/babylonlabs-io/babylon/btctxformatter" "github.com/babylonlabs-io/babylon/testutil/datagen" bbntypes "github.com/babylonlabs-io/babylon/types" - btcstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" - ckpttypes "github.com/babylonlabs-io/babylon/x/checkpointing/types" - "github.com/babylonlabs-io/btc-staker/babylonclient" - "github.com/babylonlabs-io/btc-staker/metrics" "github.com/babylonlabs-io/btc-staker/proto" "github.com/babylonlabs-io/btc-staker/staker" "github.com/babylonlabs-io/btc-staker/stakercfg" - service "github.com/babylonlabs-io/btc-staker/stakerservice" - dc "github.com/babylonlabs-io/btc-staker/stakerservice/client" "github.com/babylonlabs-io/btc-staker/types" - "github.com/babylonlabs-io/btc-staker/utils" "github.com/babylonlabs-io/btc-staker/walletcontroller" - "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" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkquerytypes "github.com/cosmos/cosmos-sdk/types/query" - sttypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/lightningnetwork/lnd/kvdb" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) -// bitcoin params used for testing -var ( - r = rand.New(rand.NewSource(time.Now().Unix())) - - regtestParams = &chaincfg.RegressionNetParams - - eventuallyWaitTimeOut = 10 * time.Second - eventuallyPollTime = 250 * time.Millisecond -) - -// keyToAddr maps the passed private to corresponding p2pkh address. -func keyToAddr(key *btcec.PrivateKey, net *chaincfg.Params) (btcutil.Address, error) { - serializedKey := key.PubKey().SerializeCompressed() - pubKeyAddr, err := btcutil.NewAddressPubKey(serializedKey, net) - if err != nil { - return nil, err - } - return pubKeyAddr.AddressPubKeyHash(), nil -} - -func defaultStakerConfig(t *testing.T, walletName, passphrase, bitcoindHost string) (*stakercfg.Config, *rpcclient.Client) { - defaultConfig := stakercfg.DefaultConfig() - - // both wallet and node are bicoind - defaultConfig.BtcNodeBackendConfig.ActiveWalletBackend = types.BitcoindWalletBackend - defaultConfig.BtcNodeBackendConfig.ActiveNodeBackend = types.BitcoindNodeBackend - defaultConfig.ActiveNetParams = *regtestParams - - // Fees configuration - defaultConfig.BtcNodeBackendConfig.FeeMode = "dynamic" - defaultConfig.BtcNodeBackendConfig.EstimationMode = types.DynamicFeeEstimation - - bitcoindUser := "user" - bitcoindPass := "pass" - - // Wallet configuration - defaultConfig.WalletRPCConfig.Host = bitcoindHost - defaultConfig.WalletRPCConfig.User = bitcoindUser - defaultConfig.WalletRPCConfig.Pass = bitcoindPass - defaultConfig.WalletRPCConfig.DisableTLS = true - defaultConfig.WalletConfig.WalletPass = passphrase - defaultConfig.WalletConfig.WalletName = walletName - - // node configuration - defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCHost = bitcoindHost - defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCUser = bitcoindUser - defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCPass = bitcoindPass - - // Use rpc polling, as it is our default mode and it is a bit more troublesome - // to configure ZMQ from inside the bitcoind docker container - defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCPolling = true - defaultConfig.BtcNodeBackendConfig.Bitcoind.BlockPollingInterval = 1 * time.Second - defaultConfig.BtcNodeBackendConfig.Bitcoind.TxPollingInterval = 1 * time.Second - - defaultConfig.StakerConfig.BabylonStallingInterval = 1 * time.Second - defaultConfig.StakerConfig.UnbondingTxCheckInterval = 1 * time.Second - defaultConfig.StakerConfig.CheckActiveInterval = 1 * time.Second - - // TODO: After bumping relayer version sending transactions concurrently fails wih - // fatal error: concurrent map writes - // For now diable concurrent sends but this need to be sorted out - defaultConfig.StakerConfig.MaxConcurrentTransactions = 1 - - testRpcClient, err := rpcclient.New(&rpcclient.ConnConfig{ - Host: bitcoindHost, - User: bitcoindUser, - Pass: bitcoindPass, - DisableTLS: true, - DisableConnectOnNew: true, - DisableAutoReconnect: false, - // we use post mode as it sure it works with either bitcoind or btcwallet - // we may need to re-consider it later if we need any notifications - HTTPPostMode: true, - }, nil) - require.NoError(t, err) - - return &defaultConfig, testRpcClient -} - -type TestManager struct { - Config *stakercfg.Config - Db kvdb.Backend - Sa *staker.App - BabylonClient *babylonclient.BabylonController - WalletPubKey *btcec.PublicKey - MinerAddr btcutil.Address - wg *sync.WaitGroup - serviceAddress string - StakerClient *dc.StakerServiceJSONRPCClient - CovenantPrivKeys []*btcec.PrivateKey - BitcoindHandler *BitcoindTestHandler - TestRpcClient *rpcclient.Client - manger *containers.Manager -} - -type testStakingData struct { - StakerKey *btcec.PublicKey - StakerBabylonAddr sdk.AccAddress - FinalityProviderBabylonPrivKeys []*secp256k1.PrivKey - FinalityProviderBabylonAddrs []sdk.AccAddress - FinalityProviderBtcPrivKeys []*btcec.PrivateKey - FinalityProviderBtcKeys []*btcec.PublicKey - StakingTime uint16 - StakingAmount int64 -} - -func (d *testStakingData) GetNumRestakedFPs() int { - return len(d.FinalityProviderBabylonPrivKeys) -} - -func (tm *TestManager) getTestStakingData( - t *testing.T, - stakerKey *btcec.PublicKey, - stakingTime uint16, - stakingAmount int64, - numRestakedFPs int, -) *testStakingData { - fpBTCSKs, fpBTCPKs, err := datagen.GenRandomBTCKeyPairs(r, numRestakedFPs) - require.NoError(t, err) - - fpBBNSKs, fpBBNAddrs := make([]*secp256k1.PrivKey, numRestakedFPs), make([]sdk.AccAddress, numRestakedFPs) - strAddrs := make([]string, numRestakedFPs) - for i := 0; i < numRestakedFPs; i++ { - fpBBNSK := secp256k1.GenPrivKey() - fpAddr := sdk.AccAddress(fpBBNSK.PubKey().Address().Bytes()) - - fpBBNSKs[i] = fpBBNSK - fpBBNAddrs[i] = fpAddr - strAddrs[i] = fpAddr.String() - } - - _, _, err = tm.manger.BabylondTxBankMultiSend(t, "node0", "1000000ubbn", strAddrs...) - require.NoError(t, err) - - return &testStakingData{ - StakerKey: stakerKey, - // the staker babylon addr needs to be the same one that is going to sign - // the transaction in the end - StakerBabylonAddr: tm.BabylonClient.GetKeyAddress(), - FinalityProviderBabylonPrivKeys: fpBBNSKs, - FinalityProviderBabylonAddrs: fpBBNAddrs, - FinalityProviderBtcPrivKeys: fpBTCSKs, - FinalityProviderBtcKeys: fpBTCPKs, - StakingTime: stakingTime, - StakingAmount: stakingAmount, - } -} - -func (td *testStakingData) withStakingTime(time uint16) *testStakingData { - tdCopy := *td - tdCopy.StakingTime = time - return &tdCopy -} - -func (td *testStakingData) withStakingAmout(amout int64) *testStakingData { - tdCopy := *td - tdCopy.StakingAmount = int64(amout) - return &tdCopy -} - -func StartManager( - t *testing.T, - ctx context.Context, - numMatureOutputsInWallet uint32, -) *TestManager { - manager, err := containers.NewManager(t) - require.NoError(t, err) - - bitcoindHandler := NewBitcoindHandler(t, manager) - bitcoind := bitcoindHandler.Start() - passphrase := "pass" - walletName := "test-wallet" - _ = bitcoindHandler.CreateWallet(walletName, passphrase) - // only outputs which are 100 deep are mature - br := bitcoindHandler.GenerateBlocks(int(numMatureOutputsInWallet) + 100) - - minerAddressDecoded, err := btcutil.DecodeAddress(br.Address, regtestParams) - require.NoError(t, err) - - quorum := 2 - numCovenants := 3 - var coventantPrivKeys []*btcec.PrivateKey - for i := 0; i < numCovenants; i++ { - covenantPrivKey, err := btcec.NewPrivateKey() - require.NoError(t, err) - coventantPrivKeys = append(coventantPrivKeys, covenantPrivKey) - } - - var buff bytes.Buffer - err = regtestParams.GenesisBlock.Header.Serialize(&buff) - require.NoError(t, err) - baseHeaderHex := hex.EncodeToString(buff.Bytes()) - - pkScript, err := txscript.PayToAddrScript(minerAddressDecoded) - require.NoError(t, err) - - tmpDir, err := testutil.TempDir(t) - require.NoError(t, err) - babylond, err := manager.RunBabylondResource( - t, - tmpDir, - quorum, - baseHeaderHex, - hex.EncodeToString(pkScript), // all slashing will be sent back to wallet - coventantPrivKeys[0].PubKey(), - coventantPrivKeys[1].PubKey(), - coventantPrivKeys[2].PubKey(), - ) - require.NoError(t, err) - - rpcHost := fmt.Sprintf("127.0.0.1:%s", bitcoind.GetPort("18443/tcp")) - cfg, c := defaultStakerConfig(t, walletName, passphrase, rpcHost) - cfg.BtcNodeBackendConfig.Bitcoind.RPCHost = rpcHost - cfg.WalletRPCConfig.Host = fmt.Sprintf("127.0.0.1:%s", bitcoind.GetPort("18443/tcp")) - - // update port with the dynamically allocated one from docker - cfg.BabylonConfig.RPCAddr = fmt.Sprintf("http://localhost:%s", babylond.GetPort("26657/tcp")) - cfg.BabylonConfig.GRPCAddr = fmt.Sprintf("https://localhost:%s", babylond.GetPort("9090/tcp")) - - logger := logrus.New() - logger.SetLevel(logrus.DebugLevel) - logger.Out = os.Stdout - - // babylon configs for sending transactions - cfg.BabylonConfig.KeyDirectory = filepath.Join(tmpDir, "node0", "babylond") - // need to use this one to send otherwise we will have account sequence mismatch - // errors - cfg.BabylonConfig.Key = "test-spending-key" - - // Big adjustment to make sure we have enough gas in our transactions - cfg.BabylonConfig.GasAdjustment = 3.0 - - dirPath := filepath.Join(os.TempDir(), "stakerd", "e2etest") - err = os.MkdirAll(dirPath, 0755) - require.NoError(t, err) - dbTempDir, err := os.MkdirTemp(dirPath, "db") - require.NoError(t, err) - cfg.DBConfig.DBPath = dbTempDir - - dbbackend, err := stakercfg.GetDBBackend(cfg.DBConfig) - require.NoError(t, err) - - m := metrics.NewStakerMetrics() - stakerApp, err := staker.NewStakerAppFromConfig(cfg, logger, zapLogger, dbbackend, m) - require.NoError(t, err) - // we require separate client to send BTC headers to babylon node (interface does not need this method?) - bl, err := babylonclient.NewBabylonController(cfg.BabylonConfig, &cfg.ActiveNetParams, logger, zapLogger) - require.NoError(t, err) - - walletClient := stakerApp.Wallet() - - err = walletClient.UnlockWallet(20) - require.NoError(t, err) - - info, err := c.GetAddressInfo(br.Address) - require.NoError(t, err) - - pubKeyHex := *info.PubKey - pubKeyBytes, err := hex.DecodeString(pubKeyHex) - require.NoError(t, err) - walletPubKey, err := btcec.ParsePubKey(pubKeyBytes) - require.NoError(t, err) - - addressString := fmt.Sprintf("127.0.0.1:%d", testutil.AllocateUniquePort(t)) - addrPort := netip.MustParseAddrPort(addressString) - address := net.TCPAddrFromAddrPort(addrPort) - cfg.RPCListeners = append(cfg.RPCListeners, address) // todo(lazar): check with konrad who uses this - - stakerService := service.NewStakerService( - cfg, - stakerApp, - logger, - dbbackend, - ) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - err := stakerService.RunUntilShutdown(ctx) - if err != nil { - t.Fatalf("Error running server: %v", err) - } - }() - // Wait for the server to start - time.Sleep(3 * time.Second) - - stakerClient, err := dc.NewStakerServiceJSONRPCClient("tcp://" + addressString) - require.NoError(t, err) - - return &TestManager{ - Config: cfg, - Db: dbbackend, - Sa: stakerApp, - BabylonClient: bl, - WalletPubKey: walletPubKey, - MinerAddr: minerAddressDecoded, - wg: &wg, - serviceAddress: addressString, - StakerClient: stakerClient, - CovenantPrivKeys: coventantPrivKeys, - BitcoindHandler: bitcoindHandler, - TestRpcClient: c, - manger: manager, - } -} - -func (tm *TestManager) Stop(t *testing.T, cancelFunc context.CancelFunc) { - cancelFunc() - tm.wg.Wait() - err := tm.manger.ClearResources() - require.NoError(t, err) - err = os.RemoveAll(tm.Config.DBConfig.DBPath) - require.NoError(t, err) -} - -func (tm *TestManager) RestartApp(t *testing.T, newCtx context.Context, cancelFunc context.CancelFunc) { - // Restart the app with no-op action - tm.RestartAppWithAction(t, newCtx, cancelFunc, func(t *testing.T) {}) -} - -// RestartAppWithAction: -// 1. Stop the staker app -// 2. Perform provided action. Warning:this action must not use staker app as -// app is stopped at this point -// 3. Start the staker app -func (tm *TestManager) RestartAppWithAction(t *testing.T, ctx context.Context, cancelFunc context.CancelFunc, action func(t *testing.T)) { - // First stop the app - cancelFunc() - tm.wg.Wait() - - // Perform the action - action(t) - - // Now reset all components and start again - logger := logrus.New() - logger.SetLevel(logrus.DebugLevel) - logger.Out = os.Stdout - - dbbackend, err := stakercfg.GetDBBackend(tm.Config.DBConfig) - require.NoError(t, err) - m := metrics.NewStakerMetrics() - stakerApp, err := staker.NewStakerAppFromConfig(tm.Config, logger, zapLogger, dbbackend, m) - require.NoError(t, err) - - service := service.NewStakerService( - tm.Config, - stakerApp, - logger, - dbbackend, - ) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - err := service.RunUntilShutdown(ctx) - if err != nil { - t.Fatalf("Error running server: %v", err) - } - }() - // Wait for the server to start - time.Sleep(3 * time.Second) - - tm.wg = &wg - tm.Db = dbbackend - tm.Sa = stakerApp - stakerClient, err := dc.NewStakerServiceJSONRPCClient("tcp://" + tm.serviceAddress) - require.NoError(t, err) - tm.StakerClient = stakerClient -} - -func retrieveTransactionFromMempool(t *testing.T, client *rpcclient.Client, hashes []*chainhash.Hash) []*btcutil.Tx { - var txes []*btcutil.Tx - for _, txHash := range hashes { - tx, err := client.GetRawTransaction(txHash) - - if err != nil { - // this is e2e helper method, so this error most probably some of the - // transactions are still not in the mempool - return []*btcutil.Tx{} - } - - txes = append(txes, tx) - } - return txes -} - -func GetAllMinedBtcHeadersSinceGenesis(t *testing.T, c *rpcclient.Client) []*wire.BlockHeader { - height, err := c.GetBlockCount() - require.NoError(t, err) - - var headers []*wire.BlockHeader - - for i := 1; i <= int(height); i++ { - hash, err := c.GetBlockHash(int64(i)) - require.NoError(t, err) - header, err := c.GetBlockHeader(hash) - require.NoError(t, err) - headers = append(headers, header) - } - - return headers -} - -func opReturnScript(data []byte) []byte { - builder := txscript.NewScriptBuilder() - script, err := builder.AddOp(txscript.OP_RETURN).AddData(data).Script() - if err != nil { - panic(err) - } - return script -} - -func txToBytes(tx *wire.MsgTx) []byte { - buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) - _ = tx.Serialize(buf) - return buf.Bytes() -} - -func txsToBytes(txs []*wire.MsgTx) [][]byte { - var txsBytes [][]byte - for _, tx := range txs { - txsBytes = append(txsBytes, txToBytes(tx)) - } - return txsBytes -} - -func (tm *TestManager) FinalizeUntilEpoch(t *testing.T, epoch uint64) { - bbnClient := tm.BabylonClient.GetBBNClient() - ckptParams, err := bbnClient.BTCCheckpointParams() - require.NoError(t, err) - // wait until the checkpoint of this epoch is sealed - require.Eventually(t, func() bool { - lastSealedCkpt, err := bbnClient.LatestEpochFromStatus(ckpttypes.Sealed) - if err != nil { - return false - } - return epoch <= lastSealedCkpt.RawCheckpoint.EpochNum - }, 1*time.Minute, 1*time.Second) - - t.Logf("start finalizing epochs till %d", epoch) - // Random source for the generation of BTC data - // r := rand.New(rand.NewSource(time.Now().Unix())) - - // get all checkpoints of these epochs - pagination := &sdkquerytypes.PageRequest{ - Key: ckpttypes.CkptsObjectKey(1), - Limit: epoch, - } - resp, err := bbnClient.RawCheckpoints(pagination) - require.NoError(t, err) - require.Equal(t, int(epoch), len(resp.RawCheckpoints)) - - submitter := tm.BabylonClient.GetKeyAddress() - - for _, checkpoint := range resp.RawCheckpoints { - // currentBtcTipResp, err := tm.BabylonClient.QueryBtcLightClientTip() - // require.NoError(t, err) - // tipHeader, err := bbntypes.NewBTCHeaderBytesFromHex(currentBtcTipResp.HeaderHex) - // require.NoError(t, err) - - rawCheckpoint, err := checkpoint.Ckpt.ToRawCheckpoint() - require.NoError(t, err) - - btcCheckpoint, err := ckpttypes.FromRawCkptToBTCCkpt(rawCheckpoint, submitter) - require.NoError(t, err) - - babylonTagBytes, err := hex.DecodeString("01020304") - require.NoError(t, err) - - p1, p2, err := txformat.EncodeCheckpointData( - babylonTagBytes, - txformat.CurrentVersion, - btcCheckpoint, - ) - - err = tm.Sa.Wallet().UnlockWallet(60) - require.NoError(t, err) - tx1, err := tm.Sa.Wallet().CreateAndSignTx( - []*wire.TxOut{ - wire.NewTxOut(0, opReturnScript(p1)), - }, - 2000, - tm.MinerAddr, - nil, - ) - require.NoError(t, err) - _, err = tm.Sa.Wallet().SendRawTransaction(tx1, true) - require.NoError(t, err) - - resp1 := tm.BitcoindHandler.GenerateBlocks(1) - - tx2, err := tm.Sa.Wallet().CreateAndSignTx( - []*wire.TxOut{ - wire.NewTxOut(0, opReturnScript(p2)), - }, - 2000, - tm.MinerAddr, - nil, - ) - require.NoError(t, err) - _, err = tm.Sa.Wallet().SendRawTransaction(tx2, true) - require.NoError(t, err) - resp2 := tm.BitcoindHandler.GenerateBlocks(1) - - block1Hash, err := chainhash.NewHashFromStr(resp1.Blocks[0]) - require.NoError(t, err) - block2Hash, err := chainhash.NewHashFromStr(resp2.Blocks[0]) - require.NoError(t, err) - - block1, err := tm.TestRpcClient.GetBlock(block1Hash) - require.NoError(t, err) - block2, err := tm.TestRpcClient.GetBlock(block2Hash) - require.NoError(t, err) - - _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{ - &block1.Header, - &block2.Header, - }) - - header1Bytes := bbntypes.NewBTCHeaderBytesFromBlockHeader(&block1.Header) - header2Bytes := bbntypes.NewBTCHeaderBytesFromBlockHeader(&block2.Header) - - proof1, err := btcctypes.SpvProofFromHeaderAndTransactions(&header1Bytes, txsToBytes(block1.Transactions), 1) - require.NoError(t, err) - proof2, err := btcctypes.SpvProofFromHeaderAndTransactions(&header2Bytes, txsToBytes(block2.Transactions), 1) - require.NoError(t, err) - - _, err = tm.BabylonClient.InsertSpvProofs(submitter.String(), []*btcctypes.BTCSpvProof{ - proof1, - proof2, - }) - require.NoError(t, err) - - // // wait until this checkpoint is submitted - require.Eventually(t, func() bool { - ckpt, err := bbnClient.RawCheckpoint(checkpoint.Ckpt.EpochNum) - require.NoError(t, err) - return ckpt.RawCheckpoint.Status == ckpttypes.Submitted - }, eventuallyWaitTimeOut, eventuallyPollTime) - } - - tm.mineNEmptyBlocks(t, uint32(ckptParams.Params.CheckpointFinalizationTimeout), true) - - // // wait until the checkpoint of this epoch is finalised - require.Eventually(t, func() bool { - lastFinalizedCkpt, err := bbnClient.LatestEpochFromStatus(ckpttypes.Finalized) - if err != nil { - t.Logf("failed to get last finalized epoch: %v", err) - return false - } - return epoch <= lastFinalizedCkpt.RawCheckpoint.EpochNum - }, eventuallyWaitTimeOut, 1*time.Second) - - t.Logf("epoch %d is finalised", epoch) -} - -func (tm *TestManager) createAndRegisterFinalityProviders(t *testing.T, testStakingData *testStakingData) { - params, err := tm.BabylonClient.QueryStakingTracker() - require.NoError(t, err) - - for i := 0; i < testStakingData.GetNumRestakedFPs(); i++ { - // ensure the finality provider in testStakingData does not exist yet - fpResp, err := tm.BabylonClient.QueryFinalityProvider(testStakingData.FinalityProviderBtcKeys[i]) - require.Nil(t, fpResp) - require.Error(t, err) - require.True(t, errors.Is(err, babylonclient.ErrFinalityProviderDoesNotExist)) - - pop, err := btcstypes.NewPoPBTC(testStakingData.FinalityProviderBabylonAddrs[i], testStakingData.FinalityProviderBtcPrivKeys[i]) - require.NoError(t, err) - - btcFpKey := bbntypes.NewBIP340PubKeyFromBTCPK(testStakingData.FinalityProviderBtcKeys[i]) - - // get current finality providers - resp, err := tm.BabylonClient.QueryFinalityProviders(100, 0) - require.NoError(t, err) - // register the generated finality provider - err = tm.BabylonClient.RegisterFinalityProvider( - testStakingData.FinalityProviderBabylonAddrs[i], - testStakingData.FinalityProviderBabylonPrivKeys[i], - btcFpKey, - ¶ms.MinComissionRate, - &sttypes.Description{ - Moniker: "tester", - }, - pop, - ) - require.NoError(t, err) - - require.Eventually(t, func() bool { - resp2, err := tm.BabylonClient.QueryFinalityProviders(100, 0) - require.NoError(t, err) - - // After registration we should have one finality provider - return len(resp2.FinalityProviders) == len(resp.FinalityProviders)+1 - }, time.Minute, 250*time.Millisecond) - } -} - -func (tm *TestManager) sendHeadersToBabylon(t *testing.T, headers []*wire.BlockHeader) { - _, err := tm.BabylonClient.InsertBtcBlockHeaders(headers) - require.NoError(t, err) -} - -func (tm *TestManager) mineNEmptyBlocks(t *testing.T, numHeaders uint32, sendToBabylon bool) []*wire.BlockHeader { - resp := tm.BitcoindHandler.GenerateBlocks(int(numHeaders)) - - var minedHeaders []*wire.BlockHeader - for _, hash := range resp.Blocks { - hash, err := chainhash.NewHashFromStr(hash) - require.NoError(t, err) - header, err := tm.TestRpcClient.GetBlockHeader(hash) - require.NoError(t, err) - minedHeaders = append(minedHeaders, header) - } - - if sendToBabylon { - tm.sendHeadersToBabylon(t, minedHeaders) - } - - return minedHeaders -} - -func (tm *TestManager) mineBlock(t *testing.T) *wire.MsgBlock { - resp := tm.BitcoindHandler.GenerateBlocks(1) - hash, err := chainhash.NewHashFromStr(resp.Blocks[0]) - require.NoError(t, err) - header, err := tm.TestRpcClient.GetBlock(hash) - require.NoError(t, err) - return header -} - -func (tm *TestManager) sendStakingTxBTC( - t *testing.T, - testStakingData *testStakingData, - sendToBabylonFirst bool, -) *chainhash.Hash { - fpBTCPKs := []string{} - for i := 0; i < testStakingData.GetNumRestakedFPs(); i++ { - fpBTCPK := hex.EncodeToString(schnorr.SerializePubKey(testStakingData.FinalityProviderBtcKeys[i])) - fpBTCPKs = append(fpBTCPKs, fpBTCPK) - } - res, err := tm.StakerClient.Stake( - context.Background(), - tm.MinerAddr.String(), - testStakingData.StakingAmount, - fpBTCPKs, - int64(testStakingData.StakingTime), - sendToBabylonFirst, - ) - require.NoError(t, err) - txHash := res.TxHash - - stakingDetails, err := tm.StakerClient.StakingDetails(context.Background(), txHash) - require.NoError(t, err) - require.Equal(t, stakingDetails.StakingTxHash, txHash) - - if sendToBabylonFirst { - require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BABYLON.String()) - } else { - require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BTC.String()) - } - hashFromString, err := chainhash.NewHashFromStr(txHash) - require.NoError(t, err) - - // only wait for blocks if we are using the old flow, and send staking tx to BTC - // first - if !sendToBabylonFirst { - require.Eventually(t, func() bool { - txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcClient, []*chainhash.Hash{hashFromString}) - return len(txFromMempool) == 1 - }, eventuallyWaitTimeOut, eventuallyPollTime) - - mBlock := tm.mineBlock(t) - require.Equal(t, 2, len(mBlock.Transactions)) - - _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) - require.NoError(t, err) - } - return hashFromString -} - -func (tm *TestManager) sendMultipleStakingTx(t *testing.T, testStakingData []*testStakingData, sendToBabylonFirst bool) []*chainhash.Hash { - var hashes []*chainhash.Hash - for _, data := range testStakingData { - fpBTCPKs := []string{} - for i := 0; i < data.GetNumRestakedFPs(); i++ { - fpBTCPK := hex.EncodeToString(schnorr.SerializePubKey(data.FinalityProviderBtcKeys[i])) - fpBTCPKs = append(fpBTCPKs, fpBTCPK) - } - res, err := tm.StakerClient.Stake( - context.Background(), - tm.MinerAddr.String(), - data.StakingAmount, - fpBTCPKs, - int64(data.StakingTime), - sendToBabylonFirst, - ) - require.NoError(t, err) - txHash, err := chainhash.NewHashFromStr(res.TxHash) - require.NoError(t, err) - hashes = append(hashes, txHash) - } - - for _, txHash := range hashes { - txHash := txHash - hashStr := txHash.String() - stakingDetails, err := tm.StakerClient.StakingDetails(context.Background(), hashStr) - require.NoError(t, err) - require.Equal(t, stakingDetails.StakingTxHash, hashStr) - - if sendToBabylonFirst { - require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BABYLON.String()) - } else { - require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BTC.String()) - } - } - - if !sendToBabylonFirst { - mBlock := tm.mineBlock(t) - require.Equal(t, len(hashes)+1, len(mBlock.Transactions)) - - _, err := tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) - require.NoError(t, err) - } - return hashes -} - -func (tm *TestManager) sendWatchedStakingTx( - t *testing.T, - testStakingData *testStakingData, - params *babylonclient.StakingParams, -) *chainhash.Hash { - unbondingTme := params.MinUnbondingTime - - stakingInfo, err := staking.BuildStakingInfo( - testStakingData.StakerKey, - testStakingData.FinalityProviderBtcKeys, - params.CovenantPks, - params.CovenantQuruomThreshold, - testStakingData.StakingTime, - btcutil.Amount(testStakingData.StakingAmount), - regtestParams, - ) - require.NoError(t, err) - - err = tm.Sa.Wallet().UnlockWallet(20) - require.NoError(t, err) - - tx, err := tm.Sa.Wallet().CreateAndSignTx( - []*wire.TxOut{stakingInfo.StakingOutput}, - 2000, - tm.MinerAddr, - nil, - ) - require.NoError(t, err) - txHash := tx.TxHash() - _, err = tm.Sa.Wallet().SendRawTransaction(tx, true) - require.NoError(t, err) - - // Wait for tx to be in mempool - require.Eventually(t, func() bool { - tx, err := tm.TestRpcClient.GetRawTransaction(&txHash) - if err != nil { - return false - } - - if tx == nil { - return false - } - - return true - }, 1*time.Minute, eventuallyPollTime) - - stakingOutputIdx := 0 - - require.NoError(t, err) - - slashingTx, err := staking.BuildSlashingTxFromStakingTxStrict( - tx, - uint32(stakingOutputIdx), - params.SlashingPkScript, - testStakingData.StakerKey, - unbondingTme, - int64(params.MinSlashingTxFeeSat)+10, - params.SlashingRate, - regtestParams, - ) - require.NoError(t, err) - - stakingTxSlashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() - require.NoError(t, err) - - slashingSigResult, err := tm.Sa.Wallet().SignOneInputTaprootSpendingTransaction( - &walletcontroller.TaprootSigningRequest{ - FundingOutput: stakingInfo.StakingOutput, - TxToSign: slashingTx, - SignerAddress: tm.MinerAddr, - SpendDescription: &walletcontroller.SpendPathDescription{ - ControlBlock: &stakingTxSlashingPathInfo.ControlBlock, - ScriptLeaf: &stakingTxSlashingPathInfo.RevealedLeaf, - }, - }, - ) - - require.NoError(t, err) - require.NotNil(t, slashingSigResult.Signature) - - serializedStakingTx, err := utils.SerializeBtcTransaction(tx) - require.NoError(t, err) - serializedSlashingTx, err := utils.SerializeBtcTransaction(slashingTx) - require.NoError(t, err) - // Build unbonding related data - unbondingFee := params.UnbondingFee - unbondingAmount := btcutil.Amount(testStakingData.StakingAmount) - unbondingFee - - unbondingInfo, err := staking.BuildUnbondingInfo( - testStakingData.StakerKey, - testStakingData.FinalityProviderBtcKeys, - params.CovenantPks, - params.CovenantQuruomThreshold, - unbondingTme, - unbondingAmount, - regtestParams, - ) - require.NoError(t, err) - - unbondingSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() - require.NoError(t, err) - - unbondingTx := wire.NewMsgTx(2) - unbondingTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, uint32(stakingOutputIdx)), nil, nil)) - unbondingTx.AddTxOut(unbondingInfo.UnbondingOutput) - - slashUnbondingTx, err := staking.BuildSlashingTxFromStakingTxStrict( - unbondingTx, - 0, - params.SlashingPkScript, - testStakingData.StakerKey, - unbondingTme, - int64(params.MinSlashingTxFeeSat)+10, - params.SlashingRate, - regtestParams, - ) - require.NoError(t, err) - - slashingUnbondingSigResult, err := tm.Sa.Wallet().SignOneInputTaprootSpendingTransaction( - &walletcontroller.TaprootSigningRequest{ - FundingOutput: unbondingTx.TxOut[0], - TxToSign: slashUnbondingTx, - SignerAddress: tm.MinerAddr, - SpendDescription: &walletcontroller.SpendPathDescription{ - ControlBlock: &unbondingSlashingPathInfo.ControlBlock, - ScriptLeaf: &unbondingSlashingPathInfo.RevealedLeaf, - }, - }, - ) - - require.NoError(t, err) - require.NotNil(t, slashingUnbondingSigResult.Signature) - - serializedUnbondingTx, err := utils.SerializeBtcTransaction(unbondingTx) - require.NoError(t, err) - serializedSlashUnbondingTx, err := utils.SerializeBtcTransaction(slashUnbondingTx) - require.NoError(t, err) - - babylonAddrHash := tmhash.Sum(testStakingData.StakerBabylonAddr.Bytes()) - - sig, err := tm.Sa.Wallet().SignBip322NativeSegwit(babylonAddrHash, tm.MinerAddr) - require.NoError(t, err) - - pop, err := babylonclient.NewBabylonBip322Pop( - babylonAddrHash, - sig, - tm.MinerAddr, - ) - require.NoError(t, err) - - fpBTCPKs := []string{} - for i := 0; i < testStakingData.GetNumRestakedFPs(); i++ { - fpBTCPK := hex.EncodeToString(schnorr.SerializePubKey(testStakingData.FinalityProviderBtcKeys[i])) - fpBTCPKs = append(fpBTCPKs, fpBTCPK) - } - _, err = tm.StakerClient.WatchStaking( - context.Background(), - hex.EncodeToString(serializedStakingTx), - int(testStakingData.StakingTime), - int(testStakingData.StakingAmount), - hex.EncodeToString(schnorr.SerializePubKey(testStakingData.StakerKey)), - fpBTCPKs, - hex.EncodeToString(serializedSlashingTx), - hex.EncodeToString(slashingSigResult.Signature.Serialize()), - testStakingData.StakerBabylonAddr.String(), - tm.MinerAddr.String(), - hex.EncodeToString(pop.BtcSig), - hex.EncodeToString(serializedUnbondingTx), - hex.EncodeToString(serializedSlashUnbondingTx), - hex.EncodeToString(slashingUnbondingSigResult.Signature.Serialize()), - int(unbondingTme), - // Use schnor verification - int(btcstypes.BTCSigType_BIP322), - ) - require.NoError(t, err) - - txs := retrieveTransactionFromMempool(t, tm.TestRpcClient, []*chainhash.Hash{&txHash}) - require.Len(t, txs, 1) - - mBlock := tm.mineBlock(t) - require.Equal(t, 2, len(mBlock.Transactions)) - _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) - require.NoError(t, err) - - return &txHash -} - -func (tm *TestManager) spendStakingTxWithHash(t *testing.T, stakingTxHash *chainhash.Hash) (*chainhash.Hash, *btcutil.Amount) { - res, err := tm.StakerClient.SpendStakingTransaction(context.Background(), stakingTxHash.String()) - require.NoError(t, err) - spendTxHash, err := chainhash.NewHashFromStr(res.TxHash) - require.NoError(t, err) - - iAmount, err := strconv.ParseInt(res.TxValue, 10, 64) - require.NoError(t, err) - spendTxValue := btcutil.Amount(iAmount) - - require.Eventually(t, func() bool { - txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcClient, []*chainhash.Hash{spendTxHash}) - return len(txFromMempool) == 1 - }, eventuallyWaitTimeOut, eventuallyPollTime) - - sendTx := retrieveTransactionFromMempool(t, tm.TestRpcClient, []*chainhash.Hash{spendTxHash})[0] - - // Tx is in mempool - txDetails, txState, err := tm.Sa.Wallet().TxDetails(spendTxHash, sendTx.MsgTx().TxOut[0].PkScript) - require.NoError(t, err) - require.Nil(t, txDetails) - require.Equal(t, txState, walletcontroller.TxInMemPool) - - // Block with spend is mined - mBlock1 := tm.mineBlock(t) - require.Equal(t, 2, len(mBlock1.Transactions)) - - // Tx is in chain - txDetails, txState, err = tm.Sa.Wallet().TxDetails(spendTxHash, sendTx.MsgTx().TxOut[0].PkScript) - require.NoError(t, err) - require.NotNil(t, txDetails) - require.Equal(t, txState, walletcontroller.TxInChain) - - return spendTxHash, &spendTxValue -} - -func (tm *TestManager) waitForStakingTxState(t *testing.T, txHash *chainhash.Hash, expectedState proto.TransactionState) { - require.Eventually(t, func() bool { - detailResult, err := tm.StakerClient.StakingDetails(context.Background(), txHash.String()) - if err != nil { - return false - } - return detailResult.StakingState == expectedState.String() - }, 1*time.Minute, eventuallyPollTime) -} - -func (tm *TestManager) walletUnspentsOutputsContainsOutput(t *testing.T, from btcutil.Address, withValue btcutil.Amount) bool { - unspentOutputs, err := tm.Sa.ListUnspentOutputs() - require.NoError(t, err) - - var containsOutput bool = false - - for _, output := range unspentOutputs { - if output.Address == tm.MinerAddr.String() && int64(output.Amount) == int64(withValue) { - containsOutput = true - } - } - - return containsOutput -} - -func (tm *TestManager) insertAllMinedBlocksToBabylon(t *testing.T) { - headers := GetAllMinedBtcHeadersSinceGenesis(t, tm.TestRpcClient) - _, err := tm.BabylonClient.InsertBtcBlockHeaders(headers) - require.NoError(t, err) -} - -func (tm *TestManager) insertCovenantSigForDelegation( - t *testing.T, - btcDel *btcstypes.BTCDelegationResponse, -) { - fpBTCPKs, err := bbntypes.NewBTCPKsFromBIP340PKs(btcDel.FpBtcPkList) - require.NoError(t, err) - - slashingTxBytes, err := hex.DecodeString(btcDel.SlashingTxHex) - require.NoError(t, err) - slashingTx := btcstypes.BTCSlashingTx(slashingTxBytes) - stakingTx := btcDel.StakingTxHex - stakingMsgTx, _, err := bbntypes.NewBTCTxFromHex(stakingTx) - require.NoError(t, err) - - cl := tm.Sa.BabylonController() - params, err := cl.Params() - require.NoError(t, err) - - stakingInfo, err := staking.BuildStakingInfo( - btcDel.BtcPk.MustToBTCPK(), - fpBTCPKs, - params.CovenantPks, - params.CovenantQuruomThreshold, - uint16(btcDel.EndHeight-btcDel.StartHeight), - btcutil.Amount(btcDel.TotalSat), - regtestParams, - ) - slashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() - require.NoError(t, err) - - covenantSlashingTxSigs, err := datagen.GenCovenantAdaptorSigs( - tm.CovenantPrivKeys, - fpBTCPKs, - stakingMsgTx, - slashingPathInfo.GetPkScriptPath(), - &slashingTx, - ) - require.NoError(t, err) - - // slash unbonding tx spends unbonding tx - unbondingMsgTx, _, err := bbntypes.NewBTCTxFromHex(btcDel.UndelegationResponse.UnbondingTxHex) - require.NoError(t, err) - unbondingInfo, err := staking.BuildUnbondingInfo( - btcDel.BtcPk.MustToBTCPK(), - fpBTCPKs, - params.CovenantPks, - params.CovenantQuruomThreshold, - uint16(btcDel.UnbondingTime), - btcutil.Amount(unbondingMsgTx.TxOut[0].Value), - regtestParams, - ) - unbondingSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() - require.NoError(t, err) - - // generate all covenant signatures from all covenant members - unbondingSlashingTx, err := btcstypes.NewBTCSlashingTxFromHex(btcDel.UndelegationResponse.SlashingTxHex) - require.NoError(t, err) - covenantUnbondingSlashingTxSigs, err := datagen.GenCovenantAdaptorSigs( - tm.CovenantPrivKeys, - fpBTCPKs, - unbondingMsgTx, - unbondingSlashingPathInfo.GetPkScriptPath(), - unbondingSlashingTx, - ) - require.NoError(t, err) - - // each covenant member submits signatures - unbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() - require.NoError(t, err) - covUnbondingSigs, err := datagen.GenCovenantUnbondingSigs( - tm.CovenantPrivKeys, - stakingMsgTx, - btcDel.StakingOutputIdx, - unbondingPathInfo.GetPkScriptPath(), - unbondingMsgTx, - ) - require.NoError(t, err) - - var messages []*btcstypes.MsgAddCovenantSigs - for i := 0; i < len(tm.CovenantPrivKeys); i++ { - msg := tm.BabylonClient.CreateCovenantMessage( - bbntypes.NewBIP340PubKeyFromBTCPK(tm.CovenantPrivKeys[i].PubKey()), - stakingMsgTx.TxHash().String(), - covenantSlashingTxSigs[i].AdaptorSigs, - bbntypes.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]), - covenantUnbondingSlashingTxSigs[i].AdaptorSigs, - ) - messages = append(messages, msg) - } - // we insert are covenant signatures in on message, this way staker - // program must handle the case of all signatures being present in Babylon - // delegation - // it also speeds up the tests - _, err = tm.BabylonClient.SubmitMultipleCovenantMessages(messages) - require.NoError(t, err) -} - func TestStakingFailures(t *testing.T) { t.Parallel() numMatureOutputs := uint32(200) @@ -1296,7 +194,7 @@ func TestSendingStakingTransactionWithPreApproval(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_VERIFIED) require.Eventually(t, func() bool { - txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcClient, []*chainhash.Hash{txHash}) + txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcBtcClient, []*chainhash.Hash{txHash}) return len(txFromMempool) == 1 }, eventuallyWaitTimeOut, eventuallyPollTime) @@ -1332,7 +230,7 @@ func TestSendingStakingTransactionWithPreApproval(t *testing.T) { require.NoError(t, err) require.Eventually(t, func() bool { - tx, err := tm.TestRpcClient.GetRawTransaction(unbondingTxHash) + tx, err := tm.TestRpcBtcClient.GetRawTransaction(unbondingTxHash) if err != nil { return false } @@ -1481,7 +379,7 @@ func TestMultiplePreApprovalTransactions(t *testing.T) { // Ultimately we will get 3 tx in the mempool meaning all staking transactions // use valid inputs require.Eventually(t, func() bool { - txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcClient, txHashes) + txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcBtcClient, txHashes) return len(txFromMempool) == 3 }, eventuallyWaitTimeOut, eventuallyPollTime) } @@ -1625,7 +523,7 @@ func TestStakingUnbonding(t *testing.T) { require.NoError(t, err) require.Eventually(t, func() bool { - tx, err := tm.TestRpcClient.GetRawTransaction(unbondingTxHash) + tx, err := tm.TestRpcBtcClient.GetRawTransaction(unbondingTxHash) if err != nil { return false } @@ -1701,7 +599,7 @@ func TestUnbondingRestartWaitingForSignatures(t *testing.T) { require.NoError(t, err) require.Eventually(t, func() bool { - tx, err := tm.TestRpcClient.GetRawTransaction(unbondingTxHash) + tx, err := tm.TestRpcBtcClient.GetRawTransaction(unbondingTxHash) if err != nil { return false } @@ -1819,7 +717,7 @@ func TestBitcoindWalletBip322Signing(t *testing.T) { _ = h.CreateWallet(walletName, passphrase) rpcHost := fmt.Sprintf("127.0.0.1:%s", bitcoind.GetPort("18443/tcp")) - cfg, c := defaultStakerConfig(t, walletName, passphrase, rpcHost) + cfg, c := defaultStakerConfigAndBtc(t, walletName, passphrase, rpcHost) segwitAddress, err := c.GetNewAddress("") require.NoError(t, err) @@ -1886,7 +784,7 @@ func TestRecoverAfterRestartDuringWithdrawal(t *testing.T) { unbondingTxHash, err := chainhash.NewHashFromStr(unbondResponse.UnbondingTxHash) require.NoError(t, err) require.Eventually(t, func() bool { - tx, err := tm.TestRpcClient.GetRawTransaction(unbondingTxHash) + tx, err := tm.TestRpcBtcClient.GetRawTransaction(unbondingTxHash) if err != nil { return false } @@ -1911,3 +809,177 @@ func TestRecoverAfterRestartDuringWithdrawal(t *testing.T) { // it should be possible ot spend from unbonding tx tm.spendStakingTxWithHash(t, txHash) } + +func TestStakeFromPhase1(t *testing.T) { + numMatureOutputsInWallet := uint32(200) + ctx, cancel := context.WithCancel(context.Background()) + manager, err := containers.NewManager(t) + require.NoError(t, err) + + tmBTC := StartManagerBtc(t, ctx, numMatureOutputsInWallet, manager) + + minStakingTime := uint16(100) + stakerAddr := datagen.GenRandomAccount().GetAddress() + testStakingData := GetTestStakingData(t, tmBTC.WalletPubKey, minStakingTime, 10000, 1, stakerAddr) + + fpPkHex := hex.EncodeToString(schnorr.SerializePubKey(testStakingData.FinalityProviderBtcKeys[0])) + btcStakerPkHex := hex.EncodeToString(schnorr.SerializePubKey(testStakingData.StakerKey)) + + appCli := testutil.TestApp() + + coventantPrivKeys := genCovenants(t, 1) + covenantPkSerializedHex := hex.EncodeToString(schnorr.SerializePubKey(coventantPrivKeys[0].PubKey())) + covenantPkHex := hex.EncodeToString(coventantPrivKeys[0].PubKey().SerializeCompressed()) + + lastParams := &parser.VersionedGlobalParams{ + Version: 0, + ActivationHeight: 100, + StakingCap: 3000000, + CapHeight: 0, + Tag: "01020304", + CovenantPks: []string{ + covenantPkHex, + }, + CovenantQuorum: 1, + UnbondingTime: 1000, + UnbondingFee: 1000, + MaxStakingAmount: 300000, + MinStakingAmount: 3000, + MaxStakingTime: 10000, + MinStakingTime: 100, + ConfirmationDepth: 10, + } + + globalParams := parser.GlobalParams{ + Versions: []*parser.VersionedGlobalParams{ + lastParams, + }, + } + + globalParamsMarshalled, err := json.Marshal(globalParams) + require.NoError(t, err) + + paramsFilePath := testutil.CreateTempFileWithData(t, "tmpParams-*.json", globalParamsMarshalled) + stakingAmount := lastParams.MaxStakingAmount - 1 + inclusionHeight := lastParams.ActivationHeight + 1 + stakingTime := lastParams.MaxStakingTime + + commonFlags := []string{ + fmt.Sprintf("--covenant-committee-pks=%s", covenantPkSerializedHex), + fmt.Sprintf("--tag=%s", lastParams.Tag), + "--covenant-quorum=1", "--network=regtest", + } + + createTxCmdArgs := []string{ + paramsFilePath, + fmt.Sprintf("--staker-pk=%s", btcStakerPkHex), + fmt.Sprintf("--finality-provider-pk=%s", fpPkHex), + fmt.Sprintf("--staking-amount=%d", stakingAmount), + fmt.Sprintf("--tx-inclusion-height=%d", inclusionHeight), + fmt.Sprintf("--staking-time=%d", stakingTime), + } + resP1StkTx := testutil.AppRunCreatePhase1StakingTxWithParams(r, t, appCli, append(createTxCmdArgs, commonFlags...)) + require.NotNil(t, resP1StkTx) + + stkTx, err := hex.DecodeString(resP1StkTx.StakingTxHex) + require.NoError(t, err) + + var tx wire.MsgTx + rbuf := bytes.NewReader(stkTx) + err = tx.DeserializeNoWitness(rbuf) + require.NoError(t, err) + + rpcBtc := tmBTC.TestRpcBtcClient + err = rpcBtc.WalletPassphrase(tmBTC.WalletPassphrase, 20) + require.NoError(t, err) + + resFundRawStkTx, err := rpcBtc.FundRawTransaction(&tx, btcjson.FundRawTransactionOpts{ + FeeRate: btcjson.Float64(0.02), + // by setting the ChangePosition to 1 we make sure that the staking output will be at index 0 + ChangePosition: btcjson.Int(1), + }, btcjson.Bool(false)) + require.NoError(t, err) + require.NotNil(t, resFundRawStkTx) + + signedStkTx, complete, err := rpcBtc.SignRawTransactionWithWallet(resFundRawStkTx.Transaction) + require.True(t, complete) + require.NoError(t, err) + require.NotNil(t, signedStkTx) + + txHash, err := rpcBtc.SendRawTransaction(signedStkTx, false) + require.NoError(t, err) + require.NotNil(t, txHash) + require.Equal(t, txHash.String(), signedStkTx.TxHash().String()) + + tmBTC.BitcoindHandler.GenerateBlocks(15) + + stkTxResult, err := rpcBtc.GetTransaction(txHash) + require.NoError(t, err) + require.NotNil(t, stkTxResult) + + parsedGlobalParams, err := parser.ParseGlobalParams(&globalParams) + require.NoError(t, err) + + lastParamsVersioned := parsedGlobalParams.Versions[len(parsedGlobalParams.Versions)-1] + + // Makes sure it is able to parse the staking tx + paserdStkTx, err := btcstaking.ParseV0StakingTx( + signedStkTx, + lastParamsVersioned.Tag, + lastParamsVersioned.CovenantPks, + lastParamsVersioned.CovenantQuorum, + regtestParams, + ) + require.NoError(t, err) + require.NotNil(t, paserdStkTx) + + // at this point the BTC staking transaction is confirmed and was mined in BTC + // so the babylon chain can start and try to transition this staking BTC tx + // into a babylon BTC delegation in the cosmos side. + tmStakerApp := StartManagerStakerApp(t, ctx, tmBTC, manager, 1, coventantPrivKeys) + + tm := &TestManager{ + manager: manager, + TestManagerStakerApp: *tmStakerApp, + TestManagerBTC: *tmBTC, + } + defer tm.Stop(t, cancel) + + // verify that the chain is healthy + require.Eventually(t, func() bool { + _, err := tm.BabylonClient.Params() + return err == nil + }, time.Minute, 200*time.Millisecond) + + // funds the fpd + _, _, err = tm.manager.BabylondTxBankMultiSend(t, "node0", "1000000ubbn", testStakingData.FinalityProviderBabylonAddrs[0].String()) + require.NoError(t, err) + + tm.insertAllMinedBlocksToBabylon(t) + tm.createAndRegisterFinalityProviders(t, testStakingData) + + stakerAddrStr := tmBTC.MinerAddr.String() + stkTxHash := signedStkTx.TxHash().String() + res, err := tmStakerApp.StakerClient.BtcDelegationFromBtcStakingTx(ctx, stakerAddrStr, stkTxHash, lastParamsVersioned) + require.NoError(t, err) + require.NotNil(t, res) + + // wait for BTC delegation to become active + cl := tm.Sa.BabylonController() + params, err := cl.Params() + require.NoError(t, err) + + tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks+1, true) + tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) + + pend, err := tm.BabylonClient.QueryPendingBTCDelegations() + require.NoError(t, err) + require.Len(t, pend, 1) + + tm.insertCovenantSigForDelegation(t, pend[0]) + tm.waitForStakingTxState(t, txHash, proto.TransactionState_DELEGATION_ACTIVE) + + delInfo, err := tm.BabylonClient.QueryDelegationInfo(txHash) + require.NoError(t, err) + require.True(t, delInfo.Active) +} diff --git a/itest/manager.go b/itest/manager.go new file mode 100644 index 0000000..6f0306b --- /dev/null +++ b/itest/manager.go @@ -0,0 +1,1218 @@ +//go:build e2e +// +build e2e + +package e2etest + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "fmt" + "math/rand" + "net" + "net/netip" + "os" + "path/filepath" + "strconv" + "sync" + "testing" + "time" + + "github.com/babylonlabs-io/btc-staker/itest/containers" + "github.com/babylonlabs-io/btc-staker/itest/testutil" + "github.com/ory/dockertest/v3" + + btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" + "github.com/cometbft/cometbft/crypto/tmhash" + + staking "github.com/babylonlabs-io/babylon/btcstaking" + txformat "github.com/babylonlabs-io/babylon/btctxformatter" + "github.com/babylonlabs-io/babylon/testutil/datagen" + bbntypes "github.com/babylonlabs-io/babylon/types" + btcstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + ckpttypes "github.com/babylonlabs-io/babylon/x/checkpointing/types" + "github.com/babylonlabs-io/btc-staker/babylonclient" + "github.com/babylonlabs-io/btc-staker/metrics" + "github.com/babylonlabs-io/btc-staker/proto" + "github.com/babylonlabs-io/btc-staker/staker" + "github.com/babylonlabs-io/btc-staker/stakercfg" + service "github.com/babylonlabs-io/btc-staker/stakerservice" + dc "github.com/babylonlabs-io/btc-staker/stakerservice/client" + "github.com/babylonlabs-io/btc-staker/types" + "github.com/babylonlabs-io/btc-staker/utils" + "github.com/babylonlabs-io/btc-staker/walletcontroller" + "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" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkquerytypes "github.com/cosmos/cosmos-sdk/types/query" + sttypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +// bitcoin params used for testing +var ( + r = rand.New(rand.NewSource(time.Now().Unix())) + + regtestParams = &chaincfg.RegressionNetParams + + eventuallyWaitTimeOut = 10 * time.Second + eventuallyPollTime = 250 * time.Millisecond + + bitcoindUser = "user" + bitcoindPass = "pass" +) + +// keyToAddr maps the passed private to corresponding p2pkh address. +func keyToAddr(key *btcec.PrivateKey, net *chaincfg.Params) (btcutil.Address, error) { + serializedKey := key.PubKey().SerializeCompressed() + pubKeyAddr, err := btcutil.NewAddressPubKey(serializedKey, net) + if err != nil { + return nil, err + } + return pubKeyAddr.AddressPubKeyHash(), nil +} + +func defaultStakerConfigAndBtc(t *testing.T, walletName, passphrase, bitcoindHost string) (*stakercfg.Config, *rpcclient.Client) { + return defaultStakerConfig(t, walletName, passphrase, bitcoindHost), btcRpcTestClient(t, bitcoindHost) +} + +func defaultStakerConfig(t *testing.T, walletName, passphrase, bitcoindHost string) *stakercfg.Config { + defaultConfig := stakercfg.DefaultConfig() + + // both wallet and node are bicoind + defaultConfig.BtcNodeBackendConfig.ActiveWalletBackend = types.BitcoindWalletBackend + defaultConfig.BtcNodeBackendConfig.ActiveNodeBackend = types.BitcoindNodeBackend + defaultConfig.ActiveNetParams = *regtestParams + + // Fees configuration + defaultConfig.BtcNodeBackendConfig.FeeMode = "dynamic" + defaultConfig.BtcNodeBackendConfig.EstimationMode = types.DynamicFeeEstimation + + // Wallet configuration + defaultConfig.WalletRPCConfig.Host = bitcoindHost + defaultConfig.WalletRPCConfig.User = bitcoindUser + defaultConfig.WalletRPCConfig.Pass = bitcoindPass + defaultConfig.WalletRPCConfig.DisableTLS = true + defaultConfig.WalletConfig.WalletPass = passphrase + defaultConfig.WalletConfig.WalletName = walletName + + // node configuration + defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCHost = bitcoindHost + defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCUser = bitcoindUser + defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCPass = bitcoindPass + + // Use rpc polling, as it is our default mode and it is a bit more troublesome + // to configure ZMQ from inside the bitcoind docker container + defaultConfig.BtcNodeBackendConfig.Bitcoind.RPCPolling = true + defaultConfig.BtcNodeBackendConfig.Bitcoind.BlockPollingInterval = 1 * time.Second + defaultConfig.BtcNodeBackendConfig.Bitcoind.TxPollingInterval = 1 * time.Second + + defaultConfig.StakerConfig.BabylonStallingInterval = 1 * time.Second + defaultConfig.StakerConfig.UnbondingTxCheckInterval = 1 * time.Second + defaultConfig.StakerConfig.CheckActiveInterval = 1 * time.Second + + // TODO: After bumping relayer version sending transactions concurrently fails wih + // fatal error: concurrent map writes + // For now diable concurrent sends but this need to be sorted out + defaultConfig.StakerConfig.MaxConcurrentTransactions = 1 + + return &defaultConfig +} + +func btcRpcTestClient(t *testing.T, bitcoindHost string) *rpcclient.Client { + testRpcBtcClient, err := rpcclient.New(&rpcclient.ConnConfig{ + Host: bitcoindHost, + User: bitcoindUser, + Pass: bitcoindPass, + DisableTLS: true, + DisableConnectOnNew: true, + DisableAutoReconnect: false, + // we use post mode as it sure it works with either bitcoind or btcwallet + // we may need to re-consider it later if we need any notifications + HTTPPostMode: true, + }, nil) + require.NoError(t, err) + return testRpcBtcClient +} + +type TestManager struct { + manager *containers.Manager + TestManagerStakerApp + TestManagerBTC +} + +type TestManagerStakerApp struct { + Config *stakercfg.Config + Db kvdb.Backend + Sa *staker.App + BabylonClient *babylonclient.BabylonController + wg *sync.WaitGroup + serviceAddress string + StakerClient *dc.StakerServiceJSONRPCClient + CovenantPrivKeys []*btcec.PrivateKey +} + +type TestManagerBTC struct { + MinerAddr btcutil.Address + BitcoindHandler *BitcoindTestHandler + Bitcoind *dockertest.Resource + WalletName string + WalletPassphrase string + BitcoindHost string + WalletPubKey *btcec.PublicKey + WalletAddrInfo *btcjson.GetAddressInfoResult + TestRpcBtcClient *rpcclient.Client +} + +type testStakingData struct { + StakerKey *btcec.PublicKey + StakerBabylonAddr sdk.AccAddress + FinalityProviderBabylonPrivKeys []*secp256k1.PrivKey + FinalityProviderBabylonAddrs []sdk.AccAddress + FinalityProviderBtcPrivKeys []*btcec.PrivateKey + FinalityProviderBtcKeys []*btcec.PublicKey + StakingTime uint16 + StakingAmount int64 +} + +func (d *testStakingData) GetNumRestakedFPs() int { + return len(d.FinalityProviderBabylonPrivKeys) +} + +func (tm *TestManager) getTestStakingData( + t *testing.T, + stakerKey *btcec.PublicKey, + stakingTime uint16, + stakingAmount int64, + numRestakedFPs int, +) *testStakingData { + stkData := GetTestStakingData(t, stakerKey, stakingTime, stakingAmount, numRestakedFPs, tm.BabylonClient.GetKeyAddress()) + + strAddrs := make([]string, numRestakedFPs) + for i := 0; i < numRestakedFPs; i++ { + strAddrs[i] = stkData.FinalityProviderBabylonAddrs[i].String() + } + + _, _, err := tm.manager.BabylondTxBankMultiSend(t, "node0", "1000000ubbn", strAddrs...) + require.NoError(t, err) + return stkData +} + +func GetTestStakingData( + t *testing.T, + stakerKey *btcec.PublicKey, + stakingTime uint16, + stakingAmount int64, + numRestakedFPs int, + stakerBabylonAddr sdk.AccAddress, +) *testStakingData { + fpBTCSKs, fpBTCPKs, err := datagen.GenRandomBTCKeyPairs(r, numRestakedFPs) + require.NoError(t, err) + + fpBBNSKs, fpBBNAddrs := make([]*secp256k1.PrivKey, numRestakedFPs), make([]sdk.AccAddress, numRestakedFPs) + for i := 0; i < numRestakedFPs; i++ { + fpBBNSK := secp256k1.GenPrivKey() + fpAddr := sdk.AccAddress(fpBBNSK.PubKey().Address().Bytes()) + + fpBBNSKs[i] = fpBBNSK + fpBBNAddrs[i] = fpAddr + } + + return &testStakingData{ + StakerKey: stakerKey, + // the staker babylon addr needs to be the same one that is going to sign + // the transaction in the end + StakerBabylonAddr: stakerBabylonAddr, + FinalityProviderBabylonPrivKeys: fpBBNSKs, + FinalityProviderBabylonAddrs: fpBBNAddrs, + FinalityProviderBtcPrivKeys: fpBTCSKs, + FinalityProviderBtcKeys: fpBTCPKs, + StakingTime: stakingTime, + StakingAmount: stakingAmount, + } +} + +func (td *testStakingData) withStakingTime(time uint16) *testStakingData { + tdCopy := *td + tdCopy.StakingTime = time + return &tdCopy +} + +func (td *testStakingData) withStakingAmout(amout int64) *testStakingData { + tdCopy := *td + tdCopy.StakingAmount = int64(amout) + return &tdCopy +} + +func StartManagerBtc( + t *testing.T, + ctx context.Context, + numMatureOutputsInWallet uint32, + manager *containers.Manager, +) *TestManagerBTC { + bitcoindHandler := NewBitcoindHandler(t, manager) + bitcoind := bitcoindHandler.Start() + passphrase := "pass" + walletName := "test-wallet" + _ = bitcoindHandler.CreateWallet(walletName, passphrase) + // only outputs which are 100 deep are mature + br := bitcoindHandler.GenerateBlocks(int(numMatureOutputsInWallet) + 100) + + minerAddressDecoded, err := btcutil.DecodeAddress(br.Address, regtestParams) + require.NoError(t, err) + + bitcoindHost := fmt.Sprintf("127.0.0.1:%s", bitcoind.GetPort("18443/tcp")) + rpcBtc := btcRpcTestClient(t, bitcoindHost) + + err = rpcBtc.WalletPassphrase(passphrase, 20) + require.NoError(t, err) + + info, err := rpcBtc.GetAddressInfo(br.Address) + require.NoError(t, err) + + pubKeyHex := *info.PubKey + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + require.NoError(t, err) + + walletPubKey, err := btcec.ParsePubKey(pubKeyBytes) + require.NoError(t, err) + + return &TestManagerBTC{ + MinerAddr: minerAddressDecoded, + BitcoindHandler: bitcoindHandler, + Bitcoind: bitcoind, + WalletName: walletName, + WalletPassphrase: passphrase, + BitcoindHost: bitcoindHost, + WalletPubKey: walletPubKey, + WalletAddrInfo: info, + TestRpcBtcClient: rpcBtc, + } +} + +func StartManager( + t *testing.T, + ctx context.Context, + numMatureOutputsInWallet uint32, +) *TestManager { + manager, err := containers.NewManager(t) + require.NoError(t, err) + + tmBTC := StartManagerBtc(t, ctx, numMatureOutputsInWallet, manager) + + quorum := 2 + coventantPrivKeys := genCovenants(t, 3) + tmStakerApp := StartManagerStakerApp(t, ctx, tmBTC, manager, quorum, coventantPrivKeys) + + return &TestManager{ + manager: manager, + TestManagerStakerApp: *tmStakerApp, + TestManagerBTC: *tmBTC, + } +} + +func StartManagerStakerApp( + t *testing.T, + ctx context.Context, + tmBTC *TestManagerBTC, + manager *containers.Manager, + covenantQuorum int, + coventantPrivKeys []*btcec.PrivateKey, +) *TestManagerStakerApp { + coventantPubKeys := make([]*btcec.PublicKey, len(coventantPrivKeys)) + for i, cvPrivKey := range coventantPrivKeys { + coventantPubKeys[i] = cvPrivKey.PubKey() + } + + var buff bytes.Buffer + err := regtestParams.GenesisBlock.Header.Serialize(&buff) + require.NoError(t, err) + baseHeaderHex := hex.EncodeToString(buff.Bytes()) + + pkScript, err := txscript.PayToAddrScript(tmBTC.MinerAddr) + require.NoError(t, err) + + tmpDir, err := testutil.TempDir(t) + require.NoError(t, err) + babylond, err := manager.RunBabylondResource( + t, + tmpDir, + covenantQuorum, + baseHeaderHex, + hex.EncodeToString(pkScript), // all slashing will be sent back to wallet + coventantPubKeys..., + ) + require.NoError(t, err) + + cfg := defaultStakerConfig(t, tmBTC.WalletName, tmBTC.WalletPassphrase, tmBTC.BitcoindHost) + // update port with the dynamically allocated one from docker + cfg.BabylonConfig.RPCAddr = fmt.Sprintf("http://localhost:%s", babylond.GetPort("26657/tcp")) + cfg.BabylonConfig.GRPCAddr = fmt.Sprintf("https://localhost:%s", babylond.GetPort("9090/tcp")) + + logger := logrus.New() + logger.SetLevel(logrus.DebugLevel) + logger.Out = os.Stdout + + // babylon configs for sending transactions + cfg.BabylonConfig.KeyDirectory = filepath.Join(tmpDir, "node0", "babylond") + // need to use this one to send otherwise we will have account sequence mismatch + // errors + cfg.BabylonConfig.Key = "test-spending-key" + + // Big adjustment to make sure we have enough gas in our transactions + cfg.BabylonConfig.GasAdjustment = 3.0 + + dirPath := filepath.Join(os.TempDir(), "stakerd", "e2etest") + err = os.MkdirAll(dirPath, 0755) + require.NoError(t, err) + dbTempDir, err := os.MkdirTemp(dirPath, "db") + require.NoError(t, err) + cfg.DBConfig.DBPath = dbTempDir + + dbbackend, err := stakercfg.GetDBBackend(cfg.DBConfig) + require.NoError(t, err) + + m := metrics.NewStakerMetrics() + stakerApp, err := staker.NewStakerAppFromConfig(cfg, logger, zapLogger, dbbackend, m) + require.NoError(t, err) + // we require separate client to send BTC headers to babylon node (interface does not need this method?) + bl, err := babylonclient.NewBabylonController(cfg.BabylonConfig, &cfg.ActiveNetParams, logger, zapLogger) + require.NoError(t, err) + + addressString := fmt.Sprintf("127.0.0.1:%d", testutil.AllocateUniquePort(t)) + addrPort := netip.MustParseAddrPort(addressString) + address := net.TCPAddrFromAddrPort(addrPort) + cfg.RPCListeners = append(cfg.RPCListeners, address) // todo(lazar): check with konrad who uses this + + stakerService := service.NewStakerService( + cfg, + stakerApp, + logger, + dbbackend, + ) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + err := stakerService.RunUntilShutdown(ctx) + if err != nil { + t.Fatalf("Error running server: %v", err) + } + }() + // Wait for the server to start + time.Sleep(3 * time.Second) + + stakerClient, err := dc.NewStakerServiceJSONRPCClient("tcp://" + addressString) + require.NoError(t, err) + + fmt.Printf("\n log config %+v", cfg) + + return &TestManagerStakerApp{ + Config: cfg, + Db: dbbackend, + Sa: stakerApp, + BabylonClient: bl, + wg: &wg, + serviceAddress: addressString, + StakerClient: stakerClient, + CovenantPrivKeys: coventantPrivKeys, + } +} + +func genCovenants(t *testing.T, numCovenants int) []*btcec.PrivateKey { + var coventantPrivKeys []*btcec.PrivateKey + for i := 0; i < numCovenants; i++ { + covenantPrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + coventantPrivKeys = append(coventantPrivKeys, covenantPrivKey) + } + return coventantPrivKeys +} + +func (tm *TestManager) Stop(t *testing.T, cancelFunc context.CancelFunc) { + cancelFunc() + tm.wg.Wait() + err := tm.manager.ClearResources() + require.NoError(t, err) + err = os.RemoveAll(tm.Config.DBConfig.DBPath) + require.NoError(t, err) +} + +func (tm *TestManager) RestartApp(t *testing.T, newCtx context.Context, cancelFunc context.CancelFunc) { + // Restart the app with no-op action + tm.RestartAppWithAction(t, newCtx, cancelFunc, func(t *testing.T) {}) +} + +// RestartAppWithAction: +// 1. Stop the staker app +// 2. Perform provided action. Warning:this action must not use staker app as +// app is stopped at this point +// 3. Start the staker app +func (tm *TestManager) RestartAppWithAction(t *testing.T, ctx context.Context, cancelFunc context.CancelFunc, action func(t *testing.T)) { + // First stop the app + cancelFunc() + tm.wg.Wait() + + // Perform the action + action(t) + + // Now reset all components and start again + logger := logrus.New() + logger.SetLevel(logrus.DebugLevel) + logger.Out = os.Stdout + + dbbackend, err := stakercfg.GetDBBackend(tm.Config.DBConfig) + require.NoError(t, err) + m := metrics.NewStakerMetrics() + stakerApp, err := staker.NewStakerAppFromConfig(tm.Config, logger, zapLogger, dbbackend, m) + require.NoError(t, err) + + service := service.NewStakerService( + tm.Config, + stakerApp, + logger, + dbbackend, + ) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + err := service.RunUntilShutdown(ctx) + if err != nil { + t.Fatalf("Error running server: %v", err) + } + }() + // Wait for the server to start + time.Sleep(3 * time.Second) + + tm.wg = &wg + tm.Db = dbbackend + tm.Sa = stakerApp + stakerClient, err := dc.NewStakerServiceJSONRPCClient("tcp://" + tm.serviceAddress) + require.NoError(t, err) + tm.StakerClient = stakerClient +} + +func retrieveTransactionFromMempool(t *testing.T, client *rpcclient.Client, hashes []*chainhash.Hash) []*btcutil.Tx { + var txes []*btcutil.Tx + for _, txHash := range hashes { + tx, err := client.GetRawTransaction(txHash) + + if err != nil { + // this is e2e helper method, so this error most probably some of the + // transactions are still not in the mempool + return []*btcutil.Tx{} + } + + txes = append(txes, tx) + } + return txes +} + +func GetAllMinedBtcHeadersSinceGenesis(t *testing.T, c *rpcclient.Client) []*wire.BlockHeader { + height, err := c.GetBlockCount() + require.NoError(t, err) + + var headers []*wire.BlockHeader + + for i := 1; i <= int(height); i++ { + hash, err := c.GetBlockHash(int64(i)) + require.NoError(t, err) + header, err := c.GetBlockHeader(hash) + require.NoError(t, err) + headers = append(headers, header) + } + + return headers +} + +func opReturnScript(data []byte) []byte { + builder := txscript.NewScriptBuilder() + script, err := builder.AddOp(txscript.OP_RETURN).AddData(data).Script() + if err != nil { + panic(err) + } + return script +} + +func txToBytes(tx *wire.MsgTx) []byte { + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + _ = tx.Serialize(buf) + return buf.Bytes() +} + +func txsToBytes(txs []*wire.MsgTx) [][]byte { + var txsBytes [][]byte + for _, tx := range txs { + txsBytes = append(txsBytes, txToBytes(tx)) + } + return txsBytes +} + +func (tm *TestManager) FinalizeUntilEpoch(t *testing.T, epoch uint64) { + bbnClient := tm.BabylonClient.GetBBNClient() + ckptParams, err := bbnClient.BTCCheckpointParams() + require.NoError(t, err) + // wait until the checkpoint of this epoch is sealed + require.Eventually(t, func() bool { + lastSealedCkpt, err := bbnClient.LatestEpochFromStatus(ckpttypes.Sealed) + if err != nil { + return false + } + return epoch <= lastSealedCkpt.RawCheckpoint.EpochNum + }, 1*time.Minute, 1*time.Second) + + t.Logf("start finalizing epochs till %d", epoch) + // Random source for the generation of BTC data + // r := rand.New(rand.NewSource(time.Now().Unix())) + + // get all checkpoints of these epochs + pagination := &sdkquerytypes.PageRequest{ + Key: ckpttypes.CkptsObjectKey(1), + Limit: epoch, + } + resp, err := bbnClient.RawCheckpoints(pagination) + require.NoError(t, err) + require.Equal(t, int(epoch), len(resp.RawCheckpoints)) + + submitter := tm.BabylonClient.GetKeyAddress() + + for _, checkpoint := range resp.RawCheckpoints { + // currentBtcTipResp, err := tm.BabylonClient.QueryBtcLightClientTip() + // require.NoError(t, err) + // tipHeader, err := bbntypes.NewBTCHeaderBytesFromHex(currentBtcTipResp.HeaderHex) + // require.NoError(t, err) + + rawCheckpoint, err := checkpoint.Ckpt.ToRawCheckpoint() + require.NoError(t, err) + + btcCheckpoint, err := ckpttypes.FromRawCkptToBTCCkpt(rawCheckpoint, submitter) + require.NoError(t, err) + + babylonTagBytes, err := hex.DecodeString("01020304") + require.NoError(t, err) + + p1, p2, err := txformat.EncodeCheckpointData( + babylonTagBytes, + txformat.CurrentVersion, + btcCheckpoint, + ) + + err = tm.Sa.Wallet().UnlockWallet(60) + require.NoError(t, err) + tx1, err := tm.Sa.Wallet().CreateAndSignTx( + []*wire.TxOut{ + wire.NewTxOut(0, opReturnScript(p1)), + }, + 2000, + tm.MinerAddr, + nil, + ) + require.NoError(t, err) + _, err = tm.Sa.Wallet().SendRawTransaction(tx1, true) + require.NoError(t, err) + + resp1 := tm.BitcoindHandler.GenerateBlocks(1) + + tx2, err := tm.Sa.Wallet().CreateAndSignTx( + []*wire.TxOut{ + wire.NewTxOut(0, opReturnScript(p2)), + }, + 2000, + tm.MinerAddr, + nil, + ) + require.NoError(t, err) + _, err = tm.Sa.Wallet().SendRawTransaction(tx2, true) + require.NoError(t, err) + resp2 := tm.BitcoindHandler.GenerateBlocks(1) + + block1Hash, err := chainhash.NewHashFromStr(resp1.Blocks[0]) + require.NoError(t, err) + block2Hash, err := chainhash.NewHashFromStr(resp2.Blocks[0]) + require.NoError(t, err) + + block1, err := tm.TestRpcBtcClient.GetBlock(block1Hash) + require.NoError(t, err) + block2, err := tm.TestRpcBtcClient.GetBlock(block2Hash) + require.NoError(t, err) + + _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{ + &block1.Header, + &block2.Header, + }) + + header1Bytes := bbntypes.NewBTCHeaderBytesFromBlockHeader(&block1.Header) + header2Bytes := bbntypes.NewBTCHeaderBytesFromBlockHeader(&block2.Header) + + proof1, err := btcctypes.SpvProofFromHeaderAndTransactions(&header1Bytes, txsToBytes(block1.Transactions), 1) + require.NoError(t, err) + proof2, err := btcctypes.SpvProofFromHeaderAndTransactions(&header2Bytes, txsToBytes(block2.Transactions), 1) + require.NoError(t, err) + + _, err = tm.BabylonClient.InsertSpvProofs(submitter.String(), []*btcctypes.BTCSpvProof{ + proof1, + proof2, + }) + require.NoError(t, err) + + // // wait until this checkpoint is submitted + require.Eventually(t, func() bool { + ckpt, err := bbnClient.RawCheckpoint(checkpoint.Ckpt.EpochNum) + require.NoError(t, err) + return ckpt.RawCheckpoint.Status == ckpttypes.Submitted + }, eventuallyWaitTimeOut, eventuallyPollTime) + } + + tm.mineNEmptyBlocks(t, uint32(ckptParams.Params.CheckpointFinalizationTimeout), true) + + // // wait until the checkpoint of this epoch is finalised + require.Eventually(t, func() bool { + lastFinalizedCkpt, err := bbnClient.LatestEpochFromStatus(ckpttypes.Finalized) + if err != nil { + t.Logf("failed to get last finalized epoch: %v", err) + return false + } + return epoch <= lastFinalizedCkpt.RawCheckpoint.EpochNum + }, eventuallyWaitTimeOut, 1*time.Second) + + t.Logf("epoch %d is finalised", epoch) +} + +func (tm *TestManager) createAndRegisterFinalityProviders(t *testing.T, stkData *testStakingData) { + params, err := tm.BabylonClient.QueryStakingTracker() + require.NoError(t, err) + + for i := 0; i < stkData.GetNumRestakedFPs(); i++ { + // ensure the finality provider in testStakingData does not exist yet + fpResp, err := tm.BabylonClient.QueryFinalityProvider(stkData.FinalityProviderBtcKeys[i]) + require.Nil(t, fpResp) + require.Error(t, err) + require.True(t, errors.Is(err, babylonclient.ErrFinalityProviderDoesNotExist)) + + pop, err := btcstypes.NewPoPBTC(stkData.FinalityProviderBabylonAddrs[i], stkData.FinalityProviderBtcPrivKeys[i]) + require.NoError(t, err) + + btcFpKey := bbntypes.NewBIP340PubKeyFromBTCPK(stkData.FinalityProviderBtcKeys[i]) + + // get current finality providers + resp, err := tm.BabylonClient.QueryFinalityProviders(100, 0) + require.NoError(t, err) + // register the generated finality provider + err = tm.BabylonClient.RegisterFinalityProvider( + stkData.FinalityProviderBabylonAddrs[i], + stkData.FinalityProviderBabylonPrivKeys[i], + btcFpKey, + ¶ms.MinComissionRate, + &sttypes.Description{ + Moniker: "tester", + }, + pop, + ) + require.NoError(t, err) + + require.Eventually(t, func() bool { + resp2, err := tm.BabylonClient.QueryFinalityProviders(100, 0) + require.NoError(t, err) + + // After registration we should have one finality provider + return len(resp2.FinalityProviders) == len(resp.FinalityProviders)+1 + }, time.Minute, 250*time.Millisecond) + } +} + +func (tm *TestManager) sendHeadersToBabylon(t *testing.T, headers []*wire.BlockHeader) { + _, err := tm.BabylonClient.InsertBtcBlockHeaders(headers) + require.NoError(t, err) +} + +func (tm *TestManager) mineNEmptyBlocks(t *testing.T, numHeaders uint32, sendToBabylon bool) []*wire.BlockHeader { + resp := tm.BitcoindHandler.GenerateBlocks(int(numHeaders)) + + var minedHeaders []*wire.BlockHeader + for _, hash := range resp.Blocks { + hash, err := chainhash.NewHashFromStr(hash) + require.NoError(t, err) + header, err := tm.TestRpcBtcClient.GetBlockHeader(hash) + require.NoError(t, err) + minedHeaders = append(minedHeaders, header) + } + + if sendToBabylon { + tm.sendHeadersToBabylon(t, minedHeaders) + } + + return minedHeaders +} + +func (tm *TestManager) mineBlock(t *testing.T) *wire.MsgBlock { + resp := tm.BitcoindHandler.GenerateBlocks(1) + hash, err := chainhash.NewHashFromStr(resp.Blocks[0]) + require.NoError(t, err) + header, err := tm.TestRpcBtcClient.GetBlock(hash) + require.NoError(t, err) + return header +} + +func (tm *TestManager) sendStakingTxBTC( + t *testing.T, + stkData *testStakingData, + sendToBabylonFirst bool, +) *chainhash.Hash { + fpBTCPKs := []string{} + for i := 0; i < stkData.GetNumRestakedFPs(); i++ { + fpBTCPK := hex.EncodeToString(schnorr.SerializePubKey(stkData.FinalityProviderBtcKeys[i])) + fpBTCPKs = append(fpBTCPKs, fpBTCPK) + } + res, err := tm.StakerClient.Stake( + context.Background(), + tm.MinerAddr.String(), + stkData.StakingAmount, + fpBTCPKs, + int64(stkData.StakingTime), + sendToBabylonFirst, + ) + require.NoError(t, err) + txHash := res.TxHash + + stakingDetails, err := tm.StakerClient.StakingDetails(context.Background(), txHash) + require.NoError(t, err) + require.Equal(t, stakingDetails.StakingTxHash, txHash) + + if sendToBabylonFirst { + require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BABYLON.String()) + } else { + require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BTC.String()) + } + hashFromString, err := chainhash.NewHashFromStr(txHash) + require.NoError(t, err) + + // only wait for blocks if we are using the old flow, and send staking tx to BTC + // first + if !sendToBabylonFirst { + require.Eventually(t, func() bool { + txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcBtcClient, []*chainhash.Hash{hashFromString}) + return len(txFromMempool) == 1 + }, eventuallyWaitTimeOut, eventuallyPollTime) + + mBlock := tm.mineBlock(t) + require.Equal(t, 2, len(mBlock.Transactions)) + + _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) + require.NoError(t, err) + } + return hashFromString +} + +func (tm *TestManager) sendMultipleStakingTx(t *testing.T, tStkData []*testStakingData, sendToBabylonFirst bool) []*chainhash.Hash { + var hashes []*chainhash.Hash + for _, data := range tStkData { + fpBTCPKs := []string{} + for i := 0; i < data.GetNumRestakedFPs(); i++ { + fpBTCPK := hex.EncodeToString(schnorr.SerializePubKey(data.FinalityProviderBtcKeys[i])) + fpBTCPKs = append(fpBTCPKs, fpBTCPK) + } + res, err := tm.StakerClient.Stake( + context.Background(), + tm.MinerAddr.String(), + data.StakingAmount, + fpBTCPKs, + int64(data.StakingTime), + sendToBabylonFirst, + ) + require.NoError(t, err) + txHash, err := chainhash.NewHashFromStr(res.TxHash) + require.NoError(t, err) + hashes = append(hashes, txHash) + } + + for _, txHash := range hashes { + txHash := txHash + hashStr := txHash.String() + stakingDetails, err := tm.StakerClient.StakingDetails(context.Background(), hashStr) + require.NoError(t, err) + require.Equal(t, stakingDetails.StakingTxHash, hashStr) + + if sendToBabylonFirst { + require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BABYLON.String()) + } else { + require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BTC.String()) + } + } + + if !sendToBabylonFirst { + mBlock := tm.mineBlock(t) + require.Equal(t, len(hashes)+1, len(mBlock.Transactions)) + + _, err := tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) + require.NoError(t, err) + } + return hashes +} + +func (tm *TestManager) sendWatchedStakingTx( + t *testing.T, + tStkData *testStakingData, + params *babylonclient.StakingParams, +) *chainhash.Hash { + unbondingTme := uint16(params.FinalizationTimeoutBlocks) + 1 + + stakingInfo, err := staking.BuildStakingInfo( + tStkData.StakerKey, + tStkData.FinalityProviderBtcKeys, + params.CovenantPks, + params.CovenantQuruomThreshold, + tStkData.StakingTime, + btcutil.Amount(tStkData.StakingAmount), + regtestParams, + ) + require.NoError(t, err) + + err = tm.Sa.Wallet().UnlockWallet(20) + require.NoError(t, err) + + tx, err := tm.Sa.Wallet().CreateAndSignTx( + []*wire.TxOut{stakingInfo.StakingOutput}, + 2000, + tm.MinerAddr, + nil, + ) + require.NoError(t, err) + txHash := tx.TxHash() + _, err = tm.Sa.Wallet().SendRawTransaction(tx, true) + require.NoError(t, err) + + // Wait for tx to be in mempool + require.Eventually(t, func() bool { + tx, err := tm.TestRpcBtcClient.GetRawTransaction(&txHash) + if err != nil { + return false + } + + if tx == nil { + return false + } + + return true + }, 1*time.Minute, eventuallyPollTime) + + stakingOutputIdx := 0 + + require.NoError(t, err) + + slashingTx, err := staking.BuildSlashingTxFromStakingTxStrict( + tx, + uint32(stakingOutputIdx), + params.SlashingPkScript, + tStkData.StakerKey, + unbondingTme, + int64(params.MinSlashingTxFeeSat)+10, + params.SlashingRate, + regtestParams, + ) + require.NoError(t, err) + + stakingTxSlashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + slashingSigResult, err := tm.Sa.Wallet().SignOneInputTaprootSpendingTransaction( + &walletcontroller.TaprootSigningRequest{ + FundingOutput: stakingInfo.StakingOutput, + TxToSign: slashingTx, + SignerAddress: tm.MinerAddr, + SpendDescription: &walletcontroller.SpendPathDescription{ + ControlBlock: &stakingTxSlashingPathInfo.ControlBlock, + ScriptLeaf: &stakingTxSlashingPathInfo.RevealedLeaf, + }, + }, + ) + + require.NoError(t, err) + require.NotNil(t, slashingSigResult.Signature) + + serializedStakingTx, err := utils.SerializeBtcTransaction(tx) + require.NoError(t, err) + serializedSlashingTx, err := utils.SerializeBtcTransaction(slashingTx) + require.NoError(t, err) + // Build unbonding related data + unbondingFee := params.UnbondingFee + unbondingAmount := btcutil.Amount(tStkData.StakingAmount) - unbondingFee + + unbondingInfo, err := staking.BuildUnbondingInfo( + tStkData.StakerKey, + tStkData.FinalityProviderBtcKeys, + params.CovenantPks, + params.CovenantQuruomThreshold, + unbondingTme, + unbondingAmount, + regtestParams, + ) + require.NoError(t, err) + + unbondingSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + unbondingTx := wire.NewMsgTx(2) + unbondingTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, uint32(stakingOutputIdx)), nil, nil)) + unbondingTx.AddTxOut(unbondingInfo.UnbondingOutput) + + slashUnbondingTx, err := staking.BuildSlashingTxFromStakingTxStrict( + unbondingTx, + 0, + params.SlashingPkScript, + tStkData.StakerKey, + unbondingTme, + int64(params.MinSlashingTxFeeSat)+10, + params.SlashingRate, + regtestParams, + ) + require.NoError(t, err) + + slashingUnbondingSigResult, err := tm.Sa.Wallet().SignOneInputTaprootSpendingTransaction( + &walletcontroller.TaprootSigningRequest{ + FundingOutput: unbondingTx.TxOut[0], + TxToSign: slashUnbondingTx, + SignerAddress: tm.MinerAddr, + SpendDescription: &walletcontroller.SpendPathDescription{ + ControlBlock: &unbondingSlashingPathInfo.ControlBlock, + ScriptLeaf: &unbondingSlashingPathInfo.RevealedLeaf, + }, + }, + ) + + require.NoError(t, err) + require.NotNil(t, slashingUnbondingSigResult.Signature) + + serializedUnbondingTx, err := utils.SerializeBtcTransaction(unbondingTx) + require.NoError(t, err) + serializedSlashUnbondingTx, err := utils.SerializeBtcTransaction(slashUnbondingTx) + require.NoError(t, err) + + babylonAddrHash := tmhash.Sum(tStkData.StakerBabylonAddr.Bytes()) + + sig, err := tm.Sa.Wallet().SignBip322NativeSegwit(babylonAddrHash, tm.MinerAddr) + require.NoError(t, err) + + pop, err := babylonclient.NewBabylonBip322Pop( + babylonAddrHash, + sig, + tm.MinerAddr, + ) + require.NoError(t, err) + + fpBTCPKs := []string{} + for i := 0; i < tStkData.GetNumRestakedFPs(); i++ { + fpBTCPK := hex.EncodeToString(schnorr.SerializePubKey(tStkData.FinalityProviderBtcKeys[i])) + fpBTCPKs = append(fpBTCPKs, fpBTCPK) + } + _, err = tm.StakerClient.WatchStaking( + context.Background(), + hex.EncodeToString(serializedStakingTx), + int(tStkData.StakingTime), + int(tStkData.StakingAmount), + hex.EncodeToString(schnorr.SerializePubKey(tStkData.StakerKey)), + fpBTCPKs, + hex.EncodeToString(serializedSlashingTx), + hex.EncodeToString(slashingSigResult.Signature.Serialize()), + tStkData.StakerBabylonAddr.String(), + tm.MinerAddr.String(), + hex.EncodeToString(pop.BtcSig), + hex.EncodeToString(serializedUnbondingTx), + hex.EncodeToString(serializedSlashUnbondingTx), + hex.EncodeToString(slashingUnbondingSigResult.Signature.Serialize()), + int(unbondingTme), + // Use schnor verification + int(btcstypes.BTCSigType_BIP322), + ) + require.NoError(t, err) + + txs := retrieveTransactionFromMempool(t, tm.TestRpcBtcClient, []*chainhash.Hash{&txHash}) + require.Len(t, txs, 1) + + mBlock := tm.mineBlock(t) + require.Equal(t, 2, len(mBlock.Transactions)) + _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) + require.NoError(t, err) + + return &txHash +} + +func (tm *TestManager) spendStakingTxWithHash(t *testing.T, stakingTxHash *chainhash.Hash) (*chainhash.Hash, *btcutil.Amount) { + res, err := tm.StakerClient.SpendStakingTransaction(context.Background(), stakingTxHash.String()) + require.NoError(t, err) + spendTxHash, err := chainhash.NewHashFromStr(res.TxHash) + require.NoError(t, err) + + iAmount, err := strconv.ParseInt(res.TxValue, 10, 64) + require.NoError(t, err) + spendTxValue := btcutil.Amount(iAmount) + + require.Eventually(t, func() bool { + txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcBtcClient, []*chainhash.Hash{spendTxHash}) + return len(txFromMempool) == 1 + }, eventuallyWaitTimeOut, eventuallyPollTime) + + sendTx := retrieveTransactionFromMempool(t, tm.TestRpcBtcClient, []*chainhash.Hash{spendTxHash})[0] + + // Tx is in mempool + txDetails, txState, err := tm.Sa.Wallet().TxDetails(spendTxHash, sendTx.MsgTx().TxOut[0].PkScript) + require.NoError(t, err) + require.Nil(t, txDetails) + require.Equal(t, txState, walletcontroller.TxInMemPool) + + // Block with spend is mined + mBlock1 := tm.mineBlock(t) + require.Equal(t, 2, len(mBlock1.Transactions)) + + // Tx is in chain + txDetails, txState, err = tm.Sa.Wallet().TxDetails(spendTxHash, sendTx.MsgTx().TxOut[0].PkScript) + require.NoError(t, err) + require.NotNil(t, txDetails) + require.Equal(t, txState, walletcontroller.TxInChain) + + return spendTxHash, &spendTxValue +} + +func (tm *TestManager) waitForStakingTxState(t *testing.T, txHash *chainhash.Hash, expectedState proto.TransactionState) { + require.Eventually(t, func() bool { + detailResult, err := tm.StakerClient.StakingDetails(context.Background(), txHash.String()) + if err != nil { + return false + } + return detailResult.StakingState == expectedState.String() + }, 2*time.Minute, eventuallyPollTime) +} + +func (tm *TestManager) walletUnspentsOutputsContainsOutput(t *testing.T, from btcutil.Address, withValue btcutil.Amount) bool { + unspentOutputs, err := tm.Sa.ListUnspentOutputs() + require.NoError(t, err) + + var containsOutput bool = false + + for _, output := range unspentOutputs { + if output.Address == tm.MinerAddr.String() && int64(output.Amount) == int64(withValue) { + containsOutput = true + } + } + + return containsOutput +} + +func (tm *TestManager) insertAllMinedBlocksToBabylon(t *testing.T) { + headers := GetAllMinedBtcHeadersSinceGenesis(t, tm.TestRpcBtcClient) + _, err := tm.BabylonClient.InsertBtcBlockHeaders(headers) + require.NoError(t, err) +} + +func (tm *TestManager) insertCovenantSigForDelegation( + t *testing.T, + btcDel *btcstypes.BTCDelegationResponse, +) { + fpBTCPKs, err := bbntypes.NewBTCPKsFromBIP340PKs(btcDel.FpBtcPkList) + require.NoError(t, err) + + slashingTxBytes, err := hex.DecodeString(btcDel.SlashingTxHex) + require.NoError(t, err) + slashingTx := btcstypes.BTCSlashingTx(slashingTxBytes) + stakingTx := btcDel.StakingTxHex + stakingMsgTx, _, err := bbntypes.NewBTCTxFromHex(stakingTx) + require.NoError(t, err) + + cl := tm.Sa.BabylonController() + params, err := cl.Params() + require.NoError(t, err) + + stakingInfo, err := staking.BuildStakingInfo( + btcDel.BtcPk.MustToBTCPK(), + fpBTCPKs, + params.CovenantPks, + params.CovenantQuruomThreshold, + uint16(btcDel.EndHeight-btcDel.StartHeight), + btcutil.Amount(btcDel.TotalSat), + regtestParams, + ) + require.NoError(t, err) + + slashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + covenantSlashingTxSigs, err := datagen.GenCovenantAdaptorSigs( + tm.CovenantPrivKeys, + fpBTCPKs, + stakingMsgTx, + slashingPathInfo.GetPkScriptPath(), + &slashingTx, + ) + require.NoError(t, err) + + // slash unbonding tx spends unbonding tx + unbondingMsgTx, _, err := bbntypes.NewBTCTxFromHex(btcDel.UndelegationResponse.UnbondingTxHex) + require.NoError(t, err) + + unbondingInfo, err := staking.BuildUnbondingInfo( + btcDel.BtcPk.MustToBTCPK(), + fpBTCPKs, + params.CovenantPks, + params.CovenantQuruomThreshold, + uint16(btcDel.UnbondingTime), + btcutil.Amount(unbondingMsgTx.TxOut[0].Value), + regtestParams, + ) + unbondingSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + // generate all covenant signatures from all covenant members + unbondingSlashingTx, err := btcstypes.NewBTCSlashingTxFromHex(btcDel.UndelegationResponse.SlashingTxHex) + require.NoError(t, err) + covenantUnbondingSlashingTxSigs, err := datagen.GenCovenantAdaptorSigs( + tm.CovenantPrivKeys, + fpBTCPKs, + unbondingMsgTx, + unbondingSlashingPathInfo.GetPkScriptPath(), + unbondingSlashingTx, + ) + require.NoError(t, err) + + // each covenant member submits signatures + unbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() + require.NoError(t, err) + covUnbondingSigs, err := datagen.GenCovenantUnbondingSigs( + tm.CovenantPrivKeys, + stakingMsgTx, + btcDel.StakingOutputIdx, + unbondingPathInfo.GetPkScriptPath(), + unbondingMsgTx, + ) + require.NoError(t, err) + + var messages []*btcstypes.MsgAddCovenantSigs + for i := 0; i < len(tm.CovenantPrivKeys); i++ { + msg := tm.BabylonClient.CreateCovenantMessage( + bbntypes.NewBIP340PubKeyFromBTCPK(tm.CovenantPrivKeys[i].PubKey()), + stakingMsgTx.TxHash().String(), + covenantSlashingTxSigs[i].AdaptorSigs, + bbntypes.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]), + covenantUnbondingSlashingTxSigs[i].AdaptorSigs, + ) + messages = append(messages, msg) + } + // we insert are covenant signatures in on message, this way staker + // program must handle the case of all signatures being present in Babylon + // delegation + // it also speeds up the tests + _, err = tm.BabylonClient.SubmitMultipleCovenantMessages(messages) + require.NoError(t, err) +} diff --git a/itest/testutil/appcli.go b/itest/testutil/appcli.go new file mode 100644 index 0000000..4934d7a --- /dev/null +++ b/itest/testutil/appcli.go @@ -0,0 +1,127 @@ +package testutil + +import ( + "bytes" + "encoding/json" + "fmt" + "math/rand" + "os" + "path/filepath" + "testing" + + "github.com/babylonlabs-io/babylon/testutil/datagen" + cmdadmin "github.com/babylonlabs-io/btc-staker/cmd/stakercli/admin" + cmddaemon "github.com/babylonlabs-io/btc-staker/cmd/stakercli/daemon" + "github.com/babylonlabs-io/btc-staker/cmd/stakercli/transaction" + "github.com/babylonlabs-io/networks/parameters/parser" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" +) + +var ( + defaultParam = parser.VersionedGlobalParams{ + Version: 0, + ActivationHeight: 100, + StakingCap: 3000000, + CapHeight: 0, + Tag: "01020304", + CovenantPks: []string{ + "03ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "03a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "0359d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + "0357349e985e742d5131e1e2b227b5170f6350ac2e2feb72254fcc25b3cee21a18", + "03c8ccb03c379e452f10c81232b41a1ca8b63d0baf8387e57d302c987e5abb8527", + }, + CovenantQuorum: 3, + UnbondingTime: 1000, + UnbondingFee: 1000, + MaxStakingAmount: 300000, + MinStakingAmount: 3000, + MaxStakingTime: 10000, + MinStakingTime: 100, + ConfirmationDepth: 10, + } + + GlobalParams = parser.GlobalParams{ + Versions: []*parser.VersionedGlobalParams{&defaultParam}, + } + + //nolint:errchkjson + paramsMarshalled, _ = json.Marshal(GlobalParams) +) + +func TestApp() *cli.App { + app := cli.NewApp() + app.Name = "stakercli" + app.Commands = append(app.Commands, cmddaemon.DaemonCommands...) + app.Commands = append(app.Commands, cmdadmin.AdminCommands...) + app.Commands = append(app.Commands, transaction.TransactionCommands...) + return app +} + +func AppRunCreatePhase1StakingTx(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1StakingTxResponse { + args := []string{"stakercli", "transaction", "create-phase1-staking-transaction"} + args = append(args, arguments...) + output := appRunWithOutput(r, t, app, args) + + var data transaction.CreatePhase1StakingTxResponse + err := json.Unmarshal([]byte(output), &data) + require.NoError(t, err) + + return data +} + +func AppRunCreatePhase1StakingTxWithParams(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) transaction.CreatePhase1StakingTxResponse { + args := []string{"stakercli", "transaction", "create-phase1-staking-transaction-with-params"} + args = append(args, arguments...) + output := appRunWithOutput(r, t, app, args) + + var data transaction.CreatePhase1StakingTxResponse + err := json.Unmarshal([]byte(output), &data) + require.NoError(t, err) + + return data +} + +func appRunWithOutput(r *rand.Rand, t *testing.T, app *cli.App, arguments []string) string { + outPut := filepath.Join(t.TempDir(), fmt.Sprintf("%s-out.txt", datagen.GenRandomHexStr(r, 10))) + outPutFile, err := os.Create(outPut) + require.NoError(t, err) + defer outPutFile.Close() + + // set file to stdout to read. + oldStd := os.Stdout + os.Stdout = outPutFile + + err = app.Run(arguments) + require.NoError(t, err) + + // set to old stdout + os.Stdout = oldStd + return readFromFile(t, outPutFile) +} + +func readFromFile(t *testing.T, f *os.File) string { + _, err := f.Seek(0, 0) + require.NoError(t, err) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(f) + require.NoError(t, err) + return buf.String() +} + +func CreateTempFileWithParams(t require.TestingT) string { + return CreateTempFileWithData(t, "tmpParams-*.json", paramsMarshalled) +} + +func CreateTempFileWithData(t require.TestingT, pattern string, data []byte) string { + file, err := os.CreateTemp("", pattern) + require.NoError(t, err) + defer file.Close() + _, err = file.Write(data) + require.NoError(t, err) + info, err := file.Stat() + require.NoError(t, err) + return filepath.Join(os.TempDir(), info.Name()) +} diff --git a/proto/transaction.pb.go b/proto/transaction.pb.go index 9870fd3..5323319 100644 --- a/proto/transaction.pb.go +++ b/proto/transaction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.35.1 // protoc v3.6.1 // source: transaction.proto @@ -101,11 +101,9 @@ type WatchedTxData struct { func (x *WatchedTxData) Reset() { *x = WatchedTxData{} - if protoimpl.UnsafeEnabled { - mi := &file_transaction_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_transaction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *WatchedTxData) String() string { @@ -116,7 +114,7 @@ func (*WatchedTxData) ProtoMessage() {} func (x *WatchedTxData) ProtoReflect() protoreflect.Message { mi := &file_transaction_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -199,11 +197,9 @@ type BTCConfirmationInfo struct { func (x *BTCConfirmationInfo) Reset() { *x = BTCConfirmationInfo{} - if protoimpl.UnsafeEnabled { - mi := &file_transaction_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_transaction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BTCConfirmationInfo) String() string { @@ -214,7 +210,7 @@ func (*BTCConfirmationInfo) ProtoMessage() {} func (x *BTCConfirmationInfo) ProtoReflect() protoreflect.Message { mi := &file_transaction_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -254,11 +250,9 @@ type CovenantSig struct { func (x *CovenantSig) Reset() { *x = CovenantSig{} - if protoimpl.UnsafeEnabled { - mi := &file_transaction_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_transaction_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CovenantSig) String() string { @@ -269,7 +263,7 @@ func (*CovenantSig) ProtoMessage() {} func (x *CovenantSig) ProtoReflect() protoreflect.Message { mi := &file_transaction_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -313,11 +307,9 @@ type UnbondingTxData struct { func (x *UnbondingTxData) Reset() { *x = UnbondingTxData{} - if protoimpl.UnsafeEnabled { - mi := &file_transaction_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_transaction_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UnbondingTxData) String() string { @@ -328,7 +320,7 @@ func (*UnbondingTxData) ProtoMessage() {} func (x *UnbondingTxData) ProtoReflect() protoreflect.Message { mi := &file_transaction_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -393,16 +385,15 @@ type TrackedTransaction struct { State TransactionState `protobuf:"varint,10,opt,name=state,proto3,enum=proto.TransactionState" json:"state,omitempty"` Watched bool `protobuf:"varint,11,opt,name=watched,proto3" json:"watched,omitempty"` // this data is only filled if tracked transactions state is >= SENT_TO_BABYLON - UnbondingTxData *UnbondingTxData `protobuf:"bytes,12,opt,name=unbonding_tx_data,json=unbondingTxData,proto3" json:"unbonding_tx_data,omitempty"` + UnbondingTxData *UnbondingTxData `protobuf:"bytes,12,opt,name=unbonding_tx_data,json=unbondingTxData,proto3" json:"unbonding_tx_data,omitempty"` + BabylonBTCDelegationTxHash string `protobuf:"bytes,13,opt,name=babylonBTCDelegationTxHash,proto3" json:"babylonBTCDelegationTxHash,omitempty"` } func (x *TrackedTransaction) Reset() { *x = TrackedTransaction{} - if protoimpl.UnsafeEnabled { - mi := &file_transaction_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_transaction_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *TrackedTransaction) String() string { @@ -413,7 +404,7 @@ func (*TrackedTransaction) ProtoMessage() {} func (x *TrackedTransaction) ProtoReflect() protoreflect.Message { mi := &file_transaction_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -512,6 +503,13 @@ func (x *TrackedTransaction) GetUnbondingTxData() *UnbondingTxData { return nil } +func (x *TrackedTransaction) GetBabylonBTCDelegationTxHash() string { + if x != nil { + return x.BabylonBTCDelegationTxHash + } + return "" +} + var File_transaction_proto protoreflect.FileDescriptor var file_transaction_proto_rawDesc = []byte{ @@ -574,7 +572,7 @@ var file_transaction_proto_rawDesc = []byte{ 0x42, 0x54, 0x43, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x1e, 0x75, 0x6e, 0x62, 0x6f, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x42, 0x74, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x6e, 0x66, 0x6f, 0x22, 0x84, 0x05, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x54, + 0x6e, 0x66, 0x6f, 0x22, 0xc4, 0x05, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x74, 0x72, 0x61, @@ -614,7 +612,11 @@ var file_transaction_proto_rawDesc = []byte{ 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x78, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x6e, 0x62, 0x6f, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x75, 0x6e, 0x62, 0x6f, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x44, 0x61, 0x74, 0x61, 0x2a, 0xa5, 0x01, 0x0a, 0x10, 0x54, + 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x44, 0x61, 0x74, 0x61, 0x12, 0x3e, 0x0a, 0x1a, 0x62, 0x61, + 0x62, 0x79, 0x6c, 0x6f, 0x6e, 0x42, 0x54, 0x43, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x78, 0x48, 0x61, 0x73, 0x68, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, + 0x62, 0x61, 0x62, 0x79, 0x6c, 0x6f, 0x6e, 0x42, 0x54, 0x43, 0x44, 0x65, 0x6c, 0x65, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x78, 0x48, 0x61, 0x73, 0x68, 0x2a, 0xa5, 0x01, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x4f, 0x5f, 0x42, 0x54, 0x43, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x4e, @@ -645,7 +647,7 @@ func file_transaction_proto_rawDescGZIP() []byte { var file_transaction_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_transaction_proto_msgTypes = make([]protoimpl.MessageInfo, 5) -var file_transaction_proto_goTypes = []interface{}{ +var file_transaction_proto_goTypes = []any{ (TransactionState)(0), // 0: proto.TransactionState (*WatchedTxData)(nil), // 1: proto.WatchedTxData (*BTCConfirmationInfo)(nil), // 2: proto.BTCConfirmationInfo @@ -671,68 +673,6 @@ func file_transaction_proto_init() { if File_transaction_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_transaction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WatchedTxData); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_transaction_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BTCConfirmationInfo); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_transaction_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CovenantSig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_transaction_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UnbondingTxData); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_transaction_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackedTransaction); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/proto/transaction.proto b/proto/transaction.proto index 24266cd..22ebd95 100644 --- a/proto/transaction.proto +++ b/proto/transaction.proto @@ -67,4 +67,5 @@ message TrackedTransaction { bool watched = 11; // this data is only filled if tracked transactions state is >= SENT_TO_BABYLON UnbondingTxData unbonding_tx_data = 12; + string babylonBTCDelegationTxHash = 13; } diff --git a/staker/babylontypes.go b/staker/babylontypes.go index a232174..17758e7 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -27,7 +27,7 @@ type inclusionInfo struct { } type sendDelegationRequest struct { - txHash chainhash.Hash + btcTxHash chainhash.Hash // optional field, if not provided, delegation will be sent to Babylon without // the inclusion proof inclusionInfo *inclusionInfo @@ -57,7 +57,7 @@ func (app *App) buildOwnedDelegation( // valid and btc confirmed staking transacion, but for some reason we cannot // build delegation data using our own set of libraries app.logger.WithFields(logrus.Fields{ - "btcTxHash": req.txHash, + "btcTxHash": req.btcTxHash, "stakerAddress": stakerAddress, "err": err, }).Fatalf("Failed to build delegation data for already confirmed staking transaction") @@ -138,13 +138,13 @@ func (app *App) buildDelegation( stakerAddress btcutil.Address, storedTx *stakerdb.StoredTransaction) (*cl.DelegationData, error) { if storedTx.Watched { - watchedData, err := app.txTracker.GetWatchedTransactionData(&req.txHash) + watchedData, err := app.txTracker.GetWatchedTransactionData(&req.btcTxHash) if err != nil { // Fatal error as if delegation is watched, the watched data must be in database // and must be not malformed app.logger.WithFields(logrus.Fields{ - "btcTxHash": req.txHash, + "btcTxHash": req.btcTxHash, "stakerAddress": stakerAddress, "err": err, }).Fatalf("Failed to build delegation data for already confirmed staking transaction") @@ -169,6 +169,7 @@ func (app *App) buildDelegation( ) return dg, nil } + return app.buildOwnedDelegation( req, stakerAddress, @@ -482,3 +483,15 @@ func (app *App) activateVerifiedDelegation( } } } + +func newSendDelegationRequest( + btcStakingTxHash *chainhash.Hash, + inclusionInfo *inclusionInfo, + requiredInclusionBlockDepth uint32, +) *sendDelegationRequest { + return &sendDelegationRequest{ + btcTxHash: *btcStakingTxHash, + inclusionInfo: inclusionInfo, + requiredInclusionBlockDepth: requiredInclusionBlockDepth, + } +} diff --git a/staker/commands.go b/staker/commands.go index 5d82bfc..33dcc2f 100644 --- a/staker/commands.go +++ b/staker/commands.go @@ -1,6 +1,7 @@ package staker import ( + staking "github.com/babylonlabs-io/babylon/btcstaking" cl "github.com/babylonlabs-io/btc-staker/babylonclient" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -8,6 +9,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" sdk "github.com/cosmos/cosmos-sdk/types" + notifier "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -131,3 +133,28 @@ func (req *stakingRequestCmd) EventID() chainhash.Hash { func (req *stakingRequestCmd) EventDesc() string { return "STAKING_REQUESTED_CMD" } + +type migrateStakingCmd struct { + stakerAddr btcutil.Address + notifierTx *notifier.TxConfirmation + parsedStakingTx *staking.ParsedV0StakingTx + pop *cl.BabylonPop + errChan chan error + successChanTxHash chan string +} + +func newMigrateStakingCmd( + stakerAddr btcutil.Address, + notifierTx *notifier.TxConfirmation, + parsedStakingTx *staking.ParsedV0StakingTx, + pop *cl.BabylonPop, +) *migrateStakingCmd { + return &migrateStakingCmd{ + stakerAddr: stakerAddr, + notifierTx: notifierTx, + parsedStakingTx: parsedStakingTx, + pop: pop, + errChan: make(chan error, 1), + successChanTxHash: make(chan string, 1), + } +} diff --git a/staker/events.go b/staker/events.go index 49ad6fc..86e7dc2 100644 --- a/staker/events.go +++ b/staker/events.go @@ -40,9 +40,10 @@ func (event *stakingTxBtcConfirmedEvent) EventDesc() string { } type delegationSubmittedToBabylonEvent struct { - stakingTxHash chainhash.Hash - unbondingTx *wire.MsgTx - unbondingTime uint16 + stakingTxHash chainhash.Hash + babylonBTCDelegationTxHash string + unbondingTx *wire.MsgTx + unbondingTime uint16 } func (event *delegationSubmittedToBabylonEvent) EventID() chainhash.Hash { diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 34f1911..79ffa46 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -18,10 +18,13 @@ import ( "github.com/babylonlabs-io/btc-staker/types" "github.com/babylonlabs-io/btc-staker/utils" "github.com/babylonlabs-io/btc-staker/walletcontroller" + pv "github.com/cosmos/relayer/v2/relayer/provider" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "go.uber.org/zap" "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" @@ -123,6 +126,7 @@ type App struct { m *metrics.StakerMetrics stakingRequestedCmdChan chan *stakingRequestCmd + migrateStakingCmd chan *migrateStakingCmd stakingTxBtcConfirmedEvChan chan *stakingTxBtcConfirmedEvent delegationSubmittedToBabylonEvChan chan *delegationSubmittedToBabylonEvent delegationActivatedPostApprovalEvChan chan *delegationActivatedPostApprovalEvent @@ -229,6 +233,8 @@ func NewStakerAppFromDeps( logger: logger, quit: make(chan struct{}), stakingRequestedCmdChan: make(chan *stakingRequestCmd), + // channel to receive requests of transition of BTC staking tx to consumer BTC delegation + migrateStakingCmd: make(chan *migrateStakingCmd), // event for when transaction is confirmed on BTC stakingTxBtcConfirmedEvChan: make(chan *stakingTxBtcConfirmedEvent), @@ -506,6 +512,60 @@ func (app *App) mustSetTxSpentOnBtc(hash *chainhash.Hash) { } } +// SendPhase1Transaction receives the BTC staking transaction hash that +// should be already in BTC and creates the necessary data to submit +// the BTC delegation into the consumer chain +func (app *App) SendPhase1Transaction( + stakerAddr btcutil.Address, + stkTxHash *chainhash.Hash, + tag []byte, + covenantPks []*secp256k1.PublicKey, + covenantQuorum uint32, +) (babylonBTCDelegationTxHash string, err error) { + // check we are not shutting down + select { + case <-app.quit: + return "", nil + default: + } + + parsedStakingTx, notifierTx, status, err := walletcontroller.StkTxV0ParsedWithBlock(app.wc, app.network, stkTxHash, tag, covenantPks, covenantQuorum) + if err != nil { + app.logger.WithError(err).Info("err getting tx details") + return "", err + } + if status != walletcontroller.TxInChain { + app.logger.WithError(err).Info("BTC tx not on chain") + return "", err + } + + pop, err := app.unlockAndCreatePop(stakerAddr) + if err != nil { + return "", err + } + + req := newMigrateStakingCmd(stakerAddr, notifierTx, parsedStakingTx, pop) + utils.PushOrQuit[*migrateStakingCmd]( + app.migrateStakingCmd, + req, + app.quit, + ) + + select { + case reqErr := <-req.errChan: + app.logger.WithFields(logrus.Fields{ + "stakerAddress": stakerAddr, + "err": reqErr, + }).Debugf("Sending staking tx failed") + + return "", reqErr + case hash := <-req.successChanTxHash: + return hash, nil + case <-app.quit: + return "", nil + } +} + // 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 @@ -619,7 +679,6 @@ func (app *App) checkTransactionsStatus() error { stakingTxHash := txHash delegationInfo, err := app.babylonClient.QueryDelegationInfo(stakingTxHash) - if err != nil && !errors.Is(err, cl.ErrDelegationNotFound) { return err } @@ -644,10 +703,8 @@ func (app *App) checkTransactionsStatus() error { } else { // transaction which is not on babylon, is already confirmed on btc chain // get all necessary info and send it to babylon - tx, stakerAddress := app.mustGetTransactionAndStakerAddress(stakingTxHash) details, status, err := app.wc.TxDetails(stakingTxHash, tx.StakingTx.TxOut[tx.StakingOutputIndex].PkScript) - if err != nil { // we got some communication err, return error and kill app startup return err @@ -667,21 +724,12 @@ func (app *App) checkTransactionsStatus() error { "btcTxConfirmationBlockHeight": details.BlockHeight, }).Debug("Already confirmed transaction not sent to babylon yet. Initiate sending") - iclusionProof := app.mustBuildInclusionProof( - details.Block, - details.TxIndex, + req := newSendDelegationRequest( + stakingTxHash, + app.newBtcInclusionInfo(details), + stakingParams.ConfirmationTimeBlocks, ) - req := &sendDelegationRequest{ - txHash: *stakingTxHash, - inclusionInfo: &inclusionInfo{ - txIndex: details.TxIndex, - inclusionBlock: details.Block, - inclusionProof: iclusionProof, - }, - requiredInclusionBlockDepth: stakingParams.ConfirmationTimeBlocks, - } - app.wg.Add(1) go app.sendDelegationToBabylonTask(req, stakerAddress, tx) } @@ -827,22 +875,18 @@ func (app *App) checkTransactionsStatus() error { func (app *App) waitForStakingTxConfirmation( txHash chainhash.Hash, depthOnBtcChain uint32, - ev *notifier.ConfirmationEvent) { + ev *notifier.ConfirmationEvent, +) { defer app.wg.Done() - // check we are not shutting down - select { - case <-app.quit: - ev.Cancel() - return - - default: - } - for { // TODO add handling of more events like ev.NegativeConf which signals that // transaction have beer reorged out of the chain select { + case <-app.quit: + // app is quitting, cancel the event + ev.Cancel() + return case conf := <-ev.Confirmed: stakingEvent := &stakingTxBtcConfirmedEvent{ stakingTxHash: conf.Tx.TxHash(), @@ -866,10 +910,6 @@ func (app *App) waitForStakingTxConfirmation( "btcTxHash": txHash, "confLeft": u, }).Debugf("Staking transaction received confirmation") - case <-app.quit: - // app is quitting, cancel the event - ev.Cancel() - return } } } @@ -918,6 +958,17 @@ func (app *App) mustBuildInclusionProof( return proof } +func (app *App) newBtcInclusionInfo(notifierTx *notifier.TxConfirmation) *inclusionInfo { + return &inclusionInfo{ + txIndex: notifierTx.TxIndex, + inclusionBlock: notifierTx.Block, + inclusionProof: app.mustBuildInclusionProof( + notifierTx.Block, + notifierTx.TxIndex, + ), + } +} + func (app *App) retrieveExternalDelegationData(stakerAddress btcutil.Address) (*externalDelegationData, error) { params, err := app.babylonClient.Params() if err != nil { @@ -1188,17 +1239,18 @@ func (app *App) buildAndSendDelegation( req *sendDelegationRequest, stakerAddress btcutil.Address, storedTx *stakerdb.StoredTransaction, -) (*cl.DelegationData, error) { +) (*pv.RelayerTxResponse, *cl.DelegationData, error) { delegation, err := app.buildDelegation(req, stakerAddress, storedTx) if err != nil { - return nil, err + return nil, nil, err } - _, err = app.babylonMsgSender.SendDelegation(delegation, req.requiredInclusionBlockDepth) + + resp, err := app.babylonMsgSender.SendDelegation(delegation, req.requiredInclusionBlockDepth) if err != nil { - return nil, err + return nil, nil, err } - return delegation, nil + return resp, delegation, nil } func (app *App) sendDelegationToBabylonTask( @@ -1208,14 +1260,46 @@ func (app *App) sendDelegationToBabylonTask( ) { defer app.wg.Done() + delegationData, delegationTxResp, err := app.sendDelegationToBabylonTaskWithRetry(req, stakerAddress, storedTx) + if err != nil { + app.reportCriticialError( + req.btcTxHash, + err, + "Failed to deliver delegation to babylon due to error.", + ) + return + } + + // report success with the values we sent to Babylon + ev := &delegationSubmittedToBabylonEvent{ + stakingTxHash: req.btcTxHash, + babylonBTCDelegationTxHash: delegationTxResp.TxHash, + unbondingTx: delegationData.Ud.UnbondingTransaction, + unbondingTime: delegationData.Ud.UnbondingTxUnbondingTime, + } + + utils.PushOrQuit[*delegationSubmittedToBabylonEvent]( + app.delegationSubmittedToBabylonEvChan, + ev, + app.quit, + ) +} + +func (app *App) sendDelegationToBabylonTaskWithRetry( + req *sendDelegationRequest, + stakerAddress btcutil.Address, + storedTx *stakerdb.StoredTransaction, +) (*cl.DelegationData, *pv.RelayerTxResponse, error) { // using app quit context to cancel retrying when app is shutting down ctx, cancel := app.appQuitContext() defer cancel() - var delegationData *cl.DelegationData + var ( + delegationData *cl.DelegationData + response *pv.RelayerTxResponse + ) err := retry.Do(func() error { - del, err := app.buildAndSendDelegation(req, stakerAddress, storedTx) - + resp, del, err := app.buildAndSendDelegation(req, stakerAddress, storedTx) if err != nil { if errors.Is(err, cl.ErrInvalidBabylonExecution) { return retry.Unrecoverable(err) @@ -1224,35 +1308,21 @@ func (app *App) sendDelegationToBabylonTask( } delegationData = del + response = resp return nil }, longRetryOps( ctx, app.config.StakerConfig.BabylonStallingInterval, - app.onLongRetryFunc(&req.txHash, "Failed to deliver delegation to babylon due to error."), + app.onLongRetryFunc(&req.btcTxHash, "Failed to deliver delegation to babylon due to error."), )..., ) if err != nil { - app.reportCriticialError( - req.txHash, - err, - "Failed to deliver delegation to babylon due to error.", - ) - } else { - // report success with the values we sent to Babylon - ev := &delegationSubmittedToBabylonEvent{ - stakingTxHash: req.txHash, - unbondingTx: delegationData.Ud.UnbondingTransaction, - unbondingTime: delegationData.Ud.UnbondingTxUnbondingTime, - } - - utils.PushOrQuit[*delegationSubmittedToBabylonEvent]( - app.delegationSubmittedToBabylonEvChan, - ev, - app.quit, - ) + return nil, nil, err } + + return delegationData, response, nil } func (app *App) handlePreApprovalCmd( @@ -1260,57 +1330,73 @@ func (app *App) handlePreApprovalCmd( stakingTx *wire.MsgTx, stakingOutputIdx uint32, ) (*chainhash.Hash, error) { + btcTxHash, _, err := app.handleSendDelegationRequest( + cmd.stakerAddress, + cmd.stakingTime, + cmd.requiredDepthOnBtcChain, + cmd.fpBtcPks, + cmd.pop, + stakingTx, + stakingOutputIdx, + nil, + ) + return btcTxHash, err +} + +func (app *App) handleSendDelegationRequest( + stakerAddress btcutil.Address, + stakingTime uint16, + requiredDepthOnBtcChain uint32, + fpBtcPks []*secp256k1.PublicKey, + pop *cl.BabylonPop, + stakingTx *wire.MsgTx, + stakingOutputIdx uint32, + inclusionInfo *inclusionInfo, +) (btcTxHash *chainhash.Hash, btcDelTxHash string, err error) { // just to pass to buildAndSendDelegation fakeStoredTx, err := stakerdb.CreateTrackedTransaction( stakingTx, stakingOutputIdx, - cmd.stakingTime, - cmd.fpBtcPks, - babylonPopToDBPop(cmd.pop), - cmd.stakerAddress, + stakingTime, + fpBtcPks, + babylonPopToDBPop(pop), + stakerAddress, ) - if err != nil { - return nil, err + return nil, btcDelTxHash, err } stakingTxHash := stakingTx.TxHash() - req := &sendDelegationRequest{ - txHash: stakingTxHash, - inclusionInfo: nil, - requiredInclusionBlockDepth: cmd.requiredDepthOnBtcChain, - } - - delegationData, err := app.buildAndSendDelegation( + req := newSendDelegationRequest(&stakingTxHash, inclusionInfo, requiredDepthOnBtcChain) + resp, delegationData, err := app.buildAndSendDelegation( req, - cmd.stakerAddress, + stakerAddress, fakeStoredTx, ) - if err != nil { - return nil, err + return nil, btcDelTxHash, err } err = app.txTracker.AddTransactionSentToBabylon( stakingTx, stakingOutputIdx, - cmd.stakingTime, - cmd.fpBtcPks, - babylonPopToDBPop(cmd.pop), - cmd.stakerAddress, + stakingTime, + fpBtcPks, + babylonPopToDBPop(pop), + stakerAddress, delegationData.Ud.UnbondingTransaction, delegationData.Ud.UnbondingTxUnbondingTime, + resp.TxHash, ) - if err != nil { - return nil, err + return nil, btcDelTxHash, err } app.wg.Add(1) go app.checkForUnbondingTxSignaturesOnBabylon(&stakingTxHash) - return &stakingTxHash, nil + return &stakingTxHash, resp.TxHash, nil } func (app *App) handlePostApprovalCmd( @@ -1323,13 +1409,11 @@ func (app *App) handlePostApprovalCmd( bestBlockHeight := app.currentBestBlockHeight.Load() err := app.wc.UnlockWallet(defaultWalletUnlockTimeout) - if err != nil { return nil, err } tx, fullySignd, err := app.wc.SignRawTransaction(stakingTx) - if err != nil { return nil, err } @@ -1439,7 +1523,6 @@ func (app *App) handleStakingCommands() { } stakingTxHash, err := app.handleStakingCmd(cmd) - if err != nil { utils.PushOrQuit( cmd.errChan, @@ -1454,6 +1537,52 @@ func (app *App) handleStakingCommands() { ) } app.logStakingEventProcessed(cmd) + + case cmd := <-app.migrateStakingCmd: + stkTxHash := cmd.notifierTx.Tx.TxHash() + stkParams, err := app.babylonClient.Params() + if err != nil { + cmd.errChan <- err + continue + } + + bestBlockHeight := app.currentBestBlockHeight.Load() + // check confirmation is deep enough + if err := checkConfirmationDepth(bestBlockHeight, cmd.notifierTx.BlockHeight, stkParams.ConfirmationTimeBlocks); err != nil { + cmd.errChan <- err + continue + } + + _, btcDelTxHash, err := app.handleSendDelegationRequest( + cmd.stakerAddr, + cmd.parsedStakingTx.OpReturnData.StakingTime, + stkParams.ConfirmationTimeBlocks, + []*btcec.PublicKey{cmd.parsedStakingTx.OpReturnData.FinalityProviderPublicKey.PubKey}, + cmd.pop, + cmd.notifierTx.Tx, + uint32(cmd.parsedStakingTx.StakingOutputIdx), + app.newBtcInclusionInfo(cmd.notifierTx), + ) + if err != nil { + utils.PushOrQuit( + cmd.errChan, + fmt.Errorf("sending tx to babylon failed: %w", err), + app.quit, + ) + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stkTxHash, + }).WithError(err).Error("BTC delegation transaction failed") + return + } + + utils.PushOrQuit( + cmd.successChanTxHash, + btcDelTxHash, + app.quit, + ) + app.logger.WithFields(logrus.Fields{ + "consumerBtcDelegationTxHash": btcDelTxHash, + }).Debugf("Sending BTC delegation was a success") case <-app.quit: return } @@ -1484,16 +1613,15 @@ func (app *App) handleStakingEvents() { ev.txIndex, ) - req := &sendDelegationRequest{ - txHash: ev.stakingTxHash, - inclusionInfo: &inclusionInfo{ + req := newSendDelegationRequest( + &ev.stakingTxHash, + &inclusionInfo{ txIndex: ev.txIndex, inclusionBlock: ev.inlusionBlock, inclusionProof: proof, }, - requiredInclusionBlockDepth: ev.blockDepth, - } - + ev.blockDepth, + ) storedTx, stakerAddress := app.mustGetTransactionAndStakerAddress(&ev.stakingTxHash) app.m.DelegationsConfirmedOnBtc.Inc() @@ -1506,7 +1634,7 @@ func (app *App) handleStakingEvents() { case ev := <-app.delegationSubmittedToBabylonEvChan: app.logStakingEventReceived(ev) - if err := app.txTracker.SetTxSentToBabylon(&ev.stakingTxHash, ev.unbondingTx, ev.unbondingTime); err != nil { + if err := app.txTracker.SetTxSentToBabylon(&ev.stakingTxHash, ev.babylonBTCDelegationTxHash, ev.unbondingTx, ev.unbondingTime); err != nil { // TODO: handle this error somehow, it means we received confirmation for tx which we do not store // which is seems like programming error. Maybe panic? app.logger.Fatalf("Error setting state for tx %s: %s", ev.stakingTxHash, err) @@ -1814,10 +1942,7 @@ func (app *App) StakeFunds( stakingAmount, params.MinStakingValue, params.MaxStakingValue) } - // unlock wallet for the rest of the operations - // TODO consider unlock/lock with defer - err = app.wc.UnlockWallet(defaultWalletUnlockTimeout) - + pop, err := app.unlockAndCreatePop(stakerAddress) if err != nil { return nil, err } @@ -1825,25 +1950,6 @@ 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, - ) - if err != nil { return nil, err } @@ -2206,3 +2312,57 @@ func (app *App) UnbondStaking( unbondingTxHash := tx.UnbondingTxData.UnbondingTx.TxHash() return &unbondingTxHash, nil } + +func (app *App) unlockAndCreatePop(stakerAddress btcutil.Address) (*cl.BabylonPop, error) { + // unlock wallet for the rest of the operations + // TODO consider unlock/lock with defer + err := app.wc.UnlockWallet(defaultWalletUnlockTimeout) + if err != nil { + return nil, err + } + + babylonAddrHash := tmhash.Sum(app.babylonClient.GetKeyAddress().Bytes()) + // pop only works for native segwit address + sig, err := app.wc.SignBip322NativeSegwit(babylonAddrHash, stakerAddress) + if err != nil { + return nil, err + } + + return cl.NewBabylonBip322Pop( + babylonAddrHash, + sig, + stakerAddress, + ) +} + +func (app *App) BtcTxAndBlock(txHash *chainhash.Hash) (*btcjson.TxRawResult, *btcjson.GetBlockHeaderVerboseResult, error) { + tx, err := app.wc.TxVerbose(txHash) + if err != nil { + return nil, nil, err + } + + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) + if err != nil { + return nil, nil, err + } + + blk, err := app.wc.BlockHeaderVerbose(blockHash) + if err != nil { + return nil, nil, err + } + + return tx, blk, nil +} + +func checkConfirmationDepth(tipBlockHeight, txInclusionBlockHeight, confirmationTimeBlocks uint32) error { + if txInclusionBlockHeight >= tipBlockHeight { + return fmt.Errorf("inclusion block height: %d should be lower than current tip: %d", txInclusionBlockHeight, tipBlockHeight) + } + if (tipBlockHeight - txInclusionBlockHeight) < confirmationTimeBlocks { + return fmt.Errorf( + "BTC tx not deep enough, current tip: %d, tx inclusion height: %d, confirmations needed: %d", + tipBlockHeight, txInclusionBlockHeight, confirmationTimeBlocks, + ) + } + return nil +} diff --git a/stakerdb/trackedtranactionstore.go b/stakerdb/trackedtranactionstore.go index 86db762..f5ddef5 100644 --- a/stakerdb/trackedtranactionstore.go +++ b/stakerdb/trackedtranactionstore.go @@ -125,10 +125,11 @@ type StoredTransaction struct { Pop *ProofOfPossession // Returning address as string, to avoid having to know how to decode address // which requires knowing the network we are on - StakerAddress string - State proto.TransactionState - Watched bool - UnbondingTxData *UnbondingStoreData + StakerAddress string + State proto.TransactionState + Watched bool + UnbondingTxData *UnbondingStoreData + BabylonBTCDelegationTxHash string } // StakingTxConfirmedOnBtc returns true only if staking transaction was sent and confirmed on bitcoin @@ -367,10 +368,11 @@ func protoTxToStoredTransaction(ttx *proto.TrackedTransaction) (*StoredTransacti BtcSigType: ttx.BtcSigType, BtcSigOverBabylonAddr: ttx.BtcSigOverBbnStakerAddr, }, - StakerAddress: ttx.StakerAddress, - State: ttx.State, - Watched: ttx.Watched, - UnbondingTxData: utd, + StakerAddress: ttx.StakerAddress, + State: ttx.State, + Watched: ttx.Watched, + UnbondingTxData: utd, + BabylonBTCDelegationTxHash: ttx.BabylonBTCDelegationTxHash, }, nil } @@ -675,6 +677,7 @@ func (c *TrackedTransactionStore) AddTransactionSentToBabylon( stakerAddress btcutil.Address, unbondingTx *wire.MsgTx, unbondingTime uint16, + btcDelTxHash string, ) error { txHash := btcTx.TxHash() txHashBytes := txHash[:] @@ -718,6 +721,7 @@ func (c *TrackedTransactionStore) AddTransactionSentToBabylon( State: proto.TransactionState_SENT_TO_BABYLON, Watched: false, UnbondingTxData: update, + BabylonBTCDelegationTxHash: btcDelTxHash, } inputData, err := getInputData(btcTx) @@ -837,14 +841,12 @@ func (c *TrackedTransactionStore) AddWatchedTransaction( } serializedSig := slashingTxSig.Serialize() - serializedUnbondingTx, err := utils.SerializeBtcTransaction(unbondingTx) if err != nil { return err } serializedSlashUnbondingTx, err := utils.SerializeBtcTransaction(slashUnbondingTx) - if err != nil { return err } @@ -976,11 +978,11 @@ func (c *TrackedTransactionStore) SetTxConfirmed( func (c *TrackedTransactionStore) SetTxSentToBabylon( txHash *chainhash.Hash, + babylonBTCDelegationTxHash string, unbondingTx *wire.MsgTx, unbondingTime uint16, ) error { update, err := newInitialUnbondingTxData(unbondingTx, unbondingTime) - if err != nil { return err } @@ -992,6 +994,7 @@ func (c *TrackedTransactionStore) SetTxSentToBabylon( tx.State = proto.TransactionState_SENT_TO_BABYLON tx.UnbondingTxData = update + tx.BabylonBTCDelegationTxHash = babylonBTCDelegationTxHash return nil } @@ -1103,7 +1106,6 @@ func (c *TrackedTransactionStore) GetTransaction(txHash *chainhash.Hash) (*Store } txFromDB, err := protoTxToStoredTransaction(&storedTxProto) - if err != nil { return err } diff --git a/stakerdb/trackedtransactionstore_test.go b/stakerdb/trackedtransactionstore_test.go index 1d546f0..e6b8b11 100644 --- a/stakerdb/trackedtransactionstore_test.go +++ b/stakerdb/trackedtransactionstore_test.go @@ -213,7 +213,7 @@ func TestStateTransitions(t *testing.T) { require.Equal(t, height, storedTx.StakingTxConfirmationInfo.Height) // Sent to Babylon - err = s.SetTxSentToBabylon(&txHash, tx.StakingTx, tx.StakingTime) + err = s.SetTxSentToBabylon(&txHash, hash.String(), tx.StakingTx, tx.StakingTime) require.NoError(t, err) storedTx, err = s.GetTransaction(&txHash) require.NoError(t, err) @@ -357,6 +357,7 @@ func FuzzQuerySpendableTx(f *testing.F) { txHash := storedTx.StakingTx.TxHash() err := s.SetTxSentToBabylon( &txHash, + txHash.String(), storedTx.StakingTx, storedTx.StakingTime, ) @@ -412,6 +413,7 @@ func FuzzTrackInputs(f *testing.F) { stakerAddr, randomUnbondingTx, unbodningTime, + "txHashBtcDelegation", ) require.NoError(t, err) } diff --git a/stakerservice/client/rpcclient.go b/stakerservice/client/rpcclient.go index 48070cc..d42193c 100644 --- a/stakerservice/client/rpcclient.go +++ b/stakerservice/client/rpcclient.go @@ -2,8 +2,11 @@ package client import ( "context" + "encoding/hex" service "github.com/babylonlabs-io/btc-staker/stakerservice" + "github.com/babylonlabs-io/networks/parameters/parser" + "github.com/btcsuite/btcd/btcec/v2" jsonrpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" ) @@ -85,6 +88,58 @@ func (c *StakerServiceJSONRPCClient) Stake( return result, nil } +func (c *StakerServiceJSONRPCClient) BtcDelegationFromBtcStakingTx( + ctx context.Context, + stakerAddress string, + btcStkTxHash string, + versionedParams *parser.ParsedVersionedGlobalParams, +) (*service.ResultBtcDelegationFromBtcStakingTx, error) { + result := new(service.ResultBtcDelegationFromBtcStakingTx) + + params := make(map[string]interface{}) + params["stakerAddress"] = stakerAddress + params["btcStkTxHash"] = btcStkTxHash + params["tag"] = versionedParams.Tag + params["covenantPksHex"] = parseCovenantsPubKeyToHex(versionedParams.CovenantPks...) + params["covenantQuorum"] = versionedParams.CovenantQuorum + + _, 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) BtcTxDetails( + ctx context.Context, + txHash string, +) (*service.BtcTxAndBlockResponse, error) { + result := new(service.BtcTxAndBlockResponse) + + params := make(map[string]interface{}) + params["txHashStr"] = txHash + + _, err := c.client.Call(ctx, "btc_tx_blk_details", params, result) + if err != nil { + return nil, err + } + return result, nil +} + +func parseCovenantsPubKeyToHex(pks ...*btcec.PublicKey) []string { + pksHex := make([]string, len(pks)) + for i, pk := range pks { + pksHex[i] = parseCovenantPubKeyToHex(pk) + } + return pksHex +} + +// parseCovenantPubKeyFromHex parses public key into serialized compressed +// with 33 bytes and in hex string +func parseCovenantPubKeyToHex(pk *btcec.PublicKey) string { + return hex.EncodeToString(pk.SerializeCompressed()) +} + 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 913f43e..78973c5 100644 --- a/stakerservice/service.go +++ b/stakerservice/service.go @@ -125,8 +125,96 @@ func (s *StakerService) stake(_ *rpctypes.Context, }, nil } -func (s *StakerService) stakingDetails(_ *rpctypes.Context, - stakingTxHash string) (*StakingDetails, error) { +func (s *StakerService) btcDelegationFromBtcStakingTx( + _ *rpctypes.Context, + stakerAddress string, + btcStkTxHash string, + tag []byte, + covenantPksHex []string, + covenantQuorum uint32, +) (*ResultBtcDelegationFromBtcStakingTx, error) { + stkTxHash, err := chainhash.NewHashFromStr(btcStkTxHash) + if err != nil { + s.logger.WithError(err).Info("err parse tx hash") + return nil, err + } + + stakerAddr, err := btcutil.DecodeAddress(stakerAddress, &s.config.ActiveNetParams) + if err != nil { + s.logger.WithError(err).Info("err decode staker addr") + return nil, err + } + + covenantPks, err := parseCovenantsPubKeyFromHex(covenantPksHex...) + if err != nil { + s.logger.WithError(err).Infof("err decode covenant pks %s", covenantPksHex) + return nil, err + } + + babylonBTCDelegationTxHash, err := s.staker.SendPhase1Transaction(stakerAddr, stkTxHash, tag, covenantPks, covenantQuorum) + if err != nil { + s.logger.WithError(err).Info("err to send phase 1 tx") + return nil, err + } + + return &ResultBtcDelegationFromBtcStakingTx{ + BabylonBTCDelegationTxHash: babylonBTCDelegationTxHash, + }, nil +} + +func parseCovenantsPubKeyFromHex(covenantPksHex ...string) ([]*btcec.PublicKey, error) { + covenantPks := make([]*btcec.PublicKey, len(covenantPksHex)) + for i, covenantPkHex := range covenantPksHex { + covPk, err := parseCovenantPubKeyFromHex(covenantPkHex) + if err != nil { + return nil, err + } + covenantPks[i] = covPk + } + + return covenantPks, nil +} + +// parseCovenantPubKeyFromHex parses public key string to btc public key +// the input should be 33 bytes +func parseCovenantPubKeyFromHex(pkStr string) (*btcec.PublicKey, error) { + pkBytes, err := hex.DecodeString(pkStr) + if err != nil { + return nil, err + } + + pk, err := btcec.ParsePubKey(pkBytes) + if err != nil { + return nil, err + } + + return pk, nil +} + +func (s *StakerService) btcTxBlkDetails( + _ *rpctypes.Context, + txHashStr string, +) (*BtcTxAndBlockResponse, error) { + txHash, err := chainhash.NewHashFromStr(txHashStr) + if err != nil { + return nil, err + } + + tx, blk, err := s.staker.BtcTxAndBlock(txHash) + if err != nil { + return nil, err + } + + return &BtcTxAndBlockResponse{ + Tx: tx, + Blk: blk, + }, nil +} + +func (s *StakerService) stakingDetails( + _ *rpctypes.Context, + stakingTxHash string, +) (*StakingDetails, error) { txHash, err := chainhash.NewHashFromStr(stakingTxHash) if err != nil { return nil, err @@ -546,12 +634,14 @@ func (s *StakerService) GetRoutes() RoutesMap { // info AP "health": rpc.NewRPCFunc(s.health, ""), // staking API - "stake": rpc.NewRPCFunc(s.stake, "stakerAddress,stakingAmount,fpBtcPks,stakingTimeBlocks,sendToBabylonFirst"), - "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,tag,covenantPksHex,covenantQuorum"), + "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"), + "btc_tx_blk_details": rpc.NewRPCFunc(s.btcTxBlkDetails, "txHashStr"), // watch api "watch_staking_tx": rpc.NewRPCFunc(s.watchStaking, "stakingTx,stakingTime,stakingValue,stakerBtcPk,fpBtcPks,slashingTx,slashingTxSig,stakerBabylonAddr,stakerAddress,stakerBtcSig,unbondingTx,slashUnbondingTx,slashUnbondingTxSig,unbondingTime,popType"), @@ -594,7 +684,10 @@ func (s *StakerService) RunUntilShutdown(ctx context.Context) error { } defer func() { - _ = s.staker.Stop() + err := s.staker.Stop() + if err != nil { + s.logger.WithError(err).Info("staker stop with error") + } s.logger.Info("staker stop complete") }() @@ -632,7 +725,7 @@ func (s *StakerService) RunUntilShutdown(ctx context.Context) error { // TODO: Add additional middleware, like CORS, TLS, etc. // TODO: Consider we need some websockets for some notications go func() { - s.logger.Debug("Starting Json RPC HTTP server ", "address", listenAddressStr) + s.logger.Debug("Starting Json RPC HTTP server ", "address: ", listenAddressStr) err := rpc.Serve( listener, @@ -640,8 +733,10 @@ func (s *StakerService) RunUntilShutdown(ctx context.Context) error { rpcLogger, config, ) - - s.logger.Error("Json RPC HTTP server stopped ", "err", err) + if err != nil { + s.logger.WithError(err).Error("problem at JSON RPC HTTP server") + } + s.logger.Info("Json RPC HTTP server stopped ") }() listeners[i] = listener diff --git a/stakerservice/stakerdresponses.go b/stakerservice/stakerdresponses.go index 3a76474..d8171aa 100644 --- a/stakerservice/stakerdresponses.go +++ b/stakerservice/stakerdresponses.go @@ -1,7 +1,13 @@ package stakerservice +import "github.com/btcsuite/btcd/btcjson" + type ResultHealth struct{} +type ResultBtcDelegationFromBtcStakingTx struct { + BabylonBTCDelegationTxHash string `json:"babylon_btc_delegation_tx_hash"` +} + type ResultStake struct { TxHash string `json:"tx_hash"` } @@ -53,3 +59,8 @@ type WithdrawableTransactionsResponse struct { LastWithdrawableTransactionIndex string `json:"last_transaction_index"` TotalTransactionCount string `json:"total_transaction_count"` } + +type BtcTxAndBlockResponse struct { + Tx *btcjson.TxRawResult `json:"tx"` + Blk *btcjson.GetBlockHeaderVerboseResult `json:"blk"` +} diff --git a/walletcontroller/client.go b/walletcontroller/client.go index 174c9cf..acae62e 100644 --- a/walletcontroller/client.go +++ b/walletcontroller/client.go @@ -268,6 +268,21 @@ func (w *RPCWalletController) getTxDetails(req notifier.ConfRequest, msg string) return res, nofitierStateToWalletState(state), nil } +// Tx returns the raw transaction based on the transaction hash +func (w *RPCWalletController) Tx(txHash *chainhash.Hash) (*btcutil.Tx, error) { + return w.Client.GetRawTransaction(txHash) +} + +// TxVerbose returns the raw transaction verbose based on the transaction hash +func (w *RPCWalletController) TxVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) { + return w.Client.GetRawTransactionVerbose(txHash) +} + +// BlockHeaderVerbose returns the block header data based on the block hash +func (w *RPCWalletController) BlockHeaderVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockHeaderVerboseResult, error) { + return w.Client.GetBlockHeaderVerbose(blockHash) +} + // Fetch info about transaction from mempool or blockchain, requires node to have enabled transaction index func (w *RPCWalletController) TxDetails(txHash *chainhash.Hash, pkScript []byte) (*notifier.TxConfirmation, TxStatus, error) { req, err := notifier.NewConfRequest(txHash, pkScript) diff --git a/walletcontroller/interface.go b/walletcontroller/interface.go index d0e2b2d..a57a351 100644 --- a/walletcontroller/interface.go +++ b/walletcontroller/interface.go @@ -1,12 +1,17 @@ package walletcontroller import ( + staking "github.com/babylonlabs-io/babylon/btcstaking" "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" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + notifier "github.com/lightningnetwork/lnd/chainntnfs" ) @@ -66,6 +71,9 @@ 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) (*btcutil.Tx, error) + TxVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) + BlockHeaderVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockHeaderVerboseResult, error) SignBip322NativeSegwit(msg []byte, address btcutil.Address) (wire.TxWitness, error) // SignOneInputTaprootSpendingTransaction signs transactions with one taproot input that // uses script spending path. @@ -75,3 +83,30 @@ type WalletController interface { outputIdx uint32, ) (bool, error) } + +func StkTxV0ParsedWithBlock( + wc WalletController, + btcNetwork *chaincfg.Params, + stkTxHash *chainhash.Hash, + tag []byte, + covenantPks []*secp256k1.PublicKey, + covenantQuorum uint32, +) (*staking.ParsedV0StakingTx, *notifier.TxConfirmation, TxStatus, error) { + stkTx, err := wc.Tx(stkTxHash) + if err != nil { + return nil, nil, TxNotFound, err + } + + wireStkTx := stkTx.MsgTx() + parsedStakingTx, err := staking.ParseV0StakingTx(wireStkTx, tag, covenantPks, covenantQuorum, btcNetwork) + if err != nil { + return nil, nil, TxNotFound, err + } + + notifierTx, status, err := wc.TxDetails(stkTxHash, parsedStakingTx.StakingOutput.PkScript) + if err != nil { + return nil, nil, TxNotFound, err + } + + return parsedStakingTx, notifierTx, status, nil +}