Skip to content

fix(archiver): treat re-publish of preloaded protocol contracts as idempotent - #24227

Merged
PhilWindle merged 1 commit into
merge-train/spartan-v5from
spl/a-1257-archiver-idempotent-protocol-contracts
Jun 23, 2026
Merged

PhilWindle merged 1 commit into
merge-train/spartan-v5from
spl/a-1257-archiver-idempotent-protocol-contracts

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

Motivation

The archiver preloads every bundled protocol contract class (and its canonical instance) into its local store at synthetic block 0, before L1 sync. World-state genesis, however, seeds no registration nullifiers for those classes/instances. As a result a first on-chain ContractClassRegistry.publish of a bundled protocol class id is protocol-valid (fresh class-id nullifier + ContractClassPublished log). On replay the archiver recomputes the same class id and unconditionally re-inserts it, but the store throws on the pre-existing block-0 key (Contract class <id> already exists, cannot add again). Because that insert runs inside the block/checkpoint store transaction, the throw aborts persistence, and L1 sync retries the same valid checkpoint indefinitely — a sync stall.

Approach

Make the archiver treat protocol-preloaded entries as idempotent and immutable, guarded at the store layer (the single chokepoint for both the add-throw and the block-gated delete):

  • addContractClass / addContractInstance: when the key already exists and it is a protocol class id / magic protocol address, treat the (re-)publish as a no-op and keep the existing block-0 entry — crucially without bumping its recorded block number. Genuine non-protocol duplicates still throw unchanged.
  • deleteContractClass / deleteContractInstance: skip deletion for protocol entries, so a reorg of the publishing block can never roll out the preload.

Keeping the preload at block 0 is deliberate: deleteContractClass only deletes when the stored l2BlockNumber >= blockNumber, so block 0 makes protocol entries survive any reorg; bumping to the publish block would let a deep reorg delete them.

The instance-side guards are defensive only: on-chain publish_for_public_execution always emits the derived address, never a magic address, so the instance store is not reached with a magic address via the on-chain replay path today — the guards exist for symmetry and to protect future code paths.

This is a non-breaking, node-local resilience fix. A follow-up PR (PR2) seeds the protocol registration nullifiers into world-state genesis so the on-chain re-publish is rejected at the protocol level — the root-cause fix for the class path.

Changes

  • protocol-contracts: add isProtocolContractClass(classId) (sibling of the existing isProtocolContract(address)), backed by a set of the generated ProtocolContractClassId values.
  • archiver: idempotent add + protected delete for protocol classes and instances in ContractClassStore / ContractInstanceStore.
  • archiver (tests): store unit tests (idempotent re-add stays queryable with block-0 / bytecode-commitment preserved, protected delete, non-protocol duplicate still throws — for both classes and instances) and an A-1257 integration test that preloads protocol contracts, builds an L2Block carrying a ContractClassPublished log for a bundled class id, and asserts addProposedBlock commits and the class stays queryable.

Fixes A-1257

…empotent

The archiver preloads bundled protocol contract classes and instances at
synthetic block 0. When a bundled protocol class id (or instance address) was
later published on chain, the contract stores threw "already exists" while
re-adding the already-present key, stalling L1 sync.

Make the stores idempotent for protocol-preloaded entries:
- addContractClass / addContractInstance now skip (no-op) when the key already
  exists and it belongs to a bundled protocol contract, keeping the existing
  block-0 entry untouched. Genuine non-protocol redefinitions still throw.
- deleteContractClass / deleteContractInstance never delete protocol entries,
  so they survive reorgs of the publishing block.

Adds isProtocolContractClass to protocol-contracts as a sibling of
isProtocolContract, plus store unit tests and an integration test that
publishes a bundled protocol class id through ArchiverDataStoreUpdater.

Fixes A-1257
@AztecBot

Copy link
Copy Markdown
Collaborator

Flakey Tests

🤖 says: This CI run detected 2 tests that failed, but were tolerated due to a .test_patterns.yml entry.

\033FLAKED\033 (8;;http://ci.aztec-labs.com/d3a75831b3616f8d�d3a75831b3616f8d8;;�): yarn-project/kv-store/scripts/run_test.sh src/bench/sqlite-opfs-encrypted/map_bench.test.ts (1s) (code: 0)
\033FLAKED\033 (8;;http://ci.aztec-labs.com/c4ddebf2cfa9e874�c4ddebf2cfa9e8748;;�): yarn-project/kv-store/scripts/run_test.sh src/sqlite-opfs/internal/ordered-binary-browser.test.ts (1s) (code: 0)

@PhilWindle
PhilWindle merged commit 24a0bc5 into merge-train/spartan-v5 Jun 23, 2026
17 of 18 checks passed
@PhilWindle
PhilWindle deleted the spl/a-1257-archiver-idempotent-protocol-contracts branch June 23, 2026 08:40
@AztecBot

Copy link
Copy Markdown
Collaborator

❌ Failed to cherry-pick to v4 due to conflicts. (🤖) View backport run.

spalladino added a commit that referenced this pull request Jun 23, 2026
…empotent (backport #24227) (#24232)

## Summary

Backport of #24227
to the `v4` release line (base: `backport-to-v4-next-staging`).

The archiver preloads bundled protocol contract classes/instances at
synthetic block 0. A first on-chain `publish` of a bundled protocol
class id is protocol-valid, but the store threw on the pre-existing
block-0 key, aborting the block-store transaction and stalling L1 sync.
This makes the archiver treat protocol-preloaded entries as idempotent
and immutable at the store layer:

- `addContractClass` / `addContractInstance`: re-publish of an
already-present protocol class id / magic protocol address is a no-op,
keeping the block-0 entry (block number not bumped).
- `deleteContractClasses` / `deleteContractInstance`: protocol entries
are never deleted, so a reorg of the publishing block cannot roll out
the preload.
- protocol-contracts: new `isProtocolContractClass(classId)` helper
backed by the generated `ProtocolContractClassId` set.

## Conflict resolution

The automatic cherry-pick of merge commit `24a0bc5` conflicted because
the archiver store/test layout on v4 differs from `next`:

- **`contract_class_store.ts`** — kept v4's import of `FunctionSelector`
alongside the new `isProtocolContractClass` import, and kept v4's method
name `deleteContractClasses` (callers on v4 use that name) while
grafting in the protocol-delete guard.
- **`contract_instance_store.ts` / `protocol_contract.ts`** —
auto-merged; guards and the new helper applied cleanly.
- **`data_store_updater.test.ts`** — the new A-1257 idempotency
integration test was adapted to v4's `KVArchiverDataStore` harness:
dropped `next`-only imports (`createArchiverDataStores`, `L2TipsCache`,
`GENESIS_BLOCK_HEADER_HASH`, `CheckpointHeader`) and rewrote
`store.contractClasses.getContractClass(...)` to v4's
`store.getContractClass(...)`.
- **`contract_class_store.test.ts` / `contract_instance_store.test.ts`**
— these store-level unit test files do not exist on the v4 line (only
`kv_archiver_store.test.ts` does) and the PR's versions use `next`'s
array-based store API, so they were not introduced. The behavioral
change is covered by the adapted integration test above.

## Verification

The workspace was not bootstrapped (no `node_modules`/generated
artifacts), so a full `yarn build` was not run here. Every import and
store API call in the resolution was statically confirmed against
`origin/v4-next` (`registerProtocolContracts`, `ContractClassLogFields`,
`bufferAsFields`,
`CONTRACT_CLASS_LOG_SIZE_IN_FIELDS`/`CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE`,
`BundledProtocolContractsProvider`, `ProtocolContractClassId`,
`isProtocolContract`). CI on this PR provides the build/test signal.

---
*Created by
[claudebox](https://claudebox.work/v2/sessions/fe21ec2d87559552) ·
group: `slackbot`*
aminsammara added a commit that referenced this pull request Jun 26, 2026
## Summary
- Bumps v4 to 4.4.0 (version manifest was already set in a prior commit)
- Adds 4.4.0 and 4.3.1 changelog entries covering all commits since
v4.3.1
- Includes bug fixes for archiver, world-state, p2p, release-image, and
mac install

## Changes included in 4.4.0
- fix(archiver): harden v4 L2 tips cache refresh (#24237)
- fix(world-state): backport archive root guard (#24240)
- fix(archiver): treat re-publish of preloaded protocol contracts as
idempotent (#24227)
- fix(p2p): guard ENR address parsing against malformed TCP fields
(#24215)
- fix(archiver): index zero-field logs under empty tag instead of
throwing (#24212)
- fix: released contract artifact aztec version (#23470)
- fix(release-image): stamp aztec_version in published contract
artifacts
- fix: some mac install fixes (#20630)
- feat(ci): run aztec-cli acceptance test on macOS (#23309)
- fix(ci): skip aztec-cli notify job when acceptance test is skipped
(#23534)
- docs(operators): expand ATP and ATV on first use (#23254)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants