diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c979b..b52a157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,9 +37,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Misc Improvements -* [51](https://github.com/babylonlabs-io/btc-staker/pull/51) Use int64 +* [#51](https://github.com/babylonlabs-io/btc-staker/pull/51) Use int64 for satoshi amount related values. +* [#65](https://github.com/babylonlabs-io/btc-staker/pull/65) Various fixes to +pre-approval flow. Do not send signed staking transactions to Babylon. + ## v0.7.2 ### Bug fix diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 18dd2cf..3b9fc3e 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -716,7 +716,7 @@ func (tm *TestManager) sendStakingTxBTC( require.Equal(t, stakingDetails.StakingTxHash, txHash) if sendToBabylonFirst { - require.Equal(t, stakingDetails.StakingState, proto.TransactionState_TRANSACTION_CREATED.String()) + require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BABYLON.String()) } else { require.Equal(t, stakingDetails.StakingState, proto.TransactionState_SENT_TO_BTC.String()) } diff --git a/proto/transaction.pb.go b/proto/transaction.pb.go index 97b9eff..9870fd3 100644 --- a/proto/transaction.pb.go +++ b/proto/transaction.pb.go @@ -23,37 +23,34 @@ const ( type TransactionState int32 const ( - 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 + TransactionState_SENT_TO_BTC TransactionState = 0 + TransactionState_CONFIRMED_ON_BTC TransactionState = 1 + TransactionState_SENT_TO_BABYLON TransactionState = 2 + TransactionState_VERIFIED TransactionState = 3 + TransactionState_DELEGATION_ACTIVE TransactionState = 4 + TransactionState_UNBONDING_CONFIRMED_ON_BTC TransactionState = 5 + TransactionState_SPENT_ON_BTC TransactionState = 6 ) // Enum value maps for TransactionState. var ( TransactionState_name = map[int32]string{ - 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", + 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", } TransactionState_value = map[string]int32{ - "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, + "SENT_TO_BTC": 0, + "CONFIRMED_ON_BTC": 1, + "SENT_TO_BABYLON": 2, + "VERIFIED": 3, + "DELEGATION_ACTIVE": 4, + "UNBONDING_CONFIRMED_ON_BTC": 5, + "SPENT_ON_BTC": 6, } ) @@ -498,7 +495,7 @@ func (x *TrackedTransaction) GetState() TransactionState { if x != nil { return x.State } - return TransactionState_TRANSACTION_CREATED + return TransactionState_SENT_TO_BTC } func (x *TrackedTransaction) GetWatched() bool { @@ -617,23 +614,21 @@ 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, 0xbe, 0x01, 0x0a, 0x10, 0x54, + 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x44, 0x61, 0x74, 0x61, 0x2a, 0xa5, 0x01, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 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, + 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, 0x0c, 0x0a, 0x08, 0x56, + 0x45, 0x52, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x45, 0x4c, + 0x45, 0x47, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x04, + 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, 0x05, + 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x50, 0x45, 0x4e, 0x54, 0x5f, 0x4f, 0x4e, 0x5f, 0x42, 0x54, 0x43, + 0x10, 0x06, 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 1be0acd..24266cd 100644 --- a/proto/transaction.proto +++ b/proto/transaction.proto @@ -5,14 +5,13 @@ package proto; option go_package = "github.com/babylonlabs-io/btc-staker/proto"; enum TransactionState { - 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; + SENT_TO_BTC = 0; + CONFIRMED_ON_BTC = 1; + SENT_TO_BABYLON = 2; + VERIFIED = 3; + DELEGATION_ACTIVE = 4; + UNBONDING_CONFIRMED_ON_BTC = 5; + SPENT_ON_BTC = 6; } message WatchedTxData { diff --git a/staker/commands.go b/staker/commands.go new file mode 100644 index 0000000..2645a12 --- /dev/null +++ b/staker/commands.go @@ -0,0 +1,130 @@ +package staker + +import ( + cl "github.com/babylonlabs-io/btc-staker/babylonclient" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// we can make command to implement StakingEvent interface +var _ StakingEvent = (*stakingRequestCmd)(nil) + +type stakingRequestCmd struct { + stakerAddress btcutil.Address + stakingTxHash chainhash.Hash + stakingTx *wire.MsgTx + stakingOutputIdx uint32 + stakingOutputPkScript []byte + stakingTime uint16 + stakingValue btcutil.Amount + fpBtcPks []*btcec.PublicKey + requiredDepthOnBtcChain uint32 + pop *cl.BabylonPop + watchTxData *watchTxDataCmd + usePreApprovalFlow bool + errChan chan error + successChan chan *chainhash.Hash +} + +func (req *stakingRequestCmd) isWatched() bool { + return req.watchTxData != nil +} + +func newOwnedStakingCommand( + stakerAddress btcutil.Address, + stakingTx *wire.MsgTx, + stakingOutputIdx uint32, + stakingOutputPkScript []byte, + stakingTime uint16, + stakingValue btcutil.Amount, + fpBtcPks []*btcec.PublicKey, + confirmationTimeBlocks uint32, + pop *cl.BabylonPop, + usePreApprovalFlow bool, +) *stakingRequestCmd { + return &stakingRequestCmd{ + stakerAddress: stakerAddress, + stakingTxHash: stakingTx.TxHash(), + stakingTx: stakingTx, + stakingOutputIdx: stakingOutputIdx, + stakingOutputPkScript: stakingOutputPkScript, + stakingTime: stakingTime, + stakingValue: stakingValue, + fpBtcPks: fpBtcPks, + requiredDepthOnBtcChain: confirmationTimeBlocks, + pop: pop, + watchTxData: nil, + usePreApprovalFlow: usePreApprovalFlow, + errChan: make(chan error, 1), + successChan: make(chan *chainhash.Hash, 1), + } +} + +type watchTxDataCmd struct { + slashingTx *wire.MsgTx + slashingTxSig *schnorr.Signature + stakerBabylonAddr sdk.AccAddress + stakerBtcPk *btcec.PublicKey + // unbonding related data + unbondingTx *wire.MsgTx + slashUnbondingTx *wire.MsgTx + slashUnbondingTxSig *schnorr.Signature + unbondingTime uint16 +} + +func newWatchedStakingCmd( + stakerAddress btcutil.Address, + stakingTx *wire.MsgTx, + stakingOutputIdx uint32, + stakingOutputPkScript []byte, + stakingTime uint16, + stakingValue btcutil.Amount, + fpBtcPks []*btcec.PublicKey, + confirmationTimeBlocks uint32, + pop *cl.BabylonPop, + slashingTx *wire.MsgTx, + slashingTxSignature *schnorr.Signature, + stakerBabylonAddr sdk.AccAddress, + stakerBtcPk *btcec.PublicKey, + unbondingTx *wire.MsgTx, + slashUnbondingTx *wire.MsgTx, + slashUnbondingTxSig *schnorr.Signature, + unbondingTime uint16, +) *stakingRequestCmd { + return &stakingRequestCmd{ + stakerAddress: stakerAddress, + stakingTxHash: stakingTx.TxHash(), + stakingTx: stakingTx, + stakingOutputIdx: stakingOutputIdx, + stakingOutputPkScript: stakingOutputPkScript, + stakingTime: stakingTime, + stakingValue: stakingValue, + fpBtcPks: fpBtcPks, + requiredDepthOnBtcChain: confirmationTimeBlocks, + pop: pop, + watchTxData: &watchTxDataCmd{ + slashingTx: slashingTx, + slashingTxSig: slashingTxSignature, + stakerBabylonAddr: stakerBabylonAddr, + stakerBtcPk: stakerBtcPk, + unbondingTx: unbondingTx, + slashUnbondingTx: slashUnbondingTx, + slashUnbondingTxSig: slashUnbondingTxSig, + unbondingTime: unbondingTime, + }, + errChan: make(chan error, 1), + successChan: make(chan *chainhash.Hash, 1), + } +} + +func (event *stakingRequestCmd) EventId() chainhash.Hash { + return event.stakingTxHash +} + +func (event *stakingRequestCmd) EventDesc() string { + return "STAKING_REQUESTED_CMD" +} diff --git a/staker/events.go b/staker/events.go index 81a3cbd..9485f21 100644 --- a/staker/events.go +++ b/staker/events.go @@ -2,152 +2,25 @@ package staker import ( cl "github.com/babylonlabs-io/btc-staker/babylonclient" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" - sdk "github.com/cosmos/cosmos-sdk/types" "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 EventDesc() string } -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 { - stakerAddress btcutil.Address - stakingTxHash chainhash.Hash - stakingTx *wire.MsgTx - stakingOutputIdx uint32 - stakingOutputPkScript []byte - stakingTime uint16 - stakingValue btcutil.Amount - fpBtcPks []*btcec.PublicKey - requiredDepthOnBtcChain uint32 - pop *cl.BabylonPop - watchTxData *watchTxData - usePreApprovalFlow bool - errChan chan error - successChan chan *chainhash.Hash -} - -func (req *stakingRequestedEvent) isWatched() bool { - return req.watchTxData != nil -} - -func newOwnedStakingRequest( - stakerAddress btcutil.Address, - stakingTx *wire.MsgTx, - stakingOutputIdx uint32, - stakingOutputPkScript []byte, - stakingTime uint16, - stakingValue btcutil.Amount, - fpBtcPks []*btcec.PublicKey, - confirmationTimeBlocks uint32, - pop *cl.BabylonPop, - usePreApprovalFlow bool, -) *stakingRequestedEvent { - return &stakingRequestedEvent{ - stakerAddress: stakerAddress, - stakingTxHash: stakingTx.TxHash(), - stakingTx: stakingTx, - stakingOutputIdx: stakingOutputIdx, - stakingOutputPkScript: stakingOutputPkScript, - stakingTime: stakingTime, - stakingValue: stakingValue, - fpBtcPks: fpBtcPks, - requiredDepthOnBtcChain: confirmationTimeBlocks, - pop: pop, - watchTxData: nil, - usePreApprovalFlow: usePreApprovalFlow, - errChan: make(chan error, 1), - successChan: make(chan *chainhash.Hash, 1), - } -} - -type watchTxData struct { - slashingTx *wire.MsgTx - slashingTxSig *schnorr.Signature - stakerBabylonAddr sdk.AccAddress - stakerBtcPk *btcec.PublicKey - // unbonding related data - unbondingTx *wire.MsgTx - slashUnbondingTx *wire.MsgTx - slashUnbondingTxSig *schnorr.Signature - unbondingTime uint16 -} - -func newWatchedStakingRequest( - stakerAddress btcutil.Address, - stakingTx *wire.MsgTx, - stakingOutputIdx uint32, - stakingOutputPkScript []byte, - stakingTime uint16, - stakingValue btcutil.Amount, - fpBtcPks []*btcec.PublicKey, - confirmationTimeBlocks uint32, - pop *cl.BabylonPop, - slashingTx *wire.MsgTx, - slashingTxSignature *schnorr.Signature, - stakerBabylonAddr sdk.AccAddress, - stakerBtcPk *btcec.PublicKey, - unbondingTx *wire.MsgTx, - slashUnbondingTx *wire.MsgTx, - slashUnbondingTxSig *schnorr.Signature, - unbondingTime uint16, -) *stakingRequestedEvent { - return &stakingRequestedEvent{ - stakerAddress: stakerAddress, - stakingTxHash: stakingTx.TxHash(), - stakingTx: stakingTx, - stakingOutputIdx: stakingOutputIdx, - stakingOutputPkScript: stakingOutputPkScript, - stakingTime: stakingTime, - stakingValue: stakingValue, - fpBtcPks: fpBtcPks, - requiredDepthOnBtcChain: confirmationTimeBlocks, - pop: pop, - watchTxData: &watchTxData{ - slashingTx: slashingTx, - slashingTxSig: slashingTxSignature, - stakerBabylonAddr: stakerBabylonAddr, - stakerBtcPk: stakerBtcPk, - unbondingTx: unbondingTx, - slashUnbondingTx: slashUnbondingTx, - slashUnbondingTxSig: slashUnbondingTxSig, - unbondingTime: unbondingTime, - }, - errChan: make(chan error, 1), - successChan: make(chan *chainhash.Hash, 1), - } -} - -func (req *stakingRequestedEvent) EventId() chainhash.Hash { - return req.stakingTxHash -} - -func (req *stakingRequestedEvent) EventDesc() string { - return "STAKING_REQUESTED" -} - type stakingTxBtcConfirmedEvent struct { stakingTxHash chainhash.Hash txIndex uint32 @@ -248,20 +121,6 @@ func (app *StakerApp) logStakingEventProcessed(event StakingEvent) { }).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 } diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 99a6d30..d232079 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -123,8 +123,7 @@ type StakerApp struct { babylonMsgSender *cl.BabylonMsgSender m *metrics.StakerMetrics - stakingRequestedEvChan chan *stakingRequestedEvent - sendStakingTxToBTCRequestedEvChan chan *sendStakingTxToBTCRequestedEvent + stakingRequestedCmdChan chan *stakingRequestCmd stakingTxBtcConfirmedEvChan chan *stakingTxBtcConfirmedEvent delegationSubmittedToBabylonEvChan chan *delegationSubmittedToBabylonEvent delegationActiveOnBabylonEvChan chan *delegationActiveOnBabylonEvent @@ -218,21 +217,18 @@ func NewStakerAppFromDeps( metrics *metrics.StakerMetrics, ) (*StakerApp, error) { return &StakerApp{ - babylonClient: cl, - wc: walletClient, - notifier: nodeNotifier, - feeEstimator: feeEestimator, - network: &config.ActiveNetParams, - txTracker: tracker, - babylonMsgSender: babylonMsgSender, - m: metrics, - config: config, - logger: logger, - quit: make(chan struct{}), - stakingRequestedEvChan: make(chan *stakingRequestedEvent), - - sendStakingTxToBTCRequestedEvChan: make(chan *sendStakingTxToBTCRequestedEvent), - + babylonClient: cl, + wc: walletClient, + notifier: nodeNotifier, + feeEstimator: feeEestimator, + network: &config.ActiveNetParams, + txTracker: tracker, + babylonMsgSender: babylonMsgSender, + m: metrics, + config: config, + logger: logger, + quit: make(chan struct{}), + stakingRequestedCmdChan: make(chan *stakingRequestCmd), // event for when transaction is confirmed on BTC stakingTxBtcConfirmedEvChan: make(chan *stakingTxBtcConfirmedEvent), @@ -306,9 +302,10 @@ func (app *StakerApp) Start() error { app.babylonMsgSender.Start() - app.wg.Add(2) + app.wg.Add(3) go app.handleNewBlocks(blockEventNotifier) go app.handleStakingEvents() + go app.handleStakingCommands() if err := app.checkTransactionsStatus(); err != nil { startErr = err @@ -530,14 +527,12 @@ 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) @@ -553,9 +548,6 @@ 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 @@ -601,82 +593,12 @@ func (app *StakerApp) checkTransactionsStatus() error { } app.logger.WithFields(logrus.Fields{ - "num_created": len(transactionCreated), "num_sent_to_btc": len(transactionsSentToBtc), "num_confirmed_on_btc": len(transactionConfirmedOnBtc), "num_on_babylon": len(transactionsOnBabylon), "num_verified": len(transactionsVerifiedOnBabylon), }).Debug("Iteration over all database staking requests finished") - 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) - go 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: 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 - } - } - - app.logger.WithFields(logrus.Fields{ - "state": proto.TransactionState_TRANSACTION_CREATED.String(), - }).Debug("Partially fixed state of the database") - for _, txHash := range transactionsSentToBtc { stakingTxHash := txHash tx, _ := app.mustGetTransactionAndStakerAddress(stakingTxHash) @@ -703,7 +625,7 @@ func (app *StakerApp) checkTransactionsStatus() error { delegationInfo, err := app.babylonClient.QueryDelegationInfo(stakingTxHash) - if err != nil && !errors.Is(cl.ErrDelegationNotFound, err) { + if err != nil && !errors.Is(err, cl.ErrDelegationNotFound) { return err } @@ -1339,202 +1261,194 @@ func (app *StakerApp) sendDelegationToBabylonTask( } } -// main event loop for the staker app -func (app *StakerApp) handleStakingEvents() { +func (app *StakerApp) handlePreApprovalCmd(cmd *stakingRequestCmd) error { + // just to pass to buildAndSendDelegation + fakeStoredTx, err := stakerdb.CreateTrackedTransaction( + cmd.stakingTx, + cmd.stakingOutputIdx, + cmd.stakingTime, + cmd.fpBtcPks, + babylonPopToDbPop(cmd.pop), + cmd.stakerAddress, + ) + + if err != nil { + return err + } + + req := &sendDelegationRequest{ + txHash: cmd.stakingTxHash, + inclusionInfo: nil, + requiredInclusionBlockDepth: cmd.requiredDepthOnBtcChain, + } + + _, delegationData, err := app.buildAndSendDelegation( + req, + cmd.stakerAddress, + fakeStoredTx, + ) + + if err != nil { + return err + } + + err = app.txTracker.AddTransactionSentToBabylon( + cmd.stakingTx, + cmd.stakingOutputIdx, + cmd.stakingTime, + cmd.fpBtcPks, + babylonPopToDbPop(cmd.pop), + cmd.stakerAddress, + delegationData.Ud.UnbondingTransaction, + delegationData.Ud.UnbondingTxUnbondingTime, + ) + + if err != nil { + return err + } + + app.wg.Add(1) + go app.checkForUnbondingTxSignaturesOnBabylon(&cmd.stakingTxHash) + + return nil +} + +func (app *StakerApp) handlePostApprovalCmd(cmd *stakingRequestCmd) error { + bestBlockHeight := app.currentBestBlockHeight.Load() + + err := app.wc.UnlockWallet(defaultWalletUnlockTimeout) + + if err != nil { + return err + } + + tx, fullySignd, err := app.wc.SignRawTransaction(cmd.stakingTx) + + if err != nil { + return err + } + + if !fullySignd { + return fmt.Errorf("failed to fully sign transaction with hash %s", cmd.stakingTxHash) + } + + _, err = app.wc.SendRawTransaction(tx, true) + + if err != nil { + return err + } + + stakingOutputPkScript := cmd.stakingTx.TxOut[cmd.stakingOutputIdx].PkScript + + if err := app.waitForStakingTransactionConfirmation( + &cmd.stakingTxHash, + stakingOutputPkScript, + cmd.requiredDepthOnBtcChain, + uint32(bestBlockHeight), + ); err != nil { + return err + } + + if err := app.txTracker.AddTransaction( + cmd.stakingTx, + cmd.stakingOutputIdx, + cmd.stakingTime, + cmd.fpBtcPks, + babylonPopToDbPop(cmd.pop), + cmd.stakerAddress, + ); err != nil { + return err + } + + return nil +} + +func (app *StakerApp) handleStakingCmd(cmd *stakingRequestCmd) error { + if cmd.usePreApprovalFlow { + return app.handlePreApprovalCmd(cmd) + } else { + return app.handlePostApprovalCmd(cmd) + } +} + +func (app *StakerApp) handleStakingCommands() { defer app.wg.Done() for { select { - case ev := <-app.stakingRequestedEvChan: - app.logStakingEventReceived(ev) + case cmd := <-app.stakingRequestedCmdChan: + app.logStakingEventReceived(cmd) - if ev.isWatched() { + if cmd.isWatched() { bestBlockHeight := app.currentBestBlockHeight.Load() err := app.txTracker.AddWatchedTransaction( - ev.stakingTx, - ev.stakingOutputIdx, - ev.stakingTime, - ev.fpBtcPks, - babylonPopToDbPop(ev.pop), - ev.stakerAddress, - ev.watchTxData.slashingTx, - ev.watchTxData.slashingTxSig, - ev.watchTxData.stakerBabylonAddr, - ev.watchTxData.stakerBtcPk, - ev.watchTxData.unbondingTx, - ev.watchTxData.slashUnbondingTx, - ev.watchTxData.slashUnbondingTxSig, - ev.watchTxData.unbondingTime, + cmd.stakingTx, + cmd.stakingOutputIdx, + cmd.stakingTime, + cmd.fpBtcPks, + babylonPopToDbPop(cmd.pop), + cmd.stakerAddress, + cmd.watchTxData.slashingTx, + cmd.watchTxData.slashingTxSig, + cmd.watchTxData.stakerBabylonAddr, + cmd.watchTxData.stakerBtcPk, + cmd.watchTxData.unbondingTx, + cmd.watchTxData.slashUnbondingTx, + cmd.watchTxData.slashUnbondingTxSig, + cmd.watchTxData.unbondingTime, ) if err != nil { - ev.errChan <- err + cmd.errChan <- err continue } // 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, + &cmd.stakingTxHash, + cmd.stakingTx.TxOut[cmd.stakingOutputIdx].PkScript, + cmd.requiredDepthOnBtcChain, uint32(bestBlockHeight), ); err != nil { - ev.errChan <- err + cmd.errChan <- err continue } app.m.ValidReceivedDelegationRequests.Inc() - ev.successChan <- &ev.stakingTxHash - } else { - err := app.txTracker.AddTransaction( - ev.stakingTx, - ev.stakingOutputIdx, - ev.stakingTime, - ev.fpBtcPks, - babylonPopToDbPop(ev.pop), - ev.stakerAddress, - ) - - if err != nil { - ev.errChan <- err - continue - } - - app.logger.Info("Recieved staking event", "ususePreApprovalFlowe", ev.usePreApprovalFlow) - - if ev.usePreApprovalFlow { - req := &sendDelegationRequest{ - txHash: ev.stakingTxHash, - inclusionInfo: nil, - requiredInclusionBlockDepth: 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) + cmd.successChan <- &cmd.stakingTxHash + app.logStakingEventProcessed(cmd) 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 + err := app.handleStakingCmd(cmd) - if err := app.waitForStakingTransactionConfirmation( - &ev.stakingTxHash, - stakingOutputPkScript, - ev.requiredDepthOnBtcChain, - uint32(bestBlockHeight), - ); err != nil { - if ev.responseExpected != nil { - utils.PushOrQuit( - ev.responseExpected.errChan, - err, - app.quit, - ) - } - app.logStakingEventProcessed(ev) - continue - } - - if ev.responseExpected != nil { + if err != nil { utils.PushOrQuit( - ev.responseExpected.successChan, - &ev.stakingTxHash, + cmd.errChan, + err, + app.quit, + ) + } else { + utils.PushOrQuit( + cmd.successChan, + &cmd.stakingTxHash, app.quit, ) } + app.logStakingEventProcessed(cmd) + case <-app.quit: + return + } + } +} - app.logStakingEventProcessed(ev) +// main event loop for the staker app +func (app *StakerApp) handleStakingEvents() { + defer app.wg.Done() + for { + select { case ev := <-app.stakingTxBtcConfirmedEvChan: app.logStakingEventReceived(ev) @@ -1759,8 +1673,8 @@ func (app *StakerApp) WatchStaking( "btxTxHash": stakingTx.TxHash(), }).Info("Received valid staking tx to watch") - utils.PushOrQuit[*stakingRequestedEvent]( - app.stakingRequestedEvChan, + utils.PushOrQuit[*stakingRequestCmd]( + app.stakingRequestedCmdChan, watchedRequest, app.quit, ) @@ -1882,7 +1796,9 @@ func (app *StakerApp) StakeFunds( feeRate := app.feeEstimator.EstimateFeePerKb() - tx, err := app.wc.CreateAndSignTx([]*wire.TxOut{stakingInfo.StakingOutput}, btcutil.Amount(feeRate), stakerAddress) + // Create unsigned transaction by wallet without signing. Signing will happen + // in next steps + tx, err := app.wc.CreateTransaction([]*wire.TxOut{stakingInfo.StakingOutput}, btcutil.Amount(feeRate), stakerAddress) if err != nil { return nil, err @@ -1895,7 +1811,7 @@ func (app *StakerApp) StakeFunds( "fee": feeRate, }).Info("Created and signed staking transaction") - req := newOwnedStakingRequest( + req := newOwnedStakingCommand( stakerAddress, tx, 0, @@ -1908,8 +1824,8 @@ func (app *StakerApp) StakeFunds( sendToBabylonFirst, ) - utils.PushOrQuit[*stakingRequestedEvent]( - app.stakingRequestedEvChan, + utils.PushOrQuit[*stakingRequestCmd]( + app.stakingRequestedCmdChan, req, app.quit, ) diff --git a/staker/types.go b/staker/types.go index 4a2e803..6a59991 100644 --- a/staker/types.go +++ b/staker/types.go @@ -479,7 +479,7 @@ func parseWatchStakingRequest( unbondingTime uint16, currentParams *cl.StakingParams, network *chaincfg.Params, -) (*stakingRequestedEvent, error) { +) (*stakingRequestCmd, error) { // TODO(https://github.com/babylonlabs-io/btc-staker/issues/32): // This check re-implements whole babylon validation logic. We should // refactor this to use babylon validation utilities. @@ -634,7 +634,7 @@ func parseWatchStakingRequest( return nil, fmt.Errorf("failed to watch staking tx. Unbonding tx do not point to staking tx") } - req := newWatchedStakingRequest( + req := newWatchedStakingCmd( stakerAddress, stakingTx, uint32(stakingOutputIdx), diff --git a/stakerdb/trackedtranactionstore.go b/stakerdb/trackedtranactionstore.go index 8ca56a1..5393a05 100644 --- a/stakerdb/trackedtranactionstore.go +++ b/stakerdb/trackedtranactionstore.go @@ -552,6 +552,112 @@ func (c *TrackedTransactionStore) addTransactionInternal( }) } +func CreateTrackedTransaction( + btcTx *wire.MsgTx, + stakingOutputIndex uint32, + stakingTime uint16, + fpPubKeys []*btcec.PublicKey, + pop *ProofOfPossession, + stakerAddress btcutil.Address, +) (*StoredTransaction, error) { + serializedTx, err := utils.SerializeBtcTransaction(btcTx) + + if err != nil { + return nil, err + } + + if len(fpPubKeys) == 0 { + return nil, fmt.Errorf("cannot add transaction without finality providers public keys") + } + + fpPubKeysBytes := make([][]byte, len(fpPubKeys)) + + for i, pk := range fpPubKeys { + fpPubKeysBytes[i] = schnorr.SerializePubKey(pk) + } + + if pop == nil { + return nil, fmt.Errorf("cannot add transaction without proof of possession") + } + + msg := proto.TrackedTransaction{ + // Setting it to 0, proper number will be filled by `addTransactionInternal` + TrackedTransactionIdx: 0, + StakingTransaction: serializedTx, + StakingOutputIdx: stakingOutputIndex, + StakerAddress: stakerAddress.EncodeAddress(), + StakingTime: uint32(stakingTime), + FinalityProvidersBtcPks: fpPubKeysBytes, + StakingTxBtcConfirmationInfo: nil, + BtcSigType: pop.BtcSigType, + BtcSigOverBbnStakerAddr: pop.BtcSigOverBabylonAddr, + State: proto.TransactionState_SENT_TO_BTC, + Watched: false, + UnbondingTxData: nil, + } + + return protoTxToStoredTransaction(&msg) +} + +func (c *TrackedTransactionStore) AddTransactionSentToBabylon( + btcTx *wire.MsgTx, + stakingOutputIndex uint32, + stakingTime uint16, + fpPubKeys []*btcec.PublicKey, + pop *ProofOfPossession, + stakerAddress btcutil.Address, + unbondingTx *wire.MsgTx, + unbondingTime uint16, +) error { + txHash := btcTx.TxHash() + txHashBytes := txHash[:] + serializedTx, err := utils.SerializeBtcTransaction(btcTx) + + if err != nil { + return fmt.Errorf("failed to serialize Bitcoin transaction: %w", err) + } + + if len(fpPubKeys) == 0 { + return fmt.Errorf("cannot add transaction without finality providers public keys") + } + + fpPubKeysBytes := make([][]byte, len(fpPubKeys)) + + for i, pk := range fpPubKeys { + fpPubKeysBytes[i] = schnorr.SerializePubKey(pk) + } + + if pop == nil { + return fmt.Errorf("cannot add transaction without proof of possession") + } + + update, err := newInitialUnbondingTxData(unbondingTx, unbondingTime) + + if err != nil { + return fmt.Errorf("failed to create unbonding transaction data: %w", err) + } + + msg := proto.TrackedTransaction{ + // Setting it to 0, proper number will be filled by `addTransactionInternal` + TrackedTransactionIdx: 0, + StakingTransaction: serializedTx, + StakingOutputIdx: stakingOutputIndex, + StakerAddress: stakerAddress.EncodeAddress(), + StakingTime: uint32(stakingTime), + FinalityProvidersBtcPks: fpPubKeysBytes, + StakingTxBtcConfirmationInfo: nil, + BtcSigType: pop.BtcSigType, + BtcSigOverBbnStakerAddr: pop.BtcSigOverBabylonAddr, + State: proto.TransactionState_SENT_TO_BABYLON, + Watched: false, + UnbondingTxData: update, + } + + return c.addTransactionInternal( + txHashBytes[:], &msg, nil, + ) +} + func (c *TrackedTransactionStore) AddTransaction( btcTx *wire.MsgTx, stakingOutputIndex uint32, @@ -565,19 +671,23 @@ func (c *TrackedTransactionStore) AddTransaction( serializedTx, err := utils.SerializeBtcTransaction(btcTx) if err != nil { - return err + return fmt.Errorf("failed to serialize Bitcoin transaction: %w", err) } if len(fpPubKeys) == 0 { return fmt.Errorf("cannot add transaction without finality providers public keys") } - var fpPubKeysBytes [][]byte = make([][]byte, len(fpPubKeys)) + fpPubKeysBytes := make([][]byte, len(fpPubKeys)) for i, pk := range fpPubKeys { fpPubKeysBytes[i] = schnorr.SerializePubKey(pk) } + if pop == nil { + return fmt.Errorf("cannot add transaction without proof of possession") + } + msg := proto.TrackedTransaction{ // Setting it to 0, proper number will be filled by `addTransactionInternal` TrackedTransactionIdx: 0, @@ -589,13 +699,13 @@ func (c *TrackedTransactionStore) AddTransaction( StakingTxBtcConfirmationInfo: nil, BtcSigType: pop.BtcSigType, BtcSigOverBbnStakerAddr: pop.BtcSigOverBabylonAddr, - State: proto.TransactionState_TRANSACTION_CREATED, + State: proto.TransactionState_SENT_TO_BTC, Watched: false, UnbondingTxData: nil, } return c.addTransactionInternal( - txHashBytes, &msg, nil, + txHashBytes[:], &msg, nil, ) } diff --git a/stakerdb/trackedtransactionstore_test.go b/stakerdb/trackedtransactionstore_test.go index a1b4016..a22544d 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_TRANSACTION_CREATED, storedTx.State) + require.Equal(t, proto.TransactionState_SENT_TO_BTC, storedTx.State) require.Equal(t, uint64(1), storedTx.StoredTransactionIdx) // Confirmed hash := datagen.GenRandomBtcdHash(r) diff --git a/walletcontroller/interface.go b/walletcontroller/interface.go index 8dd4f4c..2a776ba 100644 --- a/walletcontroller/interface.go +++ b/walletcontroller/interface.go @@ -49,7 +49,7 @@ type WalletController interface { SignRawTransaction(tx *wire.MsgTx) (*wire.MsgTx, bool, error) // requires wallet to be unlocked CreateAndSignTx( - output []*wire.TxOut, + outputs []*wire.TxOut, feeRatePerKb btcutil.Amount, changeAddress btcutil.Address, ) (*wire.MsgTx, error)