Skip to content

Commit bbcd096

Browse files
feat: handshake-node chart
Signed-off-by: Tripura Repalle <tripura@blinklabs.io>
1 parent 5f0b497 commit bbcd096

9 files changed

Lines changed: 247 additions & 52 deletions

File tree

charts/handshake-node/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: |
55
full node from Blink Labs. This is distinct from the JavaScript hsd chart.
66
type: application
77
version: 0.1.0
8-
appVersion: "0.1.1-rc1"
8+
appVersion: "0.2.0-rc1"
99
# 1.27+ is required for the StatefulSet .spec.persistentVolumeClaimRetentionPolicy
1010
# field that the chart renders by default. Older clusters silently drop the field.
1111
kubeVersion: ">=1.27.0-0"

charts/handshake-node/README.md

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,26 @@ PersistentVolumeClaim mounted at `/home/handshake/.handshake-node`.
2222
Security-relevant defaults:
2323

2424
- The container runs as the non-root `handshake` user baked into the upstream
25-
image with `runAsNonRoot: true`, all Linux capabilities dropped, no
26-
privilege escalation, `readOnlyRootFilesystem: true`, and the
27-
`RuntimeDefault` seccomp profile.
25+
image (UID 100 / GID 101), pinned numerically via `runAsUser` /
26+
`runAsGroup` so kubelet can enforce `runAsNonRoot: true` without resolving
27+
the image's `/etc/passwd`. All Linux capabilities are dropped, no privilege
28+
escalation, `readOnlyRootFilesystem: true`, and the `RuntimeDefault`
29+
seccomp profile.
30+
- An `init-chown-data` init container (busybox) fixes ownership of the
31+
mounted PVC on every start. It drops all Linux capabilities and adds back
32+
only `CAP_CHOWN` (to change owner/group) and `CAP_DAC_READ_SEARCH` (so
33+
root can traverse the `0700` directories the daemon creates under the
34+
data root on first start; without it, a restart of the pod would fail
35+
with `Permission denied`). The container runs a selective
36+
`find /home/handshake/.handshake-node \( -not -user <uid> -o -not -group <gid> \) -exec chown <uid>:<gid> {} +`
37+
against the mount root, so only entries whose owner or group already
38+
differ from the configured `runAsUser` / `runAsGroup` are touched. That
39+
keeps the operation idempotent across restarts and cheap on a warm PVC,
40+
and it never modifies file modes — the Brontide identity key's `0600`
41+
survives, and a pre-populated `persistence.existingClaim` remains fully
42+
readable by the non-root main container. `fsGroup` is deliberately not
43+
used because kubelet's recursive `fsGroup` chmod adds group r/w to every
44+
file, which the daemon rejects for its Brontide identity key.
2845
- The peer-to-peer Service is `ClusterIP` by default. Public exposure via
2946
`NodePort` or `LoadBalancer` is an explicit operator choice.
3047
- The authenticated TLS RPC listener (port `12037`) is not exposed unless
@@ -35,6 +52,10 @@ Security-relevant defaults:
3552
- The Prometheus metrics endpoint (`12039`) and Stratum server (`12040`) are
3653
disabled by default. When enabled they are kept on private `ClusterIP`
3754
Services.
55+
- Supported `network` values are `main` and `regtest`. The upstream
56+
`handshake-node` binary does not implement `--testnet` or `--simnet`; the
57+
chart rejects those values at render time to avoid a `CreateContainerError`
58+
loop from an unknown flag.
3859

3960
## Prerequisites
4061

@@ -160,6 +181,9 @@ stratum:
160181
# Optional. Derived from stratum.service.port + allowPublic when unset.
161182
# listen: ""
162183
allowPublic: true
184+
# Required whenever stratum.enabled=true (even for private/loopback binds).
185+
# handshake-node v0.2.0-rc1 refuses to start if --stratumlisten is set
186+
# without --miningaddr.
163187
miningAddress: hs1qyourhandshakeaddress
164188
auth:
165189
existingSecret: handshake-node-stratum
@@ -168,10 +192,11 @@ stratum:
168192
type: ClusterIP
169193
```
170194

171-
Public exposure requires `stratum.allowPublic=true` and both
172-
`miningAddress` and `auth.existingSecret` (validated at render time). The
173-
Stratum Service is only rendered when `stratum.allowPublic=true` because
174-
loopback listeners cannot be reached through a ClusterIP.
195+
`stratum.miningAddress` is required whenever Stratum is enabled. Public
196+
exposure additionally requires `stratum.allowPublic=true` and
197+
`stratum.auth.existingSecret` (all validated at render time). The Stratum
198+
Service is only rendered when `stratum.allowPublic=true` because loopback
199+
listeners cannot be reached through a ClusterIP.
175200

176201
## Service exposure
177202

@@ -195,6 +220,12 @@ service:
195220
type: NodePort
196221
port: 12038
197222
nodePort: 32038
223+
config:
224+
extra:
225+
# handshake-node does not derive its advertised address from the
226+
# Service; when using NodePort, set EXTERNALIP to the routable
227+
# "<node-ip>:<nodePort>" so peers can dial you back.
228+
EXTERNALIP: "203.0.113.10:32038"
198229
```
199230

200231
## Mainnet operations examples
@@ -204,7 +235,7 @@ Full mainnet node with private RPC, metrics enabled, and a beefier PVC:
204235
```yaml
205236
network: main
206237
image:
207-
tag: "0.1.1-rc1"
238+
tag: "0.2.0-rc1"
208239
209240
persistence:
210241
size: 400Gi
@@ -215,8 +246,8 @@ rpc:
215246
existingSecret: handshake-node-rpc
216247
217248
config:
218-
# Required whenever rpc.enabled=true. Substitute the Pod CIDR of the
219-
# workloads that should be allowed to call RPC.
249+
# Required whenever rpc.enabled=true. Substitute the observed client
250+
# source CIDR (Pod CIDR unless CNI SNAT changes it to node IPs).
220251
rpcallowip: "<trusted-rpc-client-cidr>"
221252
222253
metrics:
@@ -262,8 +293,8 @@ See [`values.yaml`](values.yaml) for the full list of tunables. Key knobs:
262293
| Key | Description | Default |
263294
| -------------------------------- | ------------------------------------------------------------ | ------------------------------------ |
264295
| `image.repository` | Image name | `ghcr.io/blinklabs-io/handshake-node` |
265-
| `image.tag` | Image tag (never `latest`) | `0.1.1-rc1` |
266-
| `network` | Handshake network: `main`, `regtest`, `simnet`, `testnet` | `main` |
296+
| `image.tag` | Image tag (never `latest`) | `0.2.0-rc1` |
297+
| `network` | Handshake network: `main`, `regtest` | `main` |
267298
| `persistence.size` | PVC size | `200Gi` |
268299
| `persistence.storageClass` | StorageClass name | cluster default |
269300
| `rpc.enabled` | Enable authenticated RPC | `false` |

charts/handshake-node/templates/_helpers.tpl

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,54 @@ Chart name/version label.
3131

3232
{{/*
3333
Handshake network selector. Fails template rendering for unsupported values so
34-
labels and daemon flags cannot silently disagree.
34+
labels and daemon flags cannot silently disagree. Restricted to the networks
35+
the upstream handshake-node image actually implements as CLI flags; testnet
36+
and simnet are rejected by the daemon and therefore not offered here.
3537
*/}}
3638
{{- define "handshake-node.network" -}}
3739
{{- $n := default "main" .Values.network -}}
38-
{{- $valid := list "main" "regtest" "simnet" "testnet" -}}
40+
{{- $valid := list "main" "regtest" -}}
3941
{{- if not (has $n $valid) -}}
40-
{{- fail (printf "handshake-node: unsupported network %q. Supported: main, regtest, simnet, testnet." $n) -}}
42+
{{- fail (printf "handshake-node: unsupported network %q. Supported: main, regtest." $n) -}}
4143
{{- end -}}
4244
{{- $n -}}
4345
{{- end -}}
4446

4547
{{/*
46-
Name of the headless Service backing the StatefulSet. Reserved suffix space
47-
keeps the resulting name within the 63-char DNS label limit even with a long
48-
fullnameOverride or release name.
48+
Suffix-aware component name. Truncates the fullname so the returned value
49+
stays within the 63-character DNS label limit even when a long release name
50+
or fullnameOverride is combined with a component suffix like "-p2p".
51+
Call as: include "handshake-node.componentName" (dict "ctx" . "suffix" "-p2p")
52+
*/}}
53+
{{- define "handshake-node.componentName" -}}
54+
{{- $ctx := .ctx -}}
55+
{{- $suffix := .suffix -}}
56+
{{- $max := int (sub 63 (len $suffix)) -}}
57+
{{- printf "%s%s" (include "handshake-node.fullname" $ctx | trunc $max | trimSuffix "-") $suffix -}}
58+
{{- end -}}
59+
60+
{{/*
61+
Name of the headless Service backing the StatefulSet.
4962
*/}}
5063
{{- define "handshake-node.headlessName" -}}
51-
{{- printf "%s-headless" (include "handshake-node.fullname" . | trunc 54 | trimSuffix "-") -}}
64+
{{- include "handshake-node.componentName" (dict "ctx" . "suffix" "-headless") -}}
65+
{{- end -}}
66+
67+
{{/*
68+
Per-component Service names. Each reserves the suffix length so a maximum
69+
63-char fullname override still renders a valid Service name.
70+
*/}}
71+
{{- define "handshake-node.p2pName" -}}
72+
{{- include "handshake-node.componentName" (dict "ctx" . "suffix" "-p2p") -}}
73+
{{- end -}}
74+
{{- define "handshake-node.rpcName" -}}
75+
{{- include "handshake-node.componentName" (dict "ctx" . "suffix" "-rpc") -}}
76+
{{- end -}}
77+
{{- define "handshake-node.metricsName" -}}
78+
{{- include "handshake-node.componentName" (dict "ctx" . "suffix" "-metrics") -}}
79+
{{- end -}}
80+
{{- define "handshake-node.stratumName" -}}
81+
{{- include "handshake-node.componentName" (dict "ctx" . "suffix" "-stratum") -}}
5282
{{- end -}}
5383

5484
{{/*
@@ -66,12 +96,15 @@ handshake_network: {{ include "handshake-node.network" . }}
6696
{{- end -}}
6797

6898
{{/*
69-
Selector labels
99+
Selector labels. Deliberately excludes handshake_network because
100+
StatefulSet.spec.selector is immutable in Kubernetes; a network change on
101+
helm upgrade would fail with `spec: Forbidden`. name + instance already
102+
isolate the release. The network label is still exposed on general resource
103+
and pod metadata via handshake-node.labels for observability.
70104
*/}}
71105
{{- define "handshake-node.matchLabels" -}}
72106
app.kubernetes.io/name: {{ include "handshake-node.name" . }}
73107
app.kubernetes.io/instance: {{ .Release.Name }}
74-
handshake_network: {{ include "handshake-node.network" . }}
75108
{{- end -}}
76109

77110
{{/*

charts/handshake-node/templates/service-metrics.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
apiVersion: v1
44
kind: Service
55
metadata:
6-
name: {{ include "handshake-node.fullname" . }}-metrics
6+
name: {{ include "handshake-node.metricsName" . }}
77
labels:
88
{{- include "handshake-node.labels" . | nindent 4 }}
99
app.kubernetes.io/component: metrics

charts/handshake-node/templates/service-p2p.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
apiVersion: v1
33
kind: Service
44
metadata:
5-
name: {{ include "handshake-node.fullname" . }}-p2p
5+
name: {{ include "handshake-node.p2pName" . }}
66
labels:
77
{{- include "handshake-node.labels" . | nindent 4 }}
88
{{- with (merge (dict) .Values.service.p2p.annotations .Values.serviceAnnotations) }}

charts/handshake-node/templates/service-rpc.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
apiVersion: v1
44
kind: Service
55
metadata:
6-
name: {{ include "handshake-node.fullname" . }}-rpc
6+
name: {{ include "handshake-node.rpcName" . }}
77
labels:
88
{{- include "handshake-node.labels" . | nindent 4 }}
99
app.kubernetes.io/component: rpc

charts/handshake-node/templates/service-stratum.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
apiVersion: v1
44
kind: Service
55
metadata:
6-
name: {{ include "handshake-node.fullname" . }}-stratum
6+
name: {{ include "handshake-node.stratumName" . }}
77
labels:
88
{{- include "handshake-node.labels" . | nindent 4 }}
99
app.kubernetes.io/component: stratum

0 commit comments

Comments
 (0)