Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion internal/embed/infrastructure/values/erpc.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ replicas: 1

image:
repository: ghcr.io/erpc/erpc
tag: 0.0.64
# 0.1.x: unified selection-policy engine + in-house failsafe + dRPC REST discovery.
# NOTE: the legacy `selectionPolicy.evalFunction` blocks below are deprecated in 0.1.x
# (compat-shimmed to `selectionPolicy.eval`, emit a deprecation warning) — migrate in a follow-up.
tag: 0.1.1
pullPolicy: IfNotPresent

# Config file
Expand Down
5 changes: 5 additions & 0 deletions internal/embed/networks/hl-node/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: v2
name: hl-node-local
description: Local Hyperliquid non-validating node resources
type: application
version: 0.1.0
121 changes: 121 additions & 0 deletions internal/embed/networks/hl-node/helmfile.yaml.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
repositories:
- name: bedag
url: https://bedag.github.io/helm-charts/

releases:
# Hyperliquid non-validating node (closed x86-64 hl-visor binary, containerized on ubuntu:24.04).
# hostNetwork so gossip 4001/4002 bind to the node IP (must be publicly reachable to sync).
- name: hl-node
namespace: hl-node-{{ .Values.id }}
createNamespace: true
chart: bedag/raw
values:
- resources:
- apiVersion: apps/v1
kind: StatefulSet
metadata:
name: hl-node
namespace: hl-node-{{ .Values.id }}
labels:
app: hl-node
app.kubernetes.io/part-of: obol.stack
obol.stack/id: {{ .Values.id }}
obol.stack/app: hl-node
spec:
serviceName: hl-node
replicas: 1
selector:
matchLabels:
app: hl-node
template:
metadata:
labels:
app: hl-node
spec:
{{- if .Values.host }}
nodeSelector:
kubernetes.io/hostname: {{ .Values.host }}
{{- end }}
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: hl-node
image: {{ .Values.image }}
imagePullPolicy: IfNotPresent
env:
- name: HL_CHAIN
value: "{{ .Values.chain }}"
- name: HL_FLAGS
value: "{{ .Values.flags }}"
- name: HL_DATA_DIR
value: /hl-data
- name: HL_ROOT_IP
value: "{{ .Values.rootIp }}"
ports:
- containerPort: 4001
name: gossip1
- containerPort: 4002
name: gossip2
- containerPort: 3001
name: evm-rpc
volumeMounts:
- name: data
mountPath: /hl-data
# Official non-validator rec is 128Gi; this is best-effort on smaller nodes.
# No memory limit so the node can use host RAM + swap instead of being OOMKilled.
resources:
requests:
cpu: "4"
memory: "8Gi"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: {{ .Values.storageSize }}
- apiVersion: v1
kind: Service
metadata:
name: hl-node
namespace: hl-node-{{ .Values.id }}
labels:
app: hl-node
spec:
selector:
app: hl-node
ports:
- name: evm-rpc
port: 3001
targetPort: 3001

# Metadata ConfigMap for frontend discovery
- name: hl-node-metadata
namespace: hl-node-{{ .Values.id }}
chart: bedag/raw
values:
- resources:
- apiVersion: v1
kind: ConfigMap
metadata:
name: hl-node-{{ .Values.id }}-metadata
namespace: hl-node-{{ .Values.id }}
labels:
app.kubernetes.io/part-of: obol.stack
obol.stack/id: {{ .Values.id }}
obol.stack/app: hl-node
data:
metadata.json: |
{
"chain": "{{ .Values.chain }}",
"type": "hyperliquid-node",
"endpoints": {
"hyperevm-rpc": {
"internal": "http://hl-node.hl-node-{{ .Values.id }}.svc.cluster.local:3001/evm"
}
},
"tradeData": "node_trades/node_fills hourly JSONL under the data volume (/hl-data/data)"
}
27 changes: 27 additions & 0 deletions internal/embed/networks/hl-node/values.yaml.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Configuration via CLI flags
# Template fields populated by obol CLI during network installation

# @enum Mainnet,Testnet
# @default Mainnet
# @description Hyperliquid chain to follow
chain: {{.Chain}}

# @default --write-trades --serve-eth-rpc
# @description hl-visor run-non-validator flags. Add --write-fills/--write-order-statuses/--write-raw-book-diffs for more data (each adds tens of GB/day). --serve-eth-rpc exposes HyperEVM JSON-RPC on :3001/evm.
flags: "{{.Flags}}"

