github.com/ethereum/go-ethereum@v1.16.1/core/rawdb/eradb/eradb.go (about) 1 // Copyright 2025 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 // Package eradb implements a history backend using era1 files. 18 package eradb 19 20 import ( 21 "bytes" 22 "errors" 23 "fmt" 24 "io/fs" 25 "path/filepath" 26 "sync" 27 28 "github.com/ethereum/go-ethereum/common/lru" 29 "github.com/ethereum/go-ethereum/internal/era" 30 "github.com/ethereum/go-ethereum/log" 31 "github.com/ethereum/go-ethereum/rlp" 32 ) 33 34 const openFileLimit = 64 35 36 var errClosed = errors.New("era store is closed") 37 38 // Store manages read access to a directory of era1 files. 39 // The getter methods are thread-safe. 40 type Store struct { 41 datadir string 42 43 // The mutex protects all remaining fields. 44 mu sync.Mutex 45 cond *sync.Cond 46 lru lru.BasicLRU[uint64, *fileCacheEntry] 47 opening map[uint64]*fileCacheEntry 48 closing bool 49 } 50 51 type fileCacheEntry struct { 52 refcount int // reference count. This is protected by Store.mu! 53 opened chan struct{} // signals opening of file has completed 54 file *era.Era // the file 55 err error // error from opening the file 56 } 57 58 type fileCacheStatus byte 59 60 const ( 61 storeClosing fileCacheStatus = iota 62 fileIsNew 63 fileIsOpening 64 fileIsCached 65 ) 66 67 // New opens the store directory. 68 func New(datadir string) (*Store, error) { 69 db := &Store{ 70 datadir: datadir, 71 lru: lru.NewBasicLRU[uint64, *fileCacheEntry](openFileLimit), 72 opening: make(map[uint64]*fileCacheEntry), 73 } 74 db.cond = sync.NewCond(&db.mu) 75 log.Info("Opened Era store", "datadir", datadir) 76 return db, nil 77 } 78 79 // Close closes all open era1 files in the cache. 80 func (db *Store) Close() { 81 db.mu.Lock() 82 defer db.mu.Unlock() 83 84 // Prevent new cache additions. 85 db.closing = true 86 87 // Deref all active files. Since inactive files have a refcount of one, they will be 88 // closed right here and now after decrementing. Files which are currently being used 89 // have a refcount > 1 and will hit zero when their access finishes. 90 for _, epoch := range db.lru.Keys() { 91 entry, _ := db.lru.Peek(epoch) 92 if entry.derefAndClose(epoch) { 93 db.lru.Remove(epoch) 94 } 95 } 96 97 // Wait for all store access to finish. 98 for db.lru.Len() > 0 || len(db.opening) > 0 { 99 db.cond.Wait() 100 } 101 } 102 103 // GetRawBody returns the raw body for a given block number. 104 func (db *Store) GetRawBody(number uint64) ([]byte, error) { 105 epoch := number / uint64(era.MaxEra1Size) 106 entry := db.getEraByEpoch(epoch) 107 if entry.err != nil { 108 if errors.Is(entry.err, fs.ErrNotExist) { 109 return nil, nil 110 } 111 return nil, entry.err 112 } 113 defer db.doneWithFile(epoch, entry) 114 115 return entry.file.GetRawBodyByNumber(number) 116 } 117 118 // GetRawReceipts returns the raw receipts for a given block number. 119 func (db *Store) GetRawReceipts(number uint64) ([]byte, error) { 120 epoch := number / uint64(era.MaxEra1Size) 121 entry := db.getEraByEpoch(epoch) 122 if entry.err != nil { 123 if errors.Is(entry.err, fs.ErrNotExist) { 124 return nil, nil 125 } 126 return nil, entry.err 127 } 128 defer db.doneWithFile(epoch, entry) 129 130 data, err := entry.file.GetRawReceiptsByNumber(number) 131 if err != nil { 132 return nil, err 133 } 134 return convertReceipts(data) 135 } 136 137 // convertReceipts transforms an encoded block receipts list from the format 138 // used by era1 into the 'storage' format used by the go-ethereum ancients database. 139 func convertReceipts(input []byte) ([]byte, error) { 140 var ( 141 out bytes.Buffer 142 enc = rlp.NewEncoderBuffer(&out) 143 ) 144 blockListIter, err := rlp.NewListIterator(input) 145 if err != nil { 146 return nil, fmt.Errorf("invalid block receipts list: %v", err) 147 } 148 outerList := enc.List() 149 for i := 0; blockListIter.Next(); i++ { 150 kind, content, _, err := rlp.Split(blockListIter.Value()) 151 if err != nil { 152 return nil, fmt.Errorf("receipt %d invalid: %v", i, err) 153 } 154 var receiptData []byte 155 switch kind { 156 case rlp.Byte: 157 return nil, fmt.Errorf("receipt %d is single byte", i) 158 case rlp.String: 159 // Typed receipt - skip type. 160 receiptData = content[1:] 161 case rlp.List: 162 // Legacy receipt 163 receiptData = blockListIter.Value() 164 } 165 // Convert data list. 166 // Input is [status, gas-used, bloom, logs] 167 // Output is [status, gas-used, logs], i.e. we need to skip the bloom. 168 dataIter, err := rlp.NewListIterator(receiptData) 169 if err != nil { 170 return nil, fmt.Errorf("receipt %d has invalid data: %v", i, err) 171 } 172 innerList := enc.List() 173 for field := 0; dataIter.Next(); field++ { 174 if field == 2 { 175 continue // skip bloom 176 } 177 enc.Write(dataIter.Value()) 178 } 179 enc.ListEnd(innerList) 180 if dataIter.Err() != nil { 181 return nil, fmt.Errorf("receipt %d iterator error: %v", i, dataIter.Err()) 182 } 183 } 184 enc.ListEnd(outerList) 185 if blockListIter.Err() != nil { 186 return nil, fmt.Errorf("block receipt list iterator error: %v", blockListIter.Err()) 187 } 188 enc.Flush() 189 return out.Bytes(), nil 190 } 191 192 // getEraByEpoch opens an era file or gets it from the cache. 193 // The caller can freely access the returned entry's .file and .err 194 // db.doneWithFile must be called when it is done reading the file. 195 func (db *Store) getEraByEpoch(epoch uint64) *fileCacheEntry { 196 stat, entry := db.getCacheEntry(epoch) 197 198 switch stat { 199 case storeClosing: 200 return &fileCacheEntry{err: errClosed} 201 202 case fileIsNew: 203 // Open the file and put it into the cache. 204 e, err := db.openEraFile(epoch) 205 if err != nil { 206 db.fileFailedToOpen(epoch, entry, err) 207 } else { 208 db.fileOpened(epoch, entry, e) 209 } 210 close(entry.opened) 211 212 case fileIsOpening: 213 // Wait for open to finish. 214 <-entry.opened 215 216 case fileIsCached: 217 // Nothing to do. 218 219 default: 220 panic(fmt.Sprintf("invalid file state %d", stat)) 221 } 222 return entry 223 } 224 225 // getCacheEntry gets an open era file from the cache. 226 func (db *Store) getCacheEntry(epoch uint64) (stat fileCacheStatus, entry *fileCacheEntry) { 227 db.mu.Lock() 228 defer db.mu.Unlock() 229 230 if db.closing { 231 return storeClosing, nil 232 } 233 if entry = db.opening[epoch]; entry != nil { 234 stat = fileIsOpening 235 } else if entry, _ = db.lru.Get(epoch); entry != nil { 236 stat = fileIsCached 237 } else { 238 // It's a new file, create an entry in the opening table. Note the entry is 239 // created with an initial refcount of one. We increment the count once more 240 // before returning, but the count will return to one when the file has been 241 // accessed. When the store is closed or the file gets evicted from the cache, 242 // refcount will be decreased by one, thus allowing it to hit zero. 243 entry = &fileCacheEntry{refcount: 1, opened: make(chan struct{})} 244 db.opening[epoch] = entry 245 stat = fileIsNew 246 } 247 entry.refcount++ 248 return stat, entry 249 } 250 251 // fileOpened is called after an era file has been successfully opened. 252 func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *era.Era) { 253 db.mu.Lock() 254 defer db.mu.Unlock() 255 256 delete(db.opening, epoch) 257 db.cond.Signal() // db.opening was modified 258 259 // The database may have been closed while opening the file. When that happens, we 260 // need to close the file here, since it isn't tracked by the LRU yet. 261 if db.closing { 262 entry.err = errClosed 263 file.Close() 264 return 265 } 266 267 // Add it to the LRU. This may evict an existing item, which we have to close. 268 entry.file = file 269 evictedEpoch, evictedEntry, _ := db.lru.Add3(epoch, entry) 270 if evictedEntry != nil { 271 evictedEntry.derefAndClose(evictedEpoch) 272 } 273 } 274 275 // fileFailedToOpen is called when an era file could not be opened. 276 func (db *Store) fileFailedToOpen(epoch uint64, entry *fileCacheEntry, err error) { 277 db.mu.Lock() 278 defer db.mu.Unlock() 279 280 delete(db.opening, epoch) 281 db.cond.Signal() // db.opening was modified 282 entry.err = err 283 } 284 285 func (db *Store) openEraFile(epoch uint64) (*era.Era, error) { 286 // File name scheme is <network>-<epoch>-<root>. 287 glob := fmt.Sprintf("*-%05d-*.era1", epoch) 288 matches, err := filepath.Glob(filepath.Join(db.datadir, glob)) 289 if err != nil { 290 return nil, err 291 } 292 if len(matches) > 1 { 293 return nil, fmt.Errorf("multiple era1 files found for epoch %d", epoch) 294 } 295 if len(matches) == 0 { 296 return nil, fs.ErrNotExist 297 } 298 filename := matches[0] 299 300 e, err := era.Open(filename) 301 if err != nil { 302 return nil, err 303 } 304 // Sanity-check start block. 305 if e.Start()%uint64(era.MaxEra1Size) != 0 { 306 return nil, fmt.Errorf("pre-merge era1 file has invalid boundary. %d %% %d != 0", e.Start(), era.MaxEra1Size) 307 } 308 log.Debug("Opened era1 file", "epoch", epoch) 309 return e, nil 310 } 311 312 // doneWithFile signals that the caller has finished using a file. 313 // This decrements the refcount and ensures the file is closed by the last user. 314 func (db *Store) doneWithFile(epoch uint64, entry *fileCacheEntry) { 315 db.mu.Lock() 316 defer db.mu.Unlock() 317 318 if entry.err != nil { 319 return 320 } 321 if entry.derefAndClose(epoch) { 322 // Delete closed entry from LRU if it is still present. 323 if e, _ := db.lru.Peek(epoch); e == entry { 324 db.lru.Remove(epoch) 325 db.cond.Signal() // db.lru was modified 326 } 327 } 328 } 329 330 // derefAndClose decrements the reference counter and closes the file 331 // when it hits zero. 332 func (entry *fileCacheEntry) derefAndClose(epoch uint64) (closed bool) { 333 entry.refcount-- 334 if entry.refcount > 0 { 335 return false 336 } 337 338 closeErr := entry.file.Close() 339 if closeErr == nil { 340 log.Debug("Closed era1 file", "epoch", epoch) 341 } else { 342 log.Warn("Error closing era1 file", "epoch", epoch, "err", closeErr) 343 } 344 return true 345 }