Documentation
¶
Overview ¶
Package lifecycle implements database snapshot, restore, and truncate operations shared by the offline CLI and (later) a live-node code path. It is a pure library: it operates on an already-constructed *database.Database (or, for restore, plugin instances that have not yet been started against their real data directory) and knows nothing about node composition, CLI flags, or gRPC.
Index ¶
- Constants
- Variables
- func CloudMirrorMarkerPath(dir string) string
- func DeleteBlocksAfter(ctx context.Context, db *database.Database, afterID uint64, tipID uint64, ...) (blocksDeleted uint64, err error)
- func DeleteCloudSnapshot(ctx context.Context, registry *DestinationRegistry, snapshotURI string) (ok bool, err error)
- func IsCloudMirrored(dir string) bool
- func IsCloudMirroredTo(dir string, cloudDest string) bool
- func IsSafeCloudObjectFileName(fileName string) bool
- func JoinCloudURI(base string, sub string) string
- func LabelSnapshot(dir string, name string, description string, opts ...ManifestOption) error
- func MirrorToCloud(ctx context.Context, registry *DestinationRegistry, dir string, ...) error
- func RegisterBuiltinDestinations(*DestinationRegistry, ...ManifestOption)
- func ResolveTargetByHash(db *database.Database, hash []byte) (models.Block, error)
- func ResolveTargetByNumber(db *database.Database, number uint64) (models.Block, error)
- func ResolveTargetBySlot(db *database.Database, slot uint64) (models.Block, error)
- func RestoreRecoverable(ctx context.Context, host *plugin.Host, registry *DestinationRegistry, ...) (Manifest, *RestoreRecovery, error)
- func Truncate(ctx context.Context, db *database.Database, target models.Block, batchSize int, ...) (blocksRemoved uint64, err error)
- func WriteManifest(dir string, m Manifest, opts ...ManifestOption) error
- type CloudDeleter
- type CloudDestination
- type CloudDestinationCloser
- type CloudDestinationFactory
- type CloudManifestFetcher
- type ConfigurableCloudManifestFetcher
- type DestinationRegistry
- type Manifest
- func FetchCloudManifest(ctx context.Context, registry *DestinationRegistry, snapshotURI string, ...) (m Manifest, ok bool, err error)
- func ParseManifest(data []byte, opts ...ManifestOption) (Manifest, error)
- func PeekManifest(ctx context.Context, registry *DestinationRegistry, snapshotDir string, ...) (Manifest, error)
- func ReadManifest(dir string, opts ...ManifestOption) (Manifest, error)
- func Restore(ctx context.Context, host *plugin.Host, registry *DestinationRegistry, ...) (Manifest, error)
- func RestoreValidated(ctx context.Context, host *plugin.Host, registry *DestinationRegistry, ...) (Manifest, error)
- func Snapshot(ctx context.Context, db *database.Database, dir string, trigger string, ...) (m Manifest, err error)
- func SnapshotToCloud(ctx context.Context, registry *DestinationRegistry, db *database.Database, ...) (Manifest, error)
- type ManifestOption
- type PendingTruncate
- type RestoreRecovery
- type RestoreStorageConfig
- type SnapshotEntry
- type SnapshotLister
Constants ¶
const ( TriggerEpochBoundary = "epoch-boundary" TriggerManual = "manual" )
Trigger values recorded in Manifest.Trigger.
const ( BlobBackupFileName = "blob.bak" MetadataBackupFileName = "metadata.sqlite" )
BlobBackupFileName and MetadataBackupFileName are the fixed file names Snapshot writes a backup's blob and metadata stores under, inside the snapshot directory alongside manifest.json.
const DefaultBlockDeleteBatchSize = 10_000
DefaultBlockDeleteBatchSize bounds how many blocks are deleted per blob transaction in DeleteBlocksAfter. Chain.Rollback deletes one block per transaction, which is fine for the handful-to-low-hundreds of blocks a normal in-bounds rollback removes, but is too slow for a disaster-recovery truncate that may remove millions of blocks.
const ManifestFileName = "manifest.json"
ManifestFileName is the name of the manifest file inside a snapshot directory.
const ManifestFormatVersion = 1
ManifestFormatVersion is the current on-disk schema version of Manifest. Bump it when making a breaking change to the JSON shape so old restore tooling can reject a manifest it doesn't understand instead of silently misreading it. Adding the Gates field did not bump this -- see Gates' doc comment for why that is acceptable today.
const MaxManifestBytes = 1 << 20
MaxManifestBytes is the default bound before JSON decoding. Manifests are small metadata files; accepting an arbitrarily large object here would let a malformed local or cloud snapshot consume memory before validation.
Variables ¶
var ErrCloudSnapshotNotFound = errors.New("cloud snapshot not found")
ErrCloudSnapshotNotFound is what a CloudManifestFetcher implementation wraps around its underlying "object doesn't exist" error (e.g. S3's NoSuchKey/NotFound, GCS's storage.ErrObjectNotExist) — the one case where FetchCloudManifest's non-nil err genuinely means "confirmed absent," as opposed to a real communication failure (auth, network, timeout, throttling) that happens to occur while checking. Callers distinguishing "no such snapshot" from "couldn't check right now" (see bark's cloudSnapshotExists) should check errors.Is(err, ErrCloudSnapshotNotFound) rather than treating every non-nil err the same way.
var ErrManifestCorrupted = errors.New(
"manifest failed checksum validation (corrupted or hand-edited)",
)
ErrManifestCorrupted marks a manifest that was found and read but failed checksum validation — i.e. it (or the snapshot it belongs to) exists but is corrupted or was hand-edited, as distinct from a manifest that simply isn't there. Callers that otherwise treat a ReadManifest/ParseManifest error as "snapshot not found" (e.g. bark's resolveSnapshotSource probing whether a local copy exists) should check errors.Is against this first, so a corrupted snapshot is reported as corrupted rather than missing.
var ErrManifestTooLarge = errors.New("manifest size limit exceeded")
ErrManifestTooLarge marks a manifest that exceeds its encoded byte budget. Callers can distinguish this resource rejection from missing or corrupt data.
var ErrRestoreRollbackPending = errors.New("automatic restore rollback failed")
ErrRestoreRollbackPending means restore mutated an external store and its automatic compensation did not complete. The error includes the retained backup directory. A live caller must not reopen or serve the target until recovery succeeds.
var ErrTruncateNotStarted = errors.New(
"truncate: not started, no data was modified",
)
ErrTruncateNotStarted marks a Truncate failure that occurred entirely during read-only validation — before DeleteBlocksAfter made any on-disk change — as opposed to a failure during or after it, where a batched bulk delete spanning more than one batch may have already partially committed. Callers deciding whether it's safe to resume normal service after a failed live truncate (rather than treat the data directory as possibly inconsistent) should check errors.Is against this.
Functions ¶
func CloudMirrorMarkerPath ¶
CloudMirrorMarkerPath returns the marker file path for the local snapshot directory dir, per cloudMirrorMarkerName's doc comment.
func DeleteBlocksAfter ¶
func DeleteBlocksAfter( ctx context.Context, db *database.Database, afterID uint64, tipID uint64, batchSize int, ) (blocksDeleted uint64, err error)
DeleteBlocksAfter removes every block whose internal, sequentially assigned block ID (models.Block.ID — the basis of the blob store's "bi" index, distinct from the chain's Number/height field) falls in (afterID, tipID], deleting bp/bi/bh keys and their metadata companion via BlobStore.DeleteBlock. Deletes are batched batchSize per blob transaction instead of one transaction per block.
On success the returned count is the number of blocks actually found and deleted. On error it is a lower bound: batches that committed are counted, and a batch whose transaction failed is counted only where the store reports its writes cannot be rolled back (types.IrreversibleTxn). A commit that applied part of a batch and could not compensate (types.ErrPartialCommit) contributes nothing, because how much of it survived is not reported. Either way the caller retries the identical range, which is idempotent.
The count may be far fewer than tipID-afterID: IDs are assigned sequentially by BlockCreate for any chain built entirely through it, but a chain bootstrapped/drained from a Mithril snapshot can leave large gaps of never-imported IDs in that range (see database.BlockAtOrAfterIndex's doc comment) — every ID in (afterID, tipID] is only an upper bound on how many blocks exist there, not a count of how many actually do.
Each batch walks the ordered "bi" index keys with a single iterator seeked to the batch's start, rather than probing every numeric ID in [start, end] individually: cost is therefore proportional to how many blocks are actually stored in the batch's range, not to how wide a never-imported gap it spans. Probing one ID at a time would turn a truncate across a large sparse gap into one remote lookup per absent ID for a cloud-backed blob store (GCS/S3) — catastrophic for a deep disaster-recovery truncate that spans a big Mithril-imported gap.
This is a bulk-performance variant of what Chain.Rollback already does one block at a time via ChainManager.removeBlockByIndex — it performs no chain-manager or fork bookkeeping, so it is only safe to call against a database that is not concurrently owned by a live Chain/ChainManager (i.e. the offline CLI path, or the live path after quiescing the node).
func DeleteCloudSnapshot ¶
func DeleteCloudSnapshot( ctx context.Context, registry *DestinationRegistry, snapshotURI string, ) (ok bool, err error)
DeleteCloudSnapshot resolves the CloudDestination at the given exact snapshot URI and deletes it, if that destination type implements CloudDeleter. ok=false (nil error) means the destination type doesn't support deletion — distinct from a real deletion failure, which is a non-nil err.
func IsCloudMirrored ¶
IsCloudMirrored reports whether dir's cloud mirror marker is present — i.e. whether a previous MirrorToCloud call for this exact directory actually completed successfully, as opposed to dir merely existing locally.
func IsCloudMirroredTo ¶
IsCloudMirroredTo reports whether dir's cloud mirror marker records the destination cloudDest resolves to right now, not merely that some marker is present. This matters because cloudDest is operator-configured and can change (e.g. pointed at a new bucket) between when a snapshot was mirrored and now: a marker left over from a since-abandoned destination must not be mistaken for "already mirrored to the currently configured destination" -- that destination has never actually received this snapshot's data, and treating it as done would silently skip mirroring it there.
func IsSafeCloudObjectFileName ¶
IsSafeCloudObjectFileName reports whether fileName is safe to join onto a local restore directory (via filepath.Join/os.Create). A cloud object key is attacker- or corruption-controlled input, not a trusted local path component, so both destination_s3.go's and destination_gcs.go's DownloadDir use this rather than only checking for "/": a bare ".." resolves outside the target directory via filepath.Join's own cleaning even with no separator present, and a literal "\" is a path separator on Windows (but not Unix, where a "/"-only check would otherwise miss it) regardless of which OS actually wrote the object.
func JoinCloudURI ¶
JoinCloudURI appends sub as an additional path segment to base (e.g. "s3://bucket/prefix" + "abc123" -> "s3://bucket/prefix/abc123"). base is parsed as a URI and sub is appended to its Path specifically (not filepath.Join, which would use the host OS's separator, and not plain string concatenation onto the whole URI, which would land sub after any query string or fragment base carries instead of before it — turning "s3://bucket/prefix?region=us-east-1" + "abc123" into ".../prefix?region=us-east-1/abc123" rather than ".../prefix/abc123?region=us-east-1", silently sending every snapshot to the same base prefix regardless of sub). Exported so callers building a per-snapshot cloud location for display (e.g. bark's ListAvailableSnapshots) use the exact same join logic SnapshotToCloud uses for the actual upload.
func LabelSnapshot ¶
func LabelSnapshot(dir string, name string, description string, opts ...ManifestOption) error
LabelSnapshot sets name/description on the manifest at dir and rewrites it (recomputing its checksum), without touching the snapshot's blob or metadata backups. Intended for callers that only learn a human-readable label after Snapshot has already produced dir (e.g. bark's CreateSnapshot RPC receives name/description in the same request but Snapshot itself has no such parameters).
func MirrorToCloud ¶
func MirrorToCloud( ctx context.Context, registry *DestinationRegistry, dir string, cloudDest string, ) error
MirrorToCloud uploads dir's contents to cloudDest (a base URI like "s3://bucket/prefix" or "gcs://bucket/prefix"; see DestinationRegistry), nested one level under this snapshot's own ID (dir's base name), mirroring the local SnapshotDir/<snapshotID> layout — see SnapshotToCloud's doc comment for why. Writes CloudMirrorMarkerPath(dir) the moment the upload actually succeeds, so a caller can later tell a fully-mirrored snapshot apart from one whose local copy exists but whose cloud upload never completed, and retry only the upload in that case rather than mistaking the local-only partial success for "already done".
cloudDest == "" is a no-op (success, no marker written): nothing to mirror.
func RegisterBuiltinDestinations ¶
func RegisterBuiltinDestinations(*DestinationRegistry, ...ManifestOption)
RegisterBuiltinDestinations is a no-op in this build: S3/GCS support is only compiled in with -tags dingo_extra_plugins.
func ResolveTargetByHash ¶
ResolveTargetByHash resolves a truncate target identified by block hash.
func ResolveTargetByNumber ¶
ResolveTargetByNumber resolves a truncate target identified by chain block number (height). Block numbers are not directly indexed in the blob store (only slot, hash, and internal sequential ID are), so this binary-searches the contiguous internal-ID space bounded by the current tip, comparing each candidate's Number field, mirroring the technique Chain.BlockBeforeSlot uses for slot-ordered lookups.
func ResolveTargetBySlot ¶
ResolveTargetBySlot resolves a truncate target as the highest-slot block at or before the given slot, against whatever chain the local database currently has. Slots without a block of their own (the common case — cardano-node's ~20s average slot time means most slots are empty) resolve to their nearest ancestor, since an operator invoking a disaster-recovery truncate is very unlikely to know a block-populated slot exactly and should not have to.
func RestoreRecoverable ¶ added in v0.70.1
func RestoreRecoverable( ctx context.Context, host *plugin.Host, registry *DestinationRegistry, snapshotDir string, targetDataDir string, validate func(Manifest) error, storageConfig RestoreStorageConfig, opts ...ManifestOption, ) (Manifest, *RestoreRecovery, error)
RestoreRecoverable performs the same validated restore but retains any original external-store backups after the replacement passes lifecycle validation. The caller must call Commit after its own enclosing operation succeeds, or Rollback if a later step fails. This is used by live node restore so a local directory-swap failure cannot strand new remote metadata beside the original local blob store (or the inverse).
The supplied plugin host must remain active until Commit or Rollback returns; Rollback resolves the same providers through it to reload the retained copy. If automatic compensation fails, this returns both ErrRestoreRollbackPending and a non-nil recovery handle so a caller can recognize that the target is unsafe and retry Rollback while the host is still active.
func Truncate ¶
func Truncate( ctx context.Context, db *database.Database, target models.Block, batchSize int, delegatorInactivityEnabled bool, delegatorInactivity uint64, ) (blocksRemoved uint64, err error)
Truncate reverts the database to target: target becomes the new chain tip, every block with a strictly greater internal ID is removed from the blob store, and every metadata row (and blob-referenced UTxO/tx CBOR) added after target's slot is removed or restored to its pre-target state via database.TruncateAfterSlot.
Unlike Chain.Rollback, this does not reject a target beyond the configured security parameter — that guard protects automatic rollback during normal sync; an operator explicitly invoking Truncate (e.g. for CIP-0135 disaster recovery from a long network partition) is the informed-consent replacement for it. It still refuses to truncate to a point before the Mithril trust boundary, if one is recorded: that boundary reflects what UTxO history is actually available locally, not a policy choice, and going below it would leave the database unable to validate the first block past the (now missing) boundary.
This is an offline operation in the sense that it performs no chain- manager or in-memory ledger-state bookkeeping — it is safe to call against a database not concurrently owned by a live Chain/LedgerState (the offline CLI path, or the live path after quiescing the node).
blocksRemoved is the number of blocks DeleteBlocksAfter actually found and deleted in (target.ID, tipBlock.ID] — not simply tipBlock.ID - target.ID, since that range is only an upper bound: a chain bootstrapped/drained from a Mithril snapshot can leave gaps of never-imported IDs in it (see DeleteBlocksAfter's own doc comment), and subtracting index values there would wildly overcount how many blocks actually existed to remove.
DeleteBlocksAfter deletes blob-store blocks by ID range, while database.TruncateAfterSlot deletes metadata by slot cutoff; these agree for any normal chain (slots strictly increase with ID) except same-slot blocks — notably Byron epoch boundary blocks — where a later block sharing target's own slot would be removed from the blob store (ID > target.ID) but retained in metadata (slot not > target.Slot), diverging the two. Truncate refuses such a target outright (see the same-slot check above) rather than let that divergence happen.
func WriteManifest ¶
func WriteManifest(dir string, m Manifest, opts ...ManifestOption) error
WriteManifest computes m's checksum and writes it as indented JSON to dir/ManifestFileName. dir must already exist.
Written via a same-directory temp file plus an atomic rename, not a direct write to the final path: a direct write truncates any existing file before writing the new content, so an interruption partway through would leave a corrupt, partially-written manifest.json in its place. That matters most for LabelSnapshot, which rewrites the manifest of an already-complete snapshot purely to update its Name/Description — a truncated manifest fails ReadManifest's checksum validation, and catalog scanning (ListSnapshots) treats that identically to "this snapshot doesn't exist," silently disappearing an otherwise perfectly good snapshot from the catalog over what should have been a harmless label update. Renaming a fully-written temp file over the target is atomic on the same filesystem, so a reader always observes either the complete old manifest or the complete new one, never a partial one.
Types ¶
type CloudDeleter ¶
CloudDeleter is optionally implemented by a CloudDestination to delete everything at its own configured location — used by DeleteSnapshot to remove a snapshot's cloud copy. Like CloudManifestFetcher, meaningful on a CloudDestination parsed from a specific snapshot's own URI, not a base destination (which would have no single well-defined "everything" to delete).
type CloudDestination ¶
type CloudDestination interface {
// UploadDir uploads every regular file directly inside localDir
// (Snapshot's manifest.json/blob.bak/metadata.sqlite — it is not
// recursive) to the destination.
UploadDir(ctx context.Context, localDir string) error
// DownloadDir downloads the destination's contents into localDir,
// which must already exist and be empty.
DownloadDir(ctx context.Context, localDir string) error
}
CloudDestination mirrors a snapshot directory to/from object storage, in addition to (not instead of) the local copy Snapshot/Restore already produce/consume — see SnapshotToCloud and Restore's cloud-source handling. Implementations live in build-tag-gated files (destination_s3.go, destination_gcs.go); composition code registers the ones it wants available on a *DestinationRegistry via RegisterS3/RegisterGCS (or RegisterBuiltinDestinations for all schemes compiled into this build).
func ParseCloudDestination ¶
func ParseCloudDestination( r *DestinationRegistry, uri string, ) (CloudDestination, error)
ParseCloudDestination resolves uri to a CloudDestination using r's registered schemes. uri's scheme must have a factory registered on r (see RegisterS3/RegisterGCS/RegisterBuiltinDestinations) or this returns an error. r may be nil, which behaves as an empty registry.
type CloudDestinationCloser ¶
type CloudDestinationCloser interface {
Close() error
}
CloudDestinationCloser is optionally implemented by a CloudDestination that holds a resource needing explicit cleanup once a caller is done with it — e.g. GCS's implementation owns a persistent gRPC connection (storage.NewGRPCClient) that client.Bucket's returned handle doesn't itself expose a way to close. S3's implementation has no such resource and doesn't implement this. Every ParseCloudDestination call site in this file (and SnapshotToCloud) closes the destination via closeCloudDestination once it's done using it.
type CloudDestinationFactory ¶
type CloudDestinationFactory func(uri *url.URL) (CloudDestination, error)
CloudDestinationFactory constructs a CloudDestination from a parsed URI (e.g. "s3://bucket/prefix"). Registered per-scheme via RegisterCloudDestinationScheme.
type CloudManifestFetcher ¶
CloudManifestFetcher is optionally implemented by a CloudDestination to fetch just its own manifest.json, without downloading the rest of a (possibly very large) snapshot — used to cheaply check whether a specific snapshot exists at a destination (DeleteSnapshot's and Restore's cloud-fallback path, used when no local copy exists). Unlike SnapshotLister, this is meaningful on a CloudDestination parsed from a specific snapshot's own URI (base destination + snapshot ID), the same one UploadDir/DownloadDir operate on.
type ConfigurableCloudManifestFetcher ¶ added in v0.70.11
type ConfigurableCloudManifestFetcher interface {
FetchManifestWithOptions(context.Context, ...ManifestOption) (Manifest, error)
}
ConfigurableCloudManifestFetcher supports a caller's manifest limit in place of the destination's configured default. Implementations must bound the encoded input before reading or decoding it.
type DestinationRegistry ¶
type DestinationRegistry struct {
// contains filtered or unexported fields
}
DestinationRegistry holds the set of cloud destination schemes (e.g. "s3", "gcs") a caller has explicitly chosen to make available, resolved at construction time rather than through a process-global registry. Composition code (the node or CLI's startup path) owns creating one via NewDestinationRegistry and registering whichever schemes this build and configuration should support (RegisterS3, RegisterGCS, or RegisterBuiltinDestinations for everything compiled in), then threads it explicitly into every lifecycle call that needs cloud destination support. A nil *DestinationRegistry is valid and behaves as if it had no schemes registered — every method here is nil-safe — so callers with no use for cloud destinations at all (e.g. a purely local restore) are not forced to construct an empty one.
func NewDestinationRegistry ¶
func NewDestinationRegistry() *DestinationRegistry
NewDestinationRegistry returns an empty registry with no cloud destination schemes registered.
func (*DestinationRegistry) Register ¶
func (r *DestinationRegistry) Register( scheme string, factory CloudDestinationFactory, )
Register adds factory as the constructor for CloudDestination URIs with the given scheme (e.g. "s3", "gcs"). Panics on a duplicate scheme registration within this registry, matching the fail-fast-at- composition-time convention already used by plugin.Host.Register. r may be nil (see DestinationRegistry's doc comment on nil-safety), in which case this is a no-op: a nil registry has no map to register into and behaves as if it will always have no schemes registered, the same as an empty one.
type Manifest ¶
type Manifest struct {
FormatVersion int `json:"formatVersion"`
CreatedAt time.Time `json:"createdAt"`
Trigger string `json:"trigger"`
// Name/Description are operator-supplied labels, set after the fact
// via LabelSnapshot (Snapshot itself has no caller-facing label
// parameters) — bark's CreateSnapshot RPC is the current writer of
// these fields. Both are empty for a snapshot that was never labeled.
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
// StorageMode/Network mirror types.NodeSettings so a restore can
// refuse an incompatible target before opening the restored store;
// database.New's own CheckNodeSettings then re-validates them for
// free once the restored store is opened for real.
StorageMode string `json:"storageMode"`
Network string `json:"network"`
// CommitTimestamp is the value SetCommitTimestamp wrote to both
// stores at backup time, so database.New's checkCommitTimestamp
// passes immediately after restore rather than reporting a
// mismatch that was actually just "restore in progress".
CommitTimestamp int64 `json:"commitTimestamp"`
// Chain position captured at backup time, for CLI/gRPC display and
// for a post-restore sanity check against the restored tip.
TipSlot uint64 `json:"tipSlot"`
TipHash []byte `json:"tipHash"`
TipBlockNumber uint64 `json:"tipBlockNumber"`
// BlobPlugin/MetadataPlugin identify which plugin produced the
// backup. Restoring a badger backup into a gcs-configured store (or
// vice versa) is meaningless, so restore refuses on a mismatch.
BlobPlugin string `json:"blobPlugin"`
MetadataPlugin string `json:"metadataPlugin"`
// Gates mirrors the source database's persisted node settings gates
// (database/nodesettings.Gates) at backup time — network_magic,
// start_era, the era genesis hashes, the ledger-semantics gates, and
// so on — so CheckGateMatch can refuse restoring a snapshot whose
// consensus-relevant configuration disagrees with what the caller
// intends to run it with (e.g. a snapshot taken with CIP-0163
// full-pot rewards enabled, restored onto a node configured without
// it, would otherwise silently diverge rewards from that point
// forward).
//
// This field is optional (omitempty), and its addition did not bump
// ManifestFormatVersion, but it is not actually backward compatible:
// checksum() covers the whole struct, so a build that predates Gates
// ignores the unknown "gates" key on unmarshal (ParseManifest decodes
// with a plain json.Unmarshal, not DisallowUnknownFields) and then
// recomputes the checksum without a field it never saw, getting a
// different digest and rejecting an otherwise-valid manifest as
// ErrManifestCorrupted. That is acceptable only because this manifest
// format is unreleased: no build that predates Gates is deployed
// anywhere to hit it.
Gates nodesettings.Values `json:"gates,omitempty"`
// DingoVersion records the dingo build that produced the backup, so
// a restore across incompatible dingo versions is at least detectable rather
// than failing schema migration in a confusing way partway through.
DingoVersion string `json:"dingoVersion"`
// BlobBytes/MetadataBytes are informational (display, progress),
// not required for correctness.
BlobBytes int64 `json:"blobBytes"`
MetadataBytes int64 `json:"metadataBytes"`
// Checksum is a SHA-256 hex digest of the manifest's own JSON
// encoding with Checksum itself blanked out, guarding against a
// corrupted or hand-edited manifest file. It is not a security
// mechanism (no key), just a corruption/typo trap.
Checksum string `json:"checksum"`
}
Manifest describes a single database snapshot: what produced it, what point in the chain it captures, and enough about the source database's configuration to detect an incompatible restore before touching any data. It is written as JSON (not CBOR) since it is operator/tooling facing metadata, not chain data.
func FetchCloudManifest ¶
func FetchCloudManifest( ctx context.Context, registry *DestinationRegistry, snapshotURI string, opts ...ManifestOption, ) (m Manifest, ok bool, err error)
FetchCloudManifest resolves the CloudDestination at the given exact snapshot URI (a specific snapshot's own location — see JoinCloudURI, not a base destination) and fetches its manifest, if that destination type implements CloudManifestFetcher. ok=false (nil error) means the destination type doesn't support this. ok=true with a non-nil err means an actual fetch was attempted and failed — check errors.Is(err, ErrCloudSnapshotNotFound) to tell "confirmed absent" apart from a real communication failure (auth, network, timeout); only the former should ever be treated as equivalent to "doesn't exist" by a caller.
func ParseManifest ¶
func ParseManifest(data []byte, opts ...ManifestOption) (Manifest, error)
ParseManifest validates and decodes a manifest already read into memory — the same validation ReadManifest performs against a local file, but against bytes fetched however the caller obtained them (e.g. a cloud destination's ListSnapshots fetching a single remote manifest.json object without downloading the whole snapshot).
func PeekManifest ¶
func PeekManifest( ctx context.Context, registry *DestinationRegistry, snapshotDir string, opts ...ManifestOption, ) (Manifest, error)
PeekManifest resolves snapshotDir (a local path or a cloud destination URI — see Restore's doc comment) and reads its manifest, without restoring anything. Intended for a caller that needs to validate a snapshot's recorded plugins/network/storage mode (Manifest. CheckPluginMatch, or comparing StorageMode/Network directly) against a target's actual configuration before Restore ever touches targetDataDir — Restore's own validateRestoredDatabase only checks the manifest against itself, since it opens the restored copy using the manifest's own recorded plugins, not necessarily whatever the caller actually intends to run it with afterward.
For a cloud snapshotDir, this tries FetchCloudManifest first — which fetches just the one manifest.json object via CloudManifestFetcher, without downloading the (possibly very large) blob/metadata backups alongside it — before falling back to the full download-based resolveManifest path below. FetchCloudManifest's ok=false covers two distinct cases resolveManifest already handles correctly on its own: snapshotDir isn't a recognized cloud URI at all (a plain local path), or it is one but that destination type doesn't implement CloudManifestFetcher — either way, falling through to resolveManifest is the right move, so its own error (if any) from the failed FetchCloudManifest attempt is deliberately discarded here rather than duplicating PeekManifest's cloud-vs-local branching a second time.
func ReadManifest ¶
func ReadManifest(dir string, opts ...ManifestOption) (Manifest, error)
ReadManifest reads and validates the manifest at dir/ManifestFileName, rejecting it if the checksum doesn't match its contents or if its format version is newer than this build understands.
func Restore ¶
func Restore( ctx context.Context, host *plugin.Host, registry *DestinationRegistry, snapshotDir string, targetDataDir string, storageConfig RestoreStorageConfig, opts ...ManifestOption, ) (Manifest, error)
Restore populates targetDataDir (which must not already exist, or must be empty) from the snapshot at snapshotDir, then opens the result with database.New to confirm it passes the same startup consistency checks (CheckNodeSettings, checkCommitTimestamp) any other dingo startup does, and that its tip matches what the manifest recorded. The opened database is closed again before returning — the caller is responsible for (re)opening it for real use.
The actual restore work happens in a sibling staging directory, only atomically renamed into targetDataDir once fully validated — see RestoreValidated's doc comment for why: an interruption (including one the caller's process cannot catch, e.g. a plain, non-signal-handled process kill) leaves targetDataDir completely untouched rather than half-restored.
snapshotDir may instead be a cloud destination URI (s3://bucket/prefix or gcs://bucket/prefix; see DestinationRegistry) — Restore downloads it into a local temp directory first, then proceeds exactly as it would for a local snapshotDir. This is also how a snapshot created on one node can be restored onto another, since the two never need to share a filesystem. registry may be nil if snapshotDir is always a local path.
host resolves the manifest-recorded blob/metadata plugins against targetDataDir; composition code (node.go, cmd/dingo, internal/dblifecycle, bark) builds and owns it — typically a fresh, single-use host built just for this call via internal/plugins.NewHost, mirroring how internal/plugins.OpenDatabase builds its own scratch host for a temporary open elsewhere. This package never constructs one itself: a domain package under the database import boundary registering providers or owning a plugin host of its own would split provider ownership away from the application's composition root.
storageConfig propagates the caller's actual configured provider settings into the plugins this resolves — see RestoreStorageConfig's doc comment. Pass the zero value if the caller has no such configuration to propagate.
This is an offline operation: targetDataDir must not be concurrently held open by another *database.Database (e.g. a running node), since it restores the metadata store before starting it (metadata.Restorer) and the blob store immediately after starting it empty (blob.Restorer) — two-phase orchestration that a live store's own Start/Stop lifecycle cannot safely interleave with.
func RestoreValidated ¶
func RestoreValidated( ctx context.Context, host *plugin.Host, registry *DestinationRegistry, snapshotDir string, targetDataDir string, validate func(Manifest) error, storageConfig RestoreStorageConfig, opts ...ManifestOption, ) (Manifest, error)
RestoreValidated is Restore, but — when validate is non-nil — calls validate(manifest) immediately after resolving the snapshot's manifest and before targetDataDir is touched in any way (not even the empty/absent check), returning validate's error without doing anything destructive if it fails.
This is the hook an offline caller (see internal/dblifecycle.Service. Restore) uses to run Manifest.CheckCompatibility against its own configured plugins/network/storage mode before committing to a restore, without paying for a second cloud download to re-resolve the manifest it already checked: calling PeekManifest and then Restore separately would download a cloud snapshotDir twice.
Interruption safety: every actual restore step (metadata restore, blob restore, validateRestoredDatabase) runs against a sibling staging directory (targetDataDir + ".restore-staging"), never targetDataDir itself. Only once all of them succeed is the staging directory atomically renamed into targetDataDir's place. A caller whose context is cancelled mid-restore, or whose process is killed outright (a plain SIGKILL, or any termination path that skips Go's deferred cleanup — notably, the offline `dingo database restore` CLI's default process termination does not install a signal-aware context, so an operator's Ctrl+C takes this path, not graceful cancellation), is left with targetDataDir exactly as it was before the call: absent, or the same empty directory requireEmptyOrAbsent confirmed. The half-restored staging directory (if any) is simply an orphaned sibling that a retry's own os.RemoveAll(stagingDir) clears on its next attempt.
Crash durability (power loss, not just an interrupted process that leaves the OS itself running): atomic rename alone only guarantees targetDataDir never shows a partially-visible directory -- it says nothing about whether the renamed files, or the rename itself, survive a power loss. Before activating the rename, every directory in the staged tree is fsynced (syncDirTree); after the rename, the parent directory is fsynced too, since the rename changed its own entries. If either sync fails, this returns an error without pretending the restore completed durably; a failed pre-rename sync leaves targetDataDir untouched exactly like any other failure above, and a failed post-rename sync is still surfaced even though targetDataDir was already activated, since durability could not be confirmed.
func Snapshot ¶
func Snapshot( ctx context.Context, db *database.Database, dir string, trigger string, dingoVersion string, blobPluginName string, metadataPluginName string, opts ...ManifestOption, ) (m Manifest, err error)
Snapshot captures a point-in-time backup of db's blob and metadata stores into dir, which must not already exist, writing a manifest alongside them. Both stores are backed up via their native MVCC/versioned mechanism (blob.Backuper, metadata.Backuper), each independently consistent as of whenever it runs — but the two backup calls run concurrently (not sequentially: see the comment further down on why neither exposes a way to separate "capture a consistent point" from "stream/copy it"), so without synchronization a commit landing during either one's own window would write its commit timestamp to one store's backup and not the other's, and the restored copy would fail its cross-store consistency check. Snapshot closes that window with Database.PauseCommitsContext, which blocks new read-write Txns from being constructed (not reads, and not a quiesce — nothing is torn down or disconnected) for the full duration of whichever backup call runs longer — bounded by the slower of the two, not their sum, which is the point of running them concurrently in the first place. This is safe to call against a database a live node is actively writing to.
dingoVersion is recorded in the manifest for cross-version restore detection; pass the running binary's version string. blobPluginName and metadataPluginName are recorded in the manifest for Restore's later plugin-match check (Manifest.CheckPluginMatch) — db itself no longer knows which provider names resolved its injected Stores, so the caller (which does know, from its own plugin selection) must supply them.
func SnapshotToCloud ¶
func SnapshotToCloud( ctx context.Context, registry *DestinationRegistry, db *database.Database, dir string, trigger string, dingoVersion string, blobPluginName string, metadataPluginName string, cloudDest string, name string, description string, opts ...ManifestOption, ) (Manifest, error)
SnapshotToCloud calls Snapshot to produce the local copy at dir exactly as before, then — if name or description is non-empty — labels it (see LabelSnapshot), then — if cloudDest is non-empty — additionally uploads dir's contents to that destination (a base URI like "s3://bucket/prefix" or "gcs://bucket/prefix"; see DestinationRegistry), nested one level under this snapshot's own ID (dir's base name), mirroring the local SnapshotDir/<snapshotID> layout: the actual upload target is cloudDest + "/" + filepath.Base(dir), not cloudDest itself. This is what makes ListCloudSnapshots able to enumerate multiple snapshots stored at the same configured cloudDest — a flat, unnested upload would silently overwrite every previous snapshot's files with the newest one's. The local copy is always kept; cloudDest is a mirror, not a replacement.
Labeling happens before mirroring, not after: MirrorToCloud uploads whatever is on disk at dir the moment it runs and then writes the cloud-mirrored marker recording that destination as fully done. A caller that labeled the local manifest only after this returned would leave the already-uploaded remote copy permanently without the name/description — and since the marker already says this destination is mirrored, nothing would ever retry the upload to pick up the label. Labeling first means the directory MirrorToCloud uploads already carries it.
cloudDest == "" skips the upload — existing local-only callers are unaffected, and registry may be nil in that case.
If the upload fails, the local snapshot is still valid and left in place, but this still returns an error: the operator asked for both copies, so a cloud-only failure is a real (partial) failure, not a silent degrade to local-only.
func (Manifest) CheckCompatibility ¶
func (m Manifest) CheckCompatibility( blobPlugin, metadataPlugin, storageMode, network string, gates nodesettings.Values, ) error
CheckCompatibility returns an error if the manifest's recorded plugins, storage mode, network, or gates are incompatible with the target values given. Offline restore call sites should call this (directly, or via RestoreValidated) before targetDataDir is touched in any way: unlike the live-node restore path, which always opens the restored copy through database.New using the node's own real configured plugins (so CheckNodeSettings catches a mismatch immediately), an offline restore has no such automatic check — Restore's own validateRestoredDatabase only opens the result using the manifest's own recorded plugins, which is a self-consistency check, not a check against what the caller actually intends to run the restored store with.
func (Manifest) CheckGateMatch ¶
func (m Manifest) CheckGateMatch(configured nodesettings.Values) error
CheckGateMatch returns an error if any gate present in both m.Gates and configured is incompatible under the registry's own decision policy (nodesettings.Evaluate/Gate.apply) — the same policy startup enforcement applies via evaluateAndPersistGates — rather than raw equality. Raw equality was too strict for a LatchBool gate: restoring a snapshot recorded "off" onto a target configured "on" is exactly the one-way latch upgrade Evaluate permits at startup, and rejecting it here would make offline restore refuse a resume that a live database.New against the same manifest values would accept.
A gate present in only one of the two maps is not an error: that is what lets an older snapshot (missing a gate a newer dingo would record) restore under that newer dingo, and a newer snapshot restore when the caller has no way to supply every gate it recorded (e.g. no cardano config loaded, so no genesis hashes). Evaluate already treats a gate absent from configured as skipped, and a gate absent from persisted (the manifest) as an ordinary first-write with no mismatch, so both directions of "only one side has it" fall out of Evaluate for free.
blob_store_id is never compared, even when present in both maps: the restored blob store IS the snapshot's, so its identity always differs from whatever the caller had, and comparing it would fail every restore. metadata_plugin and blob_plugin are excluded too, since CheckPluginMatch already reports those — comparing them again here would just report the same mismatch twice. All three are filtered out of configured before Evaluate ever sees them, rather than skipped after-the-fact, so Evaluate cannot mint a Write for them either.
Every gate configured supplies is marked explicit for this call, the same as database.New's own strict validation: CheckGateMatch is a compatibility check, not a resume, so OverrideEligible's "fall back to whatever is persisted" behavior must never mask a real mismatch here.
func (Manifest) CheckPluginMatch ¶
CheckPluginMatch returns an error if the manifest was produced by a different blob or metadata plugin than the ones given.
type ManifestOption ¶ added in v0.70.11
type ManifestOption func(*manifestConfig)
ManifestOption configures manifest I/O. The same options should be used when creating, listing, labeling, and restoring a snapshot.
func WithManifestMaxBytes ¶ added in v0.70.11
func WithManifestMaxBytes(maxBytes int64) ManifestOption
WithManifestMaxBytes sets the maximum encoded manifest size. Zero uses MaxManifestBytes (1 MiB); negative values are rejected before I/O.
type PendingTruncate ¶
type PendingTruncate struct {
TargetID uint64 `json:"targetId"`
TargetSlot uint64 `json:"targetSlot"`
TargetHash []byte `json:"targetHash"`
TipID uint64 `json:"tipId"`
TipSlot uint64 `json:"tipSlot"`
TipHash []byte `json:"tipHash"`
MithrilFloor uint64 `json:"mithrilFloor"`
Checksum []byte `json:"checksum"`
}
PendingTruncate records enough information to resume a truncate whose batched blob deletion was interrupted before metadata was truncated. Checksum protects every field that controls the resumed delete range. The recorded blob tip may already have been deleted when a truncate resumes, so a lower current tip is valid partial progress; an equal or newer current tip must still agree with the marker.
func GetPendingTruncate ¶
func GetPendingTruncate(db *database.Database) (*PendingTruncate, error)
GetPendingTruncate reports a previously-started truncate that still needs completion. Its durable metadata marker prevents a partially committed blob deletion from going unnoticed on restart.
type RestoreRecovery ¶ added in v0.70.1
type RestoreRecovery struct {
// contains filtered or unexported fields
}
RestoreRecovery retains the original external stores after a successful RestoreRecoverable call until its caller completes the enclosing live-node operation. It is also returned with ErrRestoreRollbackPending so the caller can retry an unsuccessful automatic rollback while the plugin host remains active. It is safe to call Commit or Rollback more than once; the first successful disposition wins.
func (*RestoreRecovery) BackupDir ¶ added in v0.70.1
func (r *RestoreRecovery) BackupDir() string
BackupDir returns the directory holding the original external-store backups. It remains useful in an error message if automatic rollback cannot complete.
func (*RestoreRecovery) Commit ¶ added in v0.70.1
func (r *RestoreRecovery) Commit() error
Commit accepts the replacement and removes the retained original backups.
func (*RestoreRecovery) Rollback ¶ added in v0.70.1
func (r *RestoreRecovery) Rollback(ctx context.Context) error
Rollback rejects the replacement and restores both original external stores. Cancellation of the initiating operation is deliberately detached so it cannot interrupt compensation after a destructive reset has begun.
type RestoreStorageConfig ¶
RestoreStorageConfig carries the caller's configured provider-owned settings (plugin.Selection.Config, e.g. Badger/SQLite tuning options) for the blob/metadata plugins Restore resolves, mirroring what a normal dingo startup (internal/plugins.ResolveStorage) already passes instead of the nil config every resolve here used before this existed. The zero value (both nil) preserves the original behavior exactly.
A per-plugin "dataDir" override specifically is deliberately rejected (see checkNoDataDirOverride) rather than honored: restoreMetadataStore/ restoreBlobStore write into a sibling staging directory, never targetDataDir directly, so the atomic-rename interruption-safety RestoreValidated's own doc comment describes holds; a provider redirected by its own config to some other directory entirely would write outside that staging directory, bypassing the same guarantee. Refusing the restore outright when this is configured is safer than either silently writing to the wrong place (the original gap this type exists to close) or silently ignoring the override (a different, equally silent divergence from what a real subsequent startup would do).
type SnapshotEntry ¶
SnapshotEntry is one catalog entry produced by ListSnapshots: a snapshot's directory name (its ID, in callers like bark's DatabaseService that key snapshots by directory name under a fixed base directory) paired with its manifest.
func ListCloudSnapshots ¶
func ListCloudSnapshots( ctx context.Context, registry *DestinationRegistry, cloudDest string, ) (entries []SnapshotEntry, ok bool, err error)
ListCloudSnapshots lists the snapshots already stored at the base cloud destination URI cloudDest, if its scheme's implementation supports listing (SnapshotLister). ok reports whether listing was actually attempted: false (with a nil error) means cloudDest is empty or its destination type doesn't implement SnapshotLister, which callers like ListAvailableSnapshots should treat as "nothing to add," not a failure — cloud listing is an optional capability, not every CloudDestination implementation provides it.
func ListSnapshots ¶
func ListSnapshots(baseDir string, opts ...ManifestOption) ([]SnapshotEntry, error)
ListSnapshots scans baseDir's immediate subdirectories for a valid manifest.json, returning one SnapshotEntry per readable snapshot, newest first (by Manifest.CreatedAt). This covers both manually and automatically (epoch-boundary) triggered snapshots, since both are written as ordinary subdirectories of the same configured snapshot directory — there is no separate catalog store.
A subdirectory that exists but has no manifest.json at all (a snapshot still being written) is silently skipped rather than treated as a hard error, since that is an expected transient state, not corruption of the catalog itself. baseDir not existing yet (no snapshot has ever been taken) returns an empty result, not an error.
Any other ReadManifest failure for a given subdirectory — a malformed manifest, a checksum mismatch (ErrManifestCorrupted), a permission error — is a real problem, not the expected in-progress case, and is not silently swallowed the same way: that subdirectory is still left out of the returned entries (one broken snapshot must not hide every other, otherwise-valid one from the catalog), but its error is accumulated and returned via errors.Join alongside the entries found, so a caller can log or surface it instead of the catalog silently looking one snapshot smaller than it should.
type SnapshotLister ¶
type SnapshotLister interface {
// ListSnapshots returns one entry per snapshot found under this
// destination, each with its manifest already fetched and validated.
ListSnapshots(ctx context.Context) ([]SnapshotEntry, error)
}
SnapshotLister is optionally implemented by a CloudDestination to enumerate snapshots already stored under it — used by ListCloudSnapshots (in turn used by bark's ListAvailableSnapshots RPC). Only meaningful when the CloudDestination was parsed from the base destination URI operators configure (databaseLifecycle. snapshotCloudDestination), not a specific snapshot's per-ID sub-path: each snapshot lives one level under that base, mirroring the local SnapshotDir/<snapshotID> layout (see SnapshotToCloud).