Skip to content

Refactor/remove chain links proxy - #116

Merged
nol4lej merged 8 commits into
mainfrom
refactor/remove-chain-links-proxy
Aug 3, 2026
Merged

Refactor/remove chain links proxy#116
nol4lej merged 8 commits into
mainfrom
refactor/remove-chain-links-proxy

Conversation

@nol4lej

@nol4lej nol4lej commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Removes pallet-account-mapping and the private_link circuit from the runtime, and gives pallet-zk-verifier a way to erase a circuit the runtime no longer implements.

The pallet bound H160 addresses to AccountId32, managed aliases and chain links, and exposed two proxy-dispatch routes (dispatch_as_linked_account, dispatch_as_private_link) that ran arbitrary calls on behalf of another account via dispatch_bypass_filter. Private transfers never needed it — the shielded pool already accepts any address shape — and the private-link route was dead code: DisabledPrivateLinkVerifier always returned false, so the circuit had never verified a single proof on chain.

Net −6567 lines, 27 files deleted, one added.

What this removes

Component Detail
frame/account-mapping/ Pallet, RPC and runtime-API crates — 18 extrinsics, 9 storage maps, 54 tests
frame/evm/precompile/account-mapping/ Precompile at 0x0800, 21 selectors
artifacts/verification_key_private_link.json On-disk VK for circuit 5
ZkVerifierPort::verify_private_link_proof Its only consumer is gone
Runtime index 14 Retired, with a comment warning against reassignment: a new pallet there would make previously encoded calls decode as a different extrinsic

AddressMapping is untouched. The EVM↔Substrate mapping is structural ([H160 | 0x00×12]) and lives in runtime code, not in the pallet — account_mapping_runtime.rs is renamed to evm_account.rs to make that ownership obvious. Frontier keeps working exactly as before.

What this adds

purge_circuit(circuit_id) — call index 7, Root

remove_verification_key and retire_version both refuse to touch a circuit's active version, which is what stops a live circuit from ending up with no key to verify against. That same guard makes them unable to retire a circuit as a whole: its last version is, by construction, the active one. So circuit 5's key stayed on chain after the runtime stopped implementing it, and get_all_circuit_versions kept listing it — that runtime API iterates storage keys with no allowlist, so explorers displayed a circuit the runtime could not serve.

purge_circuit closes that gap and only that gap. A circuit is purgeable only when expected_public_inputs returns None for its id, so transfer (1), unshield (2) and value_proof (6) are refused for as long as they remain compiled in — regardless of what storage holds. Ids above u8::MAX are rejected outright rather than truncated into that lookup, so a future circuit numbered past 255 cannot alias onto a live id.

The call clears ActiveCircuitVersion rather than requiring it to be empty. Requiring it would have made the extrinsic unreachable: the first register_verification_key activates the version it registers, and no extrinsic ever clears that entry. See "How the tests caught a real bug" below.

migrations::v1::MigrateToV1

Drops circuit 5 during the upgrade so chains carrying the key from an earlier runtime need no governance call. The pallet had no STORAGE_VERSION and no migrations module before this; both are added.

Clears by prefix across all five maps rather than iterating one map's versions — an earlier remove_verification_key could strand entries in the satellite maps with no VerificationKeys row to enumerate them from.

Fix: remove_verification_key no longer strands entries

It cleared VerificationKeys and RetiredVersions but left VkHashes and VerificationStats behind. Those entries were unreachable by any call, and they skewed the version count store_vk uses to enforce MAX_VERSIONS_PER_CIRCUIT. All four maps are now cleared together.

How the tests caught a real bug

purge_circuit originally had a second guard: ActiveCircuitVersion had to be empty. 106 unit tests passed.

The first run of the on-chain harness failed:

✓ purge blocked while a version is active
✗ retire active version — CannotRetireActiveVersion
✗ purge succeeds — CircuitStillActive          ← deadlock

