diff --git a/babylonclient/babyloncontroller.go b/babylonclient/babyloncontroller.go index 2eb695e..69414e5 100644 --- a/babylonclient/babyloncontroller.go +++ b/babylonclient/babyloncontroller.go @@ -273,20 +273,26 @@ func (bc *BabylonController) Sign(msg []byte) ([]byte, error) { } } -type DelegationData struct { - StakingTransaction *wire.MsgTx +type StakingTransactionInclusionInfo struct { StakingTransactionIdx uint32 StakingTransactionInclusionProof []byte StakingTransactionInclusionBlockHash *chainhash.Hash - StakingTime uint16 - StakingValue btcutil.Amount - FinalityProvidersBtcPks []*btcec.PublicKey - SlashingTransaction *wire.MsgTx - SlashingTransactionSig *schnorr.Signature - BabylonStakerAddr sdk.AccAddress - StakerBtcPk *btcec.PublicKey - BabylonPop *stakerdb.ProofOfPossession - Ud *UndelegationData +} + +type DelegationData struct { + StakingTransaction *wire.MsgTx + // Optional field, if not provided, delegation will be send to Babylon without + // the inclusion proof + StakingTransactionInclusionInfo *StakingTransactionInclusionInfo + StakingTime uint16 + StakingValue btcutil.Amount + FinalityProvidersBtcPks []*btcec.PublicKey + SlashingTransaction *wire.MsgTx + SlashingTransactionSig *schnorr.Signature + BabylonStakerAddr sdk.AccAddress + StakerBtcPk *btcec.PublicKey + BabylonPop *stakerdb.ProofOfPossession + Ud *UndelegationData } type UndelegationData struct { @@ -333,8 +339,6 @@ func delegationDataToMsg(dg *DelegationData) (*btcstypes.MsgCreateBTCDelegation, return nil, err } - inclusionBlockHash := bbntypes.NewBTCHeaderHashBytesFromChainhash(dg.StakingTransactionInclusionBlockHash) - slashingTx, err := btcstypes.NewBTCSlashingTxFromMsgTx(dg.SlashingTransaction) if err != nil { @@ -374,9 +378,20 @@ func delegationDataToMsg(dg *DelegationData) (*btcstypes.MsgCreateBTCDelegation, slashUnbondingTxSig := bbntypes.NewBIP340SignatureFromBTCSig(dg.Ud.SlashUnbondingTransactionSig) - txKey := &bcctypes.TransactionKey{ - Index: dg.StakingTransactionIdx, - Hash: &inclusionBlockHash, + var stakingTransactionInclusionProof *btcstypes.InclusionProof = nil + + if dg.StakingTransactionInclusionInfo != nil { + inclusionBlockHash := bbntypes.NewBTCHeaderHashBytesFromChainhash( + dg.StakingTransactionInclusionInfo.StakingTransactionInclusionBlockHash, + ) + txKey := &bcctypes.TransactionKey{ + Index: dg.StakingTransactionInclusionInfo.StakingTransactionIdx, + Hash: &inclusionBlockHash, + } + stakingTransactionInclusionProof = btcstypes.NewInclusionProof( + txKey, + dg.StakingTransactionInclusionInfo.StakingTransactionInclusionProof, + ) } return &btcstypes.MsgCreateBTCDelegation{ @@ -393,7 +408,7 @@ func delegationDataToMsg(dg *DelegationData) (*btcstypes.MsgCreateBTCDelegation, // TODO: It is super bad that this thing (TransactionInfo) spread over whole babylon codebase, and it // is used in all modules, rpc, database etc. StakingTx: serizalizedStakingTransaction, - StakingTxInclusionProof: btcstypes.NewInclusionProof(txKey, dg.StakingTransactionInclusionProof), + StakingTxInclusionProof: stakingTransactionInclusionProof, SlashingTx: slashingTx, // Data related to unbonding DelegatorSlashingSig: slashingTxSig, @@ -893,3 +908,23 @@ func (bc *BabylonController) QueryBtcLightClientTip() (*btclctypes.BTCHeaderInfo return res.Header, nil } + +func (bc *BabylonController) ActivateDelegation( + ctx context.Context, + stakingTxHash chainhash.Hash, + proof *btcctypes.BTCSpvProof) (*pv.RelayerTxResponse, error) { + + msg := &btcstypes.MsgAddBTCDelegationInclusionProof{ + Signer: bc.getTxSigner(), + StakingTxHash: stakingTxHash.String(), + StakingTxInclusionProof: btcstypes.NewInclusionProofFromSpvProof(proof), + } + + res, err := bc.reliablySendMsgs([]sdk.Msg{msg}) + if err != nil { + return nil, err + } + + return res, nil + +} diff --git a/babylonclient/msgsender.go b/babylonclient/msgsender.go index 952aeb3..367b788 100644 --- a/babylonclient/msgsender.go +++ b/babylonclient/msgsender.go @@ -103,7 +103,12 @@ func (b *BabylonMsgSender) isBabylonBtcLcReady( requiredInclusionBlockDepth uint64, req *DelegationData, ) error { - depth, err := b.cl.QueryHeaderDepth(req.StakingTransactionInclusionBlockHash) + // no need to consult Babylon if we send delegation without inclusion proof + if req.StakingTransactionInclusionInfo == nil { + return nil + } + + depth, err := b.cl.QueryHeaderDepth(req.StakingTransactionInclusionInfo.StakingTransactionInclusionBlockHash) if err != nil { // If header is not known to babylon, or it is on LCFork, then most probably diff --git a/cmd/stakercli/daemon/daemoncommands.go b/cmd/stakercli/daemon/daemoncommands.go index c540bf4..0d20d97 100644 --- a/cmd/stakercli/daemon/daemoncommands.go +++ b/cmd/stakercli/daemon/daemoncommands.go @@ -126,6 +126,10 @@ var stakeCmd = cli.Command{ Usage: "Staking time in BTC blocks", Required: true, }, + cli.BoolFlag{ + Name: helpers.SendToBabylonFirstFlag, + Usage: "Whether staking transaction should be first to Babylon or BTC", + }, }, Action: stake, } @@ -324,8 +328,9 @@ func stake(ctx *cli.Context) error { stakingAmount := ctx.Int64(helpers.StakingAmountFlag) fpPks := ctx.StringSlice(fpPksFlag) stakingTimeBlocks := ctx.Int64(helpers.StakingTimeBlocksFlag) + sendToBabylonFirst := ctx.Bool(helpers.SendToBabylonFirstFlag) - results, err := client.Stake(sctx, stakerAddress, stakingAmount, fpPks, stakingTimeBlocks) + results, err := client.Stake(sctx, stakerAddress, stakingAmount, fpPks, stakingTimeBlocks, sendToBabylonFirst) if err != nil { return err } diff --git a/cmd/stakercli/helpers/flags.go b/cmd/stakercli/helpers/flags.go index 84dddeb..7fd4257 100644 --- a/cmd/stakercli/helpers/flags.go +++ b/cmd/stakercli/helpers/flags.go @@ -1,6 +1,7 @@ package helpers const ( - StakingAmountFlag = "staking-amount" - StakingTimeBlocksFlag = "staking-time" + StakingAmountFlag = "staking-amount" + StakingTimeBlocksFlag = "staking-time" + SendToBabylonFirstFlag = "send-to-babylon-first" ) diff --git a/example/global-params.json b/example/global-params.json index 76183a2..daf22dc 100644 --- a/example/global-params.json +++ b/example/global-params.json @@ -2,25 +2,53 @@ "versions": [ { "version": 0, - "activation_height": 1, - "staking_cap": 50000000000, - "cap_height": 0, - "tag": "01020304", + "activation_height": 857910, + "staking_cap": 100000000000, + "tag": "62626e31", "covenant_pks": [ - "0205149a0c7a95320adf210e47bca8b363b7bd966be86be6392dd6cf4f96995869", - "02e8d503cb52715249f32f3ee79cee88dfd48c2565cb0c79cf9640d291f46fd518", - "02fe81b2409a32ddfd8ec1556557e8dd949b6e4fd37047523cb7f5fefca283d542", - "02bc4a1ff485d7b44faeec320b81ad31c3cad4d097813c21fcf382b4305e4cfc82", - "02001e50601a4a1c003716d7a1ee7fe25e26e55e24e909b3642edb60d30e3c40c1" + "03d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "034b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "0223b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "02d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "038242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "03e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "03cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "03f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "03de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c" ], - "covenant_quorum": 3, - "unbonding_time": 1000, - "unbonding_fee": 20000, - "max_staking_amount": 1000000000, - "min_staking_amount": 1000000, + "covenant_quorum": 6, + "unbonding_time": 1008, + "unbonding_fee": 64000, + "max_staking_amount": 5000000, + "min_staking_amount": 500000, "max_staking_time": 64000, "min_staking_time": 64000, - "confirmation_depth": 6 - } + "confirmation_depth": 10 + }, + { + "version": 1, + "activation_height": 864790, + "cap_height": 864799, + "tag": "62626e31", + "covenant_pks": [ + "03d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "034b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "0223b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "02d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "038242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "03e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "03cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "03f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "03de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c" + ], + "covenant_quorum": 6, + "unbonding_time": 1008, + "unbonding_fee": 32000, + "max_staking_amount": 50000000000, + "min_staking_amount": 500000, + "max_staking_time": 64000, + "min_staking_time": 64000, + "confirmation_depth": 10 + } ] -} + } diff --git a/itest/e2e_test.go b/itest/e2e_test.go index f196344..ebc8c14 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -9,8 +9,6 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/babylonlabs-io/btc-staker/itest/containers" - "github.com/babylonlabs-io/btc-staker/itest/testutil" "math/rand" "net" "net/netip" @@ -21,6 +19,9 @@ import ( "testing" "time" + "github.com/babylonlabs-io/btc-staker/itest/containers" + "github.com/babylonlabs-io/btc-staker/itest/testutil" + "github.com/babylonlabs-io/babylon/crypto/bip322" btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" "github.com/cometbft/cometbft/crypto/tmhash" @@ -114,6 +115,7 @@ func defaultStakerConfig(t *testing.T, walletName, passphrase, bitcoindHost stri 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 @@ -682,7 +684,11 @@ func (tm *TestManager) mineBlock(t *testing.T) *wire.MsgBlock { return header } -func (tm *TestManager) sendStakingTxBTC(t *testing.T, testStakingData *testStakingData) *chainhash.Hash { +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])) @@ -694,6 +700,7 @@ func (tm *TestManager) sendStakingTxBTC(t *testing.T, testStakingData *testStaki testStakingData.StakingAmount, fpBTCPKs, int64(testStakingData.StakingTime), + sendToBabylonFirst, ) require.NoError(t, err) txHash := res.TxHash @@ -701,22 +708,29 @@ func (tm *TestManager) sendStakingTxBTC(t *testing.T, testStakingData *testStaki stakingDetails, err := tm.StakerClient.StakingDetails(context.Background(), txHash) require.NoError(t, err) require.Equal(t, stakingDetails.StakingTxHash, txHash) - require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BTC.String()) + if sendToBabylonFirst { + require.Equal(t, stakingDetails.StakingState, proto.TransactionState_TRANSACTION_CREATED.String()) + } else { + require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BTC.String()) + } hashFromString, err := chainhash.NewHashFromStr(txHash) require.NoError(t, err) - 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)) + // 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) - _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) - require.NoError(t, err) + 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 } @@ -734,6 +748,7 @@ func (tm *TestManager) sendMultipleStakingTx(t *testing.T, testStakingData []*te data.StakingAmount, fpBTCPKs, int64(data.StakingTime), + false, ) require.NoError(t, err) txHash, err := chainhash.NewHashFromStr(res.TxHash) @@ -1122,6 +1137,7 @@ func TestStakingFailures(t *testing.T) { testStakingData.StakingAmount, []string{fpKey, fpKey}, int64(testStakingData.StakingTime), + false, ) require.Error(t, err) @@ -1132,6 +1148,7 @@ func TestStakingFailures(t *testing.T) { testStakingData.StakingAmount, []string{}, int64(testStakingData.StakingTime), + false, ) require.Error(t, err) } @@ -1164,7 +1181,7 @@ func TestSendingStakingTransaction(t *testing.T) { tm.createAndRegisterFinalityProviders(t, testStakingData) - txHash := tm.sendStakingTxBTC(t, testStakingData) + txHash := tm.sendStakingTxBTC(t, testStakingData, false) go tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) @@ -1212,6 +1229,73 @@ func TestSendingStakingTransaction(t *testing.T) { require.Equal(t, transactionsResult.Transactions[0].StakingTxHash, txHash.String()) } +func TestSendingStakingTransactionWithPreApproval(t *testing.T) { + t.Parallel() + // need to have at least 300 block on testnet as only then segwit is activated. + // Mature output is out which has 100 confirmations, which means 200mature outputs + // will generate 300 blocks + numMatureOutputs := uint32(200) + ctx, cancel := context.WithCancel(context.Background()) + tm := StartManager(t, ctx, numMatureOutputs) + defer tm.Stop(t, cancel) + tm.insertAllMinedBlocksToBabylon(t) + + cl := tm.Sa.BabylonController() + params, err := cl.Params() + require.NoError(t, err) + + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, params.MinStakingTime, 10000, 1) + + hashed, err := chainhash.NewHash(datagen.GenRandomByteArray(r, 32)) + require.NoError(t, err) + scr, err := txscript.PayToTaprootScript(tm.CovenantPrivKeys[0].PubKey()) + require.NoError(t, err) + _, st, erro := tm.Sa.Wallet().TxDetails(hashed, scr) + // query for exsisting tx is not an error, proper state should be returned + require.NoError(t, erro) + require.Equal(t, st, walletcontroller.TxNotFound) + + tm.createAndRegisterFinalityProviders(t, testStakingData) + + txHash := tm.sendStakingTxBTC(t, testStakingData, true) + + go tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) + tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) + + pend, err := tm.BabylonClient.QueryPendingBTCDelegations() + require.NoError(t, err) + require.Len(t, pend, 1) + // need to activate delegation to unbond + tm.insertCovenantSigForDelegation(t, pend[0]) + tm.waitForStakingTxState(t, txHash, proto.TransactionState_VERIFIED) + + require.Eventually(t, func() bool { + txFromMempool := retrieveTransactionFromMempool(t, tm.TestRpcClient, []*chainhash.Hash{txHash}) + return len(txFromMempool) == 1 + }, eventuallyWaitTimeOut, eventuallyPollTime) + + mBlock := tm.mineBlock(t) + require.Equal(t, 2, len(mBlock.Transactions)) + + headerBytes := bbntypes.NewBTCHeaderBytesFromBlockHeader(&mBlock.Header) + proof, err := btcctypes.SpvProofFromHeaderAndTransactions(&headerBytes, txsToBytes(mBlock.Transactions), 1) + require.NoError(t, err) + + _, err = tm.BabylonClient.InsertBtcBlockHeaders([]*wire.BlockHeader{&mBlock.Header}) + require.NoError(t, err) + + tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) + + _, err = tm.BabylonClient.ActivateDelegation( + context.Background(), + *txHash, + proof, + ) + require.NoError(t, err) + tm.waitForStakingTxState(t, txHash, proto.TransactionState_DELEGATION_ACTIVE) + +} + func TestMultipleWithdrawableStakingTransactions(t *testing.T) { t.Parallel() // need to have at least 300 block on testnet as only then segwit is activated. @@ -1322,7 +1406,7 @@ func TestRestartingTxNotDeepEnough(t *testing.T) { testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, params.MinStakingTime, 10000, 1) tm.createAndRegisterFinalityProviders(t, testStakingData) - txHash := tm.sendStakingTxBTC(t, testStakingData) + txHash := tm.sendStakingTxBTC(t, testStakingData, false) newCtx, newCancel := context.WithCancel(context.Background()) defer newCancel() @@ -1398,7 +1482,7 @@ func TestStakingUnbonding(t *testing.T) { tm.createAndRegisterFinalityProviders(t, testStakingData) - txHash := tm.sendStakingTxBTC(t, testStakingData) + txHash := tm.sendStakingTxBTC(t, testStakingData, false) go tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) @@ -1471,7 +1555,7 @@ func TestUnbondingRestartWaitingForSignatures(t *testing.T) { tm.createAndRegisterFinalityProviders(t, testStakingData) - txHash := tm.sendStakingTxBTC(t, testStakingData) + txHash := tm.sendStakingTxBTC(t, testStakingData, false) go tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) @@ -1663,7 +1747,7 @@ func TestSendingStakingTransaction_Restaking(t *testing.T) { tm.createAndRegisterFinalityProviders(t, testStakingData) - txHash := tm.sendStakingTxBTC(t, testStakingData) + txHash := tm.sendStakingTxBTC(t, testStakingData, false) go tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) @@ -1704,7 +1788,7 @@ func TestRecoverAfterRestartDuringWithdrawal(t *testing.T) { tm.createAndRegisterFinalityProviders(t, testStakingData) - txHash := tm.sendStakingTxBTC(t, testStakingData) + txHash := tm.sendStakingTxBTC(t, testStakingData, false) go tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) // must wait for all covenant signatures to be received, to be able to unbond diff --git a/proto/transaction.pb.go b/proto/transaction.pb.go index f87d579..97b9eff 100644 --- a/proto/transaction.pb.go +++ b/proto/transaction.pb.go @@ -23,31 +23,37 @@ const ( type TransactionState int32 const ( - TransactionState_SENT_TO_BTC TransactionState = 0 - TransactionState_CONFIRMED_ON_BTC TransactionState = 1 - TransactionState_SENT_TO_BABYLON TransactionState = 2 - TransactionState_DELEGATION_ACTIVE TransactionState = 3 - TransactionState_UNBONDING_CONFIRMED_ON_BTC TransactionState = 4 - TransactionState_SPENT_ON_BTC TransactionState = 5 + TransactionState_TRANSACTION_CREATED TransactionState = 0 + TransactionState_SENT_TO_BTC TransactionState = 1 + TransactionState_CONFIRMED_ON_BTC TransactionState = 2 + TransactionState_SENT_TO_BABYLON TransactionState = 3 + TransactionState_VERIFIED TransactionState = 4 + TransactionState_DELEGATION_ACTIVE TransactionState = 5 + TransactionState_UNBONDING_CONFIRMED_ON_BTC TransactionState = 6 + TransactionState_SPENT_ON_BTC TransactionState = 7 ) // Enum value maps for TransactionState. var ( TransactionState_name = map[int32]string{ - 0: "SENT_TO_BTC", - 1: "CONFIRMED_ON_BTC", - 2: "SENT_TO_BABYLON", - 3: "DELEGATION_ACTIVE", - 4: "UNBONDING_CONFIRMED_ON_BTC", - 5: "SPENT_ON_BTC", + 0: "TRANSACTION_CREATED", + 1: "SENT_TO_BTC", + 2: "CONFIRMED_ON_BTC", + 3: "SENT_TO_BABYLON", + 4: "VERIFIED", + 5: "DELEGATION_ACTIVE", + 6: "UNBONDING_CONFIRMED_ON_BTC", + 7: "SPENT_ON_BTC", } TransactionState_value = map[string]int32{ - "SENT_TO_BTC": 0, - "CONFIRMED_ON_BTC": 1, - "SENT_TO_BABYLON": 2, - "DELEGATION_ACTIVE": 3, - "UNBONDING_CONFIRMED_ON_BTC": 4, - "SPENT_ON_BTC": 5, + "TRANSACTION_CREATED": 0, + "SENT_TO_BTC": 1, + "CONFIRMED_ON_BTC": 2, + "SENT_TO_BABYLON": 3, + "VERIFIED": 4, + "DELEGATION_ACTIVE": 5, + "UNBONDING_CONFIRMED_ON_BTC": 6, + "SPENT_ON_BTC": 7, } ) @@ -492,7 +498,7 @@ func (x *TrackedTransaction) GetState() TransactionState { if x != nil { return x.State } - return TransactionState_SENT_TO_BTC + return TransactionState_TRANSACTION_CREATED } func (x *TrackedTransaction) GetWatched() bool { @@ -611,20 +617,23 @@ 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, 0x97, 0x01, 0x0a, 0x10, 0x54, + 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x44, 0x61, 0x74, 0x61, 0x2a, 0xbe, 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, - 0x5f, 0x42, 0x54, 0x43, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x4e, 0x54, 0x5f, 0x54, - 0x4f, 0x5f, 0x42, 0x41, 0x42, 0x59, 0x4c, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x44, - 0x45, 0x4c, 0x45, 0x47, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, - 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x55, 0x4e, 0x42, 0x4f, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, - 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x4e, 0x5f, 0x42, 0x54, 0x43, - 0x10, 0x04, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x50, 0x45, 0x4e, 0x54, 0x5f, 0x4f, 0x4e, 0x5f, 0x42, - 0x54, 0x43, 0x10, 0x05, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x62, 0x61, 0x62, 0x79, 0x6c, 0x6f, 0x6e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, - 0x62, 0x74, 0x63, 0x2d, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x17, 0x0a, 0x13, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, + 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x45, 0x4e, 0x54, + 0x5f, 0x54, 0x4f, 0x5f, 0x42, 0x54, 0x43, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4f, 0x4e, + 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x4e, 0x5f, 0x42, 0x54, 0x43, 0x10, 0x02, 0x12, + 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x4f, 0x5f, 0x42, 0x41, 0x42, 0x59, 0x4c, + 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x56, 0x45, 0x52, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x45, 0x4c, 0x45, 0x47, 0x41, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x55, 0x4e, 0x42, + 0x4f, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, + 0x5f, 0x4f, 0x4e, 0x5f, 0x42, 0x54, 0x43, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x50, 0x45, + 0x4e, 0x54, 0x5f, 0x4f, 0x4e, 0x5f, 0x42, 0x54, 0x43, 0x10, 0x07, 0x42, 0x2c, 0x5a, 0x2a, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x61, 0x62, 0x79, 0x6c, 0x6f, + 0x6e, 0x6c, 0x61, 0x62, 0x73, 0x2d, 0x69, 0x6f, 0x2f, 0x62, 0x74, 0x63, 0x2d, 0x73, 0x74, 0x61, + 0x6b, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( diff --git a/proto/transaction.proto b/proto/transaction.proto index 9961e6b..1be0acd 100644 --- a/proto/transaction.proto +++ b/proto/transaction.proto @@ -5,12 +5,14 @@ package proto; option go_package = "github.com/babylonlabs-io/btc-staker/proto"; enum TransactionState { - SENT_TO_BTC = 0; - CONFIRMED_ON_BTC = 1; - SENT_TO_BABYLON = 2; - DELEGATION_ACTIVE = 3; - UNBONDING_CONFIRMED_ON_BTC = 4; - SPENT_ON_BTC = 5; + TRANSACTION_CREATED = 0; + SENT_TO_BTC = 1; + CONFIRMED_ON_BTC = 2; + SENT_TO_BABYLON = 3; + VERIFIED = 4; + DELEGATION_ACTIVE = 5; + UNBONDING_CONFIRMED_ON_BTC = 6; + SPENT_ON_BTC = 7; } message WatchedTxData { diff --git a/staker/babylontypes.go b/staker/babylontypes.go index e63a177..5bebdcd 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -8,6 +8,7 @@ import ( cl "github.com/babylonlabs-io/btc-staker/babylonclient" "github.com/babylonlabs-io/btc-staker/stakerdb" "github.com/babylonlabs-io/btc-staker/utils" + "github.com/babylonlabs-io/btc-staker/walletcontroller" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -19,10 +20,17 @@ import ( // and be part of new module which will be responsible for communication with babylon chain i.e // retrieving data from babylon chain, sending data to babylon chain, queuing data to be send etc. +type inclusionInfo struct { + txIndex uint32 + inclusionBlock *wire.MsgBlock + inclusionProof []byte +} + type sendDelegationRequest struct { - txHash chainhash.Hash - txIndex uint32 - inclusionBlock *wire.MsgBlock + txHash chainhash.Hash + // optional field, if not provided, delegation will be send to Babylon without + // the inclusion proof + inclusionInfo *inclusionInfo requiredInclusionBlockDepth uint64 } @@ -30,7 +38,6 @@ func (app *StakerApp) buildOwnedDelegation( req *sendDelegationRequest, stakerAddress btcutil.Address, storedTx *stakerdb.StoredTransaction, - stakingTxInclusionProof []byte, ) (*cl.DelegationData, error) { externalData, err := app.retrieveExternalDelegationData(stakerAddress) if err != nil { @@ -109,13 +116,11 @@ func (app *StakerApp) buildOwnedDelegation( dg := createDelegationData( externalData.stakerPublicKey, - req.inclusionBlock, - req.txIndex, + req.inclusionInfo, storedTx, stakingSlashingTx, stakingSlashingSig.Signature, externalData.babylonStakerAddr, - stakingTxInclusionProof, &cl.UndelegationData{ UnbondingTransaction: undelegationDesc.UnbondingTransaction, UnbondingTxValue: undelegationDesc.UnbondingTxValue, @@ -133,8 +138,6 @@ func (app *StakerApp) buildDelegation( stakerAddress btcutil.Address, storedTx *stakerdb.StoredTransaction) (*cl.DelegationData, error) { - stakingTxInclusionProof := app.mustBuildInclusionProof(req) - if storedTx.Watched { watchedData, err := app.txTracker.GetWatchedTransactionData(&req.txHash) @@ -158,13 +161,11 @@ func (app *StakerApp) buildDelegation( dg := createDelegationData( watchedData.StakerBtcPubKey, - req.inclusionBlock, - req.txIndex, + req.inclusionInfo, storedTx, watchedData.SlashingTx, watchedData.SlashingTxSig, watchedData.StakerBabylonAddr, - stakingTxInclusionProof, &undelegationData, ) return dg, nil @@ -173,7 +174,6 @@ func (app *StakerApp) buildDelegation( req, stakerAddress, storedTx, - stakingTxInclusionProof, ) } } @@ -241,6 +241,7 @@ func (app *StakerApp) checkForUnbondingTxSignaturesOnBabylon(stakingTxHash *chai req := &unbondingTxSignaturesConfirmedOnBabylonEvent{ stakingTxHash: *stakingTxHash, + delegationActive: di.Active, covenantUnbondingSignatures: di.UndelegationInfo.CovenantUnbondingSignatures, } @@ -278,3 +279,187 @@ func (app *StakerApp) finalityProviderExists(fpPk *btcec.PublicKey) error { return nil } + +func isTransacionFullySigned(tx *wire.MsgTx) (bool, error) { + if len(tx.TxIn) == 0 { + return false, fmt.Errorf("transaction has no inputs") + } + + signed := true + + for _, in := range tx.TxIn { + if len(in.Witness) == 0 { + signed = false + break + } + } + + return signed, nil +} + +// activateVerifiedDelegation must be run in separate goroutine whenever delegation +// reaches verified state. i.e +// - delegation is on babylon +// - delegation has received enough covenant signatures +func (app *StakerApp) activateVerifiedDelegation( + stakingTransaction *wire.MsgTx, + stakingOutputIndex uint32, + stakingTxHash *chainhash.Hash) { + checkSigTicker := time.NewTicker(app.config.StakerConfig.CheckActiveInterval) + defer checkSigTicker.Stop() + defer app.wg.Done() + + for { + select { + case <-checkSigTicker.C: + di, err := app.babylonClient.QueryDelegationInfo(stakingTxHash) + + if err != nil { + if errors.Is(err, cl.ErrDelegationNotFound) { + // As we only start this handler when we are sure delegation is already on babylon + // this can only that: + // - either we are connected to wrong babylon network + // - or babylon node lost data and is still syncing + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + }).Error("Delegation for given staking tx hash does not exsist on babylon. Check your babylon node.") + } else { + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + "err": err, + }).Error("Error getting delegation info from babylon") + } + + continue + } + + params, err := app.babylonClient.Params() + + if err != nil { + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + "err": err, + }).Error("Error getting babylon params") + // Failed to get params, we cannont do anything, most probably connection error to babylon node + // we will try again in next iteration + continue + } + + // check if check is active + // this loop assume there is at least one active vigiliante to activate delegation + if di.Active { + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + }).Debug("Delegation has been activated on the Babylon chain") + + utils.PushOrQuit[*delegationActiveOnBabylonEvent]( + app.delegationActiveOnBabylonEvChan, + &delegationActiveOnBabylonEvent{ + stakingTxHash: *stakingTxHash, + }, + app.quit, + ) + return + } + + if len(di.UndelegationInfo.CovenantUnbondingSignatures) < int(params.CovenantQuruomThreshold) { + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + "numSignatures": len(di.UndelegationInfo.CovenantUnbondingSignatures), + "required": params.CovenantQuruomThreshold, + }).Debug("Received not enough covenant unbonding signatures on babylon to wait fo activation") + continue + } + + // check if staking tx is already on BTC chain + _, status, err := app.wc.TxDetails(stakingTxHash, stakingTransaction.TxOut[stakingOutputIndex].PkScript) + + if err != nil { + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + "err": err, + }).Error("Error checking existence of staking transaction on btc chain") + continue + } + + if status != walletcontroller.TxNotFound { + app.logger.WithFields(logrus.Fields{ + "status": status, + "stakingTxHash": stakingTxHash, + }).Error("Staking transaction found on btc chain, waiting for activation on Babylon") + continue + } + + // at this point we know that: + // - delegation is not active and already have quorum of covenant signatures + // - staking transaction is not on btc chain + + // check if staking transaction is fully signed + isSigned, err := isTransacionFullySigned(stakingTransaction) + + if err != nil { + app.reportCriticialError( + *stakingTxHash, + err, + "Error checking if staking transaction is fully signed", + ) + return + } + + if isSigned { + _, err := app.wc.SendRawTransaction(stakingTransaction, true) + + if err != nil { + app.logger.WithFields(logrus.Fields{ + "err": err, + "stakingTxHash": stakingTxHash, + }).Error("failed to send staking transaction to btc chain to activate verified delegation") + } + + continue + } + + err = app.wc.UnlockWallet(defaultWalletUnlockTimeout) + + if err != nil { + app.logger.WithFields(logrus.Fields{ + "err": err, + "stakingTxHash": stakingTxHash, + }).Error("failed to unlock wallet to sign staking transaction") + continue + } + + // staking transaction is not signed, we must sign it before sending to btc chain + signedTx, fullySigned, err := app.wc.SignRawTransaction(stakingTransaction) + + if err != nil { + app.logger.WithFields(logrus.Fields{ + "err": err, + "stakingTxHash": stakingTxHash, + }).Error("failed to sign staking transaction") + continue + } + + if !fullySigned { + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + }).Debug("cannot sign staking transction with configured wallet") + continue + } + + _, err = app.wc.SendRawTransaction(signedTx, true) + + if err != nil { + app.logger.WithFields(logrus.Fields{ + "err": err, + "stakingTxHash": stakingTxHash, + }).Error("failed to send staking transaction to btc chain to activate verified delegation") + } + // at this point we send signed staking transaciton to BTC chain, we will + // still wait for its activation + + case <-app.quit: + return + } + } +} diff --git a/staker/events.go b/staker/events.go index 5edd353..81a3cbd 100644 --- a/staker/events.go +++ b/staker/events.go @@ -11,6 +11,11 @@ import ( "github.com/sirupsen/logrus" ) +type responseExpectedChan struct { + errChan chan error + successChan chan *chainhash.Hash +} + type StakingEvent interface { // Each staking event is identified by initial staking transaction hash EventId() chainhash.Hash @@ -20,9 +25,11 @@ type StakingEvent interface { var _ StakingEvent = (*stakingRequestedEvent)(nil) var _ StakingEvent = (*stakingTxBtcConfirmedEvent)(nil) var _ StakingEvent = (*delegationSubmittedToBabylonEvent)(nil) +var _ StakingEvent = (*delegationActiveOnBabylonEvent)(nil) var _ StakingEvent = (*unbondingTxSignaturesConfirmedOnBabylonEvent)(nil) var _ StakingEvent = (*unbondingTxConfirmedOnBtcEvent)(nil) var _ StakingEvent = (*spendStakeTxConfirmedOnBtcEvent)(nil) +var _ StakingEvent = (*sendStakingTxToBTCRequestedEvent)(nil) var _ StakingEvent = (*criticalErrorEvent)(nil) type stakingRequestedEvent struct { @@ -37,6 +44,7 @@ type stakingRequestedEvent struct { requiredDepthOnBtcChain uint32 pop *cl.BabylonPop watchTxData *watchTxData + usePreApprovalFlow bool errChan chan error successChan chan *chainhash.Hash } @@ -55,6 +63,7 @@ func newOwnedStakingRequest( fpBtcPks []*btcec.PublicKey, confirmationTimeBlocks uint32, pop *cl.BabylonPop, + usePreApprovalFlow bool, ) *stakingRequestedEvent { return &stakingRequestedEvent{ stakerAddress: stakerAddress, @@ -68,6 +77,7 @@ func newOwnedStakingRequest( requiredDepthOnBtcChain: confirmationTimeBlocks, pop: pop, watchTxData: nil, + usePreApprovalFlow: usePreApprovalFlow, errChan: make(chan error, 1), successChan: make(chan *chainhash.Hash, 1), } @@ -172,6 +182,7 @@ func (event *delegationSubmittedToBabylonEvent) EventDesc() string { type unbondingTxSignaturesConfirmedOnBabylonEvent struct { stakingTxHash chainhash.Hash + delegationActive bool covenantUnbondingSignatures []cl.CovenantSignatureInfo } @@ -236,3 +247,29 @@ func (app *StakerApp) logStakingEventProcessed(event StakingEvent) { "event": event.EventDesc(), }).Debug("Processed staking event") } + +type sendStakingTxToBTCRequestedEvent struct { + stakingTxHash chainhash.Hash + requiredDepthOnBtcChain uint32 + responseExpected *responseExpectedChan +} + +func (event *sendStakingTxToBTCRequestedEvent) EventId() chainhash.Hash { + return event.stakingTxHash +} + +func (event *sendStakingTxToBTCRequestedEvent) EventDesc() string { + return "SEND_STAKING_TX_TO_BTC_REQUESTED" +} + +type delegationActiveOnBabylonEvent struct { + stakingTxHash chainhash.Hash +} + +func (event *delegationActiveOnBabylonEvent) EventId() chainhash.Hash { + return event.stakingTxHash +} + +func (event *delegationActiveOnBabylonEvent) EventDesc() string { + return "DELEGATION_ACTIVE_ON_BABYLON_EVENT" +} diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 4300622..ff6dcbd 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -128,8 +128,10 @@ type StakerApp struct { m *metrics.StakerMetrics stakingRequestedEvChan chan *stakingRequestedEvent + sendStakingTxToBTCRequestedEvChan chan *sendStakingTxToBTCRequestedEvent stakingTxBtcConfirmedEvChan chan *stakingTxBtcConfirmedEvent delegationSubmittedToBabylonEvChan chan *delegationSubmittedToBabylonEvent + delegationActiveOnBabylonEvChan chan *delegationActiveOnBabylonEvent unbondingTxSignaturesConfirmedOnBabylonEvChan chan *unbondingTxSignaturesConfirmedOnBabylonEvent unbondingTxConfirmedOnBtcEvChan chan *unbondingTxConfirmedOnBtcEvent spendStakeTxConfirmedOnBtcEvChan chan *spendStakeTxConfirmedOnBtcEvent @@ -232,13 +234,17 @@ func NewStakerAppFromDeps( logger: logger, quit: make(chan struct{}), stakingRequestedEvChan: make(chan *stakingRequestedEvent), + + sendStakingTxToBTCRequestedEvChan: make(chan *sendStakingTxToBTCRequestedEvent), + // event for when transaction is confirmed on BTC stakingTxBtcConfirmedEvChan: make(chan *stakingTxBtcConfirmedEvent), // event for when delegation is sent to babylon and included in babylon delegationSubmittedToBabylonEvChan: make(chan *delegationSubmittedToBabylonEvent), - - // event emitted upon transaction which spends staking transaction is confirmed on BTC + // event for when delegation is active on babylon after being verified + delegationActiveOnBabylonEvChan: make(chan *delegationActiveOnBabylonEvent), + // event emitte d upon transaction which spends staking transaction is confirmed on BTC spendStakeTxConfirmedOnBtcEvChan: make(chan *spendStakeTxConfirmedOnBtcEvent), // channel which receives unbonding signatures from covenant for unbonding @@ -399,6 +405,7 @@ func (app *StakerApp) waitForStakingTransactionConfirmation( return err } + app.wg.Add(1) go app.waitForStakingTxConfirmation(*stakingTxHash, requiredBlockDepth, confEvent) return nil } @@ -519,14 +526,18 @@ func (app *StakerApp) checkTransactionsStatus() error { // Keep track of all staking transactions which need checking. chainhash.Hash objects are not relativly small // so it should not OOM even for larage database + var transactionCreated []*chainhash.Hash var transactionsSentToBtc []*chainhash.Hash var transactionConfirmedOnBtc []*chainhash.Hash var transactionsOnBabylon []*stakingDbInfo + var transactionsVerifiedOnBabylon []*chainhash.Hash reset := func() { + transactionCreated = make([]*chainhash.Hash, 0) transactionsSentToBtc = make([]*chainhash.Hash, 0) transactionConfirmedOnBtc = make([]*chainhash.Hash, 0) transactionsOnBabylon = make([]*stakingDbInfo, 0) + transactionsVerifiedOnBabylon = make([]*chainhash.Hash, 0) } // In our scan we only record transactions which state need to be checked, as`ScanTrackedTransactions` @@ -538,6 +549,9 @@ func (app *StakerApp) checkTransactionsStatus() error { // restarts stakingTxHash := tx.StakingTx.TxHash() switch tx.State { + case proto.TransactionState_TRANSACTION_CREATED: + transactionCreated = append(transactionCreated, &stakingTxHash) + return nil case proto.TransactionState_SENT_TO_BTC: transactionsSentToBtc = append(transactionsSentToBtc, &stakingTxHash) return nil @@ -555,6 +569,9 @@ func (app *StakerApp) checkTransactionsStatus() error { stakingTxState: tx.State, }) return nil + case proto.TransactionState_VERIFIED: + transactionsVerifiedOnBabylon = append(transactionsVerifiedOnBabylon, &stakingTxHash) + return nil case proto.TransactionState_DELEGATION_ACTIVE: transactionsOnBabylon = append(transactionsOnBabylon, &stakingDbInfo{ stakingTxHash: &stakingTxHash, @@ -579,6 +596,71 @@ func (app *StakerApp) checkTransactionsStatus() error { return err } + for _, txHash := range transactionCreated { + txHashCopy := txHash + tx, stakerAddress := app.mustGetTransactionAndStakerAddress(txHashCopy) + + alreadyDelegated, err := app.babylonClient.IsTxAlreadyPartOfDelegation(txHashCopy) + + if err != nil { + // we got some communication err, return error and kill app startup + return err + } + + _, status, err := app.wc.TxDetails(txHashCopy, tx.StakingTx.TxOut[tx.StakingOutputIndex].PkScript) + + if err != nil { + // we got some communication err, return error and kill app startup + return err + } + + // transaction: + // - in created state + // - on babylon + // - not on btc chain + // resume pre-approval flow + if alreadyDelegated { + app.wg.Add(1) + app.activateVerifiedDelegation( + tx.StakingTx, + tx.StakingOutputIndex, + txHashCopy, + ) + continue + } + + // transaction + // - not on babylon + // - not on btc chain + // - in created state + // resume pre-approval flow + if status == walletcontroller.TxNotFound { + req := &sendDelegationRequest{ + txHash: *txHashCopy, + inclusionInfo: nil, + requiredInclusionBlockDepth: uint64(stakingParams.ConfirmationTimeBlocks), + } + + app.wg.Add(1) + go app.sendDelegationToBabylonTask(req, stakerAddress, tx) + continue + } + + // transaction + // - not on babylon + // - on btc chain + // - in created state + // resume post-approval flow + if err := app.waitForStakingTransactionConfirmation( + txHashCopy, + tx.StakingTx.TxOut[tx.StakingOutputIndex].PkScript, + stakingParams.ConfirmationTimeBlocks, + app.currentBestBlockHeight.Load(), + ); err != nil { + return err + } + } + for _, txHash := range transactionsSentToBtc { stakingTxHash := txHash tx, _ := app.mustGetTransactionAndStakerAddress(stakingTxHash) @@ -648,10 +730,18 @@ func (app *StakerApp) checkTransactionsStatus() error { "btcTxConfirmationBlockHeight": details.BlockHeight, }).Debug("Already confirmed transaction not sent to babylon yet. Initiate sending") + iclusionProof := app.mustBuildInclusionProof( + details.Block, + details.TxIndex, + ) + req := &sendDelegationRequest{ - txHash: *stakingTxHash, - txIndex: details.TxIndex, - inclusionBlock: details.Block, + txHash: *stakingTxHash, + inclusionInfo: &inclusionInfo{ + txIndex: details.TxIndex, + inclusionBlock: details.Block, + inclusionProof: iclusionProof, + }, requiredInclusionBlockDepth: uint64(stakingParams.ConfirmationTimeBlocks), } @@ -765,13 +855,27 @@ func (app *StakerApp) checkTransactionsStatus() error { } } + for _, txHash := range transactionsVerifiedOnBabylon { + txHashCopy := *txHash + storedTx, _ := app.mustGetTransactionAndStakerAddress(&txHashCopy) + app.wg.Add(1) + go app.activateVerifiedDelegation( + storedTx.StakingTx, + storedTx.StakingOutputIndex, + &txHashCopy, + ) + } + return nil } +// waitForStakingTxConfirmation should be run in separate goroutine func (app *StakerApp) waitForStakingTxConfirmation( txHash chainhash.Hash, depthOnBtcChain uint32, ev *notifier.ConfirmationEvent) { + defer app.wg.Done() + // check we are not shutting down select { case <-app.quit: @@ -845,13 +949,15 @@ func (app *StakerApp) mustGetTransactionAndStakerAddress(txHash *chainhash.Hash) return ts, stakerAddress } -func (app *StakerApp) mustBuildInclusionProof(req *sendDelegationRequest) []byte { - proof, err := cl.GenerateProof(req.inclusionBlock, req.txIndex) +func (app *StakerApp) mustBuildInclusionProof( + inclusionBlock *wire.MsgBlock, + txIndex uint32, +) []byte { + proof, err := cl.GenerateProof(inclusionBlock, txIndex) if err != nil { app.logger.WithFields(logrus.Fields{ - "btcTxHash": req.txHash, - "err": err, + "err": err, }).Fatalf("Failed to build inclusion proof for already confirmed transaction") } @@ -1198,9 +1304,9 @@ func (app *StakerApp) handleStakingEvents() { case ev := <-app.stakingRequestedEvChan: app.logStakingEventReceived(ev) - bestBlockHeight := app.currentBestBlockHeight.Load() - if ev.isWatched() { + bestBlockHeight := app.currentBestBlockHeight.Load() + err := app.txTracker.AddWatchedTransaction( ev.stakingTx, ev.stakingOutputIdx, @@ -1222,15 +1328,22 @@ func (app *StakerApp) handleStakingEvents() { ev.errChan <- err continue } - } else { - // in case of owend transaction we need to send it, and then add to our tracking db. - _, err := app.wc.SendRawTransaction(ev.stakingTx, true) - if err != nil { + + // we assume tx is already on btc chain, so we need to wait for confirmation + if err := app.waitForStakingTransactionConfirmation( + &ev.stakingTxHash, + ev.stakingTx.TxOut[ev.stakingOutputIdx].PkScript, + ev.requiredDepthOnBtcChain, + uint32(bestBlockHeight), + ); err != nil { ev.errChan <- err continue } - err = app.txTracker.AddTransaction( + app.m.ValidReceivedDelegationRequests.Inc() + ev.successChan <- &ev.stakingTxHash + } else { + err := app.txTracker.AddTransaction( ev.stakingTx, ev.stakingOutputIdx, ev.stakingTime, @@ -1243,20 +1356,139 @@ func (app *StakerApp) handleStakingEvents() { ev.errChan <- err continue } + + app.logger.Info("Recieved staking event", "ususePreApprovalFlowe", ev.usePreApprovalFlow) + + if ev.usePreApprovalFlow { + req := &sendDelegationRequest{ + txHash: ev.stakingTxHash, + inclusionInfo: nil, + requiredInclusionBlockDepth: uint64(ev.requiredDepthOnBtcChain), + } + + storedTx, stakerAddress := app.mustGetTransactionAndStakerAddress(&ev.stakingTxHash) + + app.wg.Add(1) + go func( + req *sendDelegationRequest, + stakerAddress btcutil.Address, + storedTx *stakerdb.StoredTransaction, + ev *stakingRequestedEvent, + ) { + defer app.wg.Done() + _, delegationData, err := app.buildAndSendDelegation( + req, + stakerAddress, + storedTx, + ) + + if err != nil { + utils.PushOrQuit( + ev.errChan, + err, + app.quit, + ) + return + } + + submittedEv := &delegationSubmittedToBabylonEvent{ + stakingTxHash: req.txHash, + unbondingTx: delegationData.Ud.UnbondingTransaction, + unbondingTime: delegationData.Ud.UnbondingTxUnbondingTime, + } + + // push event to channel to start waiting for covenant signatures + utils.PushOrQuit[*delegationSubmittedToBabylonEvent]( + app.delegationSubmittedToBabylonEvChan, + submittedEv, + app.quit, + ) + + // send success to caller + utils.PushOrQuit( + ev.successChan, + &ev.stakingTxHash, + app.quit, + ) + }(req, stakerAddress, storedTx, ev) + } else { + // old flow, send to BTC first, end expect response to the caller + app.wg.Add(1) + go func() { + defer app.wg.Done() + utils.PushOrQuit( + app.sendStakingTxToBTCRequestedEvChan, + &sendStakingTxToBTCRequestedEvent{ + stakingTxHash: ev.stakingTxHash, + requiredDepthOnBtcChain: ev.requiredDepthOnBtcChain, + responseExpected: &responseExpectedChan{ + errChan: ev.errChan, + successChan: ev.successChan, + }, + }, + app.quit, + ) + }() + } + app.logStakingEventProcessed(ev) } + case ev := <-app.sendStakingTxToBTCRequestedEvChan: + app.logStakingEventReceived(ev) + + bestBlockHeight := app.currentBestBlockHeight.Load() + + storedTx, _ := app.mustGetTransactionAndStakerAddress(&ev.stakingTxHash) + + _, err := app.wc.SendRawTransaction(storedTx.StakingTx, true) + + if err != nil { + if ev.responseExpected != nil { + utils.PushOrQuit( + ev.responseExpected.errChan, + err, + app.quit, + ) + } + app.logStakingEventProcessed(ev) + continue + } + + if err := app.txTracker.SetTxSentToBtc( + &ev.stakingTxHash, + ); 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) + } + + stakingOutputPkScript := storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex].PkScript + if err := app.waitForStakingTransactionConfirmation( &ev.stakingTxHash, - ev.stakingOutputPkScript, + stakingOutputPkScript, ev.requiredDepthOnBtcChain, uint32(bestBlockHeight), ); err != nil { - ev.errChan <- err + if ev.responseExpected != nil { + utils.PushOrQuit( + ev.responseExpected.errChan, + err, + app.quit, + ) + } + app.logStakingEventProcessed(ev) continue } - app.m.ValidReceivedDelegationRequests.Inc() - ev.successChan <- &ev.stakingTxHash + if ev.responseExpected != nil { + utils.PushOrQuit( + ev.responseExpected.successChan, + &ev.stakingTxHash, + app.quit, + ) + } + app.logStakingEventProcessed(ev) case ev := <-app.stakingTxBtcConfirmedEvChan: @@ -1272,10 +1504,18 @@ func (app *StakerApp) handleStakingEvents() { app.logger.Fatalf("Error setting state for tx %s: %s", ev.stakingTxHash, err) } + proof := app.mustBuildInclusionProof( + ev.inlusionBlock, + ev.txIndex, + ) + req := &sendDelegationRequest{ - txHash: ev.stakingTxHash, - txIndex: ev.txIndex, - inclusionBlock: ev.inlusionBlock, + txHash: ev.stakingTxHash, + inclusionInfo: &inclusionInfo{ + txIndex: ev.txIndex, + inclusionBlock: ev.inlusionBlock, + inclusionProof: proof, + }, requiredInclusionBlockDepth: uint64(ev.blockDepth), } @@ -1310,12 +1550,26 @@ func (app *StakerApp) handleStakingEvents() { if err := app.txTracker.SetTxUnbondingSignaturesReceived( &ev.stakingTxHash, + ev.delegationActive, babylonCovSigsToDbSigSigs(ev.covenantUnbondingSignatures), ); err != nil { // TODO: handle this error somehow, it means we possilbly make invalid state transition app.logger.Fatalf("Error setting state for tx %s: %s", &ev.stakingTxHash, err) } + if !ev.delegationActive { + storedTx, _ := app.mustGetTransactionAndStakerAddress(&ev.stakingTxHash) + // if the delegation is not active here, it can only mean that statking + // is going through pre-approvel flow. Fire up task to send staking tx + // to btc chain + app.wg.Add(1) + go app.activateVerifiedDelegation( + storedTx.StakingTx, + storedTx.StakingOutputIndex, + &ev.stakingTxHash, + ) + } + app.m.DelegationsActivatedOnBabylon.Inc() app.logStakingEventProcessed(ev) @@ -1341,6 +1595,15 @@ func (app *StakerApp) handleStakingEvents() { } app.logStakingEventProcessed(ev) + case ev := <-app.delegationActiveOnBabylonEvChan: + app.logStakingEventReceived(ev) + if err := app.txTracker.SetDelegationActiveOnBabylon(&ev.stakingTxHash); err != nil { + // TODO: handle this error somehow, it means we received spend stake 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) + } + app.logStakingEventProcessed(ev) + case ev := <-app.criticalErrorEvChan: // if error is context.Canceled, it means one of started child go-routines // received quit signal and is shutting down. We just ignore it. @@ -1478,6 +1741,7 @@ func (app *StakerApp) StakeFunds( stakingAmount btcutil.Amount, fpPks []*btcec.PublicKey, stakingTimeBlocks uint16, + sendToBabylonFirst bool, ) (*chainhash.Hash, error) { // check we are not shutting down @@ -1597,6 +1861,7 @@ func (app *StakerApp) StakeFunds( fpPks, params.ConfirmationTimeBlocks, pop, + sendToBabylonFirst, ) utils.PushOrQuit[*stakingRequestedEvent]( diff --git a/staker/types.go b/staker/types.go index a8e4172..3bab322 100644 --- a/staker/types.go +++ b/staker/types.go @@ -147,32 +147,39 @@ func slashingTxForStakingTx( } func createDelegationData( - StakerBtcPk *btcec.PublicKey, - inclusionBlock *wire.MsgBlock, - stakingTxIdx uint32, + stakerBtcPk *btcec.PublicKey, + inclusionInfo *inclusionInfo, storedTx *stakerdb.StoredTransaction, slashingTx *wire.MsgTx, slashingTxSignature *schnorr.Signature, babylonStakerAddr sdk.AccAddress, - stakingTxInclusionProof []byte, undelegationData *cl.UndelegationData, ) *cl.DelegationData { - inclusionBlockHash := inclusionBlock.BlockHash() + + var incInfo *cl.StakingTransactionInclusionInfo = nil + + if inclusionInfo != nil { + inclusionBlockHash := inclusionInfo.inclusionBlock.BlockHash() + + incInfo = &cl.StakingTransactionInclusionInfo{ + StakingTransactionIdx: inclusionInfo.txIndex, + StakingTransactionInclusionProof: inclusionInfo.inclusionProof, + StakingTransactionInclusionBlockHash: &inclusionBlockHash, + } + } dg := cl.DelegationData{ - StakingTransaction: storedTx.StakingTx, - StakingTransactionIdx: stakingTxIdx, - StakingTransactionInclusionProof: stakingTxInclusionProof, - StakingTransactionInclusionBlockHash: &inclusionBlockHash, - StakingTime: storedTx.StakingTime, - StakingValue: btcutil.Amount(storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex].Value), - FinalityProvidersBtcPks: storedTx.FinalityProvidersBtcPks, - StakerBtcPk: StakerBtcPk, - SlashingTransaction: slashingTx, - SlashingTransactionSig: slashingTxSignature, - BabylonStakerAddr: babylonStakerAddr, - BabylonPop: storedTx.Pop, - Ud: undelegationData, + StakingTransaction: storedTx.StakingTx, + StakingTransactionInclusionInfo: incInfo, + StakingTime: storedTx.StakingTime, + StakingValue: btcutil.Amount(storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex].Value), + FinalityProvidersBtcPks: storedTx.FinalityProvidersBtcPks, + StakerBtcPk: stakerBtcPk, + SlashingTransaction: slashingTx, + SlashingTransactionSig: slashingTxSignature, + BabylonStakerAddr: babylonStakerAddr, + BabylonPop: storedTx.Pop, + Ud: undelegationData, } return &dg diff --git a/stakercfg/config.go b/stakercfg/config.go index ecf1297..85c4a22 100644 --- a/stakercfg/config.go +++ b/stakercfg/config.go @@ -131,6 +131,7 @@ func DefaultBtcNodeBackendConfig() BtcNodeBackendConfig { type StakerConfig struct { BabylonStallingInterval time.Duration `long:"babylonstallinginterval" description:"The interval for Babylon node BTC light client to catch up with the real chain before re-sending delegation request"` UnbondingTxCheckInterval time.Duration `long:"unbondingtxcheckinterval" description:"The interval for staker whether delegation received all covenant signatures"` + CheckActiveInterval time.Duration `long:"checkactiveinterval" description:"The interval for staker to check wheter delegation is active on Babylon node"` MaxConcurrentTransactions uint32 `long:"maxconcurrenttransactions" description:"Maximum concurrent transactions in flight to babylon node"` ExitOnCriticalError bool `long:"exitoncriticalerror" description:"Exit stakerd on critical error"` } @@ -139,6 +140,7 @@ func DefaultStakerConfig() StakerConfig { return StakerConfig{ BabylonStallingInterval: 1 * time.Minute, UnbondingTxCheckInterval: 30 * time.Second, + CheckActiveInterval: 1 * time.Minute, MaxConcurrentTransactions: 1, ExitOnCriticalError: true, } diff --git a/stakerdb/trackedtranactionstore.go b/stakerdb/trackedtranactionstore.go index 2a4435b..8ca56a1 100644 --- a/stakerdb/trackedtranactionstore.go +++ b/stakerdb/trackedtranactionstore.go @@ -589,7 +589,7 @@ func (c *TrackedTransactionStore) AddTransaction( StakingTxBtcConfirmationInfo: nil, BtcSigType: pop.BtcSigType, BtcSigOverBbnStakerAddr: pop.BtcSigOverBabylonAddr, - State: proto.TransactionState_SENT_TO_BTC, + State: proto.TransactionState_TRANSACTION_CREATED, Watched: false, UnbondingTxData: nil, } @@ -735,6 +735,15 @@ func (c *TrackedTransactionStore) setTxState( }) } +func (c *TrackedTransactionStore) SetTxSentToBtc(txHash *chainhash.Hash) error { + setTxSentToBtc := func(tx *proto.TrackedTransaction) error { + tx.State = proto.TransactionState_SENT_TO_BTC + return nil + } + + return c.setTxState(txHash, setTxSentToBtc) +} + func (c *TrackedTransactionStore) SetTxConfirmed( txHash *chainhash.Hash, blockHash *chainhash.Hash, @@ -785,8 +794,18 @@ func (c *TrackedTransactionStore) SetTxSpentOnBtc(txHash *chainhash.Hash) error return c.setTxState(txHash, setTxSpentOnBtc) } +func (c *TrackedTransactionStore) SetDelegationActiveOnBabylon(txHash *chainhash.Hash) error { + setTxSpentOnBtc := func(tx *proto.TrackedTransaction) error { + tx.State = proto.TransactionState_DELEGATION_ACTIVE + return nil + } + + return c.setTxState(txHash, setTxSpentOnBtc) +} + func (c *TrackedTransactionStore) SetTxUnbondingSignaturesReceived( txHash *chainhash.Hash, + delegationActive bool, covenantSignatures []PubKeySigPair, ) error { setUnbondingSignaturesReceived := func(tx *proto.TrackedTransaction) error { @@ -798,7 +817,11 @@ func (c *TrackedTransactionStore) SetTxUnbondingSignaturesReceived( return fmt.Errorf("cannot set unbonding signatures received, because unbonding signatures already exist: %w", ErrInvalidUnbondingDataUpdate) } - tx.State = proto.TransactionState_DELEGATION_ACTIVE + if delegationActive { + tx.State = proto.TransactionState_DELEGATION_ACTIVE + } else { + tx.State = proto.TransactionState_VERIFIED + } tx.UnbondingTxData.CovenantSignatures = covenantSigsToProto(covenantSignatures) return nil } diff --git a/stakerdb/trackedtransactionstore_test.go b/stakerdb/trackedtransactionstore_test.go index a22544d..a1b4016 100644 --- a/stakerdb/trackedtransactionstore_test.go +++ b/stakerdb/trackedtransactionstore_test.go @@ -195,7 +195,7 @@ func TestStateTransitions(t *testing.T) { // Inital state storedTx, err := s.GetTransaction(&txHash) require.NoError(t, err) - require.Equal(t, proto.TransactionState_SENT_TO_BTC, storedTx.State) + require.Equal(t, proto.TransactionState_TRANSACTION_CREATED, storedTx.State) require.Equal(t, uint64(1), storedTx.StoredTransactionIdx) // Confirmed hash := datagen.GenRandomBtcdHash(r) diff --git a/stakerservice/client/rpcclient.go b/stakerservice/client/rpcclient.go index 6013bea..7e64855 100644 --- a/stakerservice/client/rpcclient.go +++ b/stakerservice/client/rpcclient.go @@ -67,6 +67,7 @@ func (c *StakerServiceJsonRpcClient) Stake( stakingAmount int64, fpPks []string, stakingTimeBlocks int64, + sendToBabylonFirst bool, ) (*service.ResultStake, error) { result := new(service.ResultStake) @@ -75,6 +76,7 @@ func (c *StakerServiceJsonRpcClient) Stake( params["stakingAmount"] = stakingAmount params["fpBtcPks"] = fpPks params["stakingTimeBlocks"] = stakingTimeBlocks + params["sendToBabylonFirst"] = sendToBabylonFirst _, err := c.client.Call(ctx, "stake", params, result) if err != nil { diff --git a/stakerservice/service.go b/stakerservice/service.go index ace294e..66e4026 100644 --- a/stakerservice/service.go +++ b/stakerservice/service.go @@ -80,6 +80,7 @@ func (s *StakerService) stake(_ *rpctypes.Context, stakingAmount int64, fpBtcPks []string, stakingTimeBlocks int64, + sendToBabylonFirst bool, ) (*ResultStake, error) { if stakingAmount <= 0 { @@ -115,7 +116,7 @@ func (s *StakerService) stake(_ *rpctypes.Context, stakingTimeUint16 := uint16(stakingTimeBlocks) - stakingTxHash, err := s.staker.StakeFunds(stakerAddr, amount, fpPubKeys, stakingTimeUint16) + stakingTxHash, err := s.staker.StakeFunds(stakerAddr, amount, fpPubKeys, stakingTimeUint16, sendToBabylonFirst) if err != nil { return nil, err } @@ -543,7 +544,7 @@ func (s *StakerService) GetRoutes() RoutesMap { // info AP "health": rpc.NewRPCFunc(s.health, ""), // staking API - "stake": rpc.NewRPCFunc(s.stake, "stakerAddress,stakingAmount,fpBtcPks,stakingTimeBlocks"), + "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"),