github.com/MetalBlockchain/metalgo@v1.11.9/indexer/index.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package indexer 5 6 import ( 7 "errors" 8 "fmt" 9 "sync" 10 11 "go.uber.org/zap" 12 13 "github.com/MetalBlockchain/metalgo/database" 14 "github.com/MetalBlockchain/metalgo/database/prefixdb" 15 "github.com/MetalBlockchain/metalgo/database/versiondb" 16 "github.com/MetalBlockchain/metalgo/ids" 17 "github.com/MetalBlockchain/metalgo/snow" 18 "github.com/MetalBlockchain/metalgo/utils/logging" 19 "github.com/MetalBlockchain/metalgo/utils/timer/mockable" 20 ) 21 22 // Maximum number of containers IDs that can be fetched at a time in a call to 23 // GetContainerRange 24 const MaxFetchedByRange = 1024 25 26 var ( 27 // Maps to the byte representation of the next accepted index 28 nextAcceptedIndexKey = []byte{0x00} 29 indexToContainerPrefix = []byte{0x01} 30 containerToIDPrefix = []byte{0x02} 31 errNoneAccepted = errors.New("no containers have been accepted") 32 errNumToFetchInvalid = fmt.Errorf("numToFetch must be in [1,%d]", MaxFetchedByRange) 33 errNoContainerAtIndex = errors.New("no container at index") 34 35 _ snow.Acceptor = (*index)(nil) 36 ) 37 38 // index indexes containers in their order of acceptance 39 // 40 // Invariant: index is thread-safe. 41 // Invariant: index assumes that Accept is called, before the container is 42 // committed to the database of the VM, in the order they were accepted. 43 type index struct { 44 clock mockable.Clock 45 lock sync.RWMutex 46 // The index of the next accepted transaction 47 nextAcceptedIndex uint64 48 // When [baseDB] is committed, writes to [baseDB] 49 vDB *versiondb.Database 50 baseDB database.Database 51 // Both [indexToContainer] and [containerToIndex] have [vDB] underneath 52 // Index --> Container 53 indexToContainer database.Database 54 // Container ID --> Index 55 containerToIndex database.Database 56 log logging.Logger 57 } 58 59 // Create a new thread-safe index. 60 // 61 // Invariant: Closes [baseDB] on close. 62 func newIndex( 63 baseDB database.Database, 64 log logging.Logger, 65 clock mockable.Clock, 66 ) (*index, error) { 67 vDB := versiondb.New(baseDB) 68 indexToContainer := prefixdb.New(indexToContainerPrefix, vDB) 69 containerToIndex := prefixdb.New(containerToIDPrefix, vDB) 70 71 i := &index{ 72 clock: clock, 73 baseDB: baseDB, 74 vDB: vDB, 75 indexToContainer: indexToContainer, 76 containerToIndex: containerToIndex, 77 log: log, 78 } 79 80 // Get next accepted index from db 81 nextAcceptedIndex, err := database.GetUInt64(i.vDB, nextAcceptedIndexKey) 82 if err == database.ErrNotFound { 83 // Couldn't find it in the database. Must not have accepted any containers in previous runs. 84 i.log.Info("created new index", 85 zap.Uint64("nextAcceptedIndex", i.nextAcceptedIndex), 86 ) 87 return i, nil 88 } 89 if err != nil { 90 return nil, fmt.Errorf("couldn't get next accepted index from database: %w", err) 91 } 92 i.nextAcceptedIndex = nextAcceptedIndex 93 i.log.Info("created new index", 94 zap.Uint64("nextAcceptedIndex", i.nextAcceptedIndex), 95 ) 96 return i, nil 97 } 98 99 // Close this index 100 func (i *index) Close() error { 101 return errors.Join( 102 i.indexToContainer.Close(), 103 i.containerToIndex.Close(), 104 i.vDB.Close(), 105 i.baseDB.Close(), 106 ) 107 } 108 109 // Index that the given transaction is accepted 110 // Returned error should be treated as fatal; the VM should not commit [containerID] 111 // or any new containers as accepted. 112 func (i *index) Accept(ctx *snow.ConsensusContext, containerID ids.ID, containerBytes []byte) error { 113 i.lock.Lock() 114 defer i.lock.Unlock() 115 116 // It may be the case that in a previous run of this node, this index committed [containerID] 117 // as accepted and then the node shut down before the VM committed [containerID] as accepted. 118 // In that case, when the node restarts Accept will be called with the same container. 119 // Make sure we don't index the same container twice in that event. 120 _, err := i.containerToIndex.Get(containerID[:]) 121 if err == nil { 122 ctx.Log.Debug("not indexing already accepted container", 123 zap.Stringer("containerID", containerID), 124 ) 125 return nil 126 } 127 if err != database.ErrNotFound { 128 return fmt.Errorf("couldn't get whether %s is accepted: %w", containerID, err) 129 } 130 131 ctx.Log.Debug("indexing container", 132 zap.Uint64("nextAcceptedIndex", i.nextAcceptedIndex), 133 zap.Stringer("containerID", containerID), 134 ) 135 // Persist index --> Container 136 nextAcceptedIndexBytes := database.PackUInt64(i.nextAcceptedIndex) 137 bytes, err := Codec.Marshal(CodecVersion, Container{ 138 ID: containerID, 139 Bytes: containerBytes, 140 Timestamp: i.clock.Time().UnixNano(), 141 }) 142 if err != nil { 143 return fmt.Errorf("couldn't serialize container %s: %w", containerID, err) 144 } 145 if err := i.indexToContainer.Put(nextAcceptedIndexBytes, bytes); err != nil { 146 return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err) 147 } 148 149 // Persist container ID --> index 150 if err := i.containerToIndex.Put(containerID[:], nextAcceptedIndexBytes); err != nil { 151 return fmt.Errorf("couldn't map container %s to index: %w", containerID, err) 152 } 153 154 // Persist next accepted index 155 i.nextAcceptedIndex++ 156 if err := database.PutUInt64(i.vDB, nextAcceptedIndexKey, i.nextAcceptedIndex); err != nil { 157 return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err) 158 } 159 160 // Atomically commit [i.vDB], [i.indexToContainer], [i.containerToIndex] to [i.baseDB] 161 return i.vDB.Commit() 162 } 163 164 // Returns the ID of the [index]th accepted container and the container itself. 165 // For example, if [index] == 0, returns the first accepted container. 166 // If [index] == 1, returns the second accepted container, etc. 167 // Returns an error if there is no container at the given index. 168 func (i *index) GetContainerByIndex(index uint64) (Container, error) { 169 i.lock.RLock() 170 defer i.lock.RUnlock() 171 172 return i.getContainerByIndex(index) 173 } 174 175 // Assumes [i.lock] is held 176 func (i *index) getContainerByIndex(index uint64) (Container, error) { 177 lastAcceptedIndex, ok := i.lastAcceptedIndex() 178 if !ok || index > lastAcceptedIndex { 179 return Container{}, fmt.Errorf("%w %d", errNoContainerAtIndex, index) 180 } 181 indexBytes := database.PackUInt64(index) 182 return i.getContainerByIndexBytes(indexBytes) 183 } 184 185 // [indexBytes] is the byte representation of the index to fetch. 186 // Assumes [i.lock] is held 187 func (i *index) getContainerByIndexBytes(indexBytes []byte) (Container, error) { 188 containerBytes, err := i.indexToContainer.Get(indexBytes) 189 if err != nil { 190 i.log.Error("couldn't read container from database", 191 zap.Error(err), 192 ) 193 return Container{}, fmt.Errorf("couldn't read from database: %w", err) 194 } 195 var container Container 196 if _, err := Codec.Unmarshal(containerBytes, &container); err != nil { 197 return Container{}, fmt.Errorf("couldn't unmarshal container: %w", err) 198 } 199 return container, nil 200 } 201 202 // GetContainerRange returns the IDs of containers at indices 203 // [startIndex], [startIndex+1], ..., [startIndex+numToFetch-1]. 204 // [startIndex] should be <= i.lastAcceptedIndex(). 205 // [numToFetch] should be in [0, MaxFetchedByRange] 206 func (i *index) GetContainerRange(startIndex, numToFetch uint64) ([]Container, error) { 207 // Check arguments for validity 208 if numToFetch == 0 || numToFetch > MaxFetchedByRange { 209 return nil, fmt.Errorf("%w but is %d", errNumToFetchInvalid, numToFetch) 210 } 211 212 i.lock.RLock() 213 defer i.lock.RUnlock() 214 215 lastAcceptedIndex, ok := i.lastAcceptedIndex() 216 if !ok { 217 return nil, errNoneAccepted 218 } else if startIndex > lastAcceptedIndex { 219 return nil, fmt.Errorf("start index (%d) > last accepted index (%d)", startIndex, lastAcceptedIndex) 220 } 221 222 // Calculate the last index we will fetch 223 lastIndex := min(startIndex+numToFetch-1, lastAcceptedIndex) 224 // [lastIndex] is always >= [startIndex] so this is safe. 225 // [numToFetch] is limited to [MaxFetchedByRange] so [containers] is bounded in size. 226 containers := make([]Container, int(lastIndex)-int(startIndex)+1) 227 228 n := 0 229 var err error 230 for j := startIndex; j <= lastIndex; j++ { 231 containers[n], err = i.getContainerByIndex(j) 232 if err != nil { 233 return nil, fmt.Errorf("couldn't get container at index %d: %w", j, err) 234 } 235 n++ 236 } 237 return containers, nil 238 } 239 240 // Returns database.ErrNotFound if the container is not indexed as accepted 241 func (i *index) GetIndex(id ids.ID) (uint64, error) { 242 i.lock.RLock() 243 defer i.lock.RUnlock() 244 245 return database.GetUInt64(i.containerToIndex, id[:]) 246 } 247 248 func (i *index) GetContainerByID(id ids.ID) (Container, error) { 249 i.lock.RLock() 250 defer i.lock.RUnlock() 251 252 // Read index from database 253 indexBytes, err := i.containerToIndex.Get(id[:]) 254 if err != nil { 255 return Container{}, err 256 } 257 return i.getContainerByIndexBytes(indexBytes) 258 } 259 260 // GetLastAccepted returns the last accepted container. 261 // Returns an error if no containers have been accepted. 262 func (i *index) GetLastAccepted() (Container, error) { 263 i.lock.RLock() 264 defer i.lock.RUnlock() 265 266 lastAcceptedIndex, exists := i.lastAcceptedIndex() 267 if !exists { 268 return Container{}, errNoneAccepted 269 } 270 return i.getContainerByIndex(lastAcceptedIndex) 271 } 272 273 // Assumes i.lock is held 274 // Returns: 275 // 276 // 1. The index of the most recently accepted transaction, or 0 if no 277 // transactions have been accepted 278 // 2. Whether at least 1 transaction has been accepted 279 func (i *index) lastAcceptedIndex() (uint64, bool) { 280 return i.nextAcceptedIndex - 1, i.nextAcceptedIndex != 0 281 }