Tracking issue for implementing RFC-27 (rfcs/rfc27-ip-verification.md, added in #2304).
Why
create_user_core accepts any globally-routable client_ip when the AccessPass is a wildcard pass — stored at 0.0.0.0 or flagged allow_multiple_ip, which includes the EdgeSeat passes the shred-oracle issues. is_global() is the only check applied. That leaves two holes: squatting on the (client_ip, user_type) User PDA to lock out the legitimate operator, and pointing device tunnel/route provisioning at a third party's address.
RFC-27 closes them with a signed IpOwnershipProof from a DoubleZero-operated verification service, validated onchain through the Ed25519 precompile so it cannot be bypassed by calling the program directly.
Decisions made ahead of implementation
These resolve RFC open questions and the review points in #4192. The RFC text is amended to match as part of the onchain validation issue.
| Question |
Decision |
| Replay binding |
Signed message is payer || client_ip || epoch || user_pubkey, with a domain-separation prefix and version byte. Binding the target User PDA stops a routine connect proof from being replayed into a destructive operation (see #4192 item 2 and the future ReclaimUser work in #4190). |
| Freshness window |
Accept clock.epoch and clock.epoch - 1. Fixed constant, not configurable. |
| Rollout |
FeatureFlag::RequireIpOwnershipProof (bit 2). Proof optional until the flag is set per environment, then required for every user creation — wildcard and specific-IP alike. |
| Persist proofs onchain |
No. The bound client_ip plus transaction history is the audit record. |
| IPv6 |
Out of scope. The program's client_ip surface is Ipv4Addr and the User PDA derives from it. |
| Periodic re-verification |
Out of scope here. Re-proving on a schedule and releasing a user whose IP changed hands is tracked in #4193 / #4190. |
| Key rotation |
Verifier pubkey lives in GlobalState and rotates through the existing SetAuthority instruction. No cadence set; rotation invalidates outstanding proofs and clients re-verify on next connect. |
Work
Foundations:
Onchain enforcement:
Off-chain service:
Client path:
Ecosystem:
Ordering
#4195 and #4196 are independent and unblock everything. #4197 needs both. #4198 needs #4195. #4200 needs #4195 and #4197's argument shape. #4201 needs #4198 and #4200. #4203 needs #4196. #4204 needs #4198. #4205 needs #4197, #4201, and #4204. #4202 is independent and can land any time. #4225 needs #4197 and must land before require-ip-ownership-proof is set in any environment the device-stress orchestrator runs against.
Not in scope
Releasing or reclaiming a User account whose IP has changed hands (#4193, #4190), periodic re-verification, and IPv6.
Tracking issue for implementing RFC-27 (
rfcs/rfc27-ip-verification.md, added in #2304).Why
create_user_coreaccepts any globally-routableclient_ipwhen the AccessPass is a wildcard pass — stored at0.0.0.0or flaggedallow_multiple_ip, which includes theEdgeSeatpasses the shred-oracle issues.is_global()is the only check applied. That leaves two holes: squatting on the(client_ip, user_type)User PDA to lock out the legitimate operator, and pointing device tunnel/route provisioning at a third party's address.RFC-27 closes them with a signed
IpOwnershipProoffrom a DoubleZero-operated verification service, validated onchain through the Ed25519 precompile so it cannot be bypassed by calling the program directly.Decisions made ahead of implementation
These resolve RFC open questions and the review points in #4192. The RFC text is amended to match as part of the onchain validation issue.
payer || client_ip || epoch || user_pubkey, with a domain-separation prefix and version byte. Binding the target User PDA stops a routine connect proof from being replayed into a destructive operation (see #4192 item 2 and the future ReclaimUser work in #4190).clock.epochandclock.epoch - 1. Fixed constant, not configurable.FeatureFlag::RequireIpOwnershipProof(bit 2). Proof optional until the flag is set per environment, then required for every user creation — wildcard and specific-IP alike.client_ipplus transaction history is the audit record.client_ipsurface isIpv4Addrand the User PDA derives from it.GlobalStateand rotates through the existingSetAuthorityinstruction. No cadence set; rotation invalidates outstanding proofs and clients re-verify on next connect.Work
Foundations:
crates/doublezero-ip-proof: canonical proof type and signed-message layoutip_verifier_authority_pkinGlobalState, rotatable viaSetAuthorityOnchain enforcement:
FeatureFlag::RequireIpOwnershipProofOff-chain service:
crates/doublezero-ip-verifier: the signing HTTP serviceClient path:
connectand attach it0.0.0.0) AccessPass PDA incheck_accesspassEcosystem:
GlobalStatefielddev/dzctlExecutor.CreateUsercannot carry a proof and breaks once the flag is setOrdering
#4195 and #4196 are independent and unblock everything. #4197 needs both. #4198 needs #4195. #4200 needs #4195 and #4197's argument shape. #4201 needs #4198 and #4200. #4203 needs #4196. #4204 needs #4198. #4205 needs #4197, #4201, and #4204. #4202 is independent and can land any time. #4225 needs #4197 and must land before
require-ip-ownership-proofis set in any environment the device-stress orchestrator runs against.Not in scope
Releasing or reclaiming a User account whose IP has changed hands (#4193, #4190), periodic re-verification, and IPv6.