Files
zeekay 44518c0d18 feat: single-instance multi-brand bootnode with cluster routing
Single API deployment serves all brands via hostname-based routing.
Per-brand env vars use BRAND_{UPPER}_* format with ENABLE_MULTI_TENANT.
One API Ingress handles all api.*/ws.* domains; per-brand web Ingresses.
ClusterTarget type maps brands to K8s clusters for NodeCluster CR creation.

Also adds Solana, Bitcoin, Cosmos protocol drivers with recommended
hardware specs (RecommendedResources/RecommendedStorage on Driver interface).
2026-03-03 07:24:14 -08:00

182 lines
4.7 KiB
Go

package protocol
import (
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"github.com/hanzoai/nchain/api/v1alpha1"
)
const (
ethDefaultImage = "ghcr.io/hanzoai/reth:latest"
ethDataDir = "/data"
ethRPCPort = int32(8545)
ethP2PPort = int32(30303)
ethMetricsPort = int32(9090)
ethAuthRPCPort = int32(8551)
ethHealthPath = "/health"
)
// EthereumDriver implements the Driver interface for Ethereum-compatible chains.
type EthereumDriver struct{}
func (d *EthereumDriver) Name() string { return "ethereum" }
func (d *EthereumDriver) DefaultImage() string { return ethDefaultImage }
func (d *EthereumDriver) DefaultPorts() v1alpha1.PortConfig {
return v1alpha1.PortConfig{
RPC: ethRPCPort,
P2P: ethP2PPort,
Metrics: ethMetricsPort,
Additional: []v1alpha1.NamedPort{
{Name: "authrpc", Port: ethAuthRPCPort, Protocol: corev1.ProtocolTCP},
},
}
}
func (d *EthereumDriver) BuildCommand(spec *v1alpha1.NodeClusterSpec) ([]string, []string) {
args := []string{
"node",
fmt.Sprintf("--chain=%s", spec.NetworkID),
fmt.Sprintf("--datadir=%s", ethDataDir),
"--http",
"--http.addr=0.0.0.0",
fmt.Sprintf("--http.port=%d", d.rpcPort(spec)),
fmt.Sprintf("--authrpc.port=%d", ethAuthRPCPort),
fmt.Sprintf("--metrics=%d", d.metricsPort(spec)),
}
// P2P port.
args = append(args, fmt.Sprintf("--p2p.port=%d", d.p2pPort(spec)))
// Bootstrap nodes.
if len(spec.P2P.BootstrapNodes) > 0 {
for _, node := range spec.P2P.BootstrapNodes {
args = append(args, fmt.Sprintf("--bootnodes=%s", node))
}
}
return []string{"reth"}, args
}
func (d *EthereumDriver) BuildEnv(spec *v1alpha1.NodeClusterSpec) []corev1.EnvVar {
return []corev1.EnvVar{
{Name: "CHAIN_ID", Value: spec.NetworkID},
{
Name: "POD_NAME",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"},
},
},
}
}
func (d *EthereumDriver) BuildVolumeMounts(spec *v1alpha1.NodeClusterSpec) []corev1.VolumeMount {
return []corev1.VolumeMount{
{Name: "data", MountPath: ethDataDir},
}
}
func (d *EthereumDriver) BuildVolumes(_ *v1alpha1.NodeClusterSpec) []corev1.Volume {
return nil
}
func (d *EthereumDriver) HealthEndpoint(spec *v1alpha1.NodeClusterSpec) (string, int32) {
return ethHealthPath, d.rpcPort(spec)
}
func (d *EthereumDriver) ReadinessEndpoint(spec *v1alpha1.NodeClusterSpec) (string, int32) {
return ethHealthPath, d.rpcPort(spec)
}
func (d *EthereumDriver) BuildInitContainers(spec *v1alpha1.NodeClusterSpec) []corev1.Container {
var initContainers []corev1.Container
if spec.SeedRestore != nil && spec.SeedRestore.Enabled {
markerPath := spec.SeedRestore.MarkerPath
if markerPath == "" {
markerPath = "/data/.restore-complete"
}
initContainers = append(initContainers, corev1.Container{
Name: "seed-restore",
Image: "alpine:3",
Command: []string{"/bin/sh", "-c"},
Args: []string{fmt.Sprintf(`set -e
if [ "%s" = "IfEmpty" ] && [ -f "%s" ]; then
echo "Already restored, skipping"
exit 0
fi
echo "Restoring data..."
touch "%s"
`, spec.SeedRestore.RestorePolicy, markerPath, markerPath)},
VolumeMounts: []corev1.VolumeMount{
{Name: "data", MountPath: ethDataDir},
},
})
}
return initContainers
}
func (d *EthereumDriver) NeedsConfigMap(_ *v1alpha1.NodeClusterSpec) bool {
return false
}
func (d *EthereumDriver) BuildConfigMap(_ *v1alpha1.NodeClusterSpec) map[string]string {
return nil
}
func (d *EthereumDriver) rpcPort(spec *v1alpha1.NodeClusterSpec) int32 {
if spec.Ports.RPC > 0 {
return spec.Ports.RPC
}
return ethRPCPort
}
func (d *EthereumDriver) p2pPort(spec *v1alpha1.NodeClusterSpec) int32 {
if spec.Ports.P2P > 0 {
return spec.Ports.P2P
}
return ethP2PPort
}
func (d *EthereumDriver) metricsPort(spec *v1alpha1.NodeClusterSpec) int32 {
if spec.Ports.Metrics > 0 {
return spec.Ports.Metrics
}
return ethMetricsPort
}
func (d *EthereumDriver) RecommendedResources(role string) (corev1.ResourceList, corev1.ResourceList) {
switch role {
case "archive":
return corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("8"),
corev1.ResourceMemory: resource.MustParse("32Gi"),
}, corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("16"),
corev1.ResourceMemory: resource.MustParse("64Gi"),
}
default: // validator, fullnode
return corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("4"),
corev1.ResourceMemory: resource.MustParse("16Gi"),
}, corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("8"),
corev1.ResourceMemory: resource.MustParse("32Gi"),
}
}
}
func (d *EthereumDriver) RecommendedStorage(role string) string {
switch role {
case "archive":
return "14Ti"
default:
return "2Ti"
}
}