Skip to content

Commit 6a4b91f

Browse files
Merge pull request #44 from babylonchain/critical-errors-handling
Handle error of not getting covenant quorum
2 parents d3921ef + 38f6ce8 commit 6a4b91f

File tree

8 files changed

+195
-39
lines changed

8 files changed

+195
-39
lines changed

internal/db/dbclient.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ func (db *Database) FindFailedUnbodningDocuments(ctx context.Context) ([]model.U
9797
return db.findUnbondingDocumentsWithState(ctx, model.Failed)
9898
}
9999

100+
func (db *Database) FindUnbondingDocumentsWithNoCovenantQuorum(ctx context.Context) ([]model.UnbondingDocument, error) {
101+
return db.findUnbondingDocumentsWithState(ctx, model.FailedToGetCovenantSignatures)
102+
}
103+
100104
func (db *Database) FindSendUnbondingDocuments(ctx context.Context) ([]model.UnbondingDocument, error) {
101105
return db.findUnbondingDocumentsWithState(ctx, model.Send)
102106
}
@@ -129,3 +133,9 @@ func (db *Database) SetUnbondingDocumentInputAlreadySpent(
129133
unbondingTxHashHex string) error {
130134
return db.updateUnbondingDocumentState(ctx, unbondingTxHashHex, model.InputAlreadySpent)
131135
}
136+
137+
func (db *Database) SetUnbondingDocumentFailedToGetCovenantSignatures(
138+
ctx context.Context,
139+
unbondingTxHashHex string) error {
140+
return db.updateUnbondingDocumentState(ctx, unbondingTxHashHex, model.FailedToGetCovenantSignatures)
141+
}

