paritytech / paritytech/web3-storage
Provider hot/cold key separation: document public_key purpose, add restricted proxies, close operational gaps
@ilchu is already working on this.
Since Aug 3, 2026.
- Dominant language
- Rust
- Stars
- 12
- Forks
- 3
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 33
Description
Background
ProviderInfo.public_key exists but its purpose is documented nowhere — only mechanics ("raw bytes, multiple key types"). Investigation (design discussion, 2026-07-18) concluded it is exactly the right mechanism for provider hot/cold key separation and should be kept and documented as such:
- Cold account: holds stake, signs rare admin extrinsics (register, settings, deregister).
- Hot signing key (
public_key, lives on the always-online provider server): signs off-chain payloads (CommitmentPayload,CheckpointProposal, replica sync roots). - Hot proxy key: submits time-critical operational extrinsics via
pallet_proxywithout spend authority.
Server compromise then yields: false commitments (self-slash exposure only, never fund theft), filtered extrinsics, loss of two small fee floats. Stake stays cold.
Key rotation is deliberately not supported. Rotating a key under an active agreement invalidates client-held signed commitments (commit receipts, challenge_offchain evidence) — every mitigation examined (previous-key grace window bounded by MaxNonceAge, per-agreement key freezing with re-anchoring at extension) adds machinery disproportionate to the benefit. Compromise playbook instead: cold key removes the proxy (remove_proxy, immediate), stops accepting agreements via settings, winds down existing agreements, then replaces the key.
Tasks
1. Document the key's purpose
- Rustdoc on
ProviderInfo.public_keyandregister_provider: hot signing key, may (and for hot/cold setups should) differ from the account key; account key can be used where separation isn't wanted. - Design doc (implementation doc): new "Provider key management" subsection — threat model, cold/hot split, compromise playbook, explicit no-rotation decision + rationale (the extension/evidence-invalidation problem).
- Resolve the 64-byte inconsistency: registration accepts 32/33/64 (
register_provider_internal), butverify_signatureonly handles 32/33 — a 64-byte key registers fine and fails at first use. Either drop 64 at registration or document/implement what it's for ("reserved for future schemes" comment is stale either way).
2. Proxy support (runtime config, both runtimes)
- Add
pallet_proxywith customProxyType::{Any, NonTransfer, StorageProvider}(+ optionalCancelProxy— only meaningful if delayed proxies are ever used; we start with delay 0). Pallet requiresProxyType::default()to be the most permissive variant (Any). -
StorageProviderfilter = explicit whitelist:respond_to_challenge,provider_checkpoint,confirm_replica_sync,Utility::batch/batch_all/force_batch. Everything else denied — notablyProxy::*(nested-proxy origin reset is the known filter-escape),Balances::*,deregister_provider/complete_deregister/cancel_deregister,add_stake,update_provider_settings, key replacement. - Config:
ProxyDepositBase/Factor,MaxProxies,MaxPending,BlockNumberProvider = RelaychainDataProvider(delays in relay blocks; precedent: AssetHub). Precedent for the filter shape: AssetHubStakingOperator(polkadot-fellows/runtimes#1033, tightened in #1093). - Unit tests asserting the filter lattice (what
StorageProvidercan/cannot call), mirroring AssetHub's proxy filter tests.
3. Gaps to close for hot keys to actually work
-
replace_signing_keyextrinsic, cold-signed, allowed only when the provider has zero active agreements (exact predicate TBD — an active-agreement count or thecommitted_bytesbookkeeping; mind expired-but-unsettled agreements). Excluded from the proxy filter. - Provider node: submit operational extrinsics as
proxy(cold_account, call); config gains cold-account address + proxy signer. Must watchProxyExecuted { result }— the proxy extrinsic succeeds even when the inner call is filtered/fails; a silently failedrespond_to_challengeis a slash. Alert on inner failure. - Fee float management: hot proxy account pays all fees (incl. inner-call weight) from its own balance; provider node should monitor the float and alert/top-up below threshold. (Announcement deposits would also land on the hot account if delayed proxies are ever used.)
4. Restricted hot-key setup as the default in UI and tooling
- Provider dashboard (
user-interfaces/provider): registration flow defaults to hot/cold — generate/accept a separate server signing key, registerpublic_key≠ account key, guide throughadd_proxy(hot, StorageProvider); single-key setup demoted to an explicit "advanced/dev" option. Show proxy status, fee float, andProxyExecutedfailures. - SDKs / test helpers (
packages/layer0,packages/sdk,user-interfaces/shared/test-helpers, Rust client): registration helpers take (cold signer, hot signing key, hot proxy key) as the primary shape; demo/CI flows may keep single-key via the dev signers but the documented default is split keys. - Docs (provider setup guides, EXTRINSICS_REFERENCE, README quick starts): hot/cold as the canonical setup; single-key documented as dev-only shortcut.
- Optional QoL: expose pallet-proxy view functions (
check_permissions) so UIs can display what the hot key may call instead of hardcoding.
Non-goals
- Key rotation under active agreements (see Background).
- Fee sponsorship via
pallet-meta-tx(merged upstream in polkadot-sdk#6428, not deployed anywhere yet) — possible future alternative to the hot fee float; out of scope.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.