• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 11170835610

03 Oct 2024 10:41PM UTC coverage: 49.188% (-9.6%) from 58.738%
11170835610

push

github

web-flow
Merge pull request #9154 from ziggie1984/master

multi: bump btcd version.

3 of 6 new or added lines in 6 files covered. (50.0%)

26110 existing lines in 428 files now uncovered.

97359 of 197934 relevant lines covered (49.19%)

1.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.46
/watchtower/wtdb/tower_db.go
1
package wtdb
2

3
import (
4
        "bytes"
5
        "errors"
6

7
        "github.com/btcsuite/btcd/chaincfg/chainhash"
8
        "github.com/lightningnetwork/lnd/chainntnfs"
9
        "github.com/lightningnetwork/lnd/kvdb"
10
        "github.com/lightningnetwork/lnd/watchtower/blob"
11
)
12

13
var (
14
        // sessionsBkt is a bucket containing all negotiated client sessions.
15
        //  session id -> session
16
        sessionsBkt = []byte("sessions-bucket")
17

18
        // updatesBkt is a bucket containing all state updates sent by clients.
19
        // The updates are further bucketed by session id to prevent clients
20
        // from overwrite each other.
21
        //   hint => session id -> update
22
        updatesBkt = []byte("updates-bucket")
23

24
        // updateIndexBkt is a bucket that indexes all state updates by their
25
        // overarching session id. This allows for efficient lookup of updates
26
        // by their session id, which is currently used to aide deletion
27
        // performance.
28
        //  session id => hint1 -> []byte{}
29
        //             => hint2 -> []byte{}
30
        updateIndexBkt = []byte("update-index-bucket")
31

32
        // lookoutTipBkt is a bucket containing the last block epoch processed
33
        // by the lookout subsystem. It has one key, lookoutTipKey.
34
        //   lookoutTipKey -> block epoch
35
        lookoutTipBkt = []byte("lookout-tip-bucket")
36

37
        // lookoutTipKey is a static key used to retrieve lookout tip's block
38
        // epoch from the lookoutTipBkt.
39
        lookoutTipKey = []byte("lookout-tip")
40

41
        // ErrNoSessionHintIndex signals that an active session does not have an
42
        // initialized index for tracking its own state updates.
43
        ErrNoSessionHintIndex = errors.New("session hint index missing")
44

45
        // ErrInvalidBlobSize indicates that the encrypted blob provided by the
46
        // client is not valid according to the blob type of the session.
47
        ErrInvalidBlobSize = errors.New("invalid blob size")
48
)
49

50
// TowerDB is single database providing a persistent storage engine for the
51
// wtserver and lookout subsystems.
52
type TowerDB struct {
53
        db kvdb.Backend
54
}
55

56
// OpenTowerDB opens the tower database given the path to the database's
57
// directory. If no such database exists, this method will initialize a fresh
58
// one using the latest version number and bucket structure. If a database
59
// exists but has a lower version number than the current version, any necessary
60
// migrations will be applied before returning. Any attempt to open a database
61
// with a version number higher that the latest version will fail to prevent
62
// accidental reversion.
63
func OpenTowerDB(db kvdb.Backend) (*TowerDB, error) {
2✔
64
        firstInit, err := isFirstInit(db)
2✔
65
        if err != nil {
2✔
66
                return nil, err
×
67
        }
×
68

69
        towerDB := &TowerDB{
2✔
70
                db: db,
2✔
71
        }
2✔
72

2✔
73
        err = initOrSyncVersions(towerDB, firstInit, towerDBVersions)
2✔
74
        if err != nil {
2✔
75
                db.Close()
×
76
                return nil, err
×
77
        }
×
78

79
        // Now that the database version fully consistent with our latest known
80
        // version, ensure that all top-level buckets known to this version are
81
        // initialized. This allows us to assume their presence throughout all
82
        // operations. If an known top-level bucket is expected to exist but is
83
        // missing, this will trigger a ErrUninitializedDB error.
84
        err = kvdb.Update(towerDB.db, initTowerDBBuckets, func() {})
4✔
85
        if err != nil {
2✔
86
                db.Close()
×
87
                return nil, err
×
88
        }
×
89

90
        return towerDB, nil
2✔
91
}
92

