Skip to content

feat(registry)!: register an existing global contract by code hash (ENG-631) - #596

Draft
peer2f00l wants to merge 1 commit into
devfrom
feature/eng-631-registry-register-an-existing-global-contract-by-code-hash
Draft

feat(registry)!: register an existing global contract by code hash (ENG-631)#596
peer2f00l wants to merge 1 commit into
devfrom
feature/eng-631-registry-register-an-existing-global-contract-by-code-hash

Conversation

@peer2f00l

@peer2f00l peer2f00l commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Closes ENG-631. Unblocks ENG-553 (staging UA registry).

add_version took (DeployMode, code) — two parameters encoding one decision, admitting a combination that means nothing (Normal alongside bytes already published globally) and offering no way to point a version key at a global contract already on chain. A second registry serving the same code paid the global-contract storage stake a second time: 65+ NEAR for the universal-account wasm, to publish bytes the protocol already stores under that exact hash.

The fold

VersionSource::Stored(code)         = 0
VersionSource::PublishGlobal(code)  = 1
VersionSource::ExistingGlobal(hash) = 2

Discriminants 0 and 1 match DeployMode::Normal/GlobalHash, and Base64VecU8 is a transparent borsh newtype over Vec<u8>, so the encoding is byte-identical for both existing modes — the 1.1.0+ registries already deployed keep working untouched.

That equality is pinned, not trusted: borsh_is_wire_compatible_with_deploy_mode asserts it directly against the old tuples, and the three live_release_migrates_onto_current sandbox cases drive real 0.1.0/1.0.0/1.1.0 registries end to end. Pre-1.1.0 predates the tag entirely and still takes a bare (version_key, code); a code hash has no representation there at all, so that combination errors rather than encoding into something the registry would misread as a wasm blob.

The hash is verified, not trusted

ExistingGlobal inserts the entry, then a create/use_global_contract/delete receipt on probe.<registry> proves a global contract stands behind it. A hash with nothing behind it fails the receipt and the existing add_version_01_finalize frees the key again.

This is what makes an unverified typo unreachable: remove_version hard-panics on a GlobalHash entry, so without the probe one bad hash would burn a version key permanently. The probe uses a name distinct from deploy.<registry> so it cannot collide with an in-flight publish; both paths now share one scratch-account helper.

The probe costs one yoctoNEAR — measured, not estimated

The issue anticipated measuring an account-creation floor and requiring a deposit covering it. Measured in sandbox, the floor is 1 yocto: the probe account is created and deleted inside a single receipt, so it never has to satisfy a storage-staking minimum, and use_global_contract copies no code to stake for. The cost is gas.

I built the constant first, then deleted it once the measurement came back — a deposit the chain does not require would have been invented machinery. assert_one_yocto() is the contract's existing idiom for exactly this. Against ~50 NEAR to publish the same market wasm, the ratio assertion in deployment.rs is now > 1_000_000x.

Two deliberate deviations

registry.addArtifactVersion keeps DeployMode. The issue says DeployMode retires from "the gateway spec"; I read that as scoped to registry::AddVersion, where the invalid combination lived. The artifacts path resolves its own catalog-verified bytes, so ExistingGlobal is meaningless there and only store-versus-publish is open. Say the word if you meant a full retirement.

No preflight version gate yet, and no version bump. Rejecting ExistingGlobal against a registry too old to parse it keys on the version number of the release carrying this change — which does not exist until it ships. Such a registry fails on chain rather than at preflight until then. The check is prepared and stacked: branch 2, to merge only after this is released.

Acceptance criteria

  • Borsh regression: Stored/PublishGlobal encode byte-identically to the DeployMode tuples
  • A version registered by hash alone deploys through use_global_contract, identical to a published one, for a deposit orders of magnitude below
  • An unknown hash rolls back: receipt fails, get_version_code_hash is None, key is reusable
  • Gateway sandbox coverage for all three variants, each followed by registry.deploy
  • tools/manager unit tests for the code-hash source
  • gateway/METHODS.md regenerated — no diff, as the reference renders method names rather than field shapes

Verification

61/61 sandbox (registry + gateway), 665 fast tests, relayer 11/11, cargo fmt --all --check clean, and CI's cargo clippy --all-features --workspace --tests -- -D warnings clean.

Note for review

The three-variant gateway test reads its hash through a raw get_version_code_hash view rather than registry.getVersion, because the pre-existing ENG-559/560 gate puts getVersion behind ≥1.3.0 — which no deployed or sandbox registry satisfies yet. Worth a separate look: that gate makes several registry.* reads unusable against every registry currently in existence.

🤖 Generated with Claude Code


This change is Reviewable

…NG-631)

`add_version` took `(DeployMode, code)` — two parameters encoding one decision,
admitting a combination that means nothing (`Normal` alongside bytes already
published globally) and offering no way to point a version key at a global
contract already on chain. A second registry serving the same code therefore
paid the global-contract storage stake a second time: 65+ NEAR for the
universal-account wasm, to publish bytes the protocol already stores under that
exact hash.

Fold the pair into one source:

    VersionSource::Stored(code)         = 0
    VersionSource::PublishGlobal(code)  = 1
    VersionSource::ExistingGlobal(hash) = 2

Discriminants 0 and 1 match `DeployMode::Normal`/`GlobalHash`, and `Base64VecU8`
is a transparent borsh newtype over `Vec<u8>`, so the encoding is byte-identical
for both existing modes and the 1.1.0+ registries already deployed keep working
untouched. That equality is pinned by a golden-bytes test rather than trusted,
and the `live_release_migrates_onto_current` cases exercise the 0.1.0, 1.0.0 and
1.1.0 registries end to end. Pre-1.1.0 predates the tag entirely and still takes
a bare `(version_key, code)`; a code hash has no representation there at all, so
that combination is an error rather than an encoding the registry would misread
as a wasm blob.

The hash is verified, not trusted. `ExistingGlobal` inserts the entry, then a
create/`use_global_contract`/delete receipt on `probe.<registry>` proves a global
contract stands behind it; a hash with nothing behind it fails the receipt and
the existing `add_version_01_finalize` frees the key again. This is what makes an
unverified typo unreachable — `remove_version` hard-panics on a `GlobalHash`
entry, so a burned key would stay burned. The probe uses a name distinct from
`deploy.<registry>` so it cannot collide with an in-flight publish, and both
paths now share one scratch-account helper.

The probe takes one yoctoNEAR, measured rather than estimated. The account is
created and deleted inside a single receipt, so it never has to satisfy a
storage-staking minimum, and `use_global_contract` copies no code to stake for;
sandbox confirms the receipt succeeds at 1 yocto, against ~50 NEAR to publish the
same market wasm. The cost here is gas.

`DeployMode` remains for `registry.addArtifactVersion`, where the bytes are
resolved from the catalog and only store-versus-publish is open.

The gateway cannot yet reject `ExistingGlobal` against a registry too old to
parse it: that check keys on the version number of the release carrying this
change, which does not exist until it ships. Such a registry fails on chain
rather than at preflight until then; the preflight check follows in a stacked
change once this is released.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ae473533-9ba9-4aa0-b373-0af1dc522e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant