diff --git a/btcstaking-tracker/btcslasher/bootstrapping.go b/btcstaking-tracker/btcslasher/bootstrapping.go index 119c4b20..4c522c49 100644 --- a/btcstaking-tracker/btcslasher/bootstrapping.go +++ b/btcstaking-tracker/btcslasher/bootstrapping.go @@ -2,6 +2,7 @@ package btcslasher import ( "fmt" + "github.com/babylonlabs-io/babylon/types" ftypes "github.com/babylonlabs-io/babylon/x/finality/types" "github.com/cosmos/cosmos-sdk/types/query" @@ -22,16 +23,29 @@ func (bs *BTCSlasher) Bootstrap(startHeight uint64) error { // handle all evidences since the given start height, i.e., for each evidence, // extract its SK and try to slash all BTC delegations under it - err := bs.handleAllEvidences(startHeight, func(evidences []*ftypes.Evidence) error { + err := bs.handleAllEvidences(startHeight, func(evidences []*ftypes.EvidenceResponse) error { var accumulatedErrs error // we use this variable to accumulate errors for _, evidence := range evidences { - fpBTCPK := evidence.FpBtcPk - fpBTCPKHex := fpBTCPK.MarshalHex() + fpBTCPKHex := evidence.FpBtcPkHex bs.logger.Infof("found evidence for finality provider %s at height %d after start height %d", fpBTCPKHex, evidence.BlockHeight, startHeight) + btcPK, err := types.NewBIP340PubKeyFromHex(fpBTCPKHex) + if err != nil { + return fmt.Errorf("err parsing fp btc %w", err) + } + + e := ftypes.Evidence{ + FpBtcPk: btcPK, + BlockHeight: evidence.BlockHeight, + PubRand: evidence.PubRand, + CanonicalAppHash: evidence.CanonicalAppHash, + ForkAppHash: evidence.ForkAppHash, + CanonicalFinalitySig: evidence.CanonicalFinalitySig, + ForkFinalitySig: evidence.ForkFinalitySig, + } // extract the SK of the slashed finality provider - fpBTCSK, err := evidence.ExtractBTCSK() + fpBTCSK, err := e.ExtractBTCSK() if err != nil { bs.logger.Errorf("failed to extract BTC SK of the slashed finality provider %s: %v", fpBTCPKHex, err) accumulatedErrs = multierror.Append(accumulatedErrs, err) @@ -57,7 +71,7 @@ func (bs *BTCSlasher) Bootstrap(startHeight uint64) error { return nil } -func (bs *BTCSlasher) handleAllEvidences(startHeight uint64, handleFunc func(evidences []*ftypes.Evidence) error) error { +func (bs *BTCSlasher) handleAllEvidences(startHeight uint64, handleFunc func(evidences []*ftypes.EvidenceResponse) error) error { pagination := query.PageRequest{Limit: defaultPaginationLimit} for { resp, err := bs.BBNQuerier.ListEvidences(startHeight, &pagination) diff --git a/btcstaking-tracker/btcslasher/slasher_utils.go b/btcstaking-tracker/btcslasher/slasher_utils.go index b4a5959b..294ca4db 100644 --- a/btcstaking-tracker/btcslasher/slasher_utils.go +++ b/btcstaking-tracker/btcslasher/slasher_utils.go @@ -424,7 +424,7 @@ func (bs *BTCSlasher) getAllActiveAndUnbondedBTCDelegations( } if strings.EqualFold(del.StatusDesc, bstypes.BTCDelegationStatus_UNBONDED.String()) && len(del.UndelegationResponse.CovenantSlashingSigs) >= int(bsParams.CovenantQuorum) && - len(del.UndelegationResponse.DelegatorUnbondingSigHex) > 0 { + len(del.UndelegationResponse.DelegatorUnbondingInfoResponse.SpendStakeTxHex) > 0 { // NOTE: Babylon considers a BTC delegation to be unbonded once it // receives staker signature for unbonding transaction, no matter // whether the unbonding tx's timelock has expired. In monitor's view we need to try to slash every diff --git a/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go b/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go index d38ea704..2daedc39 100644 --- a/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go +++ b/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go @@ -10,8 +10,8 @@ import ( types "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" types0 "github.com/babylonlabs-io/babylon/x/btcstaking/types" - schnorr "github.com/btcsuite/btcd/btcec/v2/schnorr" chainhash "github.com/btcsuite/btcd/chaincfg/chainhash" + wire "github.com/btcsuite/btcd/wire" gomock "github.com/golang/mock/gomock" ) @@ -113,15 +113,15 @@ func (mr *MockBabylonNodeAdapterMockRecorder) IsDelegationVerified(stakingTxHash } // ReportUnbonding mocks base method. -func (m *MockBabylonNodeAdapter) ReportUnbonding(ctx context.Context, stakingTxHash chainhash.Hash, stakerUnbondingSig *schnorr.Signature) error { +func (m *MockBabylonNodeAdapter) ReportUnbonding(ctx context.Context, stakingTxHash chainhash.Hash, stakeSpendingTx *wire.MsgTx, inclusionProof *types0.InclusionProof) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReportUnbonding", ctx, stakingTxHash, stakerUnbondingSig) + ret := m.ctrl.Call(m, "ReportUnbonding", ctx, stakingTxHash, stakeSpendingTx, inclusionProof) ret0, _ := ret[0].(error) return ret0 } // ReportUnbonding indicates an expected call of ReportUnbonding. -func (mr *MockBabylonNodeAdapterMockRecorder) ReportUnbonding(ctx, stakingTxHash, stakerUnbondingSig interface{}) *gomock.Call { +func (mr *MockBabylonNodeAdapterMockRecorder) ReportUnbonding(ctx, stakingTxHash, stakeSpendingTx, inclusionProof interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportUnbonding", reflect.TypeOf((*MockBabylonNodeAdapter)(nil).ReportUnbonding), ctx, stakingTxHash, stakerUnbondingSig) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportUnbonding", reflect.TypeOf((*MockBabylonNodeAdapter)(nil).ReportUnbonding), ctx, stakingTxHash, stakeSpendingTx, inclusionProof) } diff --git a/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go b/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go index 18d48c30..dc9d5a31 100644 --- a/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go +++ b/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go @@ -443,8 +443,14 @@ func (sew *StakingEventWatcher) watchForSpend(spendEvent *notifier.SpendEvent, t sew.metrics.DetectedUnbondingTransactionsCounter.Inc() // We found valid unbonding tx. We need to try to report it to babylon. // We stop reporting if delegation is no longer active or we succeed. + proof, err := sew.waitForStakeSpendInclusionProof(quitCtx, spendingTx) + if err != nil { + // todo(lazar): log and report a metric, push to removalChan + sew.logger.Errorf("unbonding tx %s for staking tx %s proof not built", spendingTxHash, delegationId) + return + } sew.logger.Debugf("found unbonding tx %s for staking tx %s", spendingTxHash, delegationId) - sew.reportUnbondingToBabylon(quitCtx, delegationId, spendingTx, nil) + sew.reportUnbondingToBabylon(quitCtx, delegationId, spendingTx, proof) sew.logger.Debugf("unbonding tx %s for staking tx %s reported to babylon", spendingTxHash, delegationId) } @@ -457,7 +463,10 @@ func (sew *StakingEventWatcher) watchForSpend(spendEvent *notifier.SpendEvent, t func (sew *StakingEventWatcher) buildSpendingTxProof(spendingTx *wire.MsgTx) (*btcstakingtypes.InclusionProof, error) { txHash := spendingTx.TxHash() - details, status, err := sew.btcClient.TxDetails(&txHash, spendingTx.TxOut[0].PkScript) // todo(lazar):find out which index + if len(spendingTx.TxOut) == 0 { + return nil, fmt.Errorf("stake spending tx has no outputs") + } + details, status, err := sew.btcClient.TxDetails(&txHash, spendingTx.TxOut[0].PkScript) if err != nil { return nil, err } @@ -477,6 +486,40 @@ func (sew *StakingEventWatcher) buildSpendingTxProof(spendingTx *wire.MsgTx) (*b return btcstakingtypes.NewInclusionProofFromSpvProof(proof), nil } +// waitForStakeSpendInclusionProof polls btc until stake spend tx has inclusion proof built +func (sew *StakingEventWatcher) waitForStakeSpendInclusionProof( + ctx context.Context, + spendingTx *wire.MsgTx, +) (*btcstakingtypes.InclusionProof, error) { + var ( + proof *btcstakingtypes.InclusionProof + err error + ) + _ = retry.Do(func() error { + proof, err = sew.buildSpendingTxProof(spendingTx) + if err != nil { + return err + } + + if proof == nil { + return fmt.Errorf("proof not yet built") + } + + return nil + }, + retry.Context(ctx), + retryForever, + fixedDelyTypeWithJitter, + retry.MaxDelay(sew.cfg.CheckDelegationActiveInterval), + retry.MaxJitter(sew.cfg.RetryJitter), + retry.OnRetry(func(n uint, err error) { + sew.logger.Debugf("retrying checking if stake spending tx is in chain %s. Attempt: %d. Err: %v", spendingTx.TxHash(), n, err) + }), + ) + + return proof, nil +} + func (sew *StakingEventWatcher) handleUnbondedDelegations() { defer sew.wg.Done() for { diff --git a/e2etest/container/config.go b/e2etest/container/config.go index f71c3840..03b1f3ad 100644 --- a/e2etest/container/config.go +++ b/e2etest/container/config.go @@ -26,6 +26,7 @@ const ( func NewImageConfig(t *testing.T) ImageConfig { babylondVersion, err := testutil.GetBabylonVersion() require.NoError(t, err) + babylondVersion = "6a0ccacc41435a249316a77fe6e1e06aeb654d13" // todo(lazar): remove this when we have a tag return ImageConfig{ BitcoindRepository: dockerBitcoindRepository, diff --git a/e2etest/test_manager_btcstaking.go b/e2etest/test_manager_btcstaking.go index 818d4e2f..38f218b4 100644 --- a/e2etest/test_manager_btcstaking.go +++ b/e2etest/test_manager_btcstaking.go @@ -491,10 +491,15 @@ func (tm *TestManager) Undelegate( ) require.NoError(t, err) + var unbondingTxBuf bytes.Buffer + err = unbondingSlashingInfo.UnbondingTx.Serialize(&unbondingTxBuf) + require.NoError(t, err) + msgUndel := &bstypes.MsgBTCUndelegate{ - Signer: signerAddr, - StakingTxHash: stakingSlashingInfo.StakingTx.TxHash().String(), - UnbondingTxSig: bbn.NewBIP340SignatureFromBTCSig(unbondingTxSchnorrSig), + Signer: signerAddr, + StakingTxHash: stakingSlashingInfo.StakingTx.TxHash().String(), + StakeSpendingTx: unbondingTxBuf.Bytes(), + StakeSpendingTxInclusionProof: nil, } _, err = tm.BabylonClient.ReliablySendMsg(context.Background(), msgUndel, nil, nil) require.NoError(t, err) diff --git a/e2etest/unbondingwatcher_e2e_test.go b/e2etest/unbondingwatcher_e2e_test.go index f87490af..4308e890 100644 --- a/e2etest/unbondingwatcher_e2e_test.go +++ b/e2etest/unbondingwatcher_e2e_test.go @@ -103,6 +103,8 @@ func TestUnbondingWatcher(t *testing.T) { minedBlock := tm.mineBlock(t) require.Equal(t, 2, len(minedBlock.Transactions)) + tm.CatchUpBTCLightClient(t) + require.Eventually(t, func() bool { resp, err := tm.BabylonClient.BTCDelegation(stakingSlashingInfo.StakingTx.TxHash().String()) require.NoError(t, err)