93
// initTowerDBBuckets creates all top-level buckets required to handle database
94
// operations required by the latest version.
95
func initTowerDBBuckets(tx kvdb.RwTx) error {
2✔
96
        buckets := [][]byte{
2✔
97
                sessionsBkt,
2✔
98
                updateIndexBkt,
2✔
99
                updatesBkt,
2✔
100
                lookoutTipBkt,
2✔
101
        }
2✔
102

2✔
103
        for _, bucket := range buckets {
4✔
104
                _, err := tx.CreateTopLevelBucket(bucket)
2✔
105
                if err != nil {
2✔
106
                        return err
×
107
                }
×
108
        }
109

110
        return nil
2✔
111
}
112

113
// bdb returns the backing bbolt.DB instance.
114
//
115
// NOTE: Part of the versionedDB interface.
116
func (t *TowerDB) bdb() kvdb.Backend {
2✔
117
        return t.db
2✔
118
}
2✔
119

120
// Version returns the database's current version number.
121
//
122
// NOTE: Part of the versionedDB interface.
UNCOV
123
func (t *TowerDB) Version() (uint32, error) {
×
UNCOV
124
        var version uint32
×
UNCOV
125
        err := kvdb.View(t.db, func(tx kvdb.RTx) error {
×
UNCOV
126
                var err error
×
UNCOV
127
                version, err = getDBVersion(tx)
×
UNCOV
128
                return err
×
UNCOV
129
        }, func() {
×
UNCOV
130
                version = 0
×
UNCOV
131
        })
×
UNCOV
132
        if err != nil {
×
133
                return 0, err
×
134
        }
×
135

UNCOV
136
        return version, nil
×
137
}
138

139
// Close closes the underlying database.
UNCOV
140
func (t *TowerDB) Close() error {
×
UNCOV
141
        return t.db.Close()
×
UNCOV
142
}
×
143

144
// GetSessionInfo retrieves the session for the passed session id. An error is
145
// returned if the session could not be found.
146
func (t *TowerDB) GetSessionInfo(id *SessionID) (*SessionInfo, error) {
2✔
147
        var session *SessionInfo
2✔
148
        err := kvdb.View(t.db, func(tx kvdb.RTx) error {
4✔
149
                sessions := tx.ReadBucket(sessionsBkt)
2✔
150
                if sessions == nil {
2✔
151
                        return ErrUninitializedDB
×
152
                }
×
153

154
                var err error
2✔
155
                session, err = getSession(sessions, id[:])
2✔
156
                return err
2✔
157
        }, func() {
2✔
158
                session = nil
2✔
159
        })
2✔
160
        if err != nil {
4✔
161
                return nil, err
2✔
162
        }
2✔
163

UNCOV
164
        return session, nil
×
165
}
166

167
// InsertSessionInfo records a negotiated session in the tower database. An
168
// error is returned if the session already exists.
169
func (t *TowerDB) InsertSessionInfo(session *SessionInfo) error {
2✔
170
        return kvdb.Update(t.db, func(tx kvdb.RwTx) error {
4✔
171
                sessions := tx.ReadWriteBucket(sessionsBkt)
2✔
172
                if sessions == nil {
2✔
173
                        return ErrUninitializedDB
×
174
                }
×
175

176
                updateIndex := tx.ReadWriteBucket(updateIndexBkt)
2✔
177
                if updateIndex == nil {
2✔
178
                        return ErrUninitializedDB
×
179
                }
×
180

181
                dbSession, err := getSession(sessions, session.ID[:])
2✔
182
                switch {
2✔
183
                case err == ErrSessionNotFound:
2✔
184
                        // proceed.
185

186
                case err != nil:
×
187
                        return err
×
188

UNCOV
189
                case dbSession.LastApplied > 0:
×
UNCOV
190
                        return ErrSessionAlreadyExists
×
191
                }
192

193
                // Perform a quick sanity check on the session policy before
194
                // accepting.
195
                if err := session.Policy.Validate(); err != nil {
2✔
UNCOV
196
                        return err
×
UNCOV
197
                }
×
198

199
                err = putSession(sessions, session)
2✔
200
                if err != nil {
2✔
201
                        return err
×
202
                }
×
203

204
                // Initialize the session-hint index which will be used to track
205
                // all updates added for this session. Upon deletion, we will
206
                // consult the index to determine exactly which updates should
207
                // be deleted without needing to iterate over the entire
208
                // database.
209
                return touchSessionHintBkt(updateIndex, &session.ID)
2✔
210
        }, func() {})
2✔
211
}
212

