Skip to content

feat(iota-core): backfill epochs_v2 and seed it on snapshot restore#11697

Merged
muXxer merged 6 commits into
feat/formal-snapshot-with-epoch-infofrom
feat/iota-core-storeobjectv2-and-epoch-info-2
Jun 11, 2026
Merged

feat(iota-core): backfill epochs_v2 and seed it on snapshot restore#11697
muXxer merged 6 commits into
feat/formal-snapshot-with-epoch-infofrom
feat/iota-core-storeobjectv2-and-epoch-info-2

Conversation

@bingyanglin

@bingyanglin bingyanglin commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Description of change

Makes a gRPC fullnode's epochs_v2 index complete since genesis on every bootstrap path and enforces it at startup: the node closes any gap before services start, or refuses to run instead of silently serving an incomplete index — the consumer side of #11453's EPOCH_INFO file. No object/previous_transaction_checkpoint backfill: the snapshot-publisher node resyncs from genesis instead, enforced by the writer's existing refusal of None rows.

  • Synchronous startup backfill (iota-node, iota-config). A node that detects a gap (epochs_v2_gap: index short of the last executed closed epoch) fetches only MANIFEST + EPOCH_INFO from the new state_snapshot_read_config, seeds [0, snapshot_epoch], closes any residual above it from local history, seeds the open epoch's row, and re-checks; a remaining gap — or no configured source — aborts startup. Runs before live indexing exists, so the watermark has one writer at a time and the previous design's background task, retry/backoff, and epoch_watermark_lock are gone.
  • Local indexing fallback (iota-core). When the latest published snapshot lags local execution (delayed snapshot pipeline), index_missing_epochs_locally replays only the missing epochs' closing checkpoints, located via the never-pruned epoch_last_checkpoint_map; best-effort up to the pruning horizon.
  • Atomic EpochIndexed advance (iota-core). The live path advances the watermark in the same batch as the close-of-epoch row (gap-aware try_advance_epoch_indexed_watermark); reconcile_epoch_indexed_watermark remains only to jump across a seeded prefix.
  • Restore builds the whole gRPC index store (iota-tool, iota-snapshot, iota-core). download_formal_snapshot tees the restored object stream into the live-state indexers (RestoreWithGrpcIndexes), seeds the epoch rows, and finalizes the store (Watermark::Indexed, then meta — a crash before meta leaves a store the next open wipes and re-inits). The node opens it in place instead of re-indexing the whole restored state; opt out with --skip-grpc-indexes. init and the restore share one indexing implementation (GrpcLiveObjectRestorer; ParMakeLiveObjectIndexer is lifetime-generic now).
  • Chain-identity gate (iota-snapshot). verify_and_restore_epoch_info rejects a snapshot whose manifest chain_id differs from this node's chain before writing any row.
  • RestoreEpochInfo trait (iota-snapshot). Separate single-method trait instead of a new Restore method: the two cover different snapshot payloads with different targets, so each call site requires exactly the capability it uses; the unified indexer ([indexer]: startup from formal snapshots #11023) can implement both.
  • No epochs migration (iota-core). The deprecated epochs CF is dropped without migration: its rows lack the end-of-epoch fields, so they could never satisfy EpochIndexed and the backfill would overwrite them anyway.
  • Open-epoch seeding (iota-core). initialize_current_epoch_info keys off the open epoch (open_epoch_of): a restore lands on a closing checkpoint, and seeding that checkpoint's own (closed) epoch would leave the open epoch's row permanently missing and wedge the watermark. Its start checkpoint derives from epoch_last_checkpoint_map (new seq-only CheckpointStore accessor; the backwards scan over prunable summaries is deleted).
  • Default snapshot source (setups). Fullnode setups for mainnet/testnet/devnet ship a default state-snapshot-read-config pointing at the IOTA Foundation buckets; ignored unless the gRPC API is enabled and the index is incomplete.
  • Misc. Uploader honors state_snapshot_write_config.concurrency (default still 20); get_latest_available_epoch delegates to iota_snapshot::reader::latest_available_epoch; snapshots.mdx documents the restore flag and the backfill setup.

Links to any relevant issues

follow-up (PR-2) of #11453 · part of #11023

How the change has been tested

  • Basic tests (linting, compilation, formatting, unit/integration tests)
  • Patch-specific tests (correctness, functionality coverage)
  • I have added tests that prove my fix is effective or that my feature works
  • I have checked that new and existing unit tests pass locally with my changes

@iota-ci iota-ci added core-protocol node Issues related to the Core Node team labels Jun 1, 2026
@bingyanglin bingyanglin force-pushed the feat/iota-core-storeobjectv2-and-epoch-info-2 branch 3 times, most recently from 8b2edcb to fc9d053 Compare June 2, 2026 13:57
@bingyanglin bingyanglin force-pushed the feat/formal-snapshot-with-epoch-info branch from 0881407 to d8d28e2 Compare June 10, 2026 06:54
@bingyanglin bingyanglin force-pushed the feat/iota-core-storeobjectv2-and-epoch-info-2 branch 6 times, most recently from e01f51b to 43f33e5 Compare June 11, 2026 05:42
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jun 11, 2026
@muXxer muXxer marked this pull request as ready for review June 11, 2026 09:35
@muXxer muXxer requested review from a team as code owners June 11, 2026 09:35
@muXxer muXxer force-pushed the feat/formal-snapshot-with-epoch-info branch from d8d28e2 to fad8412 Compare June 11, 2026 09:51
@muXxer muXxer requested review from a team as code owners June 11, 2026 09:51
@muXxer muXxer force-pushed the feat/iota-core-storeobjectv2-and-epoch-info-2 branch from cd0917b to 22521a3 Compare June 11, 2026 10:24
@github-actions

Copy link
Copy Markdown
Contributor

✅ Vercel Preview Deployment is ready!

View Preview

@muXxer muXxer removed request for a team June 11, 2026 11:09
@github-actions

Copy link
Copy Markdown
Contributor

✅ Vercel Preview Deployment is ready!

View Preview

@muXxer muXxer merged commit b204aed into feat/formal-snapshot-with-epoch-info Jun 11, 2026
68 of 74 checks passed
@muXxer muXxer deleted the feat/iota-core-storeobjectv2-and-epoch-info-2 branch June 11, 2026 16:50
muXxer added a commit that referenced this pull request Jun 11, 2026
…11697)

