From c6e8905f523ee157b023a077b38ad3bce750fef5 Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Tue, 8 Oct 2024 16:00:18 +0200 Subject: [PATCH 1/7] Extend staker to support both flows --- example/global-params.json | 62 ++++++++++---- itest/e2e_test.go | 5 +- proto/transaction.pb.go | 69 ++++++++------- proto/transaction.proto | 13 +-- staker/events.go | 23 +++++ staker/stakerapp.go | 102 ++++++++++++++++++++--- stakerdb/trackedtranactionstore.go | 11 ++- stakerdb/trackedtransactionstore_test.go | 2 +- 8 files changed, 217 insertions(+), 70 deletions(-) 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..3e9df95 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" diff --git a/proto/transaction.pb.go b/proto/transaction.pb.go index f87d579..9ce8c88 100644 --- a/proto/transaction.pb.go +++ b/proto/transaction.pb.go @@ -23,31 +23,34 @@ 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_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: "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: "DELEGATION_ACTIVE", + 5: "UNBONDING_CONFIRMED_ON_BTC", + 6: "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, + "DELEGATION_ACTIVE": 4, + "UNBONDING_CONFIRMED_ON_BTC": 5, + "SPENT_ON_BTC": 6, } ) @@ -492,7 +495,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 +614,22 @@ 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, 0xb0, 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, 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 9961e6b..f033996 100644 --- a/proto/transaction.proto +++ b/proto/transaction.proto @@ -5,12 +5,13 @@ 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; + DELEGATION_ACTIVE = 4; + UNBONDING_CONFIRMED_ON_BTC = 5; + SPENT_ON_BTC = 6; } message WatchedTxData { diff --git a/staker/events.go b/staker/events.go index 5edd353..ef0bc9e 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 @@ -23,6 +28,7 @@ var _ StakingEvent = (*delegationSubmittedToBabylonEvent)(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 +43,7 @@ type stakingRequestedEvent struct { requiredDepthOnBtcChain uint32 pop *cl.BabylonPop watchTxData *watchTxData + usePreApprovalFlow bool errChan chan error successChan chan *chainhash.Hash } @@ -55,6 +62,7 @@ func newOwnedStakingRequest( fpBtcPks []*btcec.PublicKey, confirmationTimeBlocks uint32, pop *cl.BabylonPop, + usePreApprovalFlow bool, ) *stakingRequestedEvent { return &stakingRequestedEvent{ stakerAddress: stakerAddress, @@ -68,6 +76,7 @@ func newOwnedStakingRequest( requiredDepthOnBtcChain: confirmationTimeBlocks, pop: pop, watchTxData: nil, + usePreApprovalFlow: usePreApprovalFlow, errChan: make(chan error, 1), successChan: make(chan *chainhash.Hash, 1), } @@ -236,3 +245,17 @@ 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" +} diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 4300622..8ea0ad4 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -128,6 +128,7 @@ type StakerApp struct { m *metrics.StakerMetrics stakingRequestedEvChan chan *stakingRequestedEvent + sendStakingTxToBTCRequestedEvChan chan *sendStakingTxToBTCRequestedEvent stakingTxBtcConfirmedEvChan chan *stakingTxBtcConfirmedEvent delegationSubmittedToBabylonEvChan chan *delegationSubmittedToBabylonEvent unbondingTxSignaturesConfirmedOnBabylonEvChan chan *unbondingTxSignaturesConfirmedOnBabylonEvent @@ -232,6 +233,9 @@ 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), @@ -1198,9 +1202,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 +1226,23 @@ 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 { + stakingTxHash := ev.stakingTx.TxHash() + err := app.txTracker.AddTransaction( ev.stakingTx, ev.stakingOutputIdx, ev.stakingTime, @@ -1243,20 +1255,87 @@ func (app *StakerApp) handleStakingEvents() { ev.errChan <- err continue } + + if ev.usePreApprovalFlow { + + } 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: 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: @@ -1597,6 +1676,7 @@ func (app *StakerApp) StakeFunds( fpPks, params.ConfirmationTimeBlocks, pop, + false, ) utils.PushOrQuit[*stakingRequestedEvent]( diff --git a/stakerdb/trackedtranactionstore.go b/stakerdb/trackedtranactionstore.go index 2a4435b..0dbd963 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, 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) From c981fb976e3859da328d733492a3f1e722c7127a Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Wed, 9 Oct 2024 07:57:32 +0200 Subject: [PATCH 2/7] Move inclusion proof generation to the caller --- babylonclient/babyloncontroller.go | 49 ++++++++++------ babylonclient/msgsender.go | 7 ++- cmd/stakercli/daemon/daemoncommands.go | 7 ++- cmd/stakercli/helpers/flags.go | 5 +- itest/e2e_test.go | 80 ++++++++++++++++++++------ staker/babylontypes.go | 25 ++++---- staker/stakerapp.go | 52 +++++++++++++---- staker/types.go | 43 ++++++++------ stakerservice/client/rpcclient.go | 2 + stakerservice/service.go | 5 +- 10 files changed, 191 insertions(+), 84 deletions(-) diff --git a/babylonclient/babyloncontroller.go b/babylonclient/babyloncontroller.go index 2eb695e..4d79885 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, 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..4abd42d 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: "Wheter 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/itest/e2e_test.go b/itest/e2e_test.go index 3e9df95..20e3015 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -683,7 +683,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])) @@ -695,6 +699,7 @@ func (tm *TestManager) sendStakingTxBTC(t *testing.T, testStakingData *testStaki testStakingData.StakingAmount, fpBTCPKs, int64(testStakingData.StakingTime), + sendToBabylonFirst, ) require.NoError(t, err) txHash := res.TxHash @@ -735,6 +740,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) @@ -1099,7 +1105,7 @@ func (tm *TestManager) insertCovenantSigForDelegation(t *testing.T, btcDel *btcs } } -func TestStakingFailures(t *testing.T) { +func ATestStakingFailures(t *testing.T) { t.Parallel() numMatureOutputs := uint32(200) ctx, cancel := context.WithCancel(context.Background()) @@ -1123,6 +1129,7 @@ func TestStakingFailures(t *testing.T) { testStakingData.StakingAmount, []string{fpKey, fpKey}, int64(testStakingData.StakingTime), + false, ) require.Error(t, err) @@ -1133,11 +1140,12 @@ func TestStakingFailures(t *testing.T) { testStakingData.StakingAmount, []string{}, int64(testStakingData.StakingTime), + false, ) require.Error(t, err) } -func TestSendingStakingTransaction(t *testing.T) { +func ATestSendingStakingTransaction(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 @@ -1165,7 +1173,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) @@ -1213,7 +1221,41 @@ func TestSendingStakingTransaction(t *testing.T) { require.Equal(t, transactionsResult.Transactions[0].StakingTxHash, txHash.String()) } -func TestMultipleWithdrawableStakingTransactions(t *testing.T) { +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_CONFIRMED_ON_BTC) +} + +func ATestMultipleWithdrawableStakingTransactions(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 @@ -1281,7 +1323,7 @@ func TestMultipleWithdrawableStakingTransactions(t *testing.T) { require.Equal(t, withdrawableTransactionsResp.Transactions[2].TransactionIdx, "4") } -func TestSendingWatchedStakingTransaction(t *testing.T) { +func ATestSendingWatchedStakingTransaction(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 @@ -1305,7 +1347,7 @@ func TestSendingWatchedStakingTransaction(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) } -func TestRestartingTxNotDeepEnough(t *testing.T) { +func ATestRestartingTxNotDeepEnough(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 @@ -1323,7 +1365,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() @@ -1334,7 +1376,7 @@ func TestRestartingTxNotDeepEnough(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) } -func TestRestartingTxNotOnBabylon(t *testing.T) { +func ATestRestartingTxNotOnBabylon(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 @@ -1379,7 +1421,7 @@ func TestRestartingTxNotOnBabylon(t *testing.T) { } } -func TestStakingUnbonding(t *testing.T) { +func ATestStakingUnbonding(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 @@ -1399,7 +1441,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) @@ -1452,7 +1494,7 @@ func TestStakingUnbonding(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_SPENT_ON_BTC) } -func TestUnbondingRestartWaitingForSignatures(t *testing.T) { +func ATestUnbondingRestartWaitingForSignatures(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 @@ -1472,7 +1514,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) @@ -1528,7 +1570,7 @@ func containsOutput(outputs []walletcontroller.Utxo, address string, amount btcu return false } -func TestBitcoindWalletRpcApi(t *testing.T) { +func ATestBitcoindWalletRpcApi(t *testing.T) { t.Parallel() manager, err := containers.NewManager(t) require.NoError(t, err) @@ -1604,7 +1646,7 @@ func TestBitcoindWalletRpcApi(t *testing.T) { require.Equal(t, walletcontroller.TxInChain, status) } -func TestBitcoindWalletBip322Signing(t *testing.T) { +func ATestBitcoindWalletBip322Signing(t *testing.T) { t.Parallel() manager, err := containers.NewManager(t) require.NoError(t, err) @@ -1635,7 +1677,7 @@ func TestBitcoindWalletBip322Signing(t *testing.T) { require.NoError(t, err) } -func TestSendingStakingTransaction_Restaking(t *testing.T) { +func ATestSendingStakingTransaction_Restaking(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 @@ -1664,7 +1706,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) @@ -1677,7 +1719,7 @@ func TestSendingStakingTransaction_Restaking(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_DELEGATION_ACTIVE) } -func TestRecoverAfterRestartDuringWithdrawal(t *testing.T) { +func ATestRecoverAfterRestartDuringWithdrawal(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 @@ -1705,7 +1747,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/staker/babylontypes.go b/staker/babylontypes.go index e63a177..5b30605 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -19,10 +19,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 +37,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 +115,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,7 +137,7 @@ func (app *StakerApp) buildDelegation( stakerAddress btcutil.Address, storedTx *stakerdb.StoredTransaction) (*cl.DelegationData, error) { - stakingTxInclusionProof := app.mustBuildInclusionProof(req) + // stakingTxInclusionProof := app.mustBuildInclusionProof(req) if storedTx.Watched { watchedData, err := app.txTracker.GetWatchedTransactionData(&req.txHash) @@ -158,13 +162,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 +175,6 @@ func (app *StakerApp) buildDelegation( req, stakerAddress, storedTx, - stakingTxInclusionProof, ) } } diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 8ea0ad4..af7767b 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -652,10 +652,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), } @@ -849,13 +857,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") } @@ -1256,8 +1266,17 @@ func (app *StakerApp) handleStakingEvents() { continue } - if ev.usePreApprovalFlow { + app.logger.Info("Recieved staking event", "ususePreApprovalFlowe", ev.usePreApprovalFlow) + if ev.usePreApprovalFlow { + // req := &sendDelegationRequest{ + // txHash: ev.stakingTxHash, + // txIndex: ev.txIndex, + // inclusionBlock: ev.inlusionBlock, + // requiredInclusionBlockDepth: uint64(ev.blockDepth), + // } + + fmt.Println("Pre-approval flow") } else { // old flow, send to BTC first, end expect response to the caller app.wg.Add(1) @@ -1351,10 +1370,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), } @@ -1557,6 +1584,7 @@ func (app *StakerApp) StakeFunds( stakingAmount btcutil.Amount, fpPks []*btcec.PublicKey, stakingTimeBlocks uint16, + sendToBabylonFirst bool, ) (*chainhash.Hash, error) { // check we are not shutting down @@ -1676,7 +1704,7 @@ func (app *StakerApp) StakeFunds( fpPks, params.ConfirmationTimeBlocks, pop, - false, + 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/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"), From f3d29e36df4a45946be595185b7545f883dc8706 Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Wed, 9 Oct 2024 09:41:01 +0200 Subject: [PATCH 3/7] Sending to Babylon in pre-approval flow --- itest/e2e_test.go | 36 ++++++++++++++++++-------- staker/stakerapp.go | 62 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 20e3015..19a58ff 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -707,22 +707,29 @@ func (tm *TestManager) sendStakingTxBTC( 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 } @@ -1252,7 +1259,14 @@ func TestSendingStakingTransactionWithPreApproval(t *testing.T) { txHash := tm.sendStakingTxBTC(t, testStakingData, true) go tm.mineNEmptyBlocks(t, params.ConfirmationTimeBlocks, true) - tm.waitForStakingTxState(t, txHash, proto.TransactionState_CONFIRMED_ON_BTC) + 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_DELEGATION_ACTIVE) } func ATestMultipleWithdrawableStakingTransactions(t *testing.T) { diff --git a/staker/stakerapp.go b/staker/stakerapp.go index af7767b..5f582ad 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -1251,7 +1251,6 @@ func (app *StakerApp) handleStakingEvents() { app.m.ValidReceivedDelegationRequests.Inc() ev.successChan <- &ev.stakingTxHash } else { - stakingTxHash := ev.stakingTx.TxHash() err := app.txTracker.AddTransaction( ev.stakingTx, ev.stakingOutputIdx, @@ -1269,14 +1268,57 @@ func (app *StakerApp) handleStakingEvents() { app.logger.Info("Recieved staking event", "ususePreApprovalFlowe", ev.usePreApprovalFlow) if ev.usePreApprovalFlow { - // req := &sendDelegationRequest{ - // txHash: ev.stakingTxHash, - // txIndex: ev.txIndex, - // inclusionBlock: ev.inlusionBlock, - // requiredInclusionBlockDepth: uint64(ev.blockDepth), - // } - - fmt.Println("Pre-approval flow") + 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) @@ -1285,7 +1327,7 @@ func (app *StakerApp) handleStakingEvents() { utils.PushOrQuit( app.sendStakingTxToBTCRequestedEvChan, &sendStakingTxToBTCRequestedEvent{ - stakingTxHash: stakingTxHash, + stakingTxHash: ev.stakingTxHash, requiredDepthOnBtcChain: ev.requiredDepthOnBtcChain, responseExpected: &responseExpectedChan{ errChan: ev.errChan, From 1f4e40cd0a88afab451fdcf4e49f43dc76031f3c Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Wed, 9 Oct 2024 11:27:06 +0200 Subject: [PATCH 4/7] Add new state and new loop --- itest/e2e_test.go | 4 +- proto/transaction.pb.go | 42 ++++---- proto/transaction.proto | 7 +- staker/babylontypes.go | 157 +++++++++++++++++++++++++++++ staker/events.go | 14 +++ staker/stakerapp.go | 29 ++++++ stakerdb/trackedtranactionstore.go | 16 ++- 7 files changed, 244 insertions(+), 25 deletions(-) diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 19a58ff..ba2ad95 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -1152,7 +1152,7 @@ func ATestStakingFailures(t *testing.T) { require.Error(t, err) } -func ATestSendingStakingTransaction(t *testing.T) { +func TestSendingStakingTransaction(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 @@ -1266,7 +1266,7 @@ func TestSendingStakingTransactionWithPreApproval(t *testing.T) { require.Len(t, pend, 1) // need to activate delegation to unbond tm.insertCovenantSigForDelegation(t, pend[0]) - tm.waitForStakingTxState(t, txHash, proto.TransactionState_DELEGATION_ACTIVE) + tm.waitForStakingTxState(t, txHash, proto.TransactionState_VERIFIED) } func ATestMultipleWithdrawableStakingTransactions(t *testing.T) { diff --git a/proto/transaction.pb.go b/proto/transaction.pb.go index 9ce8c88..97b9eff 100644 --- a/proto/transaction.pb.go +++ b/proto/transaction.pb.go @@ -27,9 +27,10 @@ const ( TransactionState_SENT_TO_BTC TransactionState = 1 TransactionState_CONFIRMED_ON_BTC TransactionState = 2 TransactionState_SENT_TO_BABYLON TransactionState = 3 - TransactionState_DELEGATION_ACTIVE TransactionState = 4 - TransactionState_UNBONDING_CONFIRMED_ON_BTC TransactionState = 5 - TransactionState_SPENT_ON_BTC TransactionState = 6 + 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. @@ -39,18 +40,20 @@ var ( 1: "SENT_TO_BTC", 2: "CONFIRMED_ON_BTC", 3: "SENT_TO_BABYLON", - 4: "DELEGATION_ACTIVE", - 5: "UNBONDING_CONFIRMED_ON_BTC", - 6: "SPENT_ON_BTC", + 4: "VERIFIED", + 5: "DELEGATION_ACTIVE", + 6: "UNBONDING_CONFIRMED_ON_BTC", + 7: "SPENT_ON_BTC", } TransactionState_value = map[string]int32{ "TRANSACTION_CREATED": 0, "SENT_TO_BTC": 1, "CONFIRMED_ON_BTC": 2, "SENT_TO_BABYLON": 3, - "DELEGATION_ACTIVE": 4, - "UNBONDING_CONFIRMED_ON_BTC": 5, - "SPENT_ON_BTC": 6, + "VERIFIED": 4, + "DELEGATION_ACTIVE": 5, + "UNBONDING_CONFIRMED_ON_BTC": 6, + "SPENT_ON_BTC": 7, } ) @@ -614,22 +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, 0xb0, 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, 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, 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, + 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 f033996..1be0acd 100644 --- a/proto/transaction.proto +++ b/proto/transaction.proto @@ -9,9 +9,10 @@ enum TransactionState { SENT_TO_BTC = 1; CONFIRMED_ON_BTC = 2; SENT_TO_BABYLON = 3; - DELEGATION_ACTIVE = 4; - UNBONDING_CONFIRMED_ON_BTC = 5; - SPENT_ON_BTC = 6; + 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 5b30605..a098469 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" @@ -242,6 +243,7 @@ func (app *StakerApp) checkForUnbondingTxSignaturesOnBabylon(stakingTxHash *chai req := &unbondingTxSignaturesConfirmedOnBabylonEvent{ stakingTxHash: *stakingTxHash, + delegationActive: di.Active, covenantUnbondingSignatures: di.UndelegationInfo.CovenantUnbondingSignatures, } @@ -279,3 +281,158 @@ 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( + stakerAddress btcutil.Address, + stakingTransaction *wire.MsgTx, + stakingOutputIndex uint32, + stakingTxHash *chainhash.Hash) { + // TODO configure ticker interval + checkSigTicker := time.NewTicker(5 * time.Second) + 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 + } + + // check if check is active + // this loop assume there is at least one active vigiliante to activate delegation + if di.Active { + utils.PushOrQuit[*delegationActiveOnBabylonEvent]( + app.delegationActiveOnBabylonEvChan, + &delegationActiveOnBabylonEvent{ + stakingTxHash: *stakingTxHash, + }, + app.quit, + ) + return + } + + // 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{ + "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 { + 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 + } + } + } + + // 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 { + 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 + } + } + + // 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 ef0bc9e..81a3cbd 100644 --- a/staker/events.go +++ b/staker/events.go @@ -25,6 +25,7 @@ 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) @@ -181,6 +182,7 @@ func (event *delegationSubmittedToBabylonEvent) EventDesc() string { type unbondingTxSignaturesConfirmedOnBabylonEvent struct { stakingTxHash chainhash.Hash + delegationActive bool covenantUnbondingSignatures []cl.CovenantSignatureInfo } @@ -259,3 +261,15 @@ func (event *sendStakingTxToBTCRequestedEvent) EventId() chainhash.Hash { 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 5f582ad..429fd81 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -131,6 +131,7 @@ type StakerApp struct { sendStakingTxToBTCRequestedEvChan chan *sendStakingTxToBTCRequestedEvent stakingTxBtcConfirmedEvChan chan *stakingTxBtcConfirmedEvent delegationSubmittedToBabylonEvChan chan *delegationSubmittedToBabylonEvent + delegationActiveOnBabylonEvChan chan *delegationActiveOnBabylonEvent unbondingTxSignaturesConfirmedOnBabylonEvChan chan *unbondingTxSignaturesConfirmedOnBabylonEvent unbondingTxConfirmedOnBtcEvChan chan *unbondingTxConfirmedOnBtcEvent spendStakeTxConfirmedOnBtcEvChan chan *spendStakeTxConfirmedOnBtcEvent @@ -403,6 +404,7 @@ func (app *StakerApp) waitForStakingTransactionConfirmation( return err } + app.wg.Add(1) go app.waitForStakingTxConfirmation(*stakingTxHash, requiredBlockDepth, confEvent) return nil } @@ -780,10 +782,13 @@ func (app *StakerApp) checkTransactionsStatus() error { 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: @@ -1458,12 +1463,27 @@ 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, stakerAddress := 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( + stakerAddress, + storedTx.StakingTx, + storedTx.StakingOutputIndex, + &ev.stakingTxHash, + ) + } + app.m.DelegationsActivatedOnBabylon.Inc() app.logStakingEventProcessed(ev) @@ -1489,6 +1509,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. diff --git a/stakerdb/trackedtranactionstore.go b/stakerdb/trackedtranactionstore.go index 0dbd963..8ca56a1 100644 --- a/stakerdb/trackedtranactionstore.go +++ b/stakerdb/trackedtranactionstore.go @@ -794,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 { @@ -807,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 } From d1c31172e307c605c163fb2c7d0a0ae82acd1283 Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Wed, 9 Oct 2024 12:14:09 +0200 Subject: [PATCH 5/7] Whole pre-approval flow closed --- babylonclient/babyloncontroller.go | 20 +++++++++++++++++++ itest/e2e_test.go | 28 +++++++++++++++++++++++++- staker/babylontypes.go | 32 ++++++++++++++++-------------- staker/stakerapp.go | 5 +++-- walletcontroller/client.go | 1 + 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/babylonclient/babyloncontroller.go b/babylonclient/babyloncontroller.go index 4d79885..69414e5 100644 --- a/babylonclient/babyloncontroller.go +++ b/babylonclient/babyloncontroller.go @@ -908,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/itest/e2e_test.go b/itest/e2e_test.go index ba2ad95..d60443e 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -1152,7 +1152,7 @@ func ATestStakingFailures(t *testing.T) { require.Error(t, err) } -func TestSendingStakingTransaction(t *testing.T) { +func ATestSendingStakingTransaction(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 @@ -1267,6 +1267,32 @@ func TestSendingStakingTransactionWithPreApproval(t *testing.T) { // 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 ATestMultipleWithdrawableStakingTransactions(t *testing.T) { diff --git a/staker/babylontypes.go b/staker/babylontypes.go index a098469..897a78f 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -309,7 +309,7 @@ func (app *StakerApp) activateVerifiedDelegation( stakingOutputIndex uint32, stakingTxHash *chainhash.Hash) { // TODO configure ticker interval - checkSigTicker := time.NewTicker(5 * time.Second) + checkSigTicker := time.NewTicker(2 * time.Second) defer checkSigTicker.Stop() defer app.wg.Done() @@ -340,6 +340,10 @@ func (app *StakerApp) activateVerifiedDelegation( // 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{ @@ -363,6 +367,7 @@ func (app *StakerApp) activateVerifiedDelegation( 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 @@ -388,14 +393,13 @@ func (app *StakerApp) activateVerifiedDelegation( _, err := app.wc.SendRawTransaction(stakingTransaction, true) if err != nil { - 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 - } + app.logger.WithFields(logrus.Fields{ + "err": err, + "stakingTxHash": stakingTxHash, + }).Error("failed to send staking transaction to btc chain to activate verified delegation") } + + continue } // staking transaction is not signed, we must sign it before sending to btc chain @@ -419,13 +423,11 @@ func (app *StakerApp) activateVerifiedDelegation( _, err = app.wc.SendRawTransaction(signedTx, true) if err != nil { - 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 - } + app.logger.WithFields(logrus.Fields{ + "err": err, + "stakingTxHash": stakingTxHash, + }).Error("failed to send staking transaction to btc chain to activate verified delegation") + continue } // at this point we send signed staking transaciton to BTC chain, we will diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 429fd81..bc508d3 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -242,8 +242,9 @@ func NewStakerAppFromDeps( // 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 diff --git a/walletcontroller/client.go b/walletcontroller/client.go index 65770f0..8787dfa 100644 --- a/walletcontroller/client.go +++ b/walletcontroller/client.go @@ -211,6 +211,7 @@ func (w *RpcWalletController) SignRawTransaction(tx *wire.MsgTx) (*wire.MsgTx, b } func (w *RpcWalletController) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { + fmt.Println("Sending raw transaction") return w.Client.SendRawTransaction(tx, allowHighFees) } From 38e55290059c6da8f0f5820f33d84593fe44ebcf Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Wed, 9 Oct 2024 13:43:29 +0200 Subject: [PATCH 6/7] A bit of cleanup --- itest/e2e_test.go | 25 +++++------ staker/babylontypes.go | 36 ++++++++++++++-- staker/stakerapp.go | 88 ++++++++++++++++++++++++++++++++++++++ stakercfg/config.go | 2 + walletcontroller/client.go | 1 - 5 files changed, 135 insertions(+), 17 deletions(-) diff --git a/itest/e2e_test.go b/itest/e2e_test.go index d60443e..fa4c200 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -115,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 @@ -1112,7 +1113,7 @@ func (tm *TestManager) insertCovenantSigForDelegation(t *testing.T, btcDel *btcs } } -func ATestStakingFailures(t *testing.T) { +func TestStakingFailures(t *testing.T) { t.Parallel() numMatureOutputs := uint32(200) ctx, cancel := context.WithCancel(context.Background()) @@ -1152,7 +1153,7 @@ func ATestStakingFailures(t *testing.T) { require.Error(t, err) } -func ATestSendingStakingTransaction(t *testing.T) { +func TestSendingStakingTransaction(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 @@ -1295,7 +1296,7 @@ func TestSendingStakingTransactionWithPreApproval(t *testing.T) { } -func ATestMultipleWithdrawableStakingTransactions(t *testing.T) { +func TestMultipleWithdrawableStakingTransactions(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 @@ -1363,7 +1364,7 @@ func ATestMultipleWithdrawableStakingTransactions(t *testing.T) { require.Equal(t, withdrawableTransactionsResp.Transactions[2].TransactionIdx, "4") } -func ATestSendingWatchedStakingTransaction(t *testing.T) { +func TestSendingWatchedStakingTransaction(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 @@ -1387,7 +1388,7 @@ func ATestSendingWatchedStakingTransaction(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) } -func ATestRestartingTxNotDeepEnough(t *testing.T) { +func TestRestartingTxNotDeepEnough(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 @@ -1416,7 +1417,7 @@ func ATestRestartingTxNotDeepEnough(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_SENT_TO_BABYLON) } -func ATestRestartingTxNotOnBabylon(t *testing.T) { +func TestRestartingTxNotOnBabylon(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 @@ -1461,7 +1462,7 @@ func ATestRestartingTxNotOnBabylon(t *testing.T) { } } -func ATestStakingUnbonding(t *testing.T) { +func TestStakingUnbonding(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 @@ -1534,7 +1535,7 @@ func ATestStakingUnbonding(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_SPENT_ON_BTC) } -func ATestUnbondingRestartWaitingForSignatures(t *testing.T) { +func AestUnbondingRestartWaitingForSignatures(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 @@ -1610,7 +1611,7 @@ func containsOutput(outputs []walletcontroller.Utxo, address string, amount btcu return false } -func ATestBitcoindWalletRpcApi(t *testing.T) { +func TestBitcoindWalletRpcApi(t *testing.T) { t.Parallel() manager, err := containers.NewManager(t) require.NoError(t, err) @@ -1686,7 +1687,7 @@ func ATestBitcoindWalletRpcApi(t *testing.T) { require.Equal(t, walletcontroller.TxInChain, status) } -func ATestBitcoindWalletBip322Signing(t *testing.T) { +func TestBitcoindWalletBip322Signing(t *testing.T) { t.Parallel() manager, err := containers.NewManager(t) require.NoError(t, err) @@ -1717,7 +1718,7 @@ func ATestBitcoindWalletBip322Signing(t *testing.T) { require.NoError(t, err) } -func ATestSendingStakingTransaction_Restaking(t *testing.T) { +func TestSendingStakingTransaction_Restaking(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 @@ -1759,7 +1760,7 @@ func ATestSendingStakingTransaction_Restaking(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_DELEGATION_ACTIVE) } -func ATestRecoverAfterRestartDuringWithdrawal(t *testing.T) { +func TestRecoverAfterRestartDuringWithdrawal(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 diff --git a/staker/babylontypes.go b/staker/babylontypes.go index 897a78f..f2fff8c 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -308,8 +308,7 @@ func (app *StakerApp) activateVerifiedDelegation( stakingTransaction *wire.MsgTx, stakingOutputIndex uint32, stakingTxHash *chainhash.Hash) { - // TODO configure ticker interval - checkSigTicker := time.NewTicker(2 * time.Second) + checkSigTicker := time.NewTicker(app.config.StakerConfig.CheckActiveInterval) defer checkSigTicker.Stop() defer app.wg.Done() @@ -337,6 +336,18 @@ func (app *StakerApp) activateVerifiedDelegation( 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 { @@ -354,6 +365,15 @@ func (app *StakerApp) activateVerifiedDelegation( 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) @@ -402,6 +422,16 @@ func (app *StakerApp) activateVerifiedDelegation( 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) @@ -427,9 +457,7 @@ func (app *StakerApp) activateVerifiedDelegation( "err": err, "stakingTxHash": stakingTxHash, }).Error("failed to send staking transaction to btc chain to activate verified delegation") - continue } - // at this point we send signed staking transaciton to BTC chain, we will // still wait for its activation diff --git a/staker/stakerapp.go b/staker/stakerapp.go index bc508d3..fc244d2 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -526,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` @@ -545,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 @@ -562,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, @@ -586,6 +596,72 @@ 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( + stakerAddress, + 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) @@ -780,6 +856,18 @@ func (app *StakerApp) checkTransactionsStatus() error { } } + for _, txHash := range transactionsVerifiedOnBabylon { + txHashCopy := *txHash + storedTx, address := app.mustGetTransactionAndStakerAddress(&txHashCopy) + app.wg.Add(1) + go app.activateVerifiedDelegation( + address, + storedTx.StakingTx, + storedTx.StakingOutputIndex, + &txHashCopy, + ) + } + return nil } 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/walletcontroller/client.go b/walletcontroller/client.go index 8787dfa..65770f0 100644 --- a/walletcontroller/client.go +++ b/walletcontroller/client.go @@ -211,7 +211,6 @@ func (w *RpcWalletController) SignRawTransaction(tx *wire.MsgTx) (*wire.MsgTx, b } func (w *RpcWalletController) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { - fmt.Println("Sending raw transaction") return w.Client.SendRawTransaction(tx, allowHighFees) } From e46ef6a2a5243d9dc93650878250476c4f54f91c Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Wed, 9 Oct 2024 14:42:39 +0200 Subject: [PATCH 7/7] Pr comments --- cmd/stakercli/daemon/daemoncommands.go | 2 +- itest/e2e_test.go | 2 +- staker/babylontypes.go | 3 --- staker/stakerapp.go | 7 ++----- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/cmd/stakercli/daemon/daemoncommands.go b/cmd/stakercli/daemon/daemoncommands.go index 4abd42d..0d20d97 100644 --- a/cmd/stakercli/daemon/daemoncommands.go +++ b/cmd/stakercli/daemon/daemoncommands.go @@ -128,7 +128,7 @@ var stakeCmd = cli.Command{ }, cli.BoolFlag{ Name: helpers.SendToBabylonFirstFlag, - Usage: "Wheter staking transaction should be first to Babylon or BTC", + Usage: "Whether staking transaction should be first to Babylon or BTC", }, }, Action: stake, diff --git a/itest/e2e_test.go b/itest/e2e_test.go index fa4c200..ebc8c14 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -1535,7 +1535,7 @@ func TestStakingUnbonding(t *testing.T) { tm.waitForStakingTxState(t, txHash, proto.TransactionState_SPENT_ON_BTC) } -func AestUnbondingRestartWaitingForSignatures(t *testing.T) { +func TestUnbondingRestartWaitingForSignatures(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 diff --git a/staker/babylontypes.go b/staker/babylontypes.go index f2fff8c..5bebdcd 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -138,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) @@ -304,7 +302,6 @@ func isTransacionFullySigned(tx *wire.MsgTx) (bool, error) { // - delegation is on babylon // - delegation has received enough covenant signatures func (app *StakerApp) activateVerifiedDelegation( - stakerAddress btcutil.Address, stakingTransaction *wire.MsgTx, stakingOutputIndex uint32, stakingTxHash *chainhash.Hash) { diff --git a/staker/stakerapp.go b/staker/stakerapp.go index fc244d2..ff6dcbd 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -622,7 +622,6 @@ func (app *StakerApp) checkTransactionsStatus() error { if alreadyDelegated { app.wg.Add(1) app.activateVerifiedDelegation( - stakerAddress, tx.StakingTx, tx.StakingOutputIndex, txHashCopy, @@ -858,10 +857,9 @@ func (app *StakerApp) checkTransactionsStatus() error { for _, txHash := range transactionsVerifiedOnBabylon { txHashCopy := *txHash - storedTx, address := app.mustGetTransactionAndStakerAddress(&txHashCopy) + storedTx, _ := app.mustGetTransactionAndStakerAddress(&txHashCopy) app.wg.Add(1) go app.activateVerifiedDelegation( - address, storedTx.StakingTx, storedTx.StakingOutputIndex, &txHashCopy, @@ -1560,13 +1558,12 @@ func (app *StakerApp) handleStakingEvents() { } if !ev.delegationActive { - storedTx, stakerAddress := app.mustGetTransactionAndStakerAddress(&ev.stakingTxHash) + 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( - stakerAddress, storedTx.StakingTx, storedTx.StakingOutputIndex, &ev.stakingTxHash,