213
// InsertStateUpdate stores an update sent by the client after validating that
214
// the update is well-formed in the context of other updates sent for the same
215
// session. This include verifying that the sequence number is incremented
216
// properly and the last applied values echoed by the client are sane.
217
func (t *TowerDB) InsertStateUpdate(update *SessionStateUpdate) (uint16, error) {
2✔
218
        var lastApplied uint16
2✔
219
        err := kvdb.Update(t.db, func(tx kvdb.RwTx) error {
4✔
220
                sessions := tx.ReadWriteBucket(sessionsBkt)
2✔
221
                if sessions == nil {
2✔
222
                        return ErrUninitializedDB
×
223
                }
×
224

225
                updates := tx.ReadWriteBucket(updatesBkt)
2✔
226
                if updates == nil {
2✔
227
                        return ErrUninitializedDB
×
228
                }
×
229

230
                updateIndex := tx.ReadWriteBucket(updateIndexBkt)
2✔
231
                if updateIndex == nil {
2✔
232
                        return ErrUninitializedDB
×
233
                }
×
234

235
                // Fetch the session corresponding to the update's session id.
236
                // This will be used to validate that the update's sequence
237
                // number and last applied values are sane.
238
                session, err := getSession(sessions, update.ID[:])
2✔
239
                if err != nil {
2✔
UNCOV
240
                        return err
×
UNCOV
241
                }
×
242

243
                commitType, err := session.Policy.BlobType.CommitmentType(nil)
2✔
244
                if err != nil {
2✔
245
                        return err
×
246
                }
×
247

248
                kit, err := commitType.EmptyJusticeKit()
2✔
249
                if err != nil {
2✔
250
                        return err
×
251
                }
×
252

253
                // Assert that the blob is the correct size for the session's
254
                // blob type.
255
                expBlobSize := blob.Size(kit)
2✔
256
                if len(update.EncryptedBlob) != expBlobSize {
2✔
UNCOV
257
                        return ErrInvalidBlobSize
×
UNCOV
258
                }
×
259

260
                // Validate the update against the current state of the session.
261
                err = session.AcceptUpdateSequence(
2✔
262
                        update.SeqNum, update.LastApplied,
2✔
263
                )
2✔
264
                if err != nil {
2✔
UNCOV
265
                        return err
×
UNCOV
266
                }
×
267

268
                // Validation succeeded, therefore the update is committed and
269
                // the session's last applied value is equal to the update's
270
                // sequence number.
271
                lastApplied = session.LastApplied
2✔
272

2✔
273
                // Store the updated session to persist the updated last applied
2✔
274
                // values.
2✔
275
                err = putSession(sessions, session)
2✔
276
                if err != nil {
2✔
277
                        return err
×
278
                }
×
279

280
                // Create or load the hint bucket for this state update's hint
281
                // and write the given update.
282
                hints, err := updates.CreateBucketIfNotExists(update.Hint[:])
2✔
283
                if err != nil {
2✔
284
                        return err
×
285
                }
×
286

287
                var b bytes.Buffer
2✔
288
                err = update.Encode(&b)
2✔
289
                if err != nil {
2✔
290
                        return err
×
291
                }
×
292

293
                err = hints.Put(update.ID[:], b.Bytes())
2✔
294
                if err != nil {
2✔
295
                        return err
×
296
                }
×
297

298
                // Finally, create an entry in the update index to track this
299
                // hint under its session id. This will allow us to delete the
300
                // entries efficiently if the session is ever removed.
301
                return putHintForSession(updateIndex, &update.ID, update.Hint)
2✔
302
        }, func() {
2✔
303
                lastApplied = 0
2✔
304
        })
2✔
305
        if err != nil {
2✔
UNCOV
306
                return 0, err
×
UNCOV
307
        }
×
308

309
        return lastApplied, nil
2✔
310
}
311