# Description of change

Makes a gRPC fullnode's `epochs_v2` index complete since genesis on
every bootstrap path and enforces it at startup: the node closes any gap
before services start, or refuses to run instead of silently serving an
incomplete index — the consumer side of #11453's `EPOCH_INFO` file. No
object/`previous_transaction_checkpoint` backfill: the
snapshot-publisher node resyncs from genesis instead, enforced by the
writer's existing refusal of `None` rows.

- **Synchronous startup backfill (`iota-node`, `iota-config`).** A node
that detects a gap (`epochs_v2_gap`: index short of the last executed
closed epoch) fetches only MANIFEST + `EPOCH_INFO` from the new
`state_snapshot_read_config`, seeds `[0, snapshot_epoch]`, closes any
residual above it from local history, seeds the open epoch's row, and
re-checks; a remaining gap — or no configured source — aborts startup.
Runs before live indexing exists, so the watermark has one writer at a
time and the previous design's background task, retry/backoff, and
`epoch_watermark_lock` are gone.
- **Local indexing fallback (`iota-core`).** When the latest published
snapshot lags local execution (delayed snapshot pipeline),
`index_missing_epochs_locally` replays only the missing epochs' closing
checkpoints, located via the never-pruned `epoch_last_checkpoint_map`;
best-effort up to the pruning horizon.
- **Atomic `EpochIndexed` advance (`iota-core`).** The live path
advances the watermark in the same batch as the close-of-epoch row
(gap-aware `try_advance_epoch_indexed_watermark`);
`reconcile_epoch_indexed_watermark` remains only to jump across a seeded
prefix.
- **Restore builds the whole gRPC index store (`iota-tool`,
`iota-snapshot`, `iota-core`).** `download_formal_snapshot` tees the
restored object stream into the live-state indexers
(`RestoreWithGrpcIndexes`), seeds the epoch rows, and finalizes the
store (`Watermark::Indexed`, then `meta` — a crash before `meta` leaves
a store the next open wipes and re-inits). The node opens it in place
instead of re-indexing the whole restored state; opt out with
`--skip-grpc-indexes`. `init` and the restore share one indexing
implementation (`GrpcLiveObjectRestorer`; `ParMakeLiveObjectIndexer` is
lifetime-generic now).
- **Chain-identity gate (`iota-snapshot`).**
`verify_and_restore_epoch_info` rejects a snapshot whose manifest
`chain_id` differs from this node's chain before writing any row.
- **`RestoreEpochInfo` trait (`iota-snapshot`).** Separate single-method
trait instead of a new `Restore` method: the two cover different
snapshot payloads with different targets, so each call site requires
exactly the capability it uses; the unified indexer (#11023) can
implement both.
- **No `epochs` migration (`iota-core`).** The deprecated `epochs` CF is
dropped without migration: its rows lack the end-of-epoch fields, so
they could never satisfy `EpochIndexed` and the backfill would overwrite
them anyway.
- **Open-epoch seeding (`iota-core`).** `initialize_current_epoch_info`
keys off the open epoch (`open_epoch_of`): a restore lands on a closing
checkpoint, and seeding that checkpoint's own (closed) epoch would leave
the open epoch's row permanently missing and wedge the watermark. Its
start checkpoint derives from `epoch_last_checkpoint_map` (new seq-only
`CheckpointStore` accessor; the backwards scan over prunable summaries
is deleted).
- **Default snapshot source (`setups`).** Fullnode setups for
mainnet/testnet/devnet ship a default `state-snapshot-read-config`
pointing at the IOTA Foundation buckets; ignored unless the gRPC API is
enabled and the index is incomplete.
- **Misc.** Uploader honors `state_snapshot_write_config.concurrency`
(default still 20); `get_latest_available_epoch` delegates to
`iota_snapshot::reader::latest_available_epoch`; `snapshots.mdx`
documents the restore flag and the backfill setup.

## Links to any relevant issues

follow-up (PR-2) of #11453 · part of #11023

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [x] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes

---------

Co-authored-by: muXxer <git@muxxer.de>
muXxer pushed a commit that referenced this pull request Jun 12, 2026
# Description of change

Some cleanup for #11697 

- **Local epoch backfill: only missing data counts as pruned
(`iota-core`, `iota-types`).** Real storage failures were swallowed as
"data pruned, retry once a newer snapshot is published". Absent-data
errors now carry `Kind::Missing` (was `custom`) and only those end the
best-effort replay; anything else propagates. Nothing else reads the
kind.
- **Operator doc fixes (`iota-config`, `docs`).** The config doc still
described the removed background task (the backfill is synchronous and
gates startup); the snapshots guide now documents the refuse-to-start
case when even the latest snapshot is older than the node's pruned-away
history.

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [ ] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes
muXxer added a commit that referenced this pull request Jun 12, 2026
…11697)

