-
Notifications
You must be signed in to change notification settings - Fork 288
/
utxocache.go
1086 lines (963 loc) · 40.1 KB
/
utxocache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package blockchain
import (
"context"
"fmt"
"math"
"sync"
"time"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/database/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/wire"
)
const (
// outpointSize is the size of an outpoint on a 64-bit platform. It is
// equivalent to what unsafe.Sizeof(wire.OutPoint{}) returns on a 64-bit
// platform.
outpointSize = 56
// pointerSize is the size of a pointer on a 64-bit platform.
pointerSize = 8
// p2pkhScriptLen is the length of a standard pay-to-pubkey-hash script. It
// is used in the calculation to approximate the average size of a utxo
// entry when setting the initial capacity of the cache.
p2pkhScriptLen = 25
// mapOverhead is the number of bytes per entry to use when approximating
// the memory overhead of the entries map itself (i.e. the memory usage due
// to internals of the map, such as the underlying buckets that are
// allocated). This number was determined by inspecting the true size of
// the map with various numbers of entries and comparing it with the total
// size of all entries in the map. The average overhead came out to 57
// bytes per entry.
mapOverhead = 57
// evictionPercentage is the targeted percentage of entries to evict from
// the cache when its maximum size has been reached.
//
// A lower percentage will result in a higher overall hit ratio for the
// cache, and thus better performance, but will require eviction again
// sooner. This value was selected to keep the hit ratio of the cache as
// high as possible while still evicting a significant portion of the cache
// when it reaches its maximum allowed size.
evictionPercentage = 0.15
// periodicFlushInterval is the amount of time to wait before a periodic
// flush is required.
//
// The cache is flushed periodically during initial block download to avoid
// requiring a flush that would take a significant amount of time on
// shutdown (or, in the case of an unclean shutdown, a significant amount of
// time to initialize the cache when restarted).
periodicFlushInterval = time.Minute * 2
)
// UtxoCacher represents a utxo cache that sits on top of a utxo set backend.
//
// The interface contract requires that all of these methods are safe for
// concurrent access.
type UtxoCacher interface {
// Commit updates the cache based on the state of each entry in the provided
// view.
//
// All entries in the provided view that are marked as modified and spent
// are removed from the view. Additionally, all entries that are added to
// the cache are removed from the provided view.
Commit(view *UtxoViewpoint) error
// FetchBackendState returns the current state of the UTXO set in the
// backend.
FetchBackendState() (*UtxoSetState, error)
// FetchEntries adds the requested transaction outputs to the provided view.
// It first checks the cache for each output, and if an output does not
// exist in the cache, it will fetch it from the backend.
//
// Upon completion of this function, the view will contain an entry for each
// requested outpoint. Spent outputs, or those which otherwise don't exist,
// will result in a nil entry in the view.
FetchEntries(filteredSet ViewFilteredSet, view *UtxoViewpoint) error
// FetchEntry returns the specified transaction output from the utxo set.
// If the output exists in the cache, it is returned immediately.
// Otherwise, it fetches the output from the backend, caches it, and returns
// it to the caller. The entry that is returned can safely be mutated by
// the caller without invalidating the cache.
//
// When there is no entry for the provided output, nil will be returned for
// both the entry and the error.
FetchEntry(outpoint wire.OutPoint) (*UtxoEntry, error)
// FetchStats returns statistics on the current utxo set.
FetchStats(bestHash *chainhash.Hash, bestHeight uint32) (*UtxoStats, error)
// Initialize initializes the utxo cache and underlying utxo backend. This
// entails running any database migrations as well as ensuring that the utxo
// set is caught up to the tip of the best chain.
Initialize(ctx context.Context, b *BlockChain) error
// MaybeFlush conditionally flushes the cache to the backend. A flush can
// be forced by setting the force flush parameter.
MaybeFlush(bestHash *chainhash.Hash, bestHeight uint32, forceFlush bool,
logFlush bool) error
}
// UtxoCache is an unspent transaction output cache that sits on top of the
// utxo set backend and provides significant runtime performance benefits at
// the cost of some additional memory usage. It drastically reduces the amount
// of reading and writing to disk, especially during initial block download when
// a very large number of blocks are being processed in quick succession.
//
// The UtxoCache is a read-through cache. All utxo reads go through the cache.
// When there is a cache miss, the cache loads the missing data from the
// backend, caches it, and returns it to the caller.
//
// The UtxoCache is a write-back cache. Writes to the cache are acknowledged
// by the cache immediately but are only periodically flushed to the backend.
// This allows intermediate steps to effectively be skipped. For example, a
// utxo that is created and then spent in between flushes never needs to be
// written to the utxo set in the backend.
//
// Due to the write-back nature of the cache, at any given time the backend
// may not be in sync with the cache, and therefore all utxo reads and writes
// MUST go through the cache, and never read or write to the backend directly.
type UtxoCache struct {
// backend is the backend that contains the UTXO set. It is set when the
// instance is created and is not changed afterward.
backend UtxoBackend
// flushBlockDB defines the function to use to flush the block database to
// disk. The block database is always flushed to disk before the UTXO cache
// writes to disk in order to maintain a recoverable state in the event of
// an unclean shutdown. It is set when the instance is created and is not
// changed afterward.
flushBlockDB func() error
// maxSize is the maximum allowed size of the utxo cache, in bytes. It is
// set when the instance is created and is not changed afterward.
maxSize uint64
// cacheLock protects access to the fields in the struct below this point.
// A standard mutex is used rather than a read-write mutex since the cache
// will often write when reads result in a cache miss, so it is generally
// not worth the additional overhead of using a read-write mutex.
cacheLock sync.Mutex
// entries holds the cached utxo entries.
entries map[wire.OutPoint]*UtxoEntry
// lastFlushHash is the block hash of the last flush. It is used to compare
// the state of the cache to the utxo set state in the backend so that the
// utxo set can properly be initialized in the case that the latest utxo
// data had not been flushed to the backend yet.
lastFlushHash chainhash.Hash
// lastFlushTime is the last time that the cache was flushed to the backend.
// It is used to determine when to periodically flush the cache to the
// backend during initial block download even if the cache isn't full to
// minimize the amount of progress lost if an unclean shutdown occurs.
lastFlushTime time.Time
// lastEvictionHeight is the block height of the last eviction. When the
// cache reaches the maximum allowed size, entries are evicted based on the
// height of the block that they are contained in, and last eviction height
// is updated to the current height.
lastEvictionHeight uint32
// totalEntrySize is the total size of all utxo entries in the cache, in
// bytes. It is updated whenever an entry is added or removed from the
// cache.
totalEntrySize uint64
// The following fields track the total number of cache hits and misses and
// are used to measure the overall cache hit ratio.
hits uint64
misses uint64
// timeNow defines the function to use to get the current local time. It
// defaults to time.Now but an alternative function can be provided for
// testing purposes.
timeNow func() time.Time
// maybeFlushFn defines the function to use for potentially flushing the
// cache to the backend. It defaults to the exported MaybeFlush method of
// this cache instance, but an alternative can be provided for testing
// purposes.
maybeFlushFn func(*chainhash.Hash, uint32, bool, bool) error
}
// Ensure UtxoCache implements the UtxoCacher interface.
var _ UtxoCacher = (*UtxoCache)(nil)
// UtxoCacheConfig is a descriptor which specifies the utxo cache instance
// configuration.
type UtxoCacheConfig struct {
// Backend defines the backend which houses the utxo set.
//
// This field is required.
Backend UtxoBackend
// FlushBlockDB defines the function to use to flush the block database to
// disk. The block database is always flushed to disk before the UTXO cache
// writes to disk in order to maintain a recoverable state in the event of
// an unclean shutdown.
//
// This field is required.
FlushBlockDB func() error
// MaxSize defines the maximum allowed size of the utxo cache, in bytes.
//
// This field is required.
MaxSize uint64
}
// NewUtxoCache returns a UtxoCache instance using the provided configuration
// details.
func NewUtxoCache(config *UtxoCacheConfig) *UtxoCache {
// Approximate the maximum number of entries allowed in the cache in order
// to set the initial capacity of the entries map.
avgEntrySize := mapOverhead + outpointSize + pointerSize + baseEntrySize +
p2pkhScriptLen
maxEntries := math.Ceil(float64(config.MaxSize) / float64(avgEntrySize))
c := &UtxoCache{
backend: config.Backend,
flushBlockDB: config.FlushBlockDB,
maxSize: config.MaxSize,
entries: make(map[wire.OutPoint]*UtxoEntry, uint64(maxEntries)),
lastFlushTime: time.Now(),
timeNow: time.Now,
}
c.maybeFlushFn = c.MaybeFlush
return c
}
// totalSize returns the total size of the cache on a 64-bit platform, in bytes.
// Note that this only takes the entries map into account, which represents the
// vast majority of the memory that the cache uses, and does not include the
// memory usage of other fields in the utxo cache struct.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) totalSize() uint64 {
numEntries := uint64(len(c.entries))
return mapOverhead*numEntries + outpointSize*numEntries +
pointerSize*numEntries + c.totalEntrySize
}
// hitRatio returns the percentage of cache lookups that resulted in a cache
// hit.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) hitRatio() float64 {
totalLookups := c.hits + c.misses
if totalLookups == 0 {
return 100
}
return float64(c.hits) / float64(totalLookups) * 100
}
// addEntry adds the specified output to the cache. The entry being added MUST
// NOT be mutated by the caller after being passed to this function.
//
// Note that this function does not check if the entry is unspendable and
// therefore the caller should ensure that the entry is spendable before adding
// it to the cache.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) addEntry(outpoint wire.OutPoint, entry *UtxoEntry) {
// The entry will either be added to the cache when there is not already
// an existing entry for the specified outpoint or will overwrite the
// existing one when there is, but in either case the resulting cache
// entry is modified and thus needs to be marked as such.
entry.state |= utxoStateModified
// Attempt to get an existing entry from the cache.
cachedEntry := c.entries[outpoint]
// Mark the entry as fresh, add it to the cache, and update the total
// cache size when there is not already an existing cache entry.
if cachedEntry == nil {
entry.state |= utxoStateFresh
c.entries[outpoint] = entry
c.totalEntrySize += entry.size()
return
}
// There is an existing entry in the cache at this point.
//
// Ensure the state of whether or not the existing entry is fresh is
// maintained, overwrite the existing entry, and update the total cache size
// accordingly.
//
// All fields are overwritten because it's possible (although extremely
// unlikely) that the existing entry is being replaced by a different
// transaction with the same hash. This is allowed so long as the previous
// transaction is fully spent.
if cachedEntry.isFresh() {
entry.state |= utxoStateFresh
} else {
entry.state &^= utxoStateFresh
}
c.entries[outpoint] = entry
c.totalEntrySize -= cachedEntry.size()
c.totalEntrySize += entry.size()
}
// spendEntry marks the specified output as spent.
//
// When the provided output does not already have an associated entry in the
// cache, the cache will be populated with the entry from the backend (if one
// exists) and marked spent to ensure the utxo is later removed from the backend
// on the next cache flush.
//
// An error will be returned in the case the output exists in the cache and is
// already spent.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) spendEntry(outpoint wire.OutPoint) error {
// Attempt to get the entry associated with the output from the cache.
//
// In the case there is a nil entry in the cache for the output, it does not
// exist in the backend. This is because either an attempt to load the utxo
// from the backend was made and the nil entry was added to the cache to
// avoid further lookups against the backend when the output is known to not
// exist or the output was added to the cache and spent in between flushes
// to the backend and thus has been pruned.
//
// In either case, the cache does not need to be updated, so there is
// nothing more to do.
cachedEntry, found := c.entries[outpoint]
if found && cachedEntry == nil {
return nil
}
// Sanity check double spends of existing entries. This should be
// impossible to hit since a view should never be committed until after
// double spend checks have already been done. However, it is good practice
// to be paranoid and double check assumptions when it comes to potential
// double spends.
if cachedEntry != nil && cachedEntry.IsSpent() {
return AssertError(fmt.Sprintf("attempt to double spend output %v in "+
"view commit", outpoint))
}
// Attempt to repopulate the cache from the backend when there is no entry
// for the output in the cache. This is necessary because the associated
// utxo might exist in the backend but not have been loaded into the cache
// yet or have otherwise been evicted.
//
// Thus, in order to ensure the utxo is removed from the backend on the next
// cache flush in the case it exists, the associated cache entry from the
// backend is loaded and marked spent.
if !found {
// Account for the cache miss and attempt to load the entry for the utxo
// from the backend.
//
// Notice that this intentionally attempts to load the entry from the
// backend directly as opposed to using the normal cache entry fetching
// method since that involves querying the cache again which would be
// wasteful since it is already known to not exist and also employs
// slightly different logic regarding populating the cache.
//
// NOTE: Missing backend entries are not an error.
c.misses++
backendEntry, err := c.backend.FetchEntry(outpoint)
if err != nil {
return err
}
// The cache does not need to be updated when the associated utxo does
// not exist in the backend, so there is nothing more to do in that
// case.
//
// Note that a nil entry is not added to cache for the output here since
// there should realistically never be any future lookups for the spent
// output given that would be a double spend.
if backendEntry == nil {
return nil
}
// Add the entry to the cache and update its total entry size.
c.entries[outpoint] = backendEntry
c.totalEntrySize += backendEntry.size()
// Mark the output as spent and modified.
backendEntry.Spend()
return nil
}
// Remove the entry associated with the output from the cache when the
// backend does not need to be updated because the backend does not have an
// associated utxo (aka the cache entry is fresh).
//
// This is an optimization to skip writing to the backend for outputs that
// are added and spent in between flushes to the backend.
if cachedEntry.isFresh() {
// The entry in the map is marked as nil rather than deleting it so that
// subsequent lookups for the outpoint will still result in a cache hit
// and avoid querying the backend.
c.entries[outpoint] = nil
c.totalEntrySize -= cachedEntry.size()
return nil
}
// Mark the output as spent and modified.
cachedEntry.Spend()
return nil
}
// fetchEntry returns the specified transaction output from the utxo set. If
// the output exists in the cache, it is returned immediately. Otherwise, it
// fetches the output from the backend, caches it, and returns it to the
// caller. A cloned copy of the entry is returned so it can safely be mutated
// by the caller without invalidating the cache. The returned entry will not
// have the modified or fresh flags set to ensure the cache state is separate
// from the caller state.
//
// When there is no entry for the provided output, nil will be returned for both
// the entry and the error.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) fetchEntry(outpoint wire.OutPoint) (*UtxoEntry, error) {
// Return a cloned copy of the cache entry when one exists for the provided
// output. A cloned copy of the entry is returned so it can safely be
// mutated by the caller without invalidating the cache.
//
// Also clear the modified and fresh flags for the returned entry to ensure
// the cache state is separate from the view state.
var entry *UtxoEntry
if entry, found := c.entries[outpoint]; found {
c.hits++
clonedEntry := entry.Clone()
if clonedEntry != nil {
clonedEntry.state &^= utxoStateModified | utxoStateFresh
}
return clonedEntry, nil
}
// Increment cache misses.
c.misses++
// Fetch the entry from the backend.
//
// NOTE: Missing entries are not considered an error here and instead
// will result in nil entries in the view. This is intentionally done
// so other code can use the presence of an entry in the view as a way
// to unnecessarily avoid attempting to reload it from the backend.
entry, err := c.backend.FetchEntry(outpoint)
if err != nil {
return nil, err
}
// Update the total entry size of the cache.
if entry != nil {
c.totalEntrySize += entry.size()
}
// Add the entry to the cache and return it. A cloned copy of the entry is
// returned so it can safely be mutated by the caller without invalidating
// the cache.
//
// Also note that all entries loaded from the backend will not have any
// state flags set since they are memory only flags.
c.entries[outpoint] = entry
return entry.Clone(), nil
}
// FetchEntry returns the specified transaction output from the utxo set. If
// the output exists in the cache, it is returned immediately. Otherwise, it
// fetches the output from the backend, caches it, and returns it to the
// caller. A cloned copy of the entry is returned so it can safely be mutated
// by the caller without invalidating the cache.
//
// When there is no entry for the provided output, nil will be returned for both
// the entry and the error.
//
// This function is safe for concurrent access.
func (c *UtxoCache) FetchEntry(outpoint wire.OutPoint) (*UtxoEntry, error) {
c.cacheLock.Lock()
entry, err := c.fetchEntry(outpoint)
c.cacheLock.Unlock()
return entry, err
}
// FetchEntries adds the requested transaction outputs to the provided view. It
// first checks the cache for each output, and if an output does not exist in
// the cache, it will fetch it from the backend.
//
// Upon completion of this function, the view will contain an entry for each
// requested outpoint. Spent outputs, or those which otherwise don't exist,
// will result in a nil entry in the view.
//
// This function is safe for concurrent access.
func (c *UtxoCache) FetchEntries(filteredSet ViewFilteredSet, view *UtxoViewpoint) error {
c.cacheLock.Lock()
for outpoint := range filteredSet {
entry, err := c.fetchEntry(outpoint)
if err != nil {
c.cacheLock.Unlock()
return err
}
// NOTE: Missing entries are not considered an error here and instead
// will result in nil entries in the view. This is intentionally done
// so other code can use the presence of an entry in the view as a way
// to unnecessarily avoid attempting to reload it from the backend.
view.entries[outpoint] = entry
}
c.cacheLock.Unlock()
return nil
}
// FetchBackendState returns the current state of the UTXO set in the backend.
func (c *UtxoCache) FetchBackendState() (*UtxoSetState, error) {
return c.backend.FetchState()
}
// FetchStats returns statistics on the current utxo set.
func (c *UtxoCache) FetchStats(bestHash *chainhash.Hash, bestHeight uint32) (*UtxoStats, error) {
// Force a UTXO cache flush. This is required in order for the backend to
// fetch statistics on the full UTXO set.
err := c.maybeFlushFn(bestHash, bestHeight, true, false)
if err != nil {
return nil, err
}
return c.backend.FetchStats()
}
// Commit updates the cache based on the state of each entry in the provided
// view.
//
// All entries in the provided view that are marked as modified and spent are
// removed from the view. Additionally, all entries that are added to the cache
// are removed from the provided view.
//
// This function is safe for concurrent access.
func (c *UtxoCache) Commit(view *UtxoViewpoint) error {
defer c.cacheLock.Unlock()
c.cacheLock.Lock()
for outpoint, entry := range view.entries {
// There is nothing to update in the cache when the view entry is nil,
// so remove it from the view and continue to the next entry.
if entry == nil {
delete(view.entries, outpoint)
continue
}
// There is nothing to update in the cache nor the view when the view
// entry was not modified.
if !entry.isModified() {
continue
}
// ---------------------------------------------------------------------
// NOTE: As an optimization in order to avoid an additional allocation,
// the cache takes ownership of all modified entries from the view.
//
// Thus, unless there is an error preventing the cache from taking
// ownership and ultimately causing the commit to fail, the entry will
// be removed from the view in all remaining paths to ensure that it is
// not mutated by the caller after being added to the cache.
//
// This does cause the view to refetch the entry if it is requested
// again after being removed. However, this only really occurs during
// reorgs, whereas committing the view to the cache happens with every
// connected block, so this optimizes for the much more common case.
// ---------------------------------------------------------------------
// There is nothing to update in the cache when the output is spent by a
// transaction later in the regular transaction tree of the same block
// since such outputs never exist beyond the scope of the view and
// therefore do not have any cache entries.
//
// Further, it is no longer needed in the view either since it's already
// spent and thus nothing else in future blocks could possibly spend it.
//
// Thus, remove the entry from the view and continue to the next one.
if entry.isSpentByZeroConf() {
// Sanity check zero confirmation spends are also marked as spent.
if !entry.IsSpent() {
return AssertError(fmt.Sprintf("output %v from view@%s is "+
"simultaneously marked as spent by a transaction later in "+
"the same block and not spent", outpoint,
view.BestHash()))
}
delete(view.entries, outpoint)
continue
}
// Mark the spent view entry as modified and spent in the cache and
// remove it from the view.
//
// This might entail populating the cache with the unspent output from
// the backend when there is not already a corresponding entry in the
// cache to ensure the utxo is later removed from the backend on the
// next cache flush.
if entry.IsSpent() {
if err := c.spendEntry(outpoint); err != nil {
return err
}
delete(view.entries, outpoint)
continue
}
// Update the existing entry in the cache or add a new one whenever the
// view entry is both modified and unspent and remove it from the view.
c.addEntry(outpoint, entry)
delete(view.entries, outpoint)
}
return nil
}
// calcEvictionHeight returns the eviction height based on the best height of
// the main chain and the last eviction height. All entries that are contained
// in a block at a height less than the eviction height will be evicted from the
// cache when the cache reaches its maximum allowed size.
//
// Eviction is based on height since the height of the block that an entry is
// contained in is a proxy for how old the utxo is. On average, recent utxos
// are much more likely to be spent in upcoming blocks than older utxos, so the
// strategy used is to evict the oldest utxos in order to maximize the hit ratio
// of the cache.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) calcEvictionHeight(bestHeight uint32) uint32 {
if bestHeight < c.lastEvictionHeight {
return bestHeight
}
lastEvictionDepth := bestHeight - c.lastEvictionHeight
numBlocksToEvict := math.Ceil(float64(lastEvictionDepth) * evictionPercentage)
return c.lastEvictionHeight + uint32(numBlocksToEvict)
}
// shouldFlush returns whether or not a flush should be performed.
//
// If the maximum size of the cache has been reached, or if the periodic flush
// interval has been reached, then a flush is required.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) shouldFlush(bestHash *chainhash.Hash) bool {
// No need to flush if the cache has already been flushed through the best
// hash.
if c.lastFlushHash == *bestHash {
return false
}
// Flush if the max size of the cache has been reached.
if c.totalSize() >= c.maxSize {
return true
}
// Flush if the periodic flush interval has been reached.
return time.Since(c.lastFlushTime) >= periodicFlushInterval
}
// flush commits all modified entries to the backend and conditionally evicts
// entries.
//
// Entries that are nil or spent are always evicted since they are
// unlikely to be accessed again. Additionally, if the cache has reached its
// maximum size, entries are evicted based on the height of the block that they
// are contained in.
//
// This function MUST be called with the cache lock held.
func (c *UtxoCache) flush(bestHash *chainhash.Hash, bestHeight uint32, logFlush bool) error {
// If the maximum allowed size of the cache has been reached, determine the
// eviction height.
var evictionHeight uint32
memUsage := c.totalSize()
if memUsage >= c.maxSize {
evictionHeight = c.calcEvictionHeight(bestHeight)
}
// Log that a flush is starting and indicate the current memory usage, hit
// ratio, and eviction height.
var hitRatio float64
var evictionLog string
var preFlushNumEntries int
if logFlush {
preFlushNumEntries = len(c.entries)
memUsageMiB := float64(memUsage) / 1024 / 1024
memUsagePercent := float64(memUsage) / float64(c.maxSize) * 100
hitRatio = c.hitRatio()
if evictionHeight != 0 {
evictionLog = fmt.Sprintf(", eviction height: %d", evictionHeight)
}
log.Debugf("UTXO cache flush starting (%d entries, %.2f MiB (%.2f%%), "+
"%.2f%% hit ratio, height: %d%s)", preFlushNumEntries, memUsageMiB,
memUsagePercent, hitRatio, bestHeight, evictionLog)
}
// Flush the block database to disk. The block database MUST always be
// flushed to disk prior to flushing the UTXO cache to the UTXO database.
// This ensures that the block database is always at least as far along as
// the UTXO database which keeps the UTXO database in a recoverable state in
// the event of an unclean shutdown.
err := c.flushBlockDB()
if err != nil {
return err
}
// Atomically flush all of the entries in the cache along with the best hash
// and best height to the backend.
err = c.backend.PutUtxos(c.entries, &UtxoSetState{
lastFlushHeight: bestHeight,
lastFlushHash: *bestHash,
})
if err != nil {
return err
}
// Update the entries in the cache after flushing to the backend. This is
// done after the updates to the backend have been successfully committed to
// ensure that an unexpected backend error would not leave the cache in an
// inconsistent state.
for outpoint, entry := range c.entries {
// Conditionally evict entries from the cache. Entries that are nil or
// spent are always evicted since they are unlikely to be accessed
// again. Additionally, entries that are contained in a block with a
// height less than the eviction height are evicted.
if entry == nil || entry.IsSpent() ||
entry.BlockHeight() < int64(evictionHeight) {
// Remove the entry from the cache.
delete(c.entries, outpoint)
// Update the total entry size of the cache.
if entry != nil {
c.totalEntrySize -= entry.size()
}
continue
}
// If the entry wasn't removed from the cache, clear the modified and
// fresh flags since it has been updated in the backend.
entry.state &^= utxoStateModified
entry.state &^= utxoStateFresh
}
// Update the last flush on the cache instance now that the flush has been
// completed.
c.lastFlushHash = *bestHash
c.lastFlushTime = c.timeNow()
// Update the last eviction height on the cache instance if we evicted just
// now.
if evictionHeight != 0 {
c.lastEvictionHeight = evictionHeight
}
// Log that the flush has been completed and indicate the updated memory
// usage as it will be reduced due to evicting entries above.
if logFlush {
remainingEntries := len(c.entries)
flushedEntries := preFlushNumEntries - remainingEntries
memUsage = c.totalSize()
memUsageMiB := float64(memUsage) / 1024 / 1024
memUsagePercent := float64(memUsage) / float64(c.maxSize) * 100
log.Debugf("UTXO cache flush completed (%d entries flushed, %d "+
"entries remaining, %.2f MiB (%.2f%%))", flushedEntries,
remainingEntries, memUsageMiB, memUsagePercent)
}
return nil
}
// MaybeFlush conditionally flushes the cache to the backend.
//
// If the maximum size of the cache has been reached, or if the periodic flush
// interval has been reached, then a flush is required. Additionally, a flush
// can be forced by setting the force flush parameter.
//
// This function is safe for concurrent access.
func (c *UtxoCache) MaybeFlush(bestHash *chainhash.Hash, bestHeight uint32,
forceFlush bool, logFlush bool) error {
c.cacheLock.Lock()
if forceFlush || c.shouldFlush(bestHash) {
err := c.flush(bestHash, bestHeight, logFlush)
c.cacheLock.Unlock()
return err
}
c.cacheLock.Unlock()
return nil
}
// Initialize initializes the utxo cache and underlying utxo backend. This
// entails running any database migrations as well as ensuring that the utxo set
// is caught up to the tip of the best chain.
//
// Since the cache is only flushed to the backend periodically, the utxo set
// may not be caught up to the tip of the best chain. This function catches the
// utxo set up by replaying all blocks from the block after the block that was
// last flushed to the tip block through the cache.
//
// This function should only be called during initialization.
func (c *UtxoCache) Initialize(ctx context.Context, b *BlockChain) error {
log.Infof("UTXO cache initializing (max size: %d MiB)...",
c.maxSize/1024/1024)
// Upgrade the UTXO backend as needed.
err := c.backend.Upgrade(ctx, b)
if err != nil {
return err
}
// Fetch the utxo set state from the backend.
state, err := c.backend.FetchState()
if err != nil {
return err
}
// If the state is nil, update the state to the tip. This should only be
// the case when starting from a fresh backend or a backend that has not
// been run with the utxo cache yet.
tip := b.bestChain.Tip()
if state == nil {
state = &UtxoSetState{
lastFlushHeight: uint32(tip.height),
lastFlushHash: tip.hash,
}
err := c.backend.PutUtxos(c.entries, state)
if err != nil {
return err
}
}
// Set the last flush hash and the last eviction height from the saved state
// since that is where we are starting from.
c.lastFlushHash = state.lastFlushHash
c.lastEvictionHeight = state.lastFlushHeight
// If state is already caught up to the tip, return as there is nothing to
// do.
if state.lastFlushHash == tip.hash {
log.Info("UTXO cache initialization completed")
return nil
}
// Find the fork point between the current tip and the last flushed block.
lastFlushedNode := b.index.LookupNode(&state.lastFlushHash)
if lastFlushedNode == nil {
// panic if the last flushed block node does not exist. This should
// never happen unless the backend is corrupted.
panicf("last flushed block node hash %v (height %v) does not exist",
state.lastFlushHash, state.lastFlushHeight)
}
fork := b.bestChain.FindFork(lastFlushedNode)
// Disconnect all of the blocks back to the point of the fork. This entails
// loading the blocks and their associated spent txos from the backend and
// using that information to unspend all of the spent txos and remove the
// utxos created by the blocks. In addition, if a block votes against its
// parent, the regular transactions are reconnected.
//
// Note that blocks will only need to be disconnected during initialization
// if an unclean shutdown occurred between a block being disconnected and
// the cache being flushed. Since the cache is always flushed immediately
// after disconnecting a block, this will occur very infrequently. In the
// typical catchup case, the fork node will be the last flushed node itself
// and this loop will be skipped.
view := NewUtxoViewpoint(c)
view.SetBestHash(&tip.hash)
var nextBlockToDetach *dcrutil.Block
n := lastFlushedNode
for n != nil && n != fork {
select {
case <-b.interrupt:
return errInterruptRequested
default:
}
// Grab the block to detach based on the node. Use the fact that the
// blocks are being detached in reverse order, so the parent of the
// current block being detached is the next one being detached.
block := nextBlockToDetach
if block == nil {
var err error
block, err = b.fetchBlockByNode(n)
if err != nil {
return err
}
}
if n.hash != *block.Hash() {
panicf("detach block node hash %v (height %v) does not match "+
"previous parent block hash %v", &n.hash, n.height,
block.Hash())
}
// Grab the parent of the current block and also save a reference to it
// as the next block to detach so it doesn't need to be loaded again on
// the next iteration.
parent, err := b.fetchBlockByNode(n.parent)
if err != nil {
return err
}
nextBlockToDetach = parent
// Determine if treasury agenda is active.
isTreasuryEnabled, err := b.isTreasuryAgendaActive(n.parent)
if err != nil {
return err
}
// Load all of the spent txos for the block from the spend journal.
var stxos []spentTxOut
err = b.db.View(func(dbTx database.Tx) error {
stxos, err = dbFetchSpendJournalEntry(dbTx, block, isTreasuryEnabled)
return err
})
if err != nil {
return err
}
// Update the view to unspend all of the spent txos and remove the utxos
// created by the block. Also, if the block votes against its parent,
// reconnect all of the regular transactions.
err = view.disconnectBlock(block, parent, stxos, isTreasuryEnabled)
if err != nil {
return err
}
// Commit all entries in the view to the utxo cache. All entries in the
// view that are marked as modified and spent are removed from the view.
// Additionally, all entries that are added to the cache are removed
// from the view.
err = c.Commit(view)
if err != nil {
return err
}
// Conditionally flush the utxo cache to the backend. Don't force flush
// since many blocks may be disconnected and connected in quick
// succession when initializing.
const forceFlush = false
const logFlush = true
err = c.maybeFlushFn(&n.parent.hash, uint32(n.parent.height),
forceFlush, logFlush)
if err != nil {
return err
}
n = n.parent
}
// Determine the blocks to attach after the fork point. Each block is added
// to the slice from back to front so they are attached in the appropriate
// order when iterating the slice below.
replayNodes := make([]*blockNode, tip.height-fork.height)
for n := tip; n != nil && n != fork; n = n.parent {
replayNodes[n.height-fork.height-1] = n
}
// Replay all of the blocks through the cache.
var prevBlockAttached *dcrutil.Block
for i, n := range replayNodes {
select {
case <-b.interrupt:
return errInterruptRequested
default:
}
// Grab the block to attach based on the node. The parent of the block
// is the previous one that was attached except for the first node being
// attached, which needs to be fetched.
block, err := b.fetchBlockByNode(n)
if err != nil {
return err
}
parent := prevBlockAttached
if i == 0 {
parent, err = b.fetchBlockByNode(n.parent)
if err != nil {
return err
}
}
if n.parent.hash != *parent.Hash() {
panicf("attach block node hash %v (height %v) parent hash %v does "+
"not match previous parent block hash %v", &n.hash, n.height,
&n.parent.hash, parent.Hash())
}