312
// DeleteSession removes all data associated with a particular session id from
313
// the tower's database.
314
func (t *TowerDB) DeleteSession(target SessionID) error {
2✔
315
        return kvdb.Update(t.db, func(tx kvdb.RwTx) error {
4✔
316
                sessions := tx.ReadWriteBucket(sessionsBkt)
2✔
317
                if sessions == nil {
2✔
318
                        return ErrUninitializedDB
×
319
                }
×
320

321
                updates := tx.ReadWriteBucket(updatesBkt)
2✔
322
                if updates == nil {
2✔
323
                        return ErrUninitializedDB
×
324
                }
×
325

326
                updateIndex := tx.ReadWriteBucket(updateIndexBkt)
2✔
327
                if updateIndex == nil {
2✔
328
                        return ErrUninitializedDB
×
329
                }
×
330

331
                // Fail if the session doesn't exit.
332
                _, err := getSession(sessions, target[:])
2✔
333
                if err != nil {
2✔
UNCOV
334
                        return err
×
UNCOV
335
                }
×
336

337
                // Remove the target session.
338
                err = sessions.Delete(target[:])
2✔
339
                if err != nil {
2✔
340
                        return err
×
341
                }
×
342

343
                // Next, check the update index for any hints that were added
344
                // under this session.
345
                hints, err := getHintsForSession(updateIndex, &target)
2✔
346
                if err != nil {
2✔
347
                        return err
×
348
                }
×
349

350
                for _, hint := range hints {
4✔
351
                        // Remove the state updates for any blobs stored under
2✔
352
                        // the target session identifier.
2✔
353
                        updatesForHint := updates.NestedReadWriteBucket(hint[:])
2✔
354
                        if updatesForHint == nil {
2✔
355
                                continue
×
356
                        }
357

358
                        update := updatesForHint.Get(target[:])
2✔
359
                        if update == nil {
2✔
360
                                continue
×
361
                        }
362

363
                        err := updatesForHint.Delete(target[:])
2✔
364
                        if err != nil {
2✔
365
                                return err
×
366
                        }
×
367

368
                        // If this was the last state update, we can also remove
369
                        // the hint that would map to an empty set.
370
                        err = isBucketEmpty(updatesForHint)
2✔
371
                        switch {
2✔
372

373
                        // Other updates exist for this hint, keep the bucket.
UNCOV
374
                        case err == errBucketNotEmpty:
×
UNCOV
375
                                continue
×
376

377
                        // Unexpected error.
378
                        case err != nil:
×
379
                                return err
×
380

381
                        // No more updates for this hint, prune hint bucket.
382
                        default:
2✔
383
                                err = updates.DeleteNestedBucket(hint[:])
2✔
384
                                if err != nil {
2✔
385
                                        return err
×
386
                                }
×
387
                        }
388
                }
389

390
                // Finally, remove this session from the update index, which
391
                // also removes any of the indexed hints beneath it.
392
                return removeSessionHintBkt(updateIndex, &target)
2✔
393
        }, func() {})
2✔
394
}
395

396
// QueryMatches searches against all known state updates for any that match the
397
// passed breachHints. More than one Match will be returned for a given hint if
398
// they exist in the database.
399
func (t *TowerDB) QueryMatches(breachHints []blob.BreachHint) ([]Match, error) {
2✔
400
        var matches []Match
2✔
401
        err := kvdb.View(t.db, func(tx kvdb.RTx) error {
4✔
402
                sessions := tx.ReadBucket(sessionsBkt)
2✔
403
                if sessions == nil {
2✔
404
                        return ErrUninitializedDB
×
405
                }
×
406

407
                updates := tx.ReadBucket(updatesBkt)
2✔
408
                if updates == nil {
2✔
409
                        return ErrUninitializedDB
×
410
                }
×
411

412
                // Iterate through the target breach hints, appending any
413
                // matching updates to the set of matches.
414
                for _, hint := range breachHints {
4✔
415
                        // If a bucket does not exist for this hint, no matches
2✔
416
                        // are known.
2✔
417
                        updatesForHint := updates.NestedReadBucket(hint[:])
2✔
418
                        if updatesForHint == nil {
4✔
419
                                continue
2✔
420
                        }
421

422
                        // Otherwise, iterate through all (session id, update)
423
                        // pairs, creating a Match for each.
424
                        err := updatesForHint.ForEach(func(k, v []byte) error {
4✔
425
                                // Load the session via the session id for this
2✔
426
                                // update. The session info contains further
2✔
427
                                // instructions for how to process the state
2✔
428
                                // update.
2✔
429
                                session, err := getSession(sessions, k)
2✔
430
                                switch {
2✔
431
                                case err == ErrSessionNotFound:
×
432
                                        log.Warnf("Missing session=%x for "+
×
433
                                                "matched state update hint=%x",
×
434
                                                k, hint)
×
435
                                        return nil
×
436

437
                                case err != nil:
×
438
                                        return err
×
439
                                }
440

441
                                // Decode the state update containing the
442
                                // encrypted blob.
443
                                update := &SessionStateUpdate{}
2✔
444
                                err = update.Decode(bytes.NewReader(v))
2✔
445
                                if err != nil {
2✔
446
                                        return err
×
447
                                }
×
448

449
                                var id SessionID
2✔
450
                                copy(id[:], k)
2✔
451

2✔
452
                                // Construct the final match using the found
2✔
453
                                // update and its session info.
2✔
454
                                match := Match{
2✔
455
                                        ID:            id,
2✔
456
                                        SeqNum:        update.SeqNum,
2✔
457
                                        Hint:          hint,
2✔
458
                                        EncryptedBlob: update.EncryptedBlob,
2✔
459
                                        SessionInfo:   session,
2✔
460
                                }
2✔
461

2✔
462
                                matches = append(matches, match)
2✔
463

2✔
464
                                return nil
2✔
465
                        })
466
                        if err != nil {
2✔
467
                                return err
×
468
                        }
×
469
                }
470

471
                return nil
2✔
472
        }, func() {
2✔
473
                matches = nil
2✔
474
        })
2✔
475
        if err != nil {
2✔
476
                return nil, err
×
477
        }
×
478

479
        return matches, nil
2✔
480
}
481

482
// SetLookoutTip stores the provided epoch as the latest lookout tip epoch in
483
// the tower database.
484
func (t *TowerDB) SetLookoutTip(epoch *chainntnfs.BlockEpoch) error {
2✔
485
        return kvdb.Update(t.db, func(tx kvdb.RwTx) error {
4✔
486
                lookoutTip := tx.ReadWriteBucket(lookoutTipBkt)
2✔
487
                if lookoutTip == nil {
2✔
488
                        return ErrUninitializedDB
×
489
                }
×
490

491
                return putLookoutEpoch(lookoutTip, epoch)
2✔
492
        }, func() {})
2✔
493
}
494

495
// GetLookoutTip retrieves the current lookout tip block epoch from the tower
496
// database.
497
func (t *TowerDB) GetLookoutTip() (*chainntnfs.BlockEpoch, error) {
2✔
498
        var epoch *chainntnfs.BlockEpoch
2✔
499
        err := kvdb.View(t.db, func(tx kvdb.RTx) error {
4✔
500
                lookoutTip := tx.ReadBucket(lookoutTipBkt)
2✔
501
                if lookoutTip == nil {
2✔
502
                        return ErrUninitializedDB
×
503
                }
×
504

505
                epoch = getLookoutEpoch(lookoutTip)
2✔
506

2✔
507
                return nil
2✔
508
        }, func() {
2✔
509
                epoch = nil
2✔
510
        })
2✔
511
        if err != nil {
2✔
512
                return nil, err
×
513
        }
×
514

515
        return epoch, nil
2✔
516
}
517

518
// getSession retrieves the session info from the sessions bucket identified by
519
// its session id. An error is returned if the session is not found or a
520
// deserialization error occurs.
521
func getSession(sessions kvdb.RBucket, id []byte) (*SessionInfo, error) {
2✔
522
        sessionBytes := sessions.Get(id)
2✔
523
        if sessionBytes == nil {
4✔
524
                return nil, ErrSessionNotFound
2✔
525
        }
2✔
526

527
        var session SessionInfo
2✔
528
        err := session.Decode(bytes.NewReader(sessionBytes))
2✔
529
        if err != nil {
2✔
530
                return nil, err
×
531
        }
×
532

533
        return &session, nil
2✔
534
}
535

536
// putSession stores the session info in the sessions bucket identified by its
537
// session id. An error is returned if a serialization error occurs.
538
func putSession(sessions kvdb.RwBucket, session *SessionInfo) error {
2✔
539
        var b bytes.Buffer
2✔
540
        err := session.Encode(&b)
2✔
541
        if err != nil {
2✔
542
                return err
×
543
        }
×
544

545
        return sessions.Put(session.ID[:], b.Bytes())
2✔
546
}
547

548
// touchSessionHintBkt initializes the session-hint bucket for a particular
549
// session id. This ensures that future calls to getHintsForSession or
550
// putHintForSession can rely on the bucket already being created, and fail if
551
// index has not been initialized as this points to improper usage.
552
func touchSessionHintBkt(updateIndex kvdb.RwBucket, id *SessionID) error {
2✔
553
        _, err := updateIndex.CreateBucketIfNotExists(id[:])
2✔
554
        return err
2✔
555
}
2✔
556