The extrinsic could never execute. register_verification_key auto-activates the first version, and grep for ActiveCircuitVersion::<T>::{remove,kill,take} across the pallet returns zero matches — set_active_version only overwrites, and both retire_version and remove_verification_key refuse the active version. A one-way trap.

The unit tests passed because a test helper called ActiveCircuitVersion::<Test>::remove() directly — a path no origin can invoke. Green, testing something unreachable. That helper is gone, and the replacement test builds the circuit using only extrinsics.

An adversarial review of the fixed code then found the CircuitPurged { removed } field reported max across the four maps rather than the total, under-reporting whenever the maps held different version sets (a stranded active pointer emitted removed: 0 having cleared one entry). Now it counts entries; the weight is still charged per version, which is what the benchmark measures.

Verification

Layer Result
pallet-zk-verifier unit tests 109 (111 with --features try-runtime)
Workspace cargo test --release --lib --all 60 crates, 0 failures
Clippy (runtime-benchmarks,skip-proof-verification,try-runtime, -D warnings) 0 issues
On-chain E2E (scripts/vk/purge_e2e.cjs) 74/74
try-runtime vs testnet snapshot migrations succeed, idempotent

try-runtime against a 541 482-key snapshot of live testnet: migrations succeed, storage root identical across two runs (idempotent), 0.10% of the block budget, PoV 2.6 KiB. With spec_version 6 it runs without --disable-spec-version-check.

E2E harness (new, scripts/vk/purge_e2e.cjs — manual, CI's cargo test --lib does not see it): 74 checks over 13 sections against a dev chain. Notable coverage:

  • purge(1), purge(2), purge(6) refused, storage byte-identical afterwards
  • purge(256/257/260/65535) refused — the u8 aliasing fix holds on chain, not just in tests
  • Full purge of a 3-version circuit; all five maps cleared, removed accurate
  • Circuit 5 disappears from the runtime API; 1/2/6 still listed
  • verify_proof on a purged circuit → CircuitNotFound; a live circuit still reaches the pairing
  • At the 64-version cap: registration refused past it, purge succeeds, removed == 129
  • sudo_unchecked_weight cannot bypass the guard

Weights

weights.rs regenerated for pallet-zk-verifier on reference hardware (Hetzner CCX33, AMD EPYC-Genoa, steps 50 / repeat 20). purge_circuit measures 59.5µs + 10.75µs × v, 9 + 4v reads, 3 + 4v writes, standard error 0.25%. At the version cap it stays well under 10% of a 2000 ms block — asserted by purge_circuit_at_the_cap_fits_in_a_block.

The benchmark itself was fixed along the way: it seeded only two of the four maps the extrinsic clears, so it would have measured half the work.

Version bump

spec_version 5 → 6, transaction_version 1 → 2.

Testnet reports spec_version 5, so a setCode carrying 5 applies nothing — try-runtime refuses it outright. transaction_version moves because purge_circuit adds call index 7; SignedExtra is unchanged, but wallets key their call encoding off it.

Logged in RUNTIME_VERSIONS.md.

Chain spec

scripts/generate-specs/testnet.sh bootnodes moved from the rpc-* hostnames to p2p-*. libp2p dials port 30333 directly, so the rpc-* names sent peer traffic down a path that only fronts the HTTP RPC. Peer IDs unchanged.

Breaking changes

  • Pallet index 14 is retired. Do not reassign — a new pallet there would make previously encoded calls decode as a different extrinsic.
  • Precompile 0x0800 is gone. Contracts calling it now revert.
  • transaction_version 1 → 2. Offline-signed extrinsics prepared against the old runtime are invalidated.
  • Circuit 5 is unregistered. Proofs against it fail with CircuitNotFound. No funds are affected: it was never a note-spending circuit, and DisabledPrivateLinkVerifier meant it never verified anything on chain.

@nol4lej
nol4lej merged commit 74aa749 into main Aug 3, 2026
6 checks passed
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