# @default
# @description Pinned gossip root peer IP for fast deterministic peering (from POST {"type":"gossipRootIps"} to api.hyperliquid.xyz/info). Empty = auto-discover. Gossip ports 4001/4002 must be publicly reachable to sync.
rootIp: "{{.RootIp}}"

# @default
# @description Cluster node hostname to pin to (nodeSelector kubernetes.io/hostname). MUST be x86-64, Ubuntu-capable, >=500GB SSD (ideally 128GB RAM). Empty = schedule anywhere.
host: "{{.Host}}"

# @default hl-node:dev
# @description Container image (built from obol-exex-indexer deploy/hl-node; must be present on the target node's container runtime or published to a registry).
image: "{{.Image}}"

# @default 1500Gi
# @description Data volume size. Chain writes ~100GB/day with write-* flags — prune/rotate or size generously.
storageSize: {{.StorageSize}}
71 changes: 60 additions & 11 deletions internal/network/erpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const (
erpcDeployment = "erpc"
)

var errNoERPCRegistration = errors.New("network does not expose an eRPC upstream")

// networkChainIDs maps network names to EVM chain IDs.
var networkChainIDs = map[string]int{
"mainnet": 1,
Expand All @@ -29,6 +31,59 @@ var networkChainIDs = map[string]int{
"base-sepolia": 84532,
}

type localERPCRegistration struct {
ChainID int
Alias string
Endpoint string
}

type localERPCValues struct {
Network string `yaml:"network"`
Chain string `yaml:"chain"`
}

func resolveLocalERPCRegistration(networkType, id string, values localERPCValues) (localERPCRegistration, error) {
namespace := fmt.Sprintf("%s-%s", networkType, id)

switch networkType {
case "ethereum":
chainID, ok := networkChainIDs[values.Network]
if !ok {
return localERPCRegistration{}, fmt.Errorf("unknown network %q — no chain ID mapping", values.Network)
}

return localERPCRegistration{
ChainID: chainID,
Alias: values.Network,
Endpoint: fmt.Sprintf("http://ethereum-execution.%s.svc.cluster.local:8545", namespace),
}, nil
case "hl-node":
chain := strings.TrimSpace(values.Chain)
if chain == "" {
chain = values.Network
}

switch strings.ToLower(strings.TrimSpace(chain)) {
case "mainnet":
return localERPCRegistration{
ChainID: 999,
Alias: "hyperevm",
Endpoint: fmt.Sprintf("http://hl-node.%s.svc.cluster.local:3001/evm", namespace),
}, nil
case "testnet":
return localERPCRegistration{
ChainID: 998,
Alias: "hyperevm-testnet",
Endpoint: fmt.Sprintf("http://hl-node.%s.svc.cluster.local:3001/evm", namespace),
}, nil
default:
return localERPCRegistration{}, fmt.Errorf("unknown hl-node chain %q — expected mainnet or testnet", chain)
}
default:
return localERPCRegistration{}, errNoERPCRegistration
}
}

// RegisterERPCUpstream reads the deployed network's RPC endpoint and adds
// it as an upstream in the eRPC ConfigMap. The local node becomes the
// primary upstream (group: "primary") with automatic fallback to existing
Expand All @@ -43,24 +98,18 @@ func RegisterERPCUpstream(cfg *config.Config, networkType, id string) error {
return fmt.Errorf("could not read values.yaml: %w", err)
}

var values struct {
Network string `yaml:"network"`
}
var values localERPCValues
if err := yaml.Unmarshal(valuesContent, &values); err != nil {
return fmt.Errorf("could not parse values.yaml: %w", err)
}

chainID, ok := networkChainIDs[values.Network]
if !ok {
return fmt.Errorf("unknown network %q — no chain ID mapping", values.Network)
reg, err := resolveLocalERPCRegistration(networkType, id, values)
if err != nil {
return err
}

// Build the internal RPC endpoint for this network's execution client
namespace := fmt.Sprintf("%s-%s", networkType, id)
endpoint := fmt.Sprintf("http://ethereum-execution.%s.svc.cluster.local:8545", namespace)
upstreamID := fmt.Sprintf("local-%s-%s", networkType, id)

return patchERPCUpstream(cfg, upstreamID, endpoint, chainID, values.Network, true)
return patchERPCUpstream(cfg, upstreamID, reg.Endpoint, reg.ChainID, reg.Alias, true)
}

// DeregisterERPCUpstream removes a previously registered local upstream
Expand Down
Loading
Loading