internal/db/model/unbonding.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ const (
99
type UnbondingState string
1010

1111
const (
12-
Inserted UnbondingState = "INSERTED"
13-
Send UnbondingState = "SEND"
14-
// TODO: This is not used now, but it will be necessary to cover the case when
15-
// we try to send unbonding transaction but someone already withdrew the staking
16-
// output
17-
InputAlreadySpent UnbondingState = "INPUT_ALREADY_SPENT"
18-
Failed UnbondingState = "FAILED"
12+
Inserted UnbondingState = "INSERTED"
13+
Send UnbondingState = "SEND"
14+
InputAlreadySpent UnbondingState = "INPUT_ALREADY_SPENT"
15+
Failed UnbondingState = "FAILED"
16+
FailedToGetCovenantSignatures UnbondingState = "FAILED_TO_GET_COVENANT_SIGNATURES"
1917
)
2018

2119
type UnbondingDocument struct {

internal/services/expected_interfaces.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,13 @@ type UnbondingStore interface {
125125

126126
GetFailedUnbondingTransactions(ctx context.Context) ([]*UnbondingTxData, error)
127127

128+
GetUnbondingTransactionsWithNoQuorum(ctx context.Context) ([]*UnbondingTxData, error)
129+
128130
SetUnbondingTransactionProcessed(ctx context.Context, utx *UnbondingTxData) error
129131

130132
SetUnbondingTransactionProcessingFailed(ctx context.Context, utx *UnbondingTxData) error
131133

132134
SetUnbondingTransactionInputAlreadySpent(ctx context.Context, utx *UnbondingTxData) error
135+
136+
SetUnbondingTransactionFailedToGetCovenantSignatures(ctx context.Context, utx *UnbondingTxData) error
133137
}

internal/services/in_mem_store.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import (
1414
type state string
1515

1616
const (
17-
inserted state = "inserted"
18-
send state = "send"
19-
inputAlreadySpent state = "input_already_spent"
20-
failed state = "failed"
17+
inserted state = "inserted"
18+
send state = "send"
19+
inputAlreadySpent state = "input_already_spent"
20+
failed state = "failed"
21+
failedToGetSignatues state = "failed_to_get_covenant_signatures"
2122
)
2223

2324
type unbondingTxDataWithCounter struct {
@@ -136,6 +137,35 @@ func (s *InMemoryUnbondingStore) GetFailedUnbondingTransactions(ctx context.Cont
136137
return resUnbondingTxData, nil
137138
}
138139

140+
func (s *InMemoryUnbondingStore) GetUnbondingTransactionsWithNoQuorum(ctx context.Context) ([]*UnbondingTxData, error) {
141+
s.mu.Lock()
142+
defer s.mu.Unlock()
143+
144+
var res []*unbondingTxDataWithCounter
145+
146+
for _, tx := range s.mapping {
147+
txCopy := tx
148+
// get only failed transactions
149+
if tx.state == failedToGetSignatues {
150+
res = append(res, txCopy)
151+
}
152+
}
153+
154+
// sort by counter
155+
sort.SliceStable(res, func(i, j int) bool {
156+
return res[i].Counter < res[j].Counter
157+
})
158+
159+
// convert to UnbondingTxData
160+
var resUnbondingTxData []*UnbondingTxData
161+
for _, tx := range res {
162+
txCopy := tx
163+
resUnbondingTxData = append(resUnbondingTxData, &txCopy.UnbondingTxData)
164+
}
165+
166+
return resUnbondingTxData, nil
167+
}
168+
139169
func (s *InMemoryUnbondingStore) SetUnbondingTransactionProcessed(_ context.Context, utx *UnbondingTxData) error {
140170
s.mu.Lock()
141171
defer s.mu.Unlock()
@@ -180,3 +210,18 @@ func (s *InMemoryUnbondingStore) SetUnbondingTransactionInputAlreadySpent(_ cont
180210

181211
return nil
182212
}
213+
214+
func (s *InMemoryUnbondingStore) SetUnbondingTransactionFailedToGetCovenantSignatures(_ context.Context, utx *UnbondingTxData) error {
215+
s.mu.Lock()
216+
defer s.mu.Unlock()
217+
218+
tx, exists := s.mapping[*utx.UnbondingTransactionHash]
219+
220+
if !exists {
221+
return fmt.Errorf("tx with hash %s does not exist", *utx.UnbondingTransactionHash)
222+
}
223+
224+
tx.state = failedToGetSignatues
225+
226+
return nil
227+
}

internal/services/persistent_store.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,13 @@ func (s *PersistentUnbondingStorage) GetFailedUnbondingTransactions(ctx context.
226226
)
227227
}
228228

229+
func (s *PersistentUnbondingStorage) GetUnbondingTransactionsWithNoQuorum(ctx context.Context) ([]*UnbondingTxData, error) {
230+
return transformDocuments(
231+
ctx,
232+
s.client.FindUnbondingDocumentsWithNoCovenantQuorum,
233+
)
234+
}
235+
229236
func (s *PersistentUnbondingStorage) GetNotProcessedUnbondingTransactions(ctx context.Context) ([]*UnbondingTxData, error) {
230237
return transformDocuments(
231238
ctx,
@@ -247,3 +254,8 @@ func (s *PersistentUnbondingStorage) SetUnbondingTransactionInputAlreadySpent(ct
247254
txHash := utx.UnbondingTransactionHash.String()
248255
return s.client.SetUnbondingDocumentFailed(ctx, txHash)
249256
}
257+
258+
func (s *PersistentUnbondingStorage) SetUnbondingTransactionFailedToGetCovenantSignatures(ctx context.Context, utx *UnbondingTxData) error {
259+
txHash := utx.UnbondingTransactionHash.String()
260+
return s.client.SetUnbondingDocumentFailedToGetCovenantSignatures(ctx, txHash)
261+
}

internal/services/unbonding_pipeline.go

Lines changed: 97 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77
"fmt"
88
"log/slog"
99

10+
"github.com/babylonchain/babylon/types"
1011
"github.com/babylonchain/cli-tools/internal/btcclient"
1112
"github.com/babylonchain/cli-tools/internal/config"
1213
"github.com/babylonchain/cli-tools/internal/db"
1314
"github.com/btcsuite/btcd/btcec/v2"
1415
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1516
"github.com/btcsuite/btcd/chaincfg"
17+
"github.com/btcsuite/btcd/chaincfg/chainhash"
1618
"github.com/btcsuite/btcd/wire"
1719
"github.com/prometheus/client_golang/prometheus/push"
1820
)
@@ -22,11 +24,26 @@ var (
2224
// code, or we allowed some invalid data into database.
2325
// When this happend we stop processing pipeline and return immediately, without
2426
// changing status of any unbonding transaction.
25-
ErrCriticalError = fmt.Errorf("critical error encountered")
27+
ErrCriticalError = fmt.Errorf("critical error")
2628
)
2729

28-
func wrapCrititical(err error) error {
29-
return fmt.Errorf("%s:%w", err.Error(), ErrCriticalError)
30+
func mustBtcTxToHex(tx *wire.MsgTx) string {
31+
bytes, err := types.SerializeBTCTx(tx)
32+
if err != nil {
33+
panic(err)
34+
}
35+
return hex.EncodeToString(bytes)
36+
}
37+
38+
func wrapCrititical(
39+
stakingTx *wire.MsgTx,
40+
stakingTxHash *chainhash.Hash,
41+
err error,
42+
) error {
43+
stakingTxHex := mustBtcTxToHex(stakingTx)
44+
return fmt.Errorf(
45+
"err: %s, staking_tx_haxh:%s, staking_tx:%s: %w", err.Error(), stakingTxHash.String(), stakingTxHex, ErrCriticalError,
46+
)
3047
}
3148

3249
func pubKeyToStringSchnorr(pubKey *btcec.PublicKey) string {
@@ -247,6 +264,7 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
247264
utx := tx
248265

249266
stakingOutputFromDb := utx.StakingOutput()
267+
250268
stakingTxHash := utx.UnbondingTransaction.TxIn[0].PreviousOutPoint.Hash
251269

252270
stakingTxInfo, err := up.sender.TxByHash(
@@ -257,14 +275,22 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
257275
// if the staking transaction is not found in btc chain, it means something is wrong
258276
// as staking service should not allow to create unbonding transaction without staking transaction
259277
if err != nil {
260-
return wrapCrititical(err)
278+
return wrapCrititical(
279+
utx.StakingTransactionData.StakingTransaction,
280+
&stakingTxHash,
281+
err,
282+
)
261283
}
262284

263285
params, err := up.retriever.ParamsByHeight(ctx, uint64(stakingTxInfo.TxInclusionHeight))
264286

265287
// we should always be able to retrieve params for the height of the staking transaction
266288
if err != nil {
267-
return wrapCrititical(err)
289+
return wrapCrititical(
290+
utx.StakingTransactionData.StakingTransaction,
291+
&stakingTxHash,
292+
err,
293+
)
268294
}
269295

270296
stakingOutputRecovered, unbondingPathSpendInfo, err := CreateUnbondingPathSpendInfo(
@@ -274,7 +300,11 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
274300
)
275301

276302
if err != nil {
277-
return wrapCrititical(err)
303+
return wrapCrititical(
304+
utx.StakingTransactionData.StakingTransaction,
305+
&stakingTxHash,
306+
err,
307+
)
278308
}
279309

280310
// This the last line check before sending unbonding transaction for signing. It checks
@@ -285,7 +315,11 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
285315
// - pipeline is run on bad BTC network
286316
// - stakingApi service has a bug
287317
if !outputsAreEqual(stakingOutputRecovered, stakingOutputFromDb) {
288-
return wrapCrititical(fmt.Errorf("staking output from staking tx and staking output re-build from params are different"))
318+
return wrapCrititical(
319+
utx.StakingTransactionData.StakingTransaction,
320+
&stakingTxHash,
321+
fmt.Errorf("staking output from staking tx and staking output re-build from params are different"),
322+
)
289323
}
290324

291325
sigs, err := up.signUnbondingTransaction(
@@ -297,7 +331,26 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
297331
)
298332

299333
if err != nil {
300-
return wrapCrititical(err)
334+
up.Metrics.RecordFailedCovenantQuorum()
335+
336+
unbondingTxHex := mustBtcTxToHex(utx.UnbondingTransaction)
337+
338+
up.logger.Error("Failed to get quorum of covenant signatures to unbond",
339+
"staking_tx_hash", tx.StakingTransactionData.StakingTransaction.TxHash().String(),
340+
"unbonding_tx_hash", tx.UnbondingTransactionHash.String(),
341+
"unbonding_tx", unbondingTxHex,
342+
)
343+
344+
// Note that we failed to get signatures from covenant members
345+
if err := up.store.SetUnbondingTransactionFailedToGetCovenantSignatures(ctx, utx); err != nil {
346+
return wrapCrititical(
347+
utx.StakingTransactionData.StakingTransaction,
348+
&stakingTxHash,
349+
err,
350+
)
351+
}
352+
353+
continue
301354
}
302355

303356
up.logger.Info("Successfully collected quorum of covenant signatures to unbond",
@@ -315,7 +368,11 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
315368
)
316369

317370
if err != nil {
318-
return wrapCrititical(err)
371+
return wrapCrititical(
372+
utx.StakingTransactionData.StakingTransaction,
373+
&stakingTxHash,
374+
err,
375+
)
319376
}
320377

321378
// We assume that this is valid unbodning transaction, with 1 input
@@ -331,7 +388,11 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
331388
up.logger.Info("The input of the unbonding transaction has already been spent",
332389
slog.String("staking_tx_hash", stakingTxHash.String()))
333390
if err := up.store.SetUnbondingTransactionInputAlreadySpent(ctx, utx); err != nil {
334-
return wrapCrititical(err)
391+
return wrapCrititical(
392+
utx.StakingTransactionData.StakingTransaction,
393+
&stakingTxHash,
394+
err,
395+
)
335396
}
336397
continue
337398
}
@@ -341,7 +402,11 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
341402
if err != nil {
342403
up.logger.Error("Failed to send unbonding transaction", "error", err)
343404
if err := up.store.SetUnbondingTransactionProcessingFailed(ctx, utx); err != nil {
344-
return wrapCrititical(err)
405+
return wrapCrititical(
406+
utx.StakingTransactionData.StakingTransaction,
407+
&stakingTxHash,
408+
err,
409+
)
345410
}
346411
up.Metrics.RecordFailedUnbodingTransaction()
347412
} else {
@@ -350,7 +415,11 @@ func (up *UnbondingPipeline) processUnbondingTransactions(
350415
slog.String("tx_hash", hash.String()),
351416
)
352417
if err := up.store.SetUnbondingTransactionProcessed(ctx, utx); err != nil {
353-
return wrapCrititical(err)
418+
return wrapCrititical(
419+
utx.StakingTransactionData.StakingTransaction,
420+
&stakingTxHash,
421+
err,
422+
)
354423
}
355424
up.Metrics.RecordSentUnbondingTransaction()
356425
}
@@ -398,12 +467,6 @@ func (up *UnbondingPipeline) ProcessNewTransactions(ctx context.Context) error {
398467
func (up *UnbondingPipeline) ProcessFailedTransactions(ctx context.Context) error {
399468
up.logger.Info("Running unbonding pipeline for failed transactions")
400469

401-
unbondingTransactions, err := up.store.GetFailedUnbondingTransactions(ctx)
402-
403-
if err != nil {
404-
return err
405-
}
406-
407470
defer func() {
408471
if up.Metrics.Config.Enabled {
409472
if err := up.pushMetrics(); err != nil {
@@ -412,9 +475,22 @@ func (up *UnbondingPipeline) ProcessFailedTransactions(ctx context.Context) erro
412475
}
413476
}()
414477

415-
if len(unbondingTransactions) == 0 {
416-
up.logger.Info("No failed unbonding transactions to process")
417-
return nil
478+
// 1. First process transactions that failed to get quorum of covenant signatures
479+
unbondingTxWithNoQuorum, err := up.store.GetUnbondingTransactionsWithNoQuorum(ctx)
480+
481+
if err != nil {
482+
return err
483+
}
484+
485+
if err := up.processUnbondingTransactions(ctx, unbondingTxWithNoQuorum); err != nil {
486+
return err
487+
}
488+
489+
// 2. Second process other failed unbonding transactions
490+
unbondingTransactions, err := up.store.GetFailedUnbondingTransactions(ctx)
491+
492+
if err != nil {
493+
return err
418494
}
419495

420496
if err := up.processUnbondingTransactions(ctx, unbondingTransactions); err != nil {

internal/services/unbonding_pipeline_metrics.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type PipelineMetrics struct {
1010
FailedSigningReqs *prometheus.CounterVec
1111
SuccessfulSentTransactions prometheus.Counter
1212
FailureSentTransactions prometheus.Counter
13+
FailureToGetCovenantQuorum prometheus.Counter
1314
Config *config.MetricsConfig
1415
}
1516

@@ -41,6 +42,12 @@ func NewPipelineMetrics(cfg *config.MetricsConfig) *PipelineMetrics {
4142
Help: "How many transactions failed to be sent to the network",
4243
},
4344
),
45+
FailureToGetCovenantQuorum: prometheus.NewCounter(
46+
prometheus.CounterOpts{
47+
Name: "number_of_failed_covenant_quorums",
48+
Help: "How many times we failed to get covenant quorum for signing request",
49+
},
50+
),
4451
Config: cfg,
4552
}
4653
}
@@ -60,3 +67,7 @@ func (pm *PipelineMetrics) RecordSentUnbondingTransaction() {
6067
func (pm *PipelineMetrics) RecordFailedUnbodingTransaction() {
6168
pm.FailureSentTransactions.Inc()
6269
}
70+
71+
func (pm *PipelineMetrics) RecordFailedCovenantQuorum() {
72+
pm.FailureToGetCovenantQuorum.Inc()
73+
}

0 commit comments

Comments
 (0)