557
// removeSessionHintBkt prunes the session-hint bucket for the given session id
558
// and all of the hints contained inside. This should be used to clean up the
559
// index upon session deletion.
560
func removeSessionHintBkt(updateIndex kvdb.RwBucket, id *SessionID) error {
2✔
561
        return updateIndex.DeleteNestedBucket(id[:])
2✔
562
}
2✔
563

564
// getHintsForSession returns all known hints belonging to the given session id.
565
// If the index for the session has not been initialized, this method returns
566
// ErrNoSessionHintIndex.
567
func getHintsForSession(updateIndex kvdb.RBucket,
568
        id *SessionID) ([]blob.BreachHint, error) {
2✔
569

2✔
570
        sessionHints := updateIndex.NestedReadBucket(id[:])
2✔
571
        if sessionHints == nil {
2✔
572
                return nil, ErrNoSessionHintIndex
×
573
        }
×
574

575
        var hints []blob.BreachHint
2✔
576
        err := sessionHints.ForEach(func(k, _ []byte) error {
4✔
577
                if len(k) != blob.BreachHintSize {
2✔
578
                        return nil
×
579
                }
×
580

581
                var hint blob.BreachHint
2✔
582
                copy(hint[:], k)
2✔
583
                hints = append(hints, hint)
2✔
584
                return nil
2✔
585
        })
586
        if err != nil {
2✔
587
                return nil, err
×
588
        }
×
589

590
        return hints, nil
2✔
591
}
592

593
// putHintForSession inserts a record into the update index for a given
594
// (session, hint) pair. The hints are coalesced under a bucket for the target
595
// session id, and used to perform efficient removal of updates. If the index
596
// for the session has not been initialized, this method returns
597
// ErrNoSessionHintIndex.
598
func putHintForSession(updateIndex kvdb.RwBucket, id *SessionID,
599
        hint blob.BreachHint) error {
2✔
600

2✔
601
        sessionHints := updateIndex.NestedReadWriteBucket(id[:])
2✔
602
        if sessionHints == nil {
2✔
603
                return ErrNoSessionHintIndex
×
604
        }
×
605

606
        return sessionHints.Put(hint[:], []byte{})
2✔
607
}
608

609
// putLookoutEpoch stores the given lookout tip block epoch in provided bucket.
610
func putLookoutEpoch(bkt kvdb.RwBucket, epoch *chainntnfs.BlockEpoch) error {
2✔
611
        epochBytes := make([]byte, 36)
2✔
612
        copy(epochBytes, epoch.Hash[:])
2✔
613
        byteOrder.PutUint32(epochBytes[32:], uint32(epoch.Height))
2✔
614

2✔
615
        return bkt.Put(lookoutTipKey, epochBytes)
2✔
616
}
2✔
617

618
// getLookoutEpoch retrieves the lookout tip block epoch from the given bucket.
619
// A nil epoch is returned if no update exists.
620
func getLookoutEpoch(bkt kvdb.RBucket) *chainntnfs.BlockEpoch {
2✔
621
        epochBytes := bkt.Get(lookoutTipKey)
2✔
622
        if len(epochBytes) != 36 {
4✔
623
                return nil
2✔
624
        }
2✔
625

UNCOV
626
        var hash chainhash.Hash
×
UNCOV
627
        copy(hash[:], epochBytes[:32])
×
UNCOV
628
        height := byteOrder.Uint32(epochBytes[32:])
×
UNCOV
629

×
UNCOV
630
        return &chainntnfs.BlockEpoch{
×
UNCOV
631
                Hash:   &hash,
×
UNCOV
632
                Height: int32(height),
×
UNCOV
633
        }
×
634
}
635

636
// errBucketNotEmpty is a helper error returned when testing whether a bucket is
637
// empty or not.
638
var errBucketNotEmpty = errors.New("bucket not empty")
639

640
// isBucketEmpty returns errBucketNotEmpty if the bucket is not empty.
641
func isBucketEmpty(bkt kvdb.RBucket) error {
2✔
642
        return bkt.ForEach(func(_, _ []byte) error {
2✔
UNCOV
643
                return errBucketNotEmpty
×
UNCOV
644
        })
×
645
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc