@blinklabs/dingo 0.6.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.
Files changed (195) hide show
  1. package/.dockerignore +5 -0
  2. package/.github/CODEOWNERS +5 -0
  3. package/.github/assets/dingo-ate-my-blockchain.png +0 -0
  4. package/.github/assets/dingo-illustration.png +0 -0
  5. package/.github/assets/dingo-logo-with-text-horizontal.png +0 -0
  6. package/.github/assets/dingo-logo-with-text.png +0 -0
  7. package/.github/dependabot.yml +19 -0
  8. package/.github/dingo-20241210.png +0 -0
  9. package/.github/dingo.md +56 -0
  10. package/.github/workflows/ci-docker.yml +36 -0
  11. package/.github/workflows/conventional-commits.yml +17 -0
  12. package/.github/workflows/go-test.yml +29 -0
  13. package/.github/workflows/golangci-lint.yml +23 -0
  14. package/.github/workflows/publish.yml +207 -0
  15. package/.golangci.yml +71 -0
  16. package/Dockerfile +25 -0
  17. package/LICENSE +201 -0
  18. package/Makefile +53 -0
  19. package/README.md +150 -0
  20. package/blockfetch.go +144 -0
  21. package/chain/chain.go +504 -0
  22. package/chain/chain_test.go +468 -0
  23. package/chain/errors.go +80 -0
  24. package/chain/event.go +33 -0
  25. package/chain/iter.go +64 -0
  26. package/chainsync/chainsync.go +97 -0
  27. package/chainsync.go +223 -0
  28. package/cmd/dingo/load.go +52 -0
  29. package/cmd/dingo/main.go +118 -0
  30. package/cmd/dingo/serve.go +49 -0
  31. package/config/cardano/node.go +192 -0
  32. package/config/cardano/node_test.go +85 -0
  33. package/config/cardano/preview/README.md +4 -0
  34. package/config/cardano/preview/alonzo-genesis.json +196 -0
  35. package/config/cardano/preview/byron-genesis.json +117 -0
  36. package/config/cardano/preview/config.json +114 -0
  37. package/config/cardano/preview/conway-genesis.json +297 -0
  38. package/config/cardano/preview/shelley-genesis.json +68 -0
  39. package/config.go +245 -0
  40. package/connmanager/connection_manager.go +105 -0
  41. package/connmanager/connection_manager_test.go +185 -0
  42. package/connmanager/event.go +37 -0
  43. package/connmanager/listener.go +140 -0
  44. package/connmanager/outbound.go +93 -0
  45. package/connmanager/socket.go +55 -0
  46. package/connmanager/unix.go +78 -0
  47. package/custom-p2p-topology.json +24 -0
  48. package/custom-p2p-topology.json.backup +24 -0
  49. package/custom-p2p-topology.json.mainnet +37 -0
  50. package/database/account.go +138 -0
  51. package/database/block.go +362 -0
  52. package/database/certs.go +53 -0
  53. package/database/commit_timestamp.go +77 -0
  54. package/database/database.go +118 -0
  55. package/database/database_test.go +62 -0
  56. package/database/drep.go +27 -0
  57. package/database/epoch.go +121 -0
  58. package/database/immutable/chunk.go +182 -0
  59. package/database/immutable/immutable.go +350 -0
  60. package/database/immutable/immutable_test.go +59 -0
  61. package/database/immutable/primary.go +106 -0
  62. package/database/immutable/secondary.go +103 -0
  63. package/database/immutable/testdata/08893.chunk +0 -0
  64. package/database/immutable/testdata/08893.primary +0 -0
  65. package/database/immutable/testdata/08893.secondary +0 -0
  66. package/database/immutable/testdata/08894.chunk +0 -0
  67. package/database/immutable/testdata/08894.primary +0 -0
  68. package/database/immutable/testdata/08894.secondary +0 -0
  69. package/database/immutable/testdata/README.md +4 -0
  70. package/database/plugin/blob/badger/commit_timestamp.go +50 -0
  71. package/database/plugin/blob/badger/database.go +152 -0
  72. package/database/plugin/blob/badger/logger.go +63 -0
  73. package/database/plugin/blob/badger/metrics.go +98 -0
  74. package/database/plugin/blob/blob.go +19 -0
  75. package/database/plugin/blob/store.go +40 -0
  76. package/database/plugin/log.go +27 -0
  77. package/database/plugin/metadata/metadata.go +19 -0
  78. package/database/plugin/metadata/sqlite/account.go +224 -0
  79. package/database/plugin/metadata/sqlite/certs.go +58 -0
  80. package/database/plugin/metadata/sqlite/commit_timestamp.go +68 -0
  81. package/database/plugin/metadata/sqlite/database.go +218 -0
  82. package/database/plugin/metadata/sqlite/epoch.go +120 -0
  83. package/database/plugin/metadata/sqlite/models/account.go +81 -0
  84. package/database/plugin/metadata/sqlite/models/auth_committee_hot.go +26 -0
  85. package/database/plugin/metadata/sqlite/models/deregistration_drep.go +26 -0
  86. package/database/plugin/metadata/sqlite/models/drep.go +27 -0
  87. package/database/plugin/metadata/sqlite/models/epoch.go +31 -0
  88. package/database/plugin/metadata/sqlite/models/models.go +45 -0
  89. package/database/plugin/metadata/sqlite/models/pool.go +97 -0
  90. package/database/plugin/metadata/sqlite/models/pparam_update.go +27 -0
  91. package/database/plugin/metadata/sqlite/models/pparams.go +27 -0
  92. package/database/plugin/metadata/sqlite/models/registration_drep.go +28 -0
  93. package/database/plugin/metadata/sqlite/models/resign_committee_cold.go +27 -0
  94. package/database/plugin/metadata/sqlite/models/stake_registration_delegation.go +27 -0
  95. package/database/plugin/metadata/sqlite/models/stake_vote_delegation.go +27 -0
  96. package/database/plugin/metadata/sqlite/models/stake_vote_registration_delegation.go +27 -0
  97. package/database/plugin/metadata/sqlite/models/tip.go +26 -0
  98. package/database/plugin/metadata/sqlite/models/update_drep.go +27 -0
  99. package/database/plugin/metadata/sqlite/models/utxo.go +30 -0
  100. package/database/plugin/metadata/sqlite/models/vote_delegation.go +26 -0
  101. package/database/plugin/metadata/sqlite/models/vote_registration_delegation.go +26 -0
  102. package/database/plugin/metadata/sqlite/pool.go +240 -0
  103. package/database/plugin/metadata/sqlite/pparams.go +110 -0
  104. package/database/plugin/metadata/sqlite/tip.go +83 -0
  105. package/database/plugin/metadata/sqlite/utxo.go +292 -0
  106. package/database/plugin/metadata/store.go +168 -0
  107. package/database/plugin/option.go +190 -0
  108. package/database/plugin/plugin.go +20 -0
  109. package/database/plugin/register.go +118 -0
  110. package/database/pparams.go +145 -0
  111. package/database/tip.go +45 -0
  112. package/database/txn.go +147 -0
  113. package/database/types/types.go +74 -0
  114. package/database/types/types_test.go +83 -0
  115. package/database/utxo.go +263 -0
  116. package/dist/artifacts.json +1 -0
  117. package/dist/checksums.txt +22 -0
  118. package/dist/config.yaml +253 -0
  119. package/dist/dingo-0.5.0-SNAPSHOT-d9431e4.tar.gz +0 -0
  120. package/dist/dingo-0.5.0-SNAPSHOT-d9431e4.tar.gz.sbom.json +1 -0
  121. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_darwin_arm64.tar.gz +0 -0
  122. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_darwin_arm64.tar.gz.sbom.json +1 -0
  123. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_darwin_x86_64.tar.gz +0 -0
  124. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_darwin_x86_64.tar.gz.sbom.json +1 -0
  125. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_amd64.apk +0 -0
  126. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_amd64.apk.sbom.json +1 -0
  127. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_amd64.deb +0 -0
  128. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_amd64.deb.sbom.json +1 -0
  129. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_amd64.rpm +0 -0
  130. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_amd64.rpm.sbom.json +1 -0
  131. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.apk +0 -0
  132. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.apk.sbom.json +1 -0
  133. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.deb +0 -0
  134. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.deb.sbom.json +1 -0
  135. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.rpm +0 -0
  136. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.rpm.sbom.json +1 -0
  137. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.tar.gz +0 -0
  138. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_arm64.tar.gz.sbom.json +1 -0
  139. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_x86_64.tar.gz +0 -0
  140. package/dist/dingo_0.5.0-SNAPSHOT-d9431e4_linux_x86_64.tar.gz.sbom.json +1 -0
  141. package/dist/dingo_darwin_amd64_v1/dingo +0 -0
  142. package/dist/dingo_darwin_arm64_v8.0/dingo +0 -0
  143. package/dist/dingo_linux_amd64_v1/dingo +0 -0
  144. package/dist/dingo_linux_arm64_v8.0/dingo +0 -0
  145. package/dist/homebrew/dingo.rb +51 -0
  146. package/dist/metadata.json +1 -0
  147. package/event/event.go +141 -0
  148. package/event/event_test.go +115 -0
  149. package/event/metrics.go +44 -0
  150. package/go.mod +98 -0
  151. package/go.sum +358 -0
  152. package/internal/config/config.go +145 -0
  153. package/internal/config/config_test.go +118 -0
  154. package/internal/node/load.go +149 -0
  155. package/internal/node/node.go +176 -0
  156. package/internal/version/version.go +33 -0
  157. package/ledger/certs.go +113 -0
  158. package/ledger/chainsync.go +578 -0
  159. package/ledger/eras/allegra.go +154 -0
  160. package/ledger/eras/alonzo.go +156 -0
  161. package/ledger/eras/babbage.go +154 -0
  162. package/ledger/eras/byron.go +42 -0
  163. package/ledger/eras/conway.go +158 -0
  164. package/ledger/eras/eras.go +44 -0
  165. package/ledger/eras/mary.go +154 -0
  166. package/ledger/eras/shelley.go +164 -0
  167. package/ledger/error.go +19 -0
  168. package/ledger/event.go +50 -0
  169. package/ledger/metrics.go +53 -0
  170. package/ledger/queries.go +260 -0
  171. package/ledger/slot.go +127 -0
  172. package/ledger/slot_test.go +147 -0
  173. package/ledger/state.go +726 -0
  174. package/ledger/view.go +73 -0
  175. package/localstatequery.go +50 -0
  176. package/localtxmonitor.go +44 -0
  177. package/localtxsubmission.go +52 -0
  178. package/mempool/consumer.go +98 -0
  179. package/mempool/mempool.go +322 -0
  180. package/node.go +320 -0
  181. package/package.json +33 -0
  182. package/peergov/event.go +27 -0
  183. package/peergov/peer.go +67 -0
  184. package/peergov/peergov.go +290 -0
  185. package/peersharing.go +70 -0
  186. package/preview-local-topology.json +23 -0
  187. package/topology/topology.go +69 -0
  188. package/topology/topology_test.go +179 -0
  189. package/tracing.go +65 -0
  190. package/txsubmission.go +233 -0
  191. package/utxorpc/query.go +311 -0
  192. package/utxorpc/submit.go +395 -0
  193. package/utxorpc/sync.go +276 -0
  194. package/utxorpc/utxorpc.go +166 -0
  195. package/utxorpc/watch.go +310 -0
@@ -0,0 +1,726 @@
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 ledger
16
+
17
+ import (
18
+ "encoding/hex"
19
+ "errors"
20
+ "fmt"
21
+ "io"
22
+ "log/slog"
23
+ "slices"
24
+ "sync"
25
+ "time"
26
+
27
+ "github.com/blinklabs-io/dingo/chain"
28
+ "github.com/blinklabs-io/dingo/config/cardano"
29
+ "github.com/blinklabs-io/dingo/database"
30
+ "github.com/blinklabs-io/dingo/event"
31
+ "github.com/blinklabs-io/dingo/ledger/eras"
32
+ ouroboros "github.com/blinklabs-io/gouroboros"
33
+ "github.com/blinklabs-io/gouroboros/cbor"
34
+ "github.com/blinklabs-io/gouroboros/ledger"
35
+ lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
36
+ ochainsync "github.com/blinklabs-io/gouroboros/protocol/chainsync"
37
+ ocommon "github.com/blinklabs-io/gouroboros/protocol/common"
38
+ "github.com/prometheus/client_golang/prometheus"
39
+ )
40
+
41
+ const (
42
+ cleanupConsumedUtxosInterval = 5 * time.Minute
43
+ cleanupConsumedUtxosSlotWindow = 50000 // TODO: calculate this from params (#395)
44
+ )
45
+
46
+ type LedgerStateConfig struct {
47
+ Logger *slog.Logger
48
+ DataDir string
49
+ EventBus *event.EventBus
50
+ CardanoNodeConfig *cardano.CardanoNodeConfig
51
+ PromRegistry prometheus.Registerer
52
+ // Callback(s)
53
+ BlockfetchRequestRangeFunc BlockfetchRequestRangeFunc
54
+ }
55
+
56
+ // BlockfetchRequestRangeFunc describes a callback function used to start a blockfetch request for
57
+ // a range of blocks
58
+ type BlockfetchRequestRangeFunc func(ouroboros.ConnectionId, ocommon.Point, ocommon.Point) error
59
+
60
+ type LedgerState struct {
61
+ sync.RWMutex
62
+ chainsyncMutex sync.Mutex
63
+ config LedgerStateConfig
64
+ db *database.Database
65
+ timerCleanupConsumedUtxos *time.Timer
66
+ currentPParams lcommon.ProtocolParameters
67
+ currentEpoch database.Epoch
68
+ epochCache []database.Epoch
69
+ currentEra eras.EraDesc
70
+ currentTip ochainsync.Tip
71
+ currentTipBlockNonce []byte
72
+ metrics stateMetrics
73
+ chainsyncBlockEvents []BlockfetchEvent
74
+ chainsyncBlockfetchBusyTime time.Time
75
+ chainsyncBlockfetchDoneChan chan struct{}
76
+ chainsyncBlockfetchMutex sync.Mutex
77
+ chainsyncBlockfetchWaiting bool
78
+ chain *chain.Chain
79
+ }
80
+
81
+ func NewLedgerState(cfg LedgerStateConfig) (*LedgerState, error) {
82
+ ls := &LedgerState{
83
+ config: cfg,
84
+ }
85
+ if cfg.Logger == nil {
86
+ // Create logger to throw away logs
87
+ // We do this so we don't have to add guards around every log operation
88
+ cfg.Logger = slog.New(slog.NewJSONHandler(io.Discard, nil))
89
+ }
90
+ // Init metrics
91
+ ls.metrics.init(ls.config.PromRegistry)
92
+ // Load database
93
+ needsRecovery := false
94
+ db, err := database.New(cfg.Logger, cfg.DataDir)
95
+ if db == nil {
96
+ ls.config.Logger.Error(
97
+ "failed to create database",
98
+ "error",
99
+ "empty database returned",
100
+ "component",
101
+ "ledger",
102
+ )
103
+ return nil, errors.New("empty database returned")
104
+ }
105
+ ls.db = db
106
+ if err != nil {
107
+ var dbErr database.CommitTimestampError
108
+ if !errors.As(err, &dbErr) {
109
+ return nil, err
110
+ }
111
+ ls.config.Logger.Warn(
112
+ "database initialization error, needs recovery",
113
+ "error",
114
+ err,
115
+ "component",
116
+ "ledger",
117
+ )
118
+ needsRecovery = true
119
+ }
120
+ // Load chain
121
+ chain, err := chain.NewChain(
122
+ ls.db,
123
+ ls.config.EventBus,
124
+ true, // persistent
125
+ )
126
+ if err != nil {
127
+ return nil, err
128
+ }
129
+ ls.chain = chain
130
+ // Run recovery if needed
131
+ if needsRecovery {
132
+ if err := ls.recoverCommitTimestampConflict(); err != nil {
133
+ return nil, fmt.Errorf("failed to recover database: %w", err)
134
+ }
135
+ }
136
+ // Setup event handlers
137
+ ls.config.EventBus.SubscribeFunc(
138
+ ChainsyncEventType,
139
+ ls.handleEventChainsync,
140
+ )
141
+ ls.config.EventBus.SubscribeFunc(
142
+ BlockfetchEventType,
143
+ ls.handleEventBlockfetch,
144
+ )
145
+ // Schedule periodic process to purge consumed UTxOs outside of the rollback window
146
+ ls.scheduleCleanupConsumedUtxos()
147
+ // Load epoch info from DB
148
+ if err := ls.loadEpochs(nil); err != nil {
149
+ return nil, err
150
+ }
151
+ // Load current protocol parameters from DB
152
+ if err := ls.loadPParams(); err != nil {
153
+ return nil, err
154
+ }
155
+ // Load current tip
156
+ if err := ls.loadTip(); err != nil {
157
+ return nil, err
158
+ }
159
+ // Create genesis block
160
+ if err := ls.createGenesisBlock(); err != nil {
161
+ return nil, err
162
+ }
163
+ // Start goroutine to process new blocks
164
+ go ls.ledgerProcessBlocks()
165
+ return ls, nil
166
+ }
167
+
168
+ func (ls *LedgerState) recoverCommitTimestampConflict() error {
169
+ // Load current ledger tip
170
+ tmpTip, err := ls.db.GetTip(nil)
171
+ if err != nil {
172
+ return err
173
+ }
174
+ // Check if we can lookup tip block in chain
175
+ _, err = ls.chain.BlockByPoint(tmpTip.Point, nil)
176
+ if err != nil {
177
+ // Rollback to raw chain tip on error
178
+ chainTip := ls.chain.Tip()
179
+ if err = ls.rollback(chainTip.Point); err != nil {
180
+ return fmt.Errorf(
181
+ "failed to rollback ledger: %w",
182
+ err,
183
+ )
184
+ }
185
+ }
186
+ return nil
187
+ }
188
+
189
+ func (ls *LedgerState) Chain() *chain.Chain {
190
+ return ls.chain
191
+ }
192
+
193
+ func (ls *LedgerState) Close() error {
194
+ return ls.db.Close()
195
+ }
196
+
197
+ func (ls *LedgerState) scheduleCleanupConsumedUtxos() {
198
+ ls.Lock()
199
+ defer ls.Unlock()
200
+ if ls.timerCleanupConsumedUtxos != nil {
201
+ ls.timerCleanupConsumedUtxos.Stop()
202
+ }
203
+ ls.timerCleanupConsumedUtxos = time.AfterFunc(
204
+ cleanupConsumedUtxosInterval,
205
+ func() {
206
+ defer func() {
207
+ // Schedule the next run
208
+ ls.scheduleCleanupConsumedUtxos()
209
+ }()
210
+ // Get the current tip, since we're querying by slot
211
+ tip := ls.Tip()
212
+ // Delete UTxOs that are marked as deleted and older than our slot window
213
+ ls.config.Logger.Debug(
214
+ "cleaning up consumed UTxOs",
215
+ "component", "ledger",
216
+ )
217
+ ls.Lock()
218
+ err := ls.db.UtxosDeleteConsumed(
219
+ tip.Point.Slot-cleanupConsumedUtxosSlotWindow,
220
+ nil,
221
+ )
222
+ ls.Unlock()
223
+ if err != nil {
224
+ ls.config.Logger.Error(
225
+ "failed to cleanup consumed UTxOs",
226
+ "component", "ledger",
227
+ "error", err,
228
+ )
229
+ return
230
+ }
231
+ },
232
+ )
233
+ }
234
+
235
+ func (ls *LedgerState) rollback(point ocommon.Point) error {
236
+ // Start a transaction
237
+ txn := ls.db.Transaction(true)
238
+ err := txn.Do(func(txn *database.Txn) error {
239
+ // Delete rolled-back UTxOs
240
+ err := ls.db.UtxosDeleteRolledback(point.Slot, txn)
241
+ if err != nil {
242
+ return fmt.Errorf("remove rolled-back UTxOs: %w", err)
243
+ }
244
+ // Restore spent UTxOs
245
+ err = ls.db.UtxosUnspend(point.Slot, txn)
246
+ if err != nil {
247
+ return fmt.Errorf(
248
+ "restore spent UTxOs after rollback: %w",
249
+ err,
250
+ )
251
+ }
252
+ // Update tip
253
+ ls.currentTip = ochainsync.Tip{
254
+ Point: point,
255
+ }
256
+ if point.Slot > 0 {
257
+ rollbackBlock, err := ls.chain.BlockByPoint(point, txn)
258
+ if err != nil {
259
+ return err
260
+ }
261
+ ls.currentTip.BlockNumber = rollbackBlock.Number
262
+ }
263
+ if err = ls.db.SetTip(ls.currentTip, txn); err != nil {
264
+ return err
265
+ }
266
+ ls.updateTipMetrics()
267
+ return nil
268
+ })
269
+ if err != nil {
270
+ return err
271
+ }
272
+ // Reload tip
273
+ if err := ls.loadTip(); err != nil {
274
+ return err
275
+ }
276
+ var hash string
277
+ if point.Slot == 0 {
278
+ hash = "<genesis>"
279
+ } else {
280
+ hash = hex.EncodeToString(point.Hash)
281
+ }
282
+ ls.config.Logger.Info(
283
+ fmt.Sprintf(
284
+ "chain rolled back, new tip: %s at slot %d",
285
+ hash,
286
+ point.Slot,
287
+ ),
288
+ "component",
289
+ "ledger",
290
+ )
291
+ return nil
292
+ }
293
+
294
+ func (ls *LedgerState) transitionToEra(
295
+ txn *database.Txn,
296
+ nextEraId uint,
297
+ startEpoch uint64,
298
+ addedSlot uint64,
299
+ ) error {
300
+ nextEra := eras.Eras[nextEraId]
301
+ if nextEra.HardForkFunc != nil {
302
+ // Perform hard fork
303
+ // This generally means upgrading pparams from previous era
304
+ newPParams, err := nextEra.HardForkFunc(
305
+ ls.config.CardanoNodeConfig,
306
+ ls.currentPParams,
307
+ )
308
+ if err != nil {
309
+ return err
310
+ }
311
+ ls.currentPParams = newPParams
312
+ ls.config.Logger.Debug(
313
+ "updated protocol params",
314
+ "pparams",
315
+ fmt.Sprintf("%#v", ls.currentPParams),
316
+ )
317
+ // Write pparams update to DB
318
+ pparamsCbor, err := cbor.Encode(&ls.currentPParams)
319
+ if err != nil {
320
+ return err
321
+ }
322
+ err = ls.db.SetPParams(
323
+ pparamsCbor,
324
+ addedSlot,
325
+ startEpoch,
326
+ nextEraId,
327
+ txn,
328
+ )
329
+ if err != nil {
330
+ return err
331
+ }
332
+ }
333
+ ls.currentEra = nextEra
334
+ return nil
335
+ }
336
+
337
+ // consumeUtxo marks a UTxO as "deleted" without actually deleting it. This allows for a UTxO
338
+ // to be easily on rollback
339
+ func (ls *LedgerState) consumeUtxo(
340
+ txn *database.Txn,
341
+ utxoId ledger.TransactionInput,
342
+ slot uint64,
343
+ ) error {
344
+ return ls.db.UtxoConsume(
345
+ utxoId,
346
+ slot,
347
+ txn,
348
+ )
349
+ }
350
+
351
+ func (ls *LedgerState) ledgerProcessBlocks() {
352
+ iter, err := ls.chain.FromPoint(ls.currentTip.Point, false)
353
+ if err != nil {
354
+ ls.config.Logger.Error(
355
+ "failed to create chain iterator: " + err.Error(),
356
+ )
357
+ return
358
+ }
359
+ shouldBlock := false
360
+ // We chose 500 as an arbitrary max batch size. A "chain extended" message will be logged after each batch
361
+ nextBatch := make([]*chain.ChainIteratorResult, 0, 500)
362
+ var next, nextRollback *chain.ChainIteratorResult
363
+ var tmpBlock ledger.Block
364
+ var needsRollback bool
365
+ var end, i int
366
+ var txn *database.Txn
367
+ for {
368
+ // Gather up next batch of blocks
369
+ 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
378
+ }
379
+ shouldBlock = true
380
+ // Break out of inner loop to flush DB transaction and log
381
+ break
382
+ }
383
+ if next == nil {
384
+ ls.config.Logger.Error("next block from chain iterator is nil")
385
+ return
386
+ }
387
+ nextBatch = append(nextBatch, next)
388
+ // End batch if there's a rollback, since we need special processing
389
+ if next.Rollback {
390
+ break
391
+ }
392
+ // Don't exceed our pre-allocated capacity
393
+ if len(nextBatch) == cap(nextBatch) {
394
+ break
395
+ }
396
+ }
397
+ // Process batch in groups of 50 to stay under DB txn limits
398
+ needsRollback = false
399
+ for i = 0; i < len(nextBatch); i += 50 {
400
+ ls.Lock()
401
+ end = min(
402
+ len(nextBatch),
403
+ i+50,
404
+ )
405
+ txn = ls.db.Transaction(true)
406
+ err = txn.Do(func(txn *database.Txn) error {
407
+ 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
+ // Process block
415
+ tmpBlock, err = next.Block.Decode()
416
+ if err != nil {
417
+ return err
418
+ }
419
+ if err = ls.ledgerProcessBlock(txn, next.Point, tmpBlock); err != nil {
420
+ return err
421
+ }
422
+ // Update tip
423
+ ls.currentTip = ochainsync.Tip{
424
+ Point: next.Point,
425
+ BlockNumber: next.Block.Number,
426
+ }
427
+ // Update tip block nonce
428
+ ls.currentTipBlockNonce = next.Block.Nonce
429
+ }
430
+ // Update tip in database
431
+ if err := ls.db.SetTip(ls.currentTip, txn); err != nil {
432
+ return err
433
+ }
434
+ ls.updateTipMetrics()
435
+ return nil
436
+ })
437
+ if err != nil {
438
+ ls.Unlock()
439
+ ls.config.Logger.Error(
440
+ "failed to process block: " + err.Error(),
441
+ )
442
+ return
443
+ }
444
+ ls.Unlock()
445
+ }
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
+ if len(nextBatch) > 0 {
468
+ // Clear out batch buffer
469
+ nextBatch = slices.Delete(nextBatch, 0, len(nextBatch))
470
+ ls.config.Logger.Info(
471
+ fmt.Sprintf(
472
+ "chain extended, new tip: %x at slot %d",
473
+ ls.currentTip.Point.Hash,
474
+ ls.currentTip.Point.Slot,
475
+ ),
476
+ "component",
477
+ "ledger",
478
+ )
479
+ }
480
+ }
481
+ }
482
+
483
+ func (ls *LedgerState) ledgerProcessBlock(
484
+ txn *database.Txn,
485
+ point ocommon.Point,
486
+ block ledger.Block,
487
+ ) error {
488
+ // Check that we're processing things in order
489
+ if len(ls.currentTip.Point.Hash) > 0 {
490
+ if string(
491
+ block.PrevHash().Bytes(),
492
+ ) != string(
493
+ ls.currentTip.Point.Hash,
494
+ ) {
495
+ return fmt.Errorf(
496
+ "block %s (with prev hash %s) does not fit on current chain tip (%x)",
497
+ block.Hash().String(),
498
+ block.PrevHash().String(),
499
+ ls.currentTip.Point.Hash,
500
+ )
501
+ }
502
+ }
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 {
510
+ return err
511
+ }
512
+ }
513
+ }
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
522
+ }
523
+ }
524
+ return nil
525
+ }
526
+
527
+ func (ls *LedgerState) updateTipMetrics() {
528
+ // Update metrics
529
+ ls.metrics.blockNum.Set(float64(ls.currentTip.BlockNumber))
530
+ ls.metrics.slotNum.Set(float64(ls.currentTip.Point.Slot))
531
+ ls.metrics.slotInEpoch.Set(
532
+ float64(ls.currentTip.Point.Slot - ls.currentEpoch.StartSlot),
533
+ )
534
+ }
535
+
536
+ func (ls *LedgerState) loadPParams() error {
537
+ pparams, err := ls.db.GetPParams(
538
+ ls.currentEpoch.EpochId,
539
+ ls.currentEra.DecodePParamsFunc,
540
+ nil,
541
+ )
542
+ if err != nil {
543
+ return err
544
+ }
545
+ ls.currentPParams = pparams
546
+ return nil
547
+ }
548
+
549
+ func (ls *LedgerState) loadEpochs(txn *database.Txn) error {
550
+ // Load and cache all epochs
551
+ epochs, err := ls.db.GetEpochs(txn)
552
+ if err != nil {
553
+ return err
554
+ }
555
+ ls.epochCache = epochs
556
+ // Set current epoch
557
+ if len(epochs) > 0 {
558
+ ls.currentEpoch = epochs[len(epochs)-1]
559
+ ls.currentEra = eras.Eras[ls.currentEpoch.EraId]
560
+ }
561
+ // Update metrics
562
+ ls.metrics.epochNum.Set(float64(ls.currentEpoch.EpochId))
563
+ return nil
564
+ }
565
+
566
+ func (ls *LedgerState) loadTip() error {
567
+ tmpTip, err := ls.db.GetTip(nil)
568
+ if err != nil {
569
+ return err
570
+ }
571
+ ls.currentTip = tmpTip
572
+ // Load tip block and set cached block nonce
573
+ if ls.currentTip.Point.Slot > 0 {
574
+ tipBlock, err := ls.chain.BlockByPoint(ls.currentTip.Point, nil)
575
+ if err != nil {
576
+ return err
577
+ }
578
+ ls.currentTipBlockNonce = tipBlock.Nonce
579
+ }
580
+ ls.updateTipMetrics()
581
+ return nil
582
+ }
583
+
584
+ func (ls *LedgerState) GetBlock(point ocommon.Point) (*database.Block, error) {
585
+ ret, err := ls.chain.BlockByPoint(point, nil)
586
+ if err != nil {
587
+ return nil, err
588
+ }
589
+ return &ret, nil
590
+ }
591
+
592
+ // RecentChainPoints returns the requested count of recent chain points in descending order. This is used mostly
593
+ // for building a set of intersect points when acting as a chainsync client
594
+ func (ls *LedgerState) RecentChainPoints(count int) ([]ocommon.Point, error) {
595
+ tmpBlocks, err := database.BlocksRecent(ls.db, count)
596
+ if err != nil {
597
+ return nil, err
598
+ }
599
+ ret := []ocommon.Point{}
600
+ var tmpBlock database.Block
601
+ for _, tmpBlock = range tmpBlocks {
602
+ ret = append(
603
+ ret,
604
+ ocommon.NewPoint(tmpBlock.Slot, tmpBlock.Hash),
605
+ )
606
+ }
607
+ return ret, nil
608
+ }
609
+
610
+ // GetIntersectPoint returns the intersect between the specified points and the current chain
611
+ func (ls *LedgerState) GetIntersectPoint(
612
+ points []ocommon.Point,
613
+ ) (*ocommon.Point, error) {
614
+ tip := ls.Tip()
615
+ var ret ocommon.Point
616
+ var tmpBlock database.Block
617
+ var err error
618
+ foundOrigin := false
619
+ txn := ls.db.Transaction(false)
620
+ err = txn.Do(func(txn *database.Txn) error {
621
+ for _, point := range points {
622
+ // Ignore points with a slot later than our current tip
623
+ if point.Slot > tip.Point.Slot {
624
+ continue
625
+ }
626
+ // Ignore points with a slot earlier than an existing match
627
+ if point.Slot < ret.Slot {
628
+ continue
629
+ }
630
+ // Check for special origin point
631
+ if point.Slot == 0 && len(point.Hash) == 0 {
632
+ foundOrigin = true
633
+ continue
634
+ }
635
+ // Lookup block in metadata DB
636
+ tmpBlock, err = ls.chain.BlockByPoint(point, txn)
637
+ if err != nil {
638
+ if errors.Is(err, chain.ErrBlockNotFound) {
639
+ continue
640
+ }
641
+ return err
642
+ }
643
+ // Update return value
644
+ ret.Slot = tmpBlock.Slot
645
+ ret.Hash = tmpBlock.Hash
646
+ }
647
+ return nil
648
+ })
649
+ if err != nil {
650
+ return nil, err
651
+ }
652
+ if ret.Slot > 0 || foundOrigin {
653
+ return &ret, nil
654
+ }
655
+ return nil, nil
656
+ }
657
+
658
+ // GetChainFromPoint returns a ChainIterator starting at the specified point. If inclusive is true, the iterator
659
+ // will start at the requested point, otherwise it will start at the next block.
660
+ func (ls *LedgerState) GetChainFromPoint(
661
+ point ocommon.Point,
662
+ inclusive bool,
663
+ ) (*chain.ChainIterator, error) {
664
+ return ls.chain.FromPoint(point, inclusive)
665
+ }
666
+
667
+ // Tip returns the current chain tip
668
+ func (ls *LedgerState) Tip() ochainsync.Tip {
669
+ return ls.currentTip
670
+ }
671
+
672
+ // GetCurrentPParams returns the currentPParams value
673
+ func (ls *LedgerState) GetCurrentPParams() lcommon.ProtocolParameters {
674
+ return ls.currentPParams
675
+ }
676
+
677
+ // UtxoByRef returns a single UTxO by reference
678
+ func (ls *LedgerState) UtxoByRef(
679
+ txId []byte,
680
+ outputIdx uint32,
681
+ ) (database.Utxo, error) {
682
+ return ls.db.UtxoByRef(txId, outputIdx, nil)
683
+ }
684
+
685
+ // UtxosByAddress returns all UTxOs that belong to the specified address
686
+ func (ls *LedgerState) UtxosByAddress(
687
+ addr ledger.Address,
688
+ ) ([]database.Utxo, error) {
689
+ ret := []database.Utxo{}
690
+ utxos, err := ls.db.UtxosByAddress(addr, nil)
691
+ if err != nil {
692
+ return ret, err
693
+ }
694
+ var tmpUtxo database.Utxo
695
+ for _, utxo := range utxos {
696
+ tmpUtxo = database.Utxo(utxo)
697
+ ret = append(ret, tmpUtxo)
698
+ }
699
+ return ret, nil
700
+ }
701
+
702
+ // ValidateTx runs ledger validation on the provided transaction
703
+ func (ls *LedgerState) ValidateTx(
704
+ tx lcommon.Transaction,
705
+ ) error {
706
+ if ls.currentEra.ValidateTxFunc != nil {
707
+ txn := ls.db.Transaction(false)
708
+ err := txn.Do(func(txn *database.Txn) error {
709
+ lv := &LedgerView{
710
+ txn: txn,
711
+ ls: ls,
712
+ }
713
+ err := ls.currentEra.ValidateTxFunc(
714
+ tx,
715
+ ls.currentTip.Point.Slot,
716
+ lv,
717
+ ls.currentPParams,
718
+ )
719
+ return err
720
+ })
721
+ if err != nil {
722
+ return fmt.Errorf("TX %s failed validation: %w", tx.Hash(), err)
723
+ }
724
+ }
725
+ return nil
726
+ }