Skip to content

Implement publishable P2P (per-pod L4 NLB + ExternalAddress) #360

Description

@bdchatham

Problem

Sei pods today are reachable for HTTP (RPC/EVM/REST) via the existing Istio L7 path → shared NLB → private pod, but they are not publicly dialable as P2P peers over TCP/26656. This blocks validators (in a deterministic set without sentries) and advertised full nodes from being addressable by ecosystem peers, and prevents internal SNDs from peering each other across clusters.

The design (platform/docs/designs/sei-publishable-p2p-nlb.md, merged in sei-protocol/platform#714) closes this gap with an opt-in networking.tcp: {} sub-struct: when set, the SeiNode reconciler stamps a per-pod Service type=LoadBalancer (AWS NLB, target-type: ip, scheme=internet-facing, cross-zone=true), reads Service.status.loadBalancer.ingress[0].hostname, and writes SeiNode.Status.ExternalAddress = "<hostname>:26656". The planner already reads ExternalAddress into p2p.external_address at planner.go:697; this issue makes that pipeline reachable.

Impact

Unblocks:

  • Validator P2P publishability — deterministic set peers each other over stable NLB DNS endpoints; combined with unconditional_peer_ids cross-config (ops, not this issue) gives stable cross-cluster validator mesh.
  • Cross-SND peer discovery — new PeerSource.seinodeDeployment variant resolves sibling SND children's Status.ExternalAddress into persistent_peers, replacing the EC2-tag-only discovery for K8s-resident publishers.
  • Ecosystem-parity for advertised full nodes — partner peers can list us as a persistent peer.

Harbor stays excluded until the Cilium overlay is replaced (CGNAT pod IPs aren't VPC-routable → target-type: ip fails); controller fails closed via a VPC-CIDR routability check.

Proposed approach

Six independently-shippable PRs. Sequence dependencies: PR-1 → PR-2 → PR-3 (the chain that makes the field gateable and propagated). PR-4 lands in parallel with PR-3 (independent code path; needs PR-1's types only). PR-5 lands any time before PR-6. PR-6 waits on PR-3 + PR-4 + PR-5.

PR-1 — Types layer (contract-only, no behavior change)

  • api/v1alpha1/networking_types.go: add HTTP *HTTPConfig + TCP *TCPConfig to NetworkingConfig; add HTTPEnabled() accessor (legacy networking: {} → treated as http: {} for backcompat).
  • api/v1alpha1/seinode_types.go: add Networking *NetworkingConfig to SeiNodeSpec; flip ExternalAddress doc-comment from "SeiNodeDeployment controller" → "SeiNode controller".
  • api/v1alpha1/common_types.go: add SeiNodeDeploymentPeerSource to PeerSource union; bump XValidation CEL sum.
  • CRD regen + smoke test: existing manifests (Networking-nil and legacy networking: {}) still validate.

PR-2 — SND template propagation

  • internal/controller/seinodedeployment/: propagate Spec.Template.Networking into child.Spec.Networking at child-creation time (single line in the existing template-merge path).
  • Envtest: SND with networking.tcp: {} in template → child SeiNodes carry Spec.Networking.TCP != nil.

PR-3 — LB Service + ExternalAddress + Service-ready gate (the heavy PR)

  • internal/controller/node/external_address.go (NEW):
    • Stamp Service type=LoadBalancer (<seinode>-p2p) when Spec.Networking.TCP != nil and pod IP is inside SEI_VPC_CIDR env (VPC-CIDR routability gate).
    • Annotations: aws-load-balancer-type=nlb, scheme=internet-facing, nlb-target-type=ip, cross-zone-load-balancing-enabled=true; externalTrafficPolicy: Local; only TCP/26656.
    • Watch Service status; on loadBalancer.ingress[0].hostname populated → write Status.ExternalAddress = "<host>:26656" (bare host:port, no nodeId@).
    • Empty-host guard: if newHost == "" { return nil } — don't clear on transient absence.
    • Service-ready gate: when tcp != nil, hold STS creation until Status.ExternalAddress is written. (Without this, the first sidecar render has empty p2p.external_address; planner doesn't detect runtime override drift at planner.go:706.)
    • Opt-out cleanup: when Spec.Networking.TCP == nil, delete the Service inline.
  • internal/controller/node/controller.go: confirm existing Owns(&corev1.Service{}) at :204 covers the new Service (predicate audit); add RBAC verbs if missing (create/update/delete on services).
  • Manager env: SEI_VPC_CIDR plumbed via the controller Deployment (set in platform/clusters/prod/sei-k8s-controller/manager-patch.yaml — same pattern as SEI_GATEWAY_PUBLIC_DOMAIN).
  • Envtest: bootstrap with tcp: {} → STS doesn't appear until Service has a (fake) hostname; opt-out path deletes the Service.

PR-4 — seinodeDeployment peer-source variant

  • internal/controller/node/peers.go: extend reconcilePeers switch with the new variant; add resolveSNDPeers that lists matching SNDs → lists child SeiNodes → emits each child's Status.ExternalAddress as host:port into ResolvedPeers. The sidecar's existing CollectAndSetPeers task at internal/planner/group.go:59 prepends nodeID@ at task-build time (:26657/status query) — same path the label variant feeds. Not a new sidecar capability.
  • internal/controller/node/controller.go: Watches(&SeiNodeDeployment{}, mapperFn); naive mapper (re-reconcile all SeiNodes in namespace on any SND change) is acceptable at the ~20-publishable scale; tighten to selector-matching scope when fanout becomes a concern.
  • RBAC: cluster-scoped seinodedeployments get/list/watch.
  • Envtest: consumer SND with peers: [{ seinodeDeployment: ... }] → resolves to producer SND children's ExternalAddress.

PR-5 — Node SG opening (in sei-protocol/platform)

  • Terraform: tcp/26656 from 0.0.0.0/0 ingress on the Sei-node SG. Lands before the first publishable SND.

PR-6 — First publishable rollout (in sei-protocol/platform)

  • Enable networking.tcp: {} on one advertised full-node SND.
  • Verify: external nc <nlb-host> 26656; :26657/net_info shows DNS form in listen_addr; a remote node connects with <node_id>@<host>:26656.
  • Validator rollout follows the same shape, one at a time across the deterministic set, with unconditional_peer_ids cross-config landed first (runbook).

Out of scope

  • Config-hash STS-template annotation for automatic runtime tcp: {} add — v2. Runtime adds require a manual pod delete after Status.ExternalAddress populates (NLB hostnames don't change once allocated, so this doesn't recur).
  • EIP-attached NLBs (stable IP, not just DNS) — DNS form only.
  • NLB-attached SG (defense in depth) — relies on node-SG.
  • Dedicated publishable EC2NodeClass / scoped SG — opens 26656 on shared node SG.
  • Orphan-cleanup walker for SND scale-down events — opt-out (tcp: {} → nil) cleanup IS in PR-3; scale-down (replica drop) orphans wait for v2.
  • Harbor publishability — gated on Cilium overlay → VPC-routable CNI migration. Controller fails closed.
  • Validator unconditional_peer_ids cross-config — operational runbook, not a controller change.
  • CometBFT sentry architecture — explicitly not adopted; deterministic validator set publishes directly.

Relevant experts

  • kubernetes-specialist — PR-2, PR-3, PR-4 (controller-runtime, CRD types, envtest)
  • platform-engineer — PR-3 manager env wiring, PR-5 (Terraform), PR-6 (manifest)
  • sei-network-specialist — PR-6 verification (CometBFT NodeInfo + dial semantics)

References

  • Design: sei-protocol/platform, docs/designs/sei-publishable-p2p-nlb.md (merged in #714)
  • Planner integration point: internal/planner/planner.go:697 (commonOverrides already reads Status.ExternalAddress)
  • Existing Service watch: internal/controller/node/controller.go:204
  • Drift-detection limit acknowledged: internal/planner/planner.go:706 (buildRunningPlan only checks image + sidecar reapproval; ExternalAddress changes after Running don't propagate)
  • AWS LBC annotation precedent: existing Istio gateway in platform/clusters/prod/gateway/gateway.yaml

One-way doors (already approved in design)

  • Status.ExternalAddress format: "<host>:26656" (bare host:port; host is raw NLB *.elb.<region>.amazonaws.com, no vanity domain)
  • NetworkingConfig.HTTP / TCP sub-struct field names (presence-signals)
  • Legacy networking: {} interpreted as networking: { http: {} }
  • SeiNodeSpec.Networking field add (additive; propagated from SND template)
  • PeerSource.seinodeDeployment variant (additive; bump union CEL)
  • Keep peers field name (not renamed)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions