mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
307 lines
9.1 KiB
Go
307 lines
9.1 KiB
Go
package filesystem
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
|
|
"github.com/OffchainLabs/prysm/v7/config/params"
|
|
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
|
|
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
|
"github.com/OffchainLabs/prysm/v7/io/file"
|
|
"github.com/OffchainLabs/prysm/v7/runtime/logging"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
func directoryPermissions() os.FileMode {
|
|
return params.BeaconIoConfig().ReadWriteExecutePermissions
|
|
}
|
|
|
|
var (
|
|
errIndexOutOfBounds = errors.New("blob index in file name >= MAX_BLOBS_PER_BLOCK")
|
|
errSidecarEmptySSZData = errors.New("sidecar marshalled to an empty ssz byte slice")
|
|
errNoBlobBasePath = errors.New("BlobStorage base path not specified in init")
|
|
)
|
|
|
|
// BlobStorageOption is a functional option for configuring a BlobStorage.
|
|
type BlobStorageOption func(*BlobStorage) error
|
|
|
|
// WithBasePath is a required option that sets the base path of blob storage.
|
|
func WithBasePath(base string) BlobStorageOption {
|
|
return func(b *BlobStorage) error {
|
|
b.base = base
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithBlobRetentionEpochs is an option that changes the number of epochs blobs will be persisted.
|
|
func WithBlobRetentionEpochs(e primitives.Epoch) BlobStorageOption {
|
|
return func(b *BlobStorage) error {
|
|
b.retentionEpochs = e
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithSaveFsync is an option that causes Save to call fsync before renaming part files for improved durability.
|
|
func WithSaveFsync(fsync bool) BlobStorageOption {
|
|
return func(b *BlobStorage) error {
|
|
b.fsync = fsync
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithFs allows the afero.Fs implementation to be customized. Used by tests
|
|
// to substitute an in-memory filesystem.
|
|
func WithFs(fs afero.Fs) BlobStorageOption {
|
|
return func(b *BlobStorage) error {
|
|
b.fs = fs
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithLayout enables the user to specify which layout scheme to use, dictating how blob files are stored on disk.
|
|
func WithLayout(name string) BlobStorageOption {
|
|
return func(b *BlobStorage) error {
|
|
b.layoutName = name
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NewBlobStorage creates a new instance of the BlobStorage object. Note that the implementation of BlobStorage may
|
|
// attempt to hold a file lock to guarantee exclusive control of the blob storage directory, so this should only be
|
|
// initialized once per beacon node.
|
|
func NewBlobStorage(opts ...BlobStorageOption) (*BlobStorage, error) {
|
|
b := &BlobStorage{}
|
|
for _, o := range opts {
|
|
if err := o(b); err != nil {
|
|
return nil, errors.Wrap(err, "failed to create blob storage")
|
|
}
|
|
}
|
|
// Allow tests to set up a different fs using WithFs.
|
|
if b.fs == nil {
|
|
if b.base == "" {
|
|
return nil, errNoBlobBasePath
|
|
}
|
|
b.base = path.Clean(b.base)
|
|
if err := file.MkdirAll(b.base); err != nil {
|
|
return nil, errors.Wrapf(err, "failed to create blob storage at %s", b.base)
|
|
}
|
|
b.fs = afero.NewBasePathFs(afero.NewOsFs(), b.base)
|
|
}
|
|
b.cache = newBlobStorageCache()
|
|
pruner := newBlobPruner(b.retentionEpochs)
|
|
if b.layoutName == "" {
|
|
b.layoutName = LayoutNameFlat
|
|
}
|
|
layout, err := newLayout(b.layoutName, b.fs, b.cache, pruner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.layout = layout
|
|
return b, nil
|
|
}
|
|
|
|
// BlobStorage is the concrete implementation of the filesystem backend for saving and retrieving BlobSidecars.
|
|
type BlobStorage struct {
|
|
base string
|
|
retentionEpochs primitives.Epoch
|
|
layoutName string
|
|
fsync bool
|
|
fs afero.Fs
|
|
layout fsLayout
|
|
cache *blobStorageSummaryCache
|
|
}
|
|
|
|
// WarmCache runs the prune routine with an expiration of slot of 0, so nothing will be pruned, but the pruner's cache
|
|
// will be populated at node startup, avoiding a costly cold prune (~4s in syscalls) during syncing.
|
|
func (bs *BlobStorage) WarmCache() {
|
|
start := time.Now()
|
|
if bs.layoutName == LayoutNameFlat {
|
|
log.Info("Blob filesystem cache warm-up started. This may take a few minutes")
|
|
} else {
|
|
log.Info("Blob filesystem cache warm-up started")
|
|
}
|
|
|
|
if err := warmCache(bs.layout, bs.cache); err != nil {
|
|
log.WithError(err).Error("Error encountered while warming up blob filesystem cache")
|
|
}
|
|
if err := bs.migrateLayouts(); err != nil {
|
|
log.WithError(err).Error("Error encountered while migrating blob storage")
|
|
}
|
|
log.WithField("elapsed", time.Since(start)).Info("Blob filesystem cache warm-up complete")
|
|
}
|
|
|
|
// If any blob storage directories are found for layouts besides the configured layout, migrate them.
|
|
func (bs *BlobStorage) migrateLayouts() error {
|
|
for _, name := range LayoutNames {
|
|
if name == bs.layoutName {
|
|
continue
|
|
}
|
|
from, err := newLayout(name, bs.fs, bs.cache, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := migrateLayout(bs.fs, from, bs.layout, bs.cache); err != nil {
|
|
if errors.Is(err, errLayoutNotDetected) {
|
|
continue
|
|
}
|
|
return errors.Wrapf(err, "failed to migrate layout from %s to %s", name, bs.layoutName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bs *BlobStorage) writePart(sidecar blocks.VerifiedROBlob) (ppath string, err error) {
|
|
ident := identForSidecar(sidecar)
|
|
sidecarData, err := sidecar.MarshalSSZ()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to serialize sidecar data")
|
|
}
|
|
if len(sidecarData) == 0 {
|
|
return "", errSidecarEmptySSZData
|
|
}
|
|
|
|
if err := bs.fs.MkdirAll(bs.layout.dir(ident), directoryPermissions()); err != nil {
|
|
return "", err
|
|
}
|
|
ppath = bs.layout.partPath(ident, fmt.Sprintf("%p", sidecarData))
|
|
|
|
// Create a partial file and write the serialized data to it.
|
|
partialFile, err := bs.fs.Create(ppath)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to create partial file")
|
|
}
|
|
defer func() {
|
|
cerr := partialFile.Close()
|
|
// The close error is probably less important than any existing error, so only overwrite nil err.
|
|
if cerr != nil && err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
|
|
n, err := partialFile.Write(sidecarData)
|
|
if err != nil {
|
|
return ppath, errors.Wrap(err, "failed to write to partial file")
|
|
}
|
|
if bs.fsync {
|
|
if err := partialFile.Sync(); err != nil {
|
|
return ppath, err
|
|
}
|
|
}
|
|
|
|
if n != len(sidecarData) {
|
|
return ppath, fmt.Errorf("failed to write the full bytes of sidecarData, wrote only %d of %d bytes", n, len(sidecarData))
|
|
}
|
|
|
|
return ppath, nil
|
|
}
|
|
|
|
// Save saves blobs given a list of sidecars.
|
|
func (bs *BlobStorage) Save(sidecar blocks.VerifiedROBlob) error {
|
|
startTime := time.Now()
|
|
|
|
ident := identForSidecar(sidecar)
|
|
sszPath := bs.layout.sszPath(ident)
|
|
exists, err := afero.Exists(bs.fs, sszPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
log.WithFields(logging.BlobFields(sidecar.ROBlob)).Debug("Ignoring a duplicate blob sidecar save attempt")
|
|
return nil
|
|
}
|
|
|
|
partialMoved := false
|
|
partPath, err := bs.writePart(sidecar)
|
|
// Ensure the partial file is deleted.
|
|
defer func() {
|
|
if partialMoved || partPath == "" {
|
|
return
|
|
}
|
|
// It's expected to error if the save is successful.
|
|
err := bs.fs.Remove(partPath)
|
|
if err == nil {
|
|
log.WithFields(logrus.Fields{
|
|
"partPath": partPath,
|
|
}).Debugf("Removed partial file")
|
|
}
|
|
}()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Atomically rename the partial file to its final name.
|
|
err = bs.fs.Rename(partPath, sszPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to rename partial file to final name")
|
|
}
|
|
partialMoved = true
|
|
|
|
if err := bs.layout.notify(ident); err != nil {
|
|
return errors.Wrapf(err, "problem maintaining pruning cache/metrics for sidecar with root=%#x", sidecar.BlockRoot())
|
|
}
|
|
blobsWrittenCounter.Inc()
|
|
blobSaveLatency.Observe(float64(time.Since(startTime).Milliseconds()))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves a single BlobSidecar by its root and index.
|
|
// Since BlobStorage only writes blobs that have undergone full verification, the return
|
|
// value is always a VerifiedROBlob.
|
|
func (bs *BlobStorage) Get(root [32]byte, idx uint64) (blocks.VerifiedROBlob, error) {
|
|
startTime := time.Now()
|
|
ident, err := bs.layout.ident(root, idx)
|
|
if err != nil {
|
|
return verification.VerifiedROBlobError(err)
|
|
}
|
|
defer func() {
|
|
blobFetchLatency.Observe(float64(time.Since(startTime).Milliseconds()))
|
|
}()
|
|
return verification.VerifiedROBlobFromDisk(bs.fs, root, bs.layout.sszPath(ident))
|
|
}
|
|
|
|
// Remove removes all blobs for a given root.
|
|
func (bs *BlobStorage) Remove(root [32]byte) error {
|
|
dirIdent, err := bs.layout.dirIdent(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = bs.layout.remove(dirIdent)
|
|
return err
|
|
}
|
|
|
|
// Summary returns the BlobStorageSummary from the layout.
|
|
// Internally, this is a cached representation of the directory listing for the given root.
|
|
func (bs *BlobStorage) Summary(root [32]byte) BlobStorageSummary {
|
|
return bs.layout.summary(root)
|
|
}
|
|
|
|
// Clear deletes all files on the filesystem.
|
|
func (bs *BlobStorage) Clear() error {
|
|
dirs, err := listDir(bs.fs, ".")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, dir := range dirs {
|
|
if err := bs.fs.RemoveAll(dir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WithinRetentionPeriod checks if the requested epoch is within the blob retention period.
|
|
func (bs *BlobStorage) WithinRetentionPeriod(requested, current primitives.Epoch) bool {
|
|
if requested > math.MaxUint64-bs.retentionEpochs {
|
|
// If there is an overflow, then the retention period was set to an extremely large number.
|
|
return true
|
|
}
|
|
return requested+bs.retentionEpochs >= current
|
|
}
|