# Description of change

Makes a gRPC fullnode's `epochs_v2` index complete since genesis on
every bootstrap path and enforces it at startup: the node closes any gap
before services start, or refuses to run instead of silently serving an
incomplete index — the consumer side of #11453's `EPOCH_INFO` file. No
object/`previous_transaction_checkpoint` backfill: the
snapshot-publisher node resyncs from genesis instead, enforced by the
writer's existing refusal of `None` rows.

- **Synchronous startup backfill (`iota-node`, `iota-config`).** A node
that detects a gap (`epochs_v2_gap`: index short of the last executed
closed epoch) fetches only MANIFEST + `EPOCH_INFO` from the new
`state_snapshot_read_config`, seeds `[0, snapshot_epoch]`, closes any
residual above it from local history, seeds the open epoch's row, and
re-checks; a remaining gap — or no configured source — aborts startup.
Runs before live indexing exists, so the watermark has one writer at a
time and the previous design's background task, retry/backoff, and
`epoch_watermark_lock` are gone.
- **Local indexing fallback (`iota-core`).** When the latest published
snapshot lags local execution (delayed snapshot pipeline),
`index_missing_epochs_locally` replays only the missing epochs' closing
checkpoints, located via the never-pruned `epoch_last_checkpoint_map`;
best-effort up to the pruning horizon.
- **Atomic `EpochIndexed` advance (`iota-core`).** The live path
advances the watermark in the same batch as the close-of-epoch row
(gap-aware `try_advance_epoch_indexed_watermark`);
`reconcile_epoch_indexed_watermark` remains only to jump across a seeded
prefix.
- **Restore builds the whole gRPC index store (`iota-tool`,
`iota-snapshot`, `iota-core`).** `download_formal_snapshot` tees the
restored object stream into the live-state indexers
(`RestoreWithGrpcIndexes`), seeds the epoch rows, and finalizes the
store (`Watermark::Indexed`, then `meta` — a crash before `meta` leaves
a store the next open wipes and re-inits). The node opens it in place
instead of re-indexing the whole restored state; opt out with
`--skip-grpc-indexes`. `init` and the restore share one indexing
implementation (`GrpcLiveObjectRestorer`; `ParMakeLiveObjectIndexer` is
lifetime-generic now).
- **Chain-identity gate (`iota-snapshot`).**
`verify_and_restore_epoch_info` rejects a snapshot whose manifest
`chain_id` differs from this node's chain before writing any row.
- **`RestoreEpochInfo` trait (`iota-snapshot`).** Separate single-method
trait instead of a new `Restore` method: the two cover different
snapshot payloads with different targets, so each call site requires
exactly the capability it uses; the unified indexer (#11023) can
implement both.
- **No `epochs` migration (`iota-core`).** The deprecated `epochs` CF is
dropped without migration: its rows lack the end-of-epoch fields, so
they could never satisfy `EpochIndexed` and the backfill would overwrite
them anyway.
- **Open-epoch seeding (`iota-core`).** `initialize_current_epoch_info`
keys off the open epoch (`open_epoch_of`): a restore lands on a closing
checkpoint, and seeding that checkpoint's own (closed) epoch would leave
the open epoch's row permanently missing and wedge the watermark. Its
start checkpoint derives from `epoch_last_checkpoint_map` (new seq-only
`CheckpointStore` accessor; the backwards scan over prunable summaries
is deleted).
- **Default snapshot source (`setups`).** Fullnode setups for
mainnet/testnet/devnet ship a default `state-snapshot-read-config`
pointing at the IOTA Foundation buckets; ignored unless the gRPC API is
enabled and the index is incomplete.
- **Misc.** Uploader honors `state_snapshot_write_config.concurrency`
(default still 20); `get_latest_available_epoch` delegates to
`iota_snapshot::reader::latest_available_epoch`; `snapshots.mdx`
documents the restore flag and the backfill setup.

## Links to any relevant issues

follow-up (PR-2) of #11453 · part of #11023

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [x] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes

---------

Co-authored-by: muXxer <git@muxxer.de>
muXxer pushed a commit that referenced this pull request Jun 12, 2026
# Description of change

Some cleanup for #11697 

- **Local epoch backfill: only missing data counts as pruned
(`iota-core`, `iota-types`).** Real storage failures were swallowed as
"data pruned, retry once a newer snapshot is published". Absent-data
errors now carry `Kind::Missing` (was `custom`) and only those end the
best-effort replay; anything else propagates. Nothing else reads the
kind.
- **Operator doc fixes (`iota-config`, `docs`).** The config doc still
described the removed background task (the backfill is synchronous and
gates startup); the snapshots guide now documents the refuse-to-start
case when even the latest snapshot is older than the node's pruned-away
history.

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [ ] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes
muXxer added a commit that referenced this pull request Jun 15, 2026
…11697)

# Description of change

Makes a gRPC fullnode's `epochs_v2` index complete since genesis on
every bootstrap path and enforces it at startup: the node closes any gap
before services start, or refuses to run instead of silently serving an
incomplete index — the consumer side of #11453's `EPOCH_INFO` file. No
object/`previous_transaction_checkpoint` backfill: the
snapshot-publisher node resyncs from genesis instead, enforced by the
writer's existing refusal of `None` rows.

- **Synchronous startup backfill (`iota-node`, `iota-config`).** A node
that detects a gap (`epochs_v2_gap`: index short of the last executed
closed epoch) fetches only MANIFEST + `EPOCH_INFO` from the new
`state_snapshot_read_config`, seeds `[0, snapshot_epoch]`, closes any
residual above it from local history, seeds the open epoch's row, and
re-checks; a remaining gap — or no configured source — aborts startup.
Runs before live indexing exists, so the watermark has one writer at a
time and the previous design's background task, retry/backoff, and
`epoch_watermark_lock` are gone.
- **Local indexing fallback (`iota-core`).** When the latest published
snapshot lags local execution (delayed snapshot pipeline),
`index_missing_epochs_locally` replays only the missing epochs' closing
checkpoints, located via the never-pruned `epoch_last_checkpoint_map`;
best-effort up to the pruning horizon.
- **Atomic `EpochIndexed` advance (`iota-core`).** The live path
advances the watermark in the same batch as the close-of-epoch row
(gap-aware `try_advance_epoch_indexed_watermark`);
`reconcile_epoch_indexed_watermark` remains only to jump across a seeded
prefix.
- **Restore builds the whole gRPC index store (`iota-tool`,
`iota-snapshot`, `iota-core`).** `download_formal_snapshot` tees the
restored object stream into the live-state indexers
(`RestoreWithGrpcIndexes`), seeds the epoch rows, and finalizes the
store (`Watermark::Indexed`, then `meta` — a crash before `meta` leaves
a store the next open wipes and re-inits). The node opens it in place
instead of re-indexing the whole restored state; opt out with
`--skip-grpc-indexes`. `init` and the restore share one indexing
implementation (`GrpcLiveObjectRestorer`; `ParMakeLiveObjectIndexer` is
lifetime-generic now).
- **Chain-identity gate (`iota-snapshot`).**
`verify_and_restore_epoch_info` rejects a snapshot whose manifest
`chain_id` differs from this node's chain before writing any row.
- **`RestoreEpochInfo` trait (`iota-snapshot`).** Separate single-method
trait instead of a new `Restore` method: the two cover different
snapshot payloads with different targets, so each call site requires
exactly the capability it uses; the unified indexer (#11023) can
implement both.
- **No `epochs` migration (`iota-core`).** The deprecated `epochs` CF is
dropped without migration: its rows lack the end-of-epoch fields, so
they could never satisfy `EpochIndexed` and the backfill would overwrite
them anyway.
- **Open-epoch seeding (`iota-core`).** `initialize_current_epoch_info`
keys off the open epoch (`open_epoch_of`): a restore lands on a closing
checkpoint, and seeding that checkpoint's own (closed) epoch would leave
the open epoch's row permanently missing and wedge the watermark. Its
start checkpoint derives from `epoch_last_checkpoint_map` (new seq-only
`CheckpointStore` accessor; the backwards scan over prunable summaries
is deleted).
- **Default snapshot source (`setups`).** Fullnode setups for
mainnet/testnet/devnet ship a default `state-snapshot-read-config`
pointing at the IOTA Foundation buckets; ignored unless the gRPC API is
enabled and the index is incomplete.
- **Misc.** Uploader honors `state_snapshot_write_config.concurrency`
(default still 20); `get_latest_available_epoch` delegates to
`iota_snapshot::reader::latest_available_epoch`; `snapshots.mdx`
documents the restore flag and the backfill setup.

## Links to any relevant issues

follow-up (PR-2) of #11453 · part of #11023

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [x] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes

---------

Co-authored-by: muXxer <git@muxxer.de>
muXxer pushed a commit that referenced this pull request Jun 15, 2026
# Description of change

Some cleanup for #11697 

- **Local epoch backfill: only missing data counts as pruned
(`iota-core`, `iota-types`).** Real storage failures were swallowed as
"data pruned, retry once a newer snapshot is published". Absent-data
errors now carry `Kind::Missing` (was `custom`) and only those end the
best-effort replay; anything else propagates. Nothing else reads the
kind.
- **Operator doc fixes (`iota-config`, `docs`).** The config doc still
described the removed background task (the backfill is synchronous and
gates startup); the snapshots guide now documents the refuse-to-start
case when even the latest snapshot is older than the node's pruned-away
history.

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [ ] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes
muXxer added a commit that referenced this pull request Jun 18, 2026
…11697)

