Scope:
schema-registryis the only contract that exposes an in-place upgrade entry point.sasandsoroban-sas-indexerare immutable once deployed — bug fixes there require deploying fresh instances and re-wiring their dependencies (SAS::set_indexer,Indexer::initbinding). Treat every Mainnetschema-registryupgrade as a staged, audited deployment.
- Genesis version is
1(set byinit).get_version()reports it. - Every upgrade must increment by exactly
1:new_version == old_version + 1. Skips or downgrades are rejected withSASError::InvalidValuebefore any WASM is written. - Only known versions are accepted. The contract stores
MAX_KNOWN_VERSION(currently2) — anynew_version > MAX_KNOWN_VERSIONis rejected withSASError::IncompatibleDependencyeven if the hash looks valid. Add a new audited release to the allow-list before deploying it. - The hash must be non-zero (
[0;32]is rejected). Pin the exactsha256of the auditedschema_registry.wasmfor each release in git (CHANGELOG.md) and verify withsha256sum target/wasm32-unknown-unknown/release/schema_registry.wasm.
- Build & audit the candidate WASM:
cargo build -p schema-registry --release --target wasm32-unknown-unknown stellar contract optimize --wasm target/wasm32-unknown-unknown/release/schema_registry.wasm # → schema_registry.optimized.wasm sha256sum schema_registry.optimized.wasm | tee WASM_HASH
- Dry-run on Testnet via
stellar contract deploy+stellar contract invoke --id <test_registry> -- upgrade --new-wasm-hash <hash> --new-version <next>and run:cargo test -p schema-registry(includestest_upgrade_preserves_schemas)- Manual
get_schema/get_schemas/validate_schemaagainst every live schema UID before and after the call. get_versionincrements by one,UPGRADEevent is emitted (see §4).
- Storage-migration gate — the on-chain
upgradeitself readsSCHEMA_COUNTto confirm the persistent layout is still readable. If the new WASM would orphanSCHEMA_COUNTorCREATORkeys, the gate fails and the upgrade reverts withIncompatibleDependency. For larger migrations, deploy a migration contract, simulate it withstellar tx simulate, and only then stage the upgrade.
- Announce the upcoming
old_version → new_versionand its hash at least one week before Mainnet activation (governance forum + on-chain event feed). - Propose the upgrade from a cold admin key (hardware wallet). Do not reuse the hot deploy key used for Testnet.
- Simulate first:
Inspect
stellar contract invoke --id $SCHEMA_REGISTRY_CONTRACT_ID \ --source-account $ADMIN_ADDRESS --rpc-url $RPC_URL --network-passphrase "$PASSPHRASE" \ -- upgrade --new-wasm-hash <64_hex> --new-version 2 --simulate --cost
--costand the returnedUPGRADEevent; abort if the resource fee exceeds the keeper's buffer. - Execute the same invocation without
--simulate:stellar contract invoke --id $SCHEMA_REGISTRY_CONTRACT_ID \ --source-account $ADMIN_ADDRESS --rpc-url $RPC_URL --network-passphrase "$PASSPHRASE" \ -- upgrade --new-wasm-hash <64_hex> --new-version 2
- Verify within the same ledger:
stellar contract invoke --id $SCHEMA_REGISTRY_CONTRACT_ID -- get_version→2stellar contract invoke --id $SCHEMA_REGISTRY_CONTRACT_ID -- get_schemas --start 0 --limit 100returns every pre-upgrade schema unchanged (checked viasha256of their XDR).validate_schemaon a known UID still returnstrue; deprecated UIDs remainfalse.
upgrade(old_version, new_version, wasm_hash) publishes:
topics: (symbol!("UPGRADE"), old_version: u32, new_version: u32)
data: (old_version, new_version, wasm_hash: BytesN<32>)
Indexers (Zephyr, The Graph) should subscribe to UPGRADE to build a tamper-evident
upgrade history. Store (old_version, new_version, hash, ledger_seq, tx_hash)
off-chain for incident response.
There is no implicit
downgradeentry point. A downgrade is a forward upgrade to a previously audited version's hash with the next monotonic version number.
- Halt new
register/deprecatecalls by rotating the admin key's signing authority off (revoke the compromised session; the contract remains readable). - Re-build the last known good WASM (e.g.
v1hashabc...orv2hashdef...). Verify itssha256matches theCHANGELOG.mdpin. - Forward-upgrade to the good hash as the next version:
The
# Suppose v2 was faulty; v3 will be a re-upload of v1's hash. stellar contract invoke --id $SCHEMA_REGISTRY_CONTRACT_ID \ --source-account $ADMIN_ADDRESS --rpc-url $RPC_URL --network-passphrase "$PASSPHRASE" \ -- upgrade --new-wasm-hash <GOOD_HASH> --new-version 3
UPGRADEevent will show2 → 3 (GOOD_HASH). - Re-verify every
get_schemaandget_schemas— the persistentSCHEMA_COUNT/CREATORmaps survive upgrades because the storage-migration gate rejects layouts that would orphan them (tested intest_upgrade_preserves_schemas_and_config). If any schema is missing, restore from the off-chain backup ofget_schemastaken before activation.
If bad data was written through the faulty logic, a hash rollback alone is insufficient. Follow the same forward-upgrade step, then:
- Replay the off-chain
SchemaRegisteredevent log to re-issue any schemas registered under the bad logic (their UIDs are deterministicsha256(schema_xdr)). - Keep the bad version's
UPGRADEevent in the on-chain history — do not delete it — and document the incident inCHANGELOG.mdwith the ledger range and remediated UIDs.
test_upgrade_preserves_schemas_and_config deploys a genesis registry, registers
two schemas and sets fee/treasury, upgrades 1→2, and asserts:
get_version() == 2and theUPGRADEevent(1,2,hash)was emitted,- both pre-upgrade
get_schema(uid)still return the exactSchemaRecord, get_schemas(0,10)returns both,fee/treasuryinstance values survive,- an upgrade with
new_version == 1(downgrade) ornew_version == 3(unknown) orhash == 0is rejected withInvalidValue/IncompatibleDependencybeforeupdate_current_contract_wasmis called.
Run it with:
cargo test -p schema-registry -- test_upgrade_preserves_schemas_and_config
cargo test -p schema-registry -- test_upgrade_rejects_incompatible_version
cargo test -p schema-registry -- test_upgrade_rejects_zero_hashKeep these green before every Mainnet upgrade.