Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix withdrawable transactions query #78

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

## Unreleased

### Bug fix

* [#78](https://github.com/babylonlabs-io/btc-staker/pull/78) Fix
`withdrawable-transactions` query bug, introduced when adding pre-approval
transactions handling

## v0.8.0

### Improvements
Expand Down
42 changes: 42 additions & 0 deletions itest/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,48 @@ func TestSendingStakingTransactionWithPreApproval(t *testing.T) {
require.NoError(t, err)
tm.waitForStakingTxState(t, txHash, proto.TransactionState_DELEGATION_ACTIVE)

// check that there is not error when qury for withdrawable transactions
withdrawableTransactionsResp, err := tm.StakerClient.WithdrawableTransactions(context.Background(), nil, nil)
require.NoError(t, err)
require.Len(t, withdrawableTransactionsResp.Transactions, 0)

// Unbond pre-approval stake
resp, err := tm.StakerClient.UnbondStaking(context.Background(), txHash.String())
require.NoError(t, err)

unbondingTxHash, err := chainhash.NewHashFromStr(resp.UnbondingTxHash)
require.NoError(t, err)

require.Eventually(t, func() bool {
tx, err := tm.TestRpcClient.GetRawTransaction(unbondingTxHash)
if err != nil {
return false
}

if tx == nil {
return false

}
return true
}, 1*time.Minute, eventuallyPollTime)

block := tm.mineBlock(t)
require.Equal(t, 2, len(block.Transactions))
require.Equal(t, block.Transactions[1].TxHash(), *unbondingTxHash)
go tm.mineNEmptyBlocks(t, staker.UnbondingTxConfirmations, false)
tm.waitForStakingTxState(t, txHash, proto.TransactionState_UNBONDING_CONFIRMED_ON_BTC)

// Spend unbonding tx of pre-approval stake
withdrawableTransactionsResp, err = tm.StakerClient.WithdrawableTransactions(context.Background(), nil, nil)
require.NoError(t, err)
require.Len(t, withdrawableTransactionsResp.Transactions, 1)

// We can spend unbonding tx immediately as in e2e test, finalization time is 4 blocks and we locked it
// finalization time + 1 i.e 5 blocks, but to consider unboning tx as confirmed we need to wait for 6 blocks
// so at this point time lock should already have passed
tm.spendStakingTxWithHash(t, txHash)
go tm.mineNEmptyBlocks(t, staker.SpendStakeTxConfirmations, false)
tm.waitForStakingTxState(t, txHash, proto.TransactionState_SPENT_ON_BTC)
}

func TestMultipleWithdrawableStakingTransactions(t *testing.T) {
Expand Down
28 changes: 25 additions & 3 deletions staker/babylontypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,32 @@ func (app *StakerApp) activateVerifiedDelegation(
"stakingTxHash": stakingTxHash,
}).Debug("Delegation has been activated on the Babylon chain")

utils.PushOrQuit[*delegationActiveOnBabylonEvent](
app.delegationActiveOnBabylonEvChan,
&delegationActiveOnBabylonEvent{
info, status, err := app.wc.TxDetails(stakingTxHash, stakingTransaction.TxOut[stakingOutputIndex].PkScript)

if err != nil {
app.logger.WithFields(logrus.Fields{
"stakingTxHash": stakingTxHash,
"err": err,
}).Error("error getting staking transaction details from btc chain")

// failed to retrieve transaction details from bitcoind node, most probably
// connection error, we will try again in next iteration
continue
}

if status != walletcontroller.TxInChain {
app.logger.WithFields(logrus.Fields{
"stakingTxHash": stakingTxHash,
}).Debug("Staking transaction active on babylon, but not on btc chain. Waiting for btc node to catch up")
continue
}

utils.PushOrQuit[*delegationActivatedPreApprovalEvent](
app.delegationActivatedPreApprovalEvChan,
&delegationActivatedPreApprovalEvent{
stakingTxHash: *stakingTxHash,
blockHash: *info.BlockHash,
blockHeight: info.BlockHeight,
},
app.quit,
)
Expand Down
25 changes: 20 additions & 5 deletions staker/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ type StakingEvent interface {

var _ StakingEvent = (*stakingTxBtcConfirmedEvent)(nil)
var _ StakingEvent = (*delegationSubmittedToBabylonEvent)(nil)
var _ StakingEvent = (*delegationActiveOnBabylonEvent)(nil)
var _ StakingEvent = (*delegationActivatedPostApprovalEvent)(nil)
var _ StakingEvent = (*delegationActivatedPreApprovalEvent)(nil)
var _ StakingEvent = (*unbondingTxSignaturesConfirmedOnBabylonEvent)(nil)
var _ StakingEvent = (*unbondingTxConfirmedOnBtcEvent)(nil)
var _ StakingEvent = (*spendStakeTxConfirmedOnBtcEvent)(nil)
Expand Down Expand Up @@ -121,14 +122,28 @@ func (app *StakerApp) logStakingEventProcessed(event StakingEvent) {
}).Debug("Processed staking event")
}

type delegationActiveOnBabylonEvent struct {
type delegationActivatedPostApprovalEvent struct {
stakingTxHash chainhash.Hash
}

func (event *delegationActiveOnBabylonEvent) EventId() chainhash.Hash {
func (event *delegationActivatedPostApprovalEvent) EventId() chainhash.Hash {
return event.stakingTxHash
}

func (event *delegationActiveOnBabylonEvent) EventDesc() string {
return "DELEGATION_ACTIVE_ON_BABYLON_EVENT"
func (event *delegationActivatedPostApprovalEvent) EventDesc() string {
return "DELEGATION_ACTIVE_ON_BABYLON_POST_APPROVAL_EVENT"
}

type delegationActivatedPreApprovalEvent struct {
stakingTxHash chainhash.Hash
blockHash chainhash.Hash
blockHeight uint32
}

func (event *delegationActivatedPreApprovalEvent) EventId() chainhash.Hash {
return event.stakingTxHash
}

func (event *delegationActivatedPreApprovalEvent) EventDesc() string {
return "DELEGATION_ACTIVE_ON_BABYLON_PRE_APPROVAL_EVENT"
}
32 changes: 22 additions & 10 deletions staker/stakerapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ type StakerApp struct {
stakingRequestedCmdChan chan *stakingRequestCmd
stakingTxBtcConfirmedEvChan chan *stakingTxBtcConfirmedEvent
delegationSubmittedToBabylonEvChan chan *delegationSubmittedToBabylonEvent
delegationActiveOnBabylonEvChan chan *delegationActiveOnBabylonEvent
delegationActivatedPostApprovalEvChan chan *delegationActivatedPostApprovalEvent
delegationActivatedPreApprovalEvChan chan *delegationActivatedPreApprovalEvent
unbondingTxSignaturesConfirmedOnBabylonEvChan chan *unbondingTxSignaturesConfirmedOnBabylonEvent
unbondingTxConfirmedOnBtcEvChan chan *unbondingTxConfirmedOnBtcEvent
spendStakeTxConfirmedOnBtcEvChan chan *spendStakeTxConfirmedOnBtcEvent
Expand Down Expand Up @@ -234,18 +235,17 @@ func NewStakerAppFromDeps(

// event for when delegation is sent to babylon and included in babylon
delegationSubmittedToBabylonEvChan: make(chan *delegationSubmittedToBabylonEvent),
// event for when delegation is active on babylon after being verified
delegationActiveOnBabylonEvChan: make(chan *delegationActiveOnBabylonEvent),
// event for when delegation is active on babylon after going through post approval flow
delegationActivatedPostApprovalEvChan: make(chan *delegationActivatedPostApprovalEvent),
// event for when delegation is active on babylon after going through pre approval flow
delegationActivatedPreApprovalEvChan: make(chan *delegationActivatedPreApprovalEvent),
// 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
// transaction
unbondingTxSignaturesConfirmedOnBabylonEvChan: make(chan *unbondingTxSignaturesConfirmedOnBabylonEvent),

// channel which receives confirmation that unbonding transaction was confirmed on BTC
unbondingTxConfirmedOnBtcEvChan: make(chan *unbondingTxConfirmedOnBtcEvent),

// channel which receives critical errors, critical errors are errors which we do not know
// how to handle, so we just log them. It is up to user to investigate, what had happend
// and report the situation
Expand Down Expand Up @@ -1542,9 +1542,9 @@ func (app *StakerApp) handleStakingEvents() {
app.wg.Add(1)
go func(hash chainhash.Hash) {
defer app.wg.Done()
utils.PushOrQuit[*delegationActiveOnBabylonEvent](
app.delegationActiveOnBabylonEvChan,
&delegationActiveOnBabylonEvent{
utils.PushOrQuit[*delegationActivatedPostApprovalEvent](
app.delegationActivatedPostApprovalEvChan,
&delegationActivatedPostApprovalEvent{
stakingTxHash: hash,
},
app.quit,
Expand Down Expand Up @@ -1587,7 +1587,7 @@ func (app *StakerApp) handleStakingEvents() {
}
app.logStakingEventProcessed(ev)

case ev := <-app.delegationActiveOnBabylonEvChan:
case ev := <-app.delegationActivatedPostApprovalEvChan:
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
Expand All @@ -1597,6 +1597,18 @@ func (app *StakerApp) handleStakingEvents() {
app.m.DelegationsActivatedOnBabylon.Inc()
app.logStakingEventProcessed(ev)

case ev := <-app.delegationActivatedPreApprovalEvChan:
app.logStakingEventReceived(ev)
if err := app.txTracker.SetDelegationActiveOnBabylonAndConfirmedOnBtc(
&ev.stakingTxHash,
&ev.blockHash,
ev.blockHeight,
); err != nil {
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.
Expand Down
4 changes: 2 additions & 2 deletions staker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func createSpendStakeTxFromStoredTx(
// This is to cover cases:
// - staker is unable to sent delegation to babylon
// - staking transaction on babylon fail to get covenant signatures
if storedtx.StakingTxConfirmedOnBtc() {
if storedtx.StakingTxConfirmedOnBtc() && !storedtx.UnbondingTxConfirmedOnBtc() {
stakingInfo, err := staking.BuildStakingInfo(
stakerBtcPk,
storedtx.FinalityProvidersBtcPks,
Expand Down Expand Up @@ -284,7 +284,7 @@ func createSpendStakeTxFromStoredTx(
fundingOutput: storedtx.StakingTx.TxOut[storedtx.StakingOutputIndex],
calculatedFee: *calculatedFee,
}, nil
} else if storedtx.IsUnbonded() {
} else if storedtx.StakingTxConfirmedOnBtc() && storedtx.UnbondingTxConfirmedOnBtc() {
data := storedtx.UnbondingTxData

unbondingInfo, err := staking.BuildUnbondingInfo(
Expand Down
35 changes: 27 additions & 8 deletions stakerdb/trackedtranactionstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,16 @@ type StoredTransaction struct {

// StakingTxConfirmedOnBtc returns true only if staking transaction was sent and confirmed on bitcoin
func (t *StoredTransaction) StakingTxConfirmedOnBtc() bool {
return t.State == proto.TransactionState_SENT_TO_BABYLON ||
t.State == proto.TransactionState_DELEGATION_ACTIVE ||
t.State == proto.TransactionState_CONFIRMED_ON_BTC
return t.StakingTxConfirmationInfo != nil
}

// IsUnbonded returns true only if unbonding transaction was sent and confirmed on bitcoin
func (t *StoredTransaction) IsUnbonded() bool {
return t.State == proto.TransactionState_UNBONDING_CONFIRMED_ON_BTC
// UnbondingTxConfirmedOnBtc returns true only if unbonding transaction was sent and confirmed on bitcoin
func (t *StoredTransaction) UnbondingTxConfirmedOnBtc() bool {
if t.UnbondingTxData == nil {
return false
}

return t.UnbondingTxData.UnbondingTxConfirmationInfo != nil
}

type WatchedTransactionData struct {
Expand Down Expand Up @@ -1020,6 +1022,23 @@ func (c *TrackedTransactionStore) SetDelegationActiveOnBabylon(txHash *chainhash
return c.setTxState(txHash, setTxSpentOnBtc)
}

func (c *TrackedTransactionStore) SetDelegationActiveOnBabylonAndConfirmedOnBtc(
txHash *chainhash.Hash,
blockHash *chainhash.Hash,
blockHeight uint32,
) error {
setDelegationActiveOnBabylon := func(tx *proto.TrackedTransaction) error {
tx.State = proto.TransactionState_DELEGATION_ACTIVE
tx.StakingTxBtcConfirmationInfo = &proto.BTCConfirmationInfo{
BlockHash: blockHash.CloneBytes(),
BlockHeight: blockHeight,
}
return nil
}

return c.setTxState(txHash, setDelegationActiveOnBabylon)
}

func (c *TrackedTransactionStore) SetTxUnbondingSignaturesReceived(
txHash *chainhash.Hash,
covenantSignatures []PubKeySigPair,
Expand Down Expand Up @@ -1222,10 +1241,10 @@ func (c *TrackedTransactionStore) QueryStoredTransactions(q StoredTransactionQue
return false, nil
}

if txFromDb.StakingTxConfirmedOnBtc() {
if txFromDb.StakingTxConfirmedOnBtc() && !txFromDb.UnbondingTxConfirmedOnBtc() {
scriptTimeLock = txFromDb.StakingTime
confirmationHeight = txFromDb.StakingTxConfirmationInfo.Height
} else if txFromDb.IsUnbonded() {
} else if txFromDb.StakingTxConfirmedOnBtc() && txFromDb.UnbondingTxConfirmedOnBtc() {
scriptTimeLock = txFromDb.UnbondingTxData.UnbondingTime
confirmationHeight = txFromDb.UnbondingTxData.UnbondingTxConfirmationInfo.Height
} else {
Expand Down
Loading