# Description of change

Makes a gRPC fullnode's `epochs_v2` index complete since genesis on
every bootstrap path and enforces it at startup: the node closes any gap
before services start, or refuses to run instead of silently serving an
incomplete index — the consumer side of #11453's `EPOCH_INFO` file. No
object/`previous_transaction_checkpoint` backfill: the
snapshot-publisher node resyncs from genesis instead, enforced by the
writer's existing refusal of `None` rows.

- **Synchronous startup backfill (`iota-node`, `iota-config`).** A node
that detects a gap (`epochs_v2_gap`: index short of the last executed
closed epoch) fetches only MANIFEST + `EPOCH_INFO` from the new
`state_snapshot_read_config`, seeds `[0, snapshot_epoch]`, closes any
residual above it from local history, seeds the open epoch's row, and
re-checks; a remaining gap — or no configured source — aborts startup.
Runs before live indexing exists, so the watermark has one writer at a
time and the previous design's background task, retry/backoff, and
`epoch_watermark_lock` are gone.
- **Local indexing fallback (`iota-core`).** When the latest published
snapshot lags local execution (delayed snapshot pipeline),
`index_missing_epochs_locally` replays only the missing epochs' closing
checkpoints, located via the never-pruned `epoch_last_checkpoint_map`;
best-effort up to the pruning horizon.
- **Atomic `EpochIndexed` advance (`iota-core`).** The live path
advances the watermark in the same batch as the close-of-epoch row
(gap-aware `try_advance_epoch_indexed_watermark`);
`reconcile_epoch_indexed_watermark` remains only to jump across a seeded
prefix.
- **Restore builds the whole gRPC index store (`iota-tool`,
`iota-snapshot`, `iota-core`).** `download_formal_snapshot` tees the
restored object stream into the live-state indexers
(`RestoreWithGrpcIndexes`), seeds the epoch rows, and finalizes the
store (`Watermark::Indexed`, then `meta` — a crash before `meta` leaves
a store the next open wipes and re-inits). The node opens it in place
instead of re-indexing the whole restored state; opt out with
`--skip-grpc-indexes`. `init` and the restore share one indexing
implementation (`GrpcLiveObjectRestorer`; `ParMakeLiveObjectIndexer` is
lifetime-generic now).
- **Chain-identity gate (`iota-snapshot`).**
`verify_and_restore_epoch_info` rejects a snapshot whose manifest
`chain_id` differs from this node's chain before writing any row.
- **`RestoreEpochInfo` trait (`iota-snapshot`).** Separate single-method
trait instead of a new `Restore` method: the two cover different
snapshot payloads with different targets, so each call site requires
exactly the capability it uses; the unified indexer (#11023) can
implement both.
- **No `epochs` migration (`iota-core`).** The deprecated `epochs` CF is
dropped without migration: its rows lack the end-of-epoch fields, so
they could never satisfy `EpochIndexed` and the backfill would overwrite
them anyway.
- **Open-epoch seeding (`iota-core`).** `initialize_current_epoch_info`
keys off the open epoch (`open_epoch_of`): a restore lands on a closing
checkpoint, and seeding that checkpoint's own (closed) epoch would leave
the open epoch's row permanently missing and wedge the watermark. Its
start checkpoint derives from `epoch_last_checkpoint_map` (new seq-only
`CheckpointStore` accessor; the backwards scan over prunable summaries
is deleted).
- **Default snapshot source (`setups`).** Fullnode setups for
mainnet/testnet/devnet ship a default `state-snapshot-read-config`
pointing at the IOTA Foundation buckets; ignored unless the gRPC API is
enabled and the index is incomplete.
- **Misc.** Uploader honors `state_snapshot_write_config.concurrency`
(default still 20); `get_latest_available_epoch` delegates to
`iota_snapshot::reader::latest_available_epoch`; `snapshots.mdx`
documents the restore flag and the backfill setup.

## Links to any relevant issues

follow-up (PR-2) of #11453 · part of #11023

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [x] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes

---------

Co-authored-by: muXxer <git@muxxer.de>
muXxer pushed a commit that referenced this pull request Jun 18, 2026
# Description of change

Some cleanup for #11697 

- **Local epoch backfill: only missing data counts as pruned
(`iota-core`, `iota-types`).** Real storage failures were swallowed as
"data pruned, retry once a newer snapshot is published". Absent-data
errors now carry `Kind::Missing` (was `custom`) and only those end the
best-effort replay; anything else propagates. Nothing else reads the
kind.
- **Operator doc fixes (`iota-config`, `docs`).** The config doc still
described the removed background task (the backfill is synchronous and
gates startup); the snapshots guide now documents the refuse-to-start
case when even the latest snapshot is older than the node's pruned-away
history.

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [ ] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes
bingyanglin added a commit that referenced this pull request Jun 23, 2026
…11697)

# Description of change

Makes a gRPC fullnode's `epochs_v2` index complete since genesis on
every bootstrap path and enforces it at startup: the node closes any gap
before services start, or refuses to run instead of silently serving an
incomplete index — the consumer side of #11453's `EPOCH_INFO` file. No
object/`previous_transaction_checkpoint` backfill: the
snapshot-publisher node resyncs from genesis instead, enforced by the
writer's existing refusal of `None` rows.

- **Synchronous startup backfill (`iota-node`, `iota-config`).** A node
that detects a gap (`epochs_v2_gap`: index short of the last executed
closed epoch) fetches only MANIFEST + `EPOCH_INFO` from the new
`state_snapshot_read_config`, seeds `[0, snapshot_epoch]`, closes any
residual above it from local history, seeds the open epoch's row, and
re-checks; a remaining gap — or no configured source — aborts startup.
Runs before live indexing exists, so the watermark has one writer at a
time and the previous design's background task, retry/backoff, and
`epoch_watermark_lock` are gone.
- **Local indexing fallback (`iota-core`).** When the latest published
snapshot lags local execution (delayed snapshot pipeline),
`index_missing_epochs_locally` replays only the missing epochs' closing
checkpoints, located via the never-pruned `epoch_last_checkpoint_map`;
best-effort up to the pruning horizon.
- **Atomic `EpochIndexed` advance (`iota-core`).** The live path
advances the watermark in the same batch as the close-of-epoch row
(gap-aware `try_advance_epoch_indexed_watermark`);
`reconcile_epoch_indexed_watermark` remains only to jump across a seeded
prefix.
- **Restore builds the whole gRPC index store (`iota-tool`,
`iota-snapshot`, `iota-core`).** `download_formal_snapshot` tees the
restored object stream into the live-state indexers
(`RestoreWithGrpcIndexes`), seeds the epoch rows, and finalizes the
store (`Watermark::Indexed`, then `meta` — a crash before `meta` leaves
a store the next open wipes and re-inits). The node opens it in place
instead of re-indexing the whole restored state; opt out with
`--skip-grpc-indexes`. `init` and the restore share one indexing
implementation (`GrpcLiveObjectRestorer`; `ParMakeLiveObjectIndexer` is
lifetime-generic now).
- **Chain-identity gate (`iota-snapshot`).**
`verify_and_restore_epoch_info` rejects a snapshot whose manifest
`chain_id` differs from this node's chain before writing any row.
- **`RestoreEpochInfo` trait (`iota-snapshot`).** Separate single-method
trait instead of a new `Restore` method: the two cover different
snapshot payloads with different targets, so each call site requires
exactly the capability it uses; the unified indexer (#11023) can
implement both.
- **No `epochs` migration (`iota-core`).** The deprecated `epochs` CF is
dropped without migration: its rows lack the end-of-epoch fields, so
they could never satisfy `EpochIndexed` and the backfill would overwrite
them anyway.
- **Open-epoch seeding (`iota-core`).** `initialize_current_epoch_info`
keys off the open epoch (`open_epoch_of`): a restore lands on a closing
checkpoint, and seeding that checkpoint's own (closed) epoch would leave
the open epoch's row permanently missing and wedge the watermark. Its
start checkpoint derives from `epoch_last_checkpoint_map` (new seq-only
`CheckpointStore` accessor; the backwards scan over prunable summaries
is deleted).
- **Default snapshot source (`setups`).** Fullnode setups for
mainnet/testnet/devnet ship a default `state-snapshot-read-config`
pointing at the IOTA Foundation buckets; ignored unless the gRPC API is
enabled and the index is incomplete.
- **Misc.** Uploader honors `state_snapshot_write_config.concurrency`
(default still 20); `get_latest_available_epoch` delegates to
`iota_snapshot::reader::latest_available_epoch`; `snapshots.mdx`
documents the restore flag and the backfill setup.

## Links to any relevant issues

follow-up (PR-2) of #11453 · part of #11023

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [x] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes

---------

Co-authored-by: muXxer <git@muxxer.de>
bingyanglin added a commit that referenced this pull request Jun 23, 2026
# Description of change

Some cleanup for #11697 

- **Local epoch backfill: only missing data counts as pruned
(`iota-core`, `iota-types`).** Real storage failures were swallowed as
"data pruned, retry once a newer snapshot is published". Absent-data
errors now carry `Kind::Missing` (was `custom`) and only those end the
best-effort replay; anything else propagates. Nothing else reads the
kind.
- **Operator doc fixes (`iota-config`, `docs`).** The config doc still
described the removed background task (the backfill is synchronous and
gates startup); the snapshots guide now documents the refuse-to-start
case when even the latest snapshot is older than the node's pruned-away
history.

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [ ] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes
muXxer added a commit that referenced this pull request Jun 24, 2026
…11697)

# Description of change

Makes a gRPC fullnode's `epochs_v2` index complete since genesis on
every bootstrap path and enforces it at startup: the node closes any gap
before services start, or refuses to run instead of silently serving an
incomplete index — the consumer side of #11453's `EPOCH_INFO` file. No
object/`previous_transaction_checkpoint` backfill: the
snapshot-publisher node resyncs from genesis instead, enforced by the
writer's existing refusal of `None` rows.

- **Synchronous startup backfill (`iota-node`, `iota-config`).** A node
that detects a gap (`epochs_v2_gap`: index short of the last executed
closed epoch) fetches only MANIFEST + `EPOCH_INFO` from the new
`state_snapshot_read_config`, seeds `[0, snapshot_epoch]`, closes any
residual above it from local history, seeds the open epoch's row, and
re-checks; a remaining gap — or no configured source — aborts startup.
Runs before live indexing exists, so the watermark has one writer at a
time and the previous design's background task, retry/backoff, and
`epoch_watermark_lock` are gone.
- **Local indexing fallback (`iota-core`).** When the latest published
snapshot lags local execution (delayed snapshot pipeline),
`index_missing_epochs_locally` replays only the missing epochs' closing
checkpoints, located via the never-pruned `epoch_last_checkpoint_map`;
best-effort up to the pruning horizon.
- **Atomic `EpochIndexed` advance (`iota-core`).** The live path
advances the watermark in the same batch as the close-of-epoch row
(gap-aware `try_advance_epoch_indexed_watermark`);
`reconcile_epoch_indexed_watermark` remains only to jump across a seeded
prefix.
- **Restore builds the whole gRPC index store (`iota-tool`,
`iota-snapshot`, `iota-core`).** `download_formal_snapshot` tees the
restored object stream into the live-state indexers
(`RestoreWithGrpcIndexes`), seeds the epoch rows, and finalizes the
store (`Watermark::Indexed`, then `meta` — a crash before `meta` leaves
a store the next open wipes and re-inits). The node opens it in place
instead of re-indexing the whole restored state; opt out with
`--skip-grpc-indexes`. `init` and the restore share one indexing
implementation (`GrpcLiveObjectRestorer`; `ParMakeLiveObjectIndexer` is
lifetime-generic now).
- **Chain-identity gate (`iota-snapshot`).**
`verify_and_restore_epoch_info` rejects a snapshot whose manifest
`chain_id` differs from this node's chain before writing any row.
- **`RestoreEpochInfo` trait (`iota-snapshot`).** Separate single-method
trait instead of a new `Restore` method: the two cover different
snapshot payloads with different targets, so each call site requires
exactly the capability it uses; the unified indexer (#11023) can
implement both.
- **No `epochs` migration (`iota-core`).** The deprecated `epochs` CF is
dropped without migration: its rows lack the end-of-epoch fields, so
they could never satisfy `EpochIndexed` and the backfill would overwrite
them anyway.
- **Open-epoch seeding (`iota-core`).** `initialize_current_epoch_info`
keys off the open epoch (`open_epoch_of`): a restore lands on a closing
checkpoint, and seeding that checkpoint's own (closed) epoch would leave
the open epoch's row permanently missing and wedge the watermark. Its
start checkpoint derives from `epoch_last_checkpoint_map` (new seq-only
`CheckpointStore` accessor; the backwards scan over prunable summaries
is deleted).
- **Default snapshot source (`setups`).** Fullnode setups for
mainnet/testnet/devnet ship a default `state-snapshot-read-config`
pointing at the IOTA Foundation buckets; ignored unless the gRPC API is
enabled and the index is incomplete.
- **Misc.** Uploader honors `state_snapshot_write_config.concurrency`
(default still 20); `get_latest_available_epoch` delegates to
`iota_snapshot::reader::latest_available_epoch`; `snapshots.mdx`
documents the restore flag and the backfill setup.

## Links to any relevant issues

follow-up (PR-2) of #11453 · part of #11023

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [x] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes

---------

Co-authored-by: muXxer <git@muxxer.de>
muXxer pushed a commit that referenced this pull request Jun 24, 2026
# Description of change

Some cleanup for #11697 

- **Local epoch backfill: only missing data counts as pruned
(`iota-core`, `iota-types`).** Real storage failures were swallowed as
"data pruned, retry once a newer snapshot is published". Absent-data
errors now carry `Kind::Missing` (was `custom`) and only those end the
best-effort replay; anything else propagates. Nothing else reads the
kind.
- **Operator doc fixes (`iota-config`, `docs`).** The config doc still
described the removed background task (the backfill is synchronous and
gates startup); the snapshots guide now documents the refuse-to-start
case when even the latest snapshot is older than the node's pruned-away
history.

## How the change has been tested

- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [ ] Patch-specific tests (correctness, functionality coverage)
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have checked that new and existing unit tests pass locally with
my changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-protocol documentation Improvements or additions to documentation node Issues related to the Core Node team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants