@blinklabs/dingo 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ledger/state.go CHANGED
@@ -41,14 +41,17 @@ import (
41
41
  const (
42
42
  cleanupConsumedUtxosInterval = 5 * time.Minute
43
43
  cleanupConsumedUtxosSlotWindow = 50000 // TODO: calculate this from params (#395)
44
+
45
+ validateHistoricalThreshold = 14 * (24 * time.Hour) // 2 weeks
44
46
  )
45
47
 
46
48
  type LedgerStateConfig struct {
47
- Logger *slog.Logger
48
- DataDir string
49
- EventBus *event.EventBus
50
- CardanoNodeConfig *cardano.CardanoNodeConfig
51
- PromRegistry prometheus.Registerer
49
+ Logger *slog.Logger
50
+ DataDir string
51
+ EventBus *event.EventBus
52
+ CardanoNodeConfig *cardano.CardanoNodeConfig
53
+ PromRegistry prometheus.Registerer
54
+ ValidateHistorical bool
52
55
  // Callback(s)
53
56
  BlockfetchRequestRangeFunc BlockfetchRequestRangeFunc
54
57
  }
@@ -106,7 +109,7 @@ func NewLedgerState(cfg LedgerStateConfig) (*LedgerState, error) {
106
109
  if err != nil {
107
110
  var dbErr database.CommitTimestampError
108
111
  if !errors.As(err, &dbErr) {
109
- return nil, err
112
+ return nil, fmt.Errorf("failed to open database: %w", err)
110
113
  }
111
114
  ls.config.Logger.Warn(
112
115
  "database initialization error, needs recovery",
@@ -124,7 +127,7 @@ func NewLedgerState(cfg LedgerStateConfig) (*LedgerState, error) {
124
127
  true, // persistent
125
128
  )
126
129
  if err != nil {
127
- return nil, err
130
+ return nil, fmt.Errorf("failed to load chain: %w", err)
128
131
  }
129
132
  ls.chain = chain
130
133
  // Run recovery if needed
@@ -146,19 +149,19 @@ func NewLedgerState(cfg LedgerStateConfig) (*LedgerState, error) {
146
149
  ls.scheduleCleanupConsumedUtxos()
147
150
  // Load epoch info from DB
148
151
  if err := ls.loadEpochs(nil); err != nil {
149
- return nil, err
152
+ return nil, fmt.Errorf("failed to load epoch info: %w", err)
150
153
  }
151
154
  // Load current protocol parameters from DB
152
155
  if err := ls.loadPParams(); err != nil {
153
- return nil, err
156
+ return nil, fmt.Errorf("failed to load pparams: %w", err)
154
157
  }
155
158
  // Load current tip
156
159
  if err := ls.loadTip(); err != nil {
157
- return nil, err
160
+ return nil, fmt.Errorf("failed to load tip: %w", err)
158
161
  }
159
162
  // Create genesis block
160
163
  if err := ls.createGenesisBlock(); err != nil {
161
- return nil, err
164
+ return nil, fmt.Errorf("failed to create genesis block: %w", err)
162
165
  }
163
166
  // Start goroutine to process new blocks
164
167
  go ls.ledgerProcessBlocks()
@@ -169,7 +172,7 @@ func (ls *LedgerState) recoverCommitTimestampConflict() error {
169
172
  // Load current ledger tip
170
173
  tmpTip, err := ls.db.GetTip(nil)
171
174
  if err != nil {
172
- return err
175
+ return fmt.Errorf("failed to get tip: %w", err)
173
176
  }
174
177
  // Check if we can lookup tip block in chain
175
178
  _, err = ls.chain.BlockByPoint(tmpTip.Point, nil)
@@ -256,12 +259,12 @@ func (ls *LedgerState) rollback(point ocommon.Point) error {
256
259
  if point.Slot > 0 {
257
260
  rollbackBlock, err := ls.chain.BlockByPoint(point, txn)
258
261
  if err != nil {
259
- return err
262
+ return fmt.Errorf("failed to get rollback block: %w", err)
260
263
  }
261
264
  ls.currentTip.BlockNumber = rollbackBlock.Number
262
265
  }
263
266
  if err = ls.db.SetTip(ls.currentTip, txn); err != nil {
264
- return err
267
+ return fmt.Errorf("failed to set tip: %w", err)
265
268
  }
266
269
  ls.updateTipMetrics()
267
270
  return nil
@@ -271,7 +274,7 @@ func (ls *LedgerState) rollback(point ocommon.Point) error {
271
274
  }
272
275
  // Reload tip
273
276
  if err := ls.loadTip(); err != nil {
274
- return err
277
+ return fmt.Errorf("failed to load tip: %w", err)
275
278
  }
276
279
  var hash string
277
280
  if point.Slot == 0 {
@@ -306,7 +309,7 @@ func (ls *LedgerState) transitionToEra(
306
309
  ls.currentPParams,
307
310
  )
308
311
  if err != nil {
309
- return err
312
+ return fmt.Errorf("hard fork failed: %w", err)
310
313
  }
311
314
  ls.currentPParams = newPParams
312
315
  ls.config.Logger.Debug(
@@ -317,7 +320,7 @@ func (ls *LedgerState) transitionToEra(
317
320
  // Write pparams update to DB
318
321
  pparamsCbor, err := cbor.Encode(&ls.currentPParams)
319
322
  if err != nil {
320
- return err
323
+ return fmt.Errorf("failed to encode pparams: %w", err)
321
324
  }
322
325
  err = ls.db.SetPParams(
323
326
  pparamsCbor,
@@ -327,7 +330,7 @@ func (ls *LedgerState) transitionToEra(
327
330
  txn,
328
331
  )
329
332
  if err != nil {
330
- return err
333
+ return fmt.Errorf("failed to set pparams: %w", err)
331
334
  }
332
335
  }
333
336
  ls.currentEra = nextEra
@@ -357,45 +360,136 @@ func (ls *LedgerState) ledgerProcessBlocks() {
357
360
  return
358
361
  }
359
362
  shouldBlock := false
363
+ shouldValidate := ls.config.ValidateHistorical
360
364
  // We chose 500 as an arbitrary max batch size. A "chain extended" message will be logged after each batch
361
365
  nextBatch := make([]*chain.ChainIteratorResult, 0, 500)
362
- var next, nextRollback *chain.ChainIteratorResult
366
+ var next, nextRollback, cachedNext *chain.ChainIteratorResult
363
367
  var tmpBlock ledger.Block
364
- var needsRollback bool
368
+ var needsRollback, needsEpochRollover bool
369
+ var nextEpochEraId uint
365
370
  var end, i int
366
371
  var txn *database.Txn
367
372
  for {
373
+ if needsEpochRollover {
374
+ needsEpochRollover = false
375
+ txn := ls.db.Transaction(true)
376
+ err := txn.Do(func(txn *database.Txn) error {
377
+ // Check for era change
378
+ if nextEpochEraId != ls.currentEra.Id {
379
+ // Transition through every era between the current and the target era
380
+ for nextEraId := ls.currentEra.Id + 1; nextEraId <= nextEpochEraId; nextEraId++ {
381
+ if err := ls.transitionToEra(txn, nextEraId, ls.currentEpoch.EpochId, ls.currentEpoch.StartSlot+uint64(ls.currentEpoch.LengthInSlots)); err != nil {
382
+ return err
383
+ }
384
+ }
385
+ }
386
+ // Process epoch rollover
387
+ if err := ls.processEpochRollover(txn); err != nil {
388
+ return err
389
+ }
390
+ return nil
391
+ })
392
+ if err != nil {
393
+ ls.config.Logger.Error(
394
+ "failed to process epoch rollover: " + err.Error(),
395
+ )
396
+ return
397
+ }
398
+ }
368
399
  // Gather up next batch of blocks
369
400
  for {
370
- next, err = iter.Next(shouldBlock)
371
- shouldBlock = false
372
- if err != nil {
373
- if !errors.Is(err, chain.ErrIteratorChainTip) {
374
- ls.config.Logger.Error(
375
- "failed to get next block from chain iterator: " + err.Error(),
376
- )
377
- return
401
+ if cachedNext != nil {
402
+ next = cachedNext
403
+ cachedNext = nil
404
+ } else {
405
+ next, err = iter.Next(shouldBlock)
406
+ shouldBlock = false
407
+ if err != nil {
408
+ if !errors.Is(err, chain.ErrIteratorChainTip) {
409
+ ls.config.Logger.Error(
410
+ "failed to get next block from chain iterator: " + err.Error(),
411
+ )
412
+ return
413
+ }
414
+ shouldBlock = true
415
+ // Break out of inner loop to flush DB transaction and log
416
+ break
378
417
  }
379
- shouldBlock = true
380
- // Break out of inner loop to flush DB transaction and log
381
- break
382
418
  }
383
419
  if next == nil {
384
420
  ls.config.Logger.Error("next block from chain iterator is nil")
385
421
  return
386
422
  }
387
- nextBatch = append(nextBatch, next)
388
- // End batch if there's a rollback, since we need special processing
389
- if next.Rollback {
423
+ // End batch and cache next if we get a block from after the current epoch end, or if we need the initial epoch
424
+ if next.Point.Slot >= (ls.currentEpoch.StartSlot+uint64(ls.currentEpoch.LengthInSlots)) ||
425
+ ls.currentEpoch.SlotLength == 0 {
426
+ cachedNext = next
427
+ needsEpochRollover = true
428
+ // Decode next block to get era ID
429
+ tmpBlock, err = next.Block.Decode()
430
+ if err != nil {
431
+ ls.config.Logger.Error(
432
+ "failed to decode block: " + err.Error(),
433
+ )
434
+ return
435
+ }
436
+ nextEpochEraId = uint(tmpBlock.Era().Id)
390
437
  break
391
438
  }
439
+ if next.Rollback {
440
+ // End existing batch and cache rollback if we have any blocks in the batch
441
+ // We need special processing for rollbacks below
442
+ if len(nextBatch) > 0 {
443
+ cachedNext = next
444
+ break
445
+ }
446
+ needsRollback = true
447
+ }
448
+ // Enable validation if we're getting near current tip
449
+ if !shouldValidate && len(nextBatch) == 0 {
450
+ // Determine wall time for next block slot
451
+ slotTime, err := ls.SlotToTime(next.Point.Slot)
452
+ if err != nil {
453
+ ls.config.Logger.Error(
454
+ "failed to convert slot to time: " + err.Error(),
455
+ )
456
+ return
457
+ }
458
+ // Check difference from current time
459
+ timeDiff := time.Since(slotTime)
460
+ if timeDiff < validateHistoricalThreshold {
461
+ shouldValidate = true
462
+ ls.config.Logger.Debug(
463
+ "enabling validation as we approach tip",
464
+ )
465
+ }
466
+ }
467
+ // Add to batch
468
+ nextBatch = append(nextBatch, next)
392
469
  // Don't exceed our pre-allocated capacity
393
470
  if len(nextBatch) == cap(nextBatch) {
394
471
  break
395
472
  }
396
473
  }
474
+ // Process rollback
475
+ if needsRollback {
476
+ needsRollback = false
477
+ // The rollback should be alone in the batch
478
+ nextRollback = nextBatch[0]
479
+ ls.Lock()
480
+ if err = ls.rollback(nextRollback.Point); err != nil {
481
+ ls.Unlock()
482
+ ls.config.Logger.Error(
483
+ "failed to process rollback: " + err.Error(),
484
+ )
485
+ return
486
+ }
487
+ ls.Unlock()
488
+ // Clear out batch buffer
489
+ nextBatch = slices.Delete(nextBatch, 0, len(nextBatch))
490
+ continue
491
+ }
397
492
  // Process batch in groups of 50 to stay under DB txn limits
398
- needsRollback = false
399
493
  for i = 0; i < len(nextBatch); i += 50 {
400
494
  ls.Lock()
401
495
  end = min(
@@ -405,18 +499,12 @@ func (ls *LedgerState) ledgerProcessBlocks() {
405
499
  txn = ls.db.Transaction(true)
406
500
  err = txn.Do(func(txn *database.Txn) error {
407
501
  for _, next := range nextBatch[i:end] {
408
- // Rollbacks need to be handled outside of the batch DB transaction
409
- // A rollback should only occur at the end of a batch
410
- if next.Rollback {
411
- needsRollback = true
412
- return nil
413
- }
414
502
  // Process block
415
503
  tmpBlock, err = next.Block.Decode()
416
504
  if err != nil {
417
- return err
505
+ return fmt.Errorf("block decode failed: %w", err)
418
506
  }
419
- if err = ls.ledgerProcessBlock(txn, next.Point, tmpBlock); err != nil {
507
+ if err = ls.ledgerProcessBlock(txn, next.Point, tmpBlock, shouldValidate); err != nil {
420
508
  return err
421
509
  }
422
510
  // Update tip
@@ -429,7 +517,7 @@ func (ls *LedgerState) ledgerProcessBlocks() {
429
517
  }
430
518
  // Update tip in database
431
519
  if err := ls.db.SetTip(ls.currentTip, txn); err != nil {
432
- return err
520
+ return fmt.Errorf("failed to set tip: %w", err)
433
521
  }
434
522
  ls.updateTipMetrics()
435
523
  return nil
@@ -443,27 +531,6 @@ func (ls *LedgerState) ledgerProcessBlocks() {
443
531
  }
444
532
  ls.Unlock()
445
533
  }
446
- // Process rollback from end of batch
447
- if needsRollback {
448
- needsRollback = false
449
- // The rollback should be at the end of the batch
450
- nextRollback = nextBatch[len(nextBatch)-1]
451
- ls.Lock()
452
- if err = ls.rollback(nextRollback.Point); err != nil {
453
- ls.Unlock()
454
- ls.config.Logger.Error(
455
- "failed to process rollback: " + err.Error(),
456
- )
457
- return
458
- }
459
- ls.Unlock()
460
- // Skip "chain extended" logging below if batch only contains a rollback
461
- if len(nextBatch) == 1 {
462
- // Clear out batch buffer
463
- nextBatch = slices.Delete(nextBatch, 0, len(nextBatch))
464
- continue
465
- }
466
- }
467
534
  if len(nextBatch) > 0 {
468
535
  // Clear out batch buffer
469
536
  nextBatch = slices.Delete(nextBatch, 0, len(nextBatch))
@@ -484,6 +551,7 @@ func (ls *LedgerState) ledgerProcessBlock(
484
551
  txn *database.Txn,
485
552
  point ocommon.Point,
486
553
  block ledger.Block,
554
+ shouldValidate bool,
487
555
  ) error {
488
556
  // Check that we're processing things in order
489
557
  if len(ls.currentTip.Point.Hash) > 0 {
@@ -500,25 +568,52 @@ func (ls *LedgerState) ledgerProcessBlock(
500
568
  )
501
569
  }
502
570
  }
503
- // TODO: track this using protocol params and hard forks
504
- // Check for era change
505
- if uint(block.Era().Id) != ls.currentEra.Id {
506
- targetEraId := uint(block.Era().Id)
507
- // Transition through every era between the current and the target era
508
- for nextEraId := ls.currentEra.Id + 1; nextEraId <= targetEraId; nextEraId++ {
509
- if err := ls.transitionToEra(txn, nextEraId, ls.currentEpoch.EpochId, point.Slot); err != nil {
571
+ // Process transactions
572
+ var delta *LedgerDelta
573
+ for _, tx := range block.Transactions() {
574
+ if delta == nil {
575
+ delta = &LedgerDelta{
576
+ Point: point,
577
+ }
578
+ }
579
+ // Validate transaction
580
+ if shouldValidate {
581
+ if ls.currentEra.ValidateTxFunc != nil {
582
+ lv := &LedgerView{
583
+ txn: txn,
584
+ ls: ls,
585
+ }
586
+ err := ls.currentEra.ValidateTxFunc(
587
+ tx,
588
+ point.Slot,
589
+ lv,
590
+ ls.currentPParams,
591
+ )
592
+ if err != nil {
593
+ ls.config.Logger.Warn(
594
+ "TX " + tx.Hash().
595
+ String() +
596
+ " failed validation: " + err.Error(),
597
+ )
598
+ // return fmt.Errorf("TX validation failure: %w", err)
599
+ }
600
+ }
601
+ }
602
+ // Populate ledger delta from transaction
603
+ if err := delta.processTransaction(tx); err != nil {
604
+ return fmt.Errorf("process transaction: %w", err)
605
+ }
606
+ // Apply delta immediately if we may need the data to validate the next TX
607
+ if shouldValidate {
608
+ if err := delta.apply(ls, txn); err != nil {
510
609
  return err
511
610
  }
611
+ delta = nil
512
612
  }
513
613
  }
514
- // Check for epoch rollover
515
- if err := ls.processEpochRollover(txn, point); err != nil {
516
- return err
517
- }
518
- // Process transactions
519
- for _, tx := range block.Transactions() {
520
- if err := ls.processTransaction(txn, tx, point); err != nil {
521
- return err
614
+ if delta != nil {
615
+ if err := delta.apply(ls, txn); err != nil {
616
+ return fmt.Errorf("apply ledger delta: %w", err)
522
617
  }
523
618
  }
524
619
  return nil
@@ -573,7 +668,7 @@ func (ls *LedgerState) loadTip() error {
573
668
  if ls.currentTip.Point.Slot > 0 {
574
669
  tipBlock, err := ls.chain.BlockByPoint(ls.currentTip.Point, nil)
575
670
  if err != nil {
576
- return err
671
+ return fmt.Errorf("failed to get tip block: %w", err)
577
672
  }
578
673
  ls.currentTipBlockNonce = tipBlock.Nonce
579
674
  }
@@ -638,7 +733,7 @@ func (ls *LedgerState) GetIntersectPoint(
638
733
  if errors.Is(err, chain.ErrBlockNotFound) {
639
734
  continue
640
735
  }
641
- return err
736
+ return fmt.Errorf("failed to get block: %w", err)
642
737
  }
643
738
  // Update return value
644
739
  ret.Slot = tmpBlock.Slot
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@blinklabs/dingo",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Dingo is a Cardano blockchain data node",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "postinstall": "golang-npm install",
8
- "preuninstall": "golang-npm uninstall",
9
- "test": "echo \"Error: no test specified\" && exit 1"
8
+ "preuninstall": "golang-npm uninstall"
10
9
  },
11
10
  "repository": {
12
11
  "type": "git",
@@ -23,11 +22,12 @@
23
22
  },
24
23
  "homepage": "https://github.com/blinklabs-io/dingo#readme",
25
24
  "dependencies": {
26
- "golang-npm": "^0.0.5"
25
+ "dingo": "^0.0.18",
26
+ "golang-npm": "^0.0.6"
27
27
  },
28
28
  "goBinary": {
29
29
  "name": "dingo",
30
30
  "path": "./bin",
31
- "url": "https://github.com/blinklabs-io/dingo/releases/download/v{{version}}/dingo-{{version}}-{{platform}}-{{arch}}.tar.gz"
31
+ "url": "https://github.com/blinklabs-io/dingo/releases/download/v{{version}}/dingo-v{{version}}-{{platform}}-{{arch}}.tar.gz"
32
32
  }
33
33
  }
@@ -1,26 +0,0 @@
1
- // Copyright 2025 Blink Labs Software
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- package models
16
-
17
- type DeregistrationDrep struct {
18
- ID uint `gorm:"primarykey"`
19
- DrepCredential []byte `gorm:"index"`
20
- AddedSlot uint64
21
- DepositAmount uint64
22
- }
23
-
24
- func (DeregistrationDrep) TableName() string {
25
- return "deregistration_drep"
26
- }
@@ -1,28 +0,0 @@
1
- // Copyright 2025 Blink Labs Software
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- package models
16
-
17
- type RegistrationDrep struct {
18
- ID uint `gorm:"primarykey"`
19
- DrepCredential []byte `gorm:"index"`
20
- AddedSlot uint64
21
- DepositAmount uint64
22
- AnchorUrl string
23
- AnchorHash []byte
24
- }
25
-
26
- func (RegistrationDrep) TableName() string {
27
- return "registration_drep"
28
- }
@@ -1,27 +0,0 @@
1
- // Copyright 2025 Blink Labs Software
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- package models
16
-
17
- type StakeRegistrationDelegation struct {
18
- ID uint `gorm:"primarykey"`
19
- StakingKey []byte `gorm:"index"`
20
- PoolKeyHash []byte `gorm:"index"`
21
- AddedSlot uint64
22
- DepositAmount uint64
23
- }
24
-
25
- func (StakeRegistrationDelegation) TableName() string {
26
- return "stake_registration_delegation"
27
- }
@@ -1,27 +0,0 @@
1
- // Copyright 2025 Blink Labs Software
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- package models
16
-
17
- type StakeVoteRegistrationDelegation struct {
18
- ID uint `gorm:"primarykey"`
19
- StakingKey []byte `gorm:"index"`
20
- PoolKeyHash []byte `gorm:"index"`
21
- Drep []byte `gorm:"index"`
22
- AddedSlot uint64
23
- }
24
-
25
- func (StakeVoteRegistrationDelegation) TableName() string {
26
- return "stake_vote_registration_delegation"
27
- }