From aa9c7c7e6e8162b7cad1afba0a6f956e1c53c7b5 Mon Sep 17 00:00:00 2001 From: "jolveira@gmail.com" Date: Wed, 26 Nov 2025 18:56:26 +0000 Subject: [PATCH 1/4] RFC: Introduce IP Ownership Verification Service for user connections --- rfcs/rfc11-ip-verification.md | 225 ++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 rfcs/rfc11-ip-verification.md diff --git a/rfcs/rfc11-ip-verification.md b/rfcs/rfc11-ip-verification.md new file mode 100644 index 0000000000..9006cca1fb --- /dev/null +++ b/rfcs/rfc11-ip-verification.md @@ -0,0 +1,225 @@ +# RFC: IP Ownership Verification Service for user connection + +## Summary + +**Status:** Draft + +This RFC introduces a mandatory **IP Ownership Verification** mechanism for DoubleZero onboarding. The goal is to ensure that only the legitimate owner of a public IP can bind that IP to an User connection. + +DoubleZero will deploy a new component, the **IP Verification Service**, which generates cryptographically signed proofs that the Serviceability program consumes before allowing IP assignment. + +This eliminates the risk of malicious users preemptively claiming IPs they do not control. + +--- + +## Motivation + +Currently, DoubleZero accepts a `client_ip` during `user create` without validating ownership. Because IPs must be unique for routing and access-control, this is a critical security gap. + +### Risk: Unauthorized IP Claim + +A malicious user may intentionally register an IP they do not control, blocking the real operator from connecting and causing a denial-of-service. + +### Goal + +Guarantee that the only entity capable of claiming an IP is the one that truly operates from it. + +--- + +## New Terminology + +## Alternatives Considered + +## Detailed Design + +This RFC proposes an **outbound proof-of-control** process using an external verification endpoint (e.g., `https://verify.doublezero.xyz`). + +## Verification Flow Diagram + +The Verification Service returns a signed **IpOwnershipProof** containing: + +- the payer pubkey +- the observed public IP +- the current epoch +- a signature by a DoubleZero verifier keypair + +The CLI includes this proof in the `CreateUser` or `CreateUserSubscriber` instruction. + +The Serviceability program validates the proof and only then binds the IP. + +--- + +### Protocol Flow + +```mermaid +sequenceDiagram + participant U as User + participant C as DoubleZero CLI + participant V as IP Verification Service + participant S as Serviceability Program (Smart Contract) + + U->>C: Run `doublezero connect ibrl` + Note right of C: No verified IP yet + + C->>V: POST verify.doublezero.xyz
payer + V->>V: Validate payer signature
Extract client_ip, epoch + V-->>C: IpOwnershipProof
{ payer, client_ip, epoch, signature } + + Note over C: CLI now holds valid
IpOwnershipProof + + C->>S: CreateUser
instruction + IpOwnershipProof + S->>S: Verify server signature
Check payer, epoch, client_ip + + alt Proof valid + S-->>C: Success — IP bound to user + C-->>U: User created / connection allowed + else Proof invalid + S-->>C: Error — invalid or expired proof + C-->>U: Verification failed + end +``` + +#### Outbound Ownership Verification + +Ownership is established by the user initiating a request **from the IP they want to claim**. + +The user runs the `connect` command, the CLI retrieves the signed public IP and creates the user by passing this information. + + ```bash + doublezero connect ibrl + ``` + +#### Steps + +1. CLI requests a verification proof: + + ```bash + GET https://verify.doublezero.xyz + { + "payer": "", + } + ``` + +2. The Verification Service extracts: + - `client_ip` = IP source from HTTP request + - `payer` pubkey from body + - `epoch` from DoubleZero side chain + +3. The Verification Service create asignature with string: "\". + +4. The service returns a signed proof: + ```json + { + "payer": "", + "client_ip": "", + "epoch": , + "signature": "" + } + ``` + +5. The CLI receive the proof and create the user + + ```bash + CreateUser(client_ip, user_payer, epoch, signature) + ``` + +6. Smart contract verifies proof: + - signature valid + - payer matches + - epoch fresh + - IP valid + +If all conditions pass → the IP is bound. + +--- + +### Proof Specification + +```rust +pub struct IpOwnershipProof { + pub payer: Pubkey, + pub client_ip: u32, // IPv4 + pub epoch: u64, + pub signature: [u8; 64], // Ed25519 +} +``` + +#### Signing Procedure + +``` +signature = sign( + verifier_private_key, + hash(payer || client_ip || epoch) +) +``` + +The corresponding public key is embedded in the Serviceability program. + +--- + +### Smart Contract Validation + +#### Required checks + +1. Decode proof. +2. Compute `H = hash(payer || client_ip || epoch)`. +3. Verify signature. +4. Validate: + - `payer == user_payer` + - epoch within allowed drift + - IP is well formed + +5. Apply IP binding. + +#### Rejection Conditions + +The program MUST reject when: + +- signature invalid +- epoch expired (~1) +- payer mismatch +- malformed IP +- reused proof outside time window + + +### Drawbacks + +- Requires maintaining centralized verification infrastructure. We can run multiple services simultaneously behind Cloudflare. +- Adds complexity to CLI + smart contract. +- Users may need re-verification if their IP changes. + +--- + +### Security Considerations + +#### Preventing IP Hijacking +Only the true operator of the IP can make the outbound request, making preemptive claiming impossible. + +#### Replay Protection +Epoch provides a freshness window; proof cannot be reused indefinitely. + +#### NAT & Shared IP Environments +Some scenarios may map multiple operators to the same IP or multiple IP on the same Server. +Documentation must warn these users about possible conflicts. + +--- + +## Backward Compatibility + +To ensure a smooth transition, the Serviceability program can support both user-creation flows for a limited number of versions: + +1. the legacy flow, where the CLI directly provides a client_ip without verification, and +2. the new verification-based flow, where the IP is bound only after presenting a signed proof generated by the IP Verification Service. + +This maintains a compatibility period until all users migrate to the new CLI version. + +--- + +## Open Questions + +- Should proofs be persisted for auditing? +- Should IP re-verification be periodic? +- Should IPv6 be supported? +- Should verifier keys rotate on a schedule? + +--- From bbc3a60a1f0c64afe849dc5a95a0459d5bc8240f Mon Sep 17 00:00:00 2001 From: "jolveira@gmail.com" Date: Wed, 26 Nov 2025 18:57:24 +0000 Subject: [PATCH 2/4] Add RFC 11 for IP Ownership Verification Service to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4951a1ccf2..016313436e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1096,6 +1096,7 @@ All notable changes to this project will be documented in this file. - RFCs - RFC-10: Version Compatibility Windows + - RFC 11 — IP Ownership Verification Service. - CLI - IP address lookups via ifconfig.me are retried up to 3 times to minimize transient network errors. - Added global `--no-version-warning` flag to the `doublezero` client and now emit version warnings to STDERR instead of STDOUT to improve scriptability and logging. From 506805d01386c8546f87e55951e79c8ffaaadfe4 Mon Sep 17 00:00:00 2001 From: "jolveira@gmail.com" Date: Wed, 26 Nov 2025 19:07:00 +0000 Subject: [PATCH 3/4] renumber to 12 --- rfcs/{rfc11-ip-verification.md => rfc12-ip-verification.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename rfcs/{rfc11-ip-verification.md => rfc12-ip-verification.md} (98%) diff --git a/rfcs/rfc11-ip-verification.md b/rfcs/rfc12-ip-verification.md similarity index 98% rename from rfcs/rfc11-ip-verification.md rename to rfcs/rfc12-ip-verification.md index 9006cca1fb..202b5e67b6 100644 --- a/rfcs/rfc11-ip-verification.md +++ b/rfcs/rfc12-ip-verification.md @@ -1,4 +1,4 @@ -# RFC: IP Ownership Verification Service for user connection +# RFC 12: IP Ownership Verification Service for user connection ## Summary From 5eb6bcc37ab6932a07e7371a4fd6cc675a18960d Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Tue, 23 Jun 2026 18:10:48 +0000 Subject: [PATCH 4/4] rfcs: renumber ip-verification to 22 and rewrite around onchain proof --- CHANGELOG.md | 2 +- rfcs/rfc12-ip-verification.md | 225 ----------------------------- rfcs/rfc22-ip-verification.md | 260 ++++++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+), 226 deletions(-) delete mode 100644 rfcs/rfc12-ip-verification.md create mode 100644 rfcs/rfc22-ip-verification.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 016313436e..526b91a4b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1096,7 +1096,7 @@ All notable changes to this project will be documented in this file. - RFCs - RFC-10: Version Compatibility Windows - - RFC 11 — IP Ownership Verification Service. + - RFC-22 — IP Ownership Verification Service. - CLI - IP address lookups via ifconfig.me are retried up to 3 times to minimize transient network errors. - Added global `--no-version-warning` flag to the `doublezero` client and now emit version warnings to STDERR instead of STDOUT to improve scriptability and logging. diff --git a/rfcs/rfc12-ip-verification.md b/rfcs/rfc12-ip-verification.md deleted file mode 100644 index 202b5e67b6..0000000000 --- a/rfcs/rfc12-ip-verification.md +++ /dev/null @@ -1,225 +0,0 @@ -# RFC 12: IP Ownership Verification Service for user connection - -## Summary - -**Status:** Draft - -This RFC introduces a mandatory **IP Ownership Verification** mechanism for DoubleZero onboarding. The goal is to ensure that only the legitimate owner of a public IP can bind that IP to an User connection. - -DoubleZero will deploy a new component, the **IP Verification Service**, which generates cryptographically signed proofs that the Serviceability program consumes before allowing IP assignment. - -This eliminates the risk of malicious users preemptively claiming IPs they do not control. - ---- - -## Motivation - -Currently, DoubleZero accepts a `client_ip` during `user create` without validating ownership. Because IPs must be unique for routing and access-control, this is a critical security gap. - -### Risk: Unauthorized IP Claim - -A malicious user may intentionally register an IP they do not control, blocking the real operator from connecting and causing a denial-of-service. - -### Goal - -Guarantee that the only entity capable of claiming an IP is the one that truly operates from it. - ---- - -## New Terminology - -## Alternatives Considered - -## Detailed Design - -This RFC proposes an **outbound proof-of-control** process using an external verification endpoint (e.g., `https://verify.doublezero.xyz`). - -## Verification Flow Diagram - -The Verification Service returns a signed **IpOwnershipProof** containing: - -- the payer pubkey -- the observed public IP -- the current epoch -- a signature by a DoubleZero verifier keypair - -The CLI includes this proof in the `CreateUser` or `CreateUserSubscriber` instruction. - -The Serviceability program validates the proof and only then binds the IP. - ---- - -### Protocol Flow - -```mermaid -sequenceDiagram - participant U as User - participant C as DoubleZero CLI - participant V as IP Verification Service - participant S as Serviceability Program (Smart Contract) - - U->>C: Run `doublezero connect ibrl` - Note right of C: No verified IP yet - - C->>V: POST verify.doublezero.xyz
payer - V->>V: Validate payer signature
Extract client_ip, epoch - V-->>C: IpOwnershipProof
{ payer, client_ip, epoch, signature } - - Note over C: CLI now holds valid
IpOwnershipProof - - C->>S: CreateUser
instruction + IpOwnershipProof - S->>S: Verify server signature
Check payer, epoch, client_ip - - alt Proof valid - S-->>C: Success — IP bound to user - C-->>U: User created / connection allowed - else Proof invalid - S-->>C: Error — invalid or expired proof - C-->>U: Verification failed - end -``` - -#### Outbound Ownership Verification - -Ownership is established by the user initiating a request **from the IP they want to claim**. - -The user runs the `connect` command, the CLI retrieves the signed public IP and creates the user by passing this information. - - ```bash - doublezero connect ibrl - ``` - -#### Steps - -1. CLI requests a verification proof: - - ```bash - GET https://verify.doublezero.xyz - { - "payer": "", - } - ``` - -2. The Verification Service extracts: - - `client_ip` = IP source from HTTP request - - `payer` pubkey from body - - `epoch` from DoubleZero side chain - -3. The Verification Service create asignature with string: "\". - -4. The service returns a signed proof: - ```json - { - "payer": "", - "client_ip": "", - "epoch": , - "signature": "" - } - ``` - -5. The CLI receive the proof and create the user - - ```bash - CreateUser(client_ip, user_payer, epoch, signature) - ``` - -6. Smart contract verifies proof: - - signature valid - - payer matches - - epoch fresh - - IP valid - -If all conditions pass → the IP is bound. - ---- - -### Proof Specification - -```rust -pub struct IpOwnershipProof { - pub payer: Pubkey, - pub client_ip: u32, // IPv4 - pub epoch: u64, - pub signature: [u8; 64], // Ed25519 -} -``` - -#### Signing Procedure - -``` -signature = sign( - verifier_private_key, - hash(payer || client_ip || epoch) -) -``` - -The corresponding public key is embedded in the Serviceability program. - ---- - -### Smart Contract Validation - -#### Required checks - -1. Decode proof. -2. Compute `H = hash(payer || client_ip || epoch)`. -3. Verify signature. -4. Validate: - - `payer == user_payer` - - epoch within allowed drift - - IP is well formed - -5. Apply IP binding. - -#### Rejection Conditions - -The program MUST reject when: - -- signature invalid -- epoch expired (~1) -- payer mismatch -- malformed IP -- reused proof outside time window - - -### Drawbacks - -- Requires maintaining centralized verification infrastructure. We can run multiple services simultaneously behind Cloudflare. -- Adds complexity to CLI + smart contract. -- Users may need re-verification if their IP changes. - ---- - -### Security Considerations - -#### Preventing IP Hijacking -Only the true operator of the IP can make the outbound request, making preemptive claiming impossible. - -#### Replay Protection -Epoch provides a freshness window; proof cannot be reused indefinitely. - -#### NAT & Shared IP Environments -Some scenarios may map multiple operators to the same IP or multiple IP on the same Server. -Documentation must warn these users about possible conflicts. - ---- - -## Backward Compatibility - -To ensure a smooth transition, the Serviceability program can support both user-creation flows for a limited number of versions: - -1. the legacy flow, where the CLI directly provides a client_ip without verification, and -2. the new verification-based flow, where the IP is bound only after presenting a signed proof generated by the IP Verification Service. - -This maintains a compatibility period until all users migrate to the new CLI version. - ---- - -## Open Questions - -- Should proofs be persisted for auditing? -- Should IP re-verification be periodic? -- Should IPv6 be supported? -- Should verifier keys rotate on a schedule? - ---- diff --git a/rfcs/rfc22-ip-verification.md b/rfcs/rfc22-ip-verification.md new file mode 100644 index 0000000000..e4edca9b39 --- /dev/null +++ b/rfcs/rfc22-ip-verification.md @@ -0,0 +1,260 @@ +# RFC 22: IP Ownership Verification Service for user connection + +## Summary + +**Status: Draft** + +This RFC introduces an **IP Ownership Verification** step for DoubleZero user creation. When a user +binds a public `client_ip` to a connection, an external **IP Verification Service** issues a +cryptographically signed `IpOwnershipProof` attesting that the request originated from that IP. The +serviceability program validates the proof **onchain** before accepting the `client_ip`. + +The motivation is a concrete gap: for **wildcard access passes** — passes stored at the unspecified +IP (`0.0.0.0`) or flagged `allow_multiple_ip`, including the `EdgeSeat` passes issued by the +shred-oracle — the program intentionally accepts *any* globally‑routable `client_ip` without +verifying that the connecting party controls it. The proof re‑introduces the missing per‑IP control +for exactly those flows, without removing the flexibility of connecting from a changing IP. + +## Motivation + +Today the serviceability program gates user creation with an **AccessPass** keyed by +`(client_ip, user_payer)`. For a pass bound to a specific IP, the program enforces that the user's +`client_ip` matches the pass, so the issuing authority effectively chose the IP. But for **wildcard +passes** the program skips that check entirely +(`smartcontract/programs/doublezero-serviceability/src/processors/user/create_core.rs:173-183`): + +```rust +// A pass stored at the UNSPECIFIED PDA (0.0.0.0) is valid for any client IP by construction +if accesspass.client_ip != Ipv4Addr::UNSPECIFIED + && accesspass.client_ip != client_ip + && !accesspass.allow_multiple_ip() +{ /* reject */ } +``` + +The only validation applied to the supplied IP is `is_global(client_ip)` +(`.../state/user.rs:381`), which checks that the address is publicly routable — **not** that the +caller owns it. + +This is a real risk for wildcard passes: + +- **IP squatting → denial of service.** The `User` PDA is derived from `(client_ip, user_type)` + (`create_core.rs:125`). Registering an IP occupies that slot and can prevent the legitimate + operator of that IP from creating their own user. +- **Traffic misdirection.** The controller provisions the GRE tunnel and routes toward the declared + `client_ip`; an IP the registrant does not control points device traffic at an unrelated third + party. + +The `EdgeSeat` flow in `doublezero-shreds` (`shred-oracle`) issues exactly such wildcard passes +(`client_ip = 0.0.0.0`, `allow_multiple_ip = true`), so this gap is on the path the project is +actively building toward. + +### Goal + +Guarantee that the `client_ip` a user binds is one the connecting party can demonstrably originate +traffic from, enforced where it cannot be bypassed (onchain), while preserving the ability to +connect from a non‑preregistered or changing IP. + +## New Terminology + +- **IP Verification Service** — A DoubleZero‑operated HTTP service that observes the source IP of an + inbound request and returns a signed `IpOwnershipProof`. +- **`IpOwnershipProof`** — A signed attestation `{ payer, client_ip, epoch, signature }` produced by + the verifier keypair. +- **Verifier keypair** — The Ed25519 keypair owned by DoubleZero whose public key is the trust root + for proof validation; its pubkey is stored in onchain global state. +- **Wildcard access pass** — An AccessPass stored at the unspecified IP (`0.0.0.0`, `IS_DYNAMIC`) + and/or flagged `ALLOW_MULTIPLE_IP`, which the program accepts for any `client_ip`. Includes the + `EdgeSeat` passes issued by the shred-oracle. +- **Proof of control** — Evidence that the holder of `payer` can originate traffic from `client_ip`, + established by issuing the verification request *from* that IP. + +## Alternatives Considered + +- **Do nothing.** Wildcard passes continue to accept any routable IP. Leaves the squatting and + traffic‑misdirection risks open for those flows. +- **Verify at access‑pass issuance.** The authority/service that issues the pass verifies IP control + and issues a **specific‑IP** pass (which the program already binds). This works and adds no onchain + crypto, but it does not cover the wildcard/`EdgeSeat` model whose whole purpose is to let a client + connect from any IP without preregistration. Good complement, not a full substitute. +- **Client‑side certification only (CLI checks the IP).** Rejected: the CLI is not a trust boundary. + An attacker calling the program/SDK directly bypasses any check that lives only in `connect`. +- **Signed proof validated onchain (this RFC).** The only option that is both non‑bypassable and + applicable to wildcard passes: the program rejects a user creation that lacks a valid proof for the + declared IP, regardless of how the instruction is submitted. + +## Detailed Design + +### Protocol Flow + +```mermaid +sequenceDiagram + participant U as User + participant C as DoubleZero CLI + participant V as IP Verification Service + participant S as Serviceability Program (onchain) + + U->>C: doublezero connect ibrl + Note right of C: client_ip not yet certified + + C->>V: POST https://verify.doublezero.xyz { payer } + Note right of V: observes the request's source IP + V-->>C: IpOwnershipProof { payer, client_ip, epoch, signature } + + Note over C: client_ip = the IP the service observed + + C->>S: CreateUser(client_ip, ...) + Ed25519 verify ix + IpOwnershipProof + S->>S: Confirm Ed25519 precompile verified (verifier_key, message, signature) + S->>S: Check payer, client_ip, epoch freshness + + alt Proof valid + S-->>C: User created, IP bound + else Proof invalid / missing / stale + S-->>C: Error + end +``` + +### Steps + +1. The CLI sends a request to the verification service. The service uses the **source IP of the + request** as `client_ip`, the `payer` from the body, and the current DoubleZero epoch: + + ``` + POST https://verify.doublezero.xyz + { "payer": "" } + ``` + +2. The service returns a signed proof: + + ```json + { + "payer": "", + "client_ip": "", + "epoch": , + "signature": "" + } + ``` + + The signed message is the byte concatenation `payer || client_ip || epoch` (the proof fields in a + fixed layout — see below), signed by the verifier keypair. + +3. The CLI submits the user‑creation transaction carrying both: + - the `IpOwnershipProof`, and + - an **Ed25519 program instruction** (the native precompile) over `(verifier_pubkey, message, + signature)`, placed in the same transaction. + +4. The serviceability program validates and, only if valid, binds `client_ip`. + +### Proof Specification + +```rust +pub struct IpOwnershipProof { + pub payer: Pubkey, // 32 + pub client_ip: Ipv4Addr, // 4 (IPv4) + pub epoch: u64, // 8 + pub signature: [u8; 64], // Ed25519 +} +``` + +The signed message is the fixed‑layout serialization of `(payer, client_ip, epoch)`. `Ipv4Addr` is +used to match the type used throughout the program; it is serialized as its 4 network‑order octets. + +### Onchain Validation + +Solana programs cannot verify an Ed25519 signature directly inside BPF cheaply. Verification uses +the **native Ed25519 precompile**: the CLI includes an `Ed25519SigVerify` instruction in the same +transaction, and the serviceability program **introspects the Instructions sysvar** to confirm that +instruction is present and that its public key, message, and signature match the expected verifier +key and the reconstructed `payer || client_ip || epoch` message. + +Required checks: + +1. Read `IpOwnershipProof` from instruction data; reconstruct `message = payer || client_ip || epoch`. +2. Load the Ed25519 instruction from the Instructions sysvar and confirm it verifies `signature` + over `message` with the **verifier public key from global state**. +3. `proof.payer == user_payer` (the account paying / owning the user). +4. `proof.client_ip == client_ip` being bound to the user. +5. `proof.epoch` is within the allowed freshness window relative to `Clock::get()?.epoch`. + +#### Rejection conditions + +The program MUST reject when any of the following holds: + +- the Ed25519 verify instruction is absent or does not match (`verifier_key`, `message`, `signature`); +- `payer` mismatch; +- `client_ip` mismatch with the value being bound; +- the proof is stale (epoch outside the freshness window); +- the proof is malformed. + +### Trust Root and Key Management + +The verifier public key is stored in onchain global state (alongside the other DoubleZero +authorities) so it can be rotated by the existing authority‑management instruction without a program +upgrade. Rotating the key invalidates outstanding proofs; clients re‑verify on next connect. + +### Relationship to AccessPass + +This proof does **not** replace AccessPass. AccessPass continues to gate *who* may connect and *what* +they may do (epoch validity, multicast allowlists, seat caps). The proof governs *which IP* a user +may bind: + +- **Specific‑IP passes** already bind the IP onchain; the proof is redundant there (it MAY still be + required uniformly for simplicity). +- **Wildcard / `EdgeSeat` passes** accept any IP today; the proof is the per‑IP control that closes + the squatting/misdirection gap for them. + +## Impact + +- **Onchain (serviceability):** new proof validation in the user‑creation path (Instructions‑sysvar + introspection), a verifier public key in global state, and new instruction arguments/accounts + (the proof and the Instructions sysvar). This is the first onchain Ed25519 verification in the + program. +- **CLI (`doublezero`):** `connect` calls the verification service and attaches the proof plus the + Ed25519 instruction to the transaction. This extends the existing public‑IP autodetection + (`look_for_ip` via ifconfig.me), which becomes a UX convenience rather than the source of truth. + Separately, `check_accesspass` should also probe the dynamic (`0.0.0.0`) AccessPass PDA, which it + does not today (`smartcontract/cli/src/requirements.rs`). +- **New off‑chain component:** the IP Verification Service (stateless signer that echoes the observed + source IP). +- **Operational:** the service must observe the real client source IP. Behind a proxy/CDN it must use + a trusted forwarded‑for header; otherwise it would sign the proxy's IP. + +## Security Considerations + +- **Non‑bypassable.** The control lives onchain. An attacker submitting the instruction directly + still needs a valid proof; the service only signs the IP it actually observed, and a TCP/TLS + handshake source cannot be spoofed off‑path, so a proof for an IP the attacker cannot originate + from is unobtainable. +- **Origin, not exclusive ownership.** A proof attests that the holder of `payer` originated a + request from `client_ip` around `epoch`. Behind NAT/CGNAT or a shared egress IP, multiple + co‑located parties could each obtain a proof for the same IP. This bounds remote squatting but does + not arbitrate between parties sharing one egress IP. DoubleZero validators with dedicated public + IPs are unaffected. +- **Source‑IP consistency.** The IP the CLI uses to reach the verification service must be the same + IP it binds as the tunnel `client_ip`. On multi‑homed hosts the client must ensure the verification + request egresses from the intended IP (e.g., source binding). +- **Centralized trust root.** The verifier keypair is a DoubleZero‑operated authority. The benefit + delivered is automated, non‑bypassable per‑IP verification — not decentralization. Key rotation is + supported via global state. +- **Replay.** The epoch window bounds proof reuse; because the proof binds `payer` and `client_ip`, + reuse only re‑asserts the same binding. A nonce or binding to the specific user account can further + constrain reuse if needed (see Open Questions). + +## Backward Compatibility + +To allow a smooth transition, the serviceability program can support both flows for a limited number +of versions: + +1. the legacy flow, where the CLI supplies `client_ip` without a proof, and +2. the new flow, where the IP is bound only after a valid `IpOwnershipProof`. + +This maintains a compatibility window until clients upgrade. Enforcement can be tightened (legacy +flow removed) once adoption is sufficient, consistent with RFC‑10 version‑compatibility windows. + +## Open Questions + +- Should proofs be persisted onchain for auditing, or is the bound `client_ip` sufficient? +- Should the proof bind to the specific user account (or a nonce) to further constrain replay within + an epoch? +- Should IP re‑verification be periodic (re‑prove on a schedule), or only at user creation? +- Should IPv6 be supported? +- On what cadence should the verifier key rotate?