Skip to content
Merged
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
30 changes: 16 additions & 14 deletions internal/embed/networks/ethereum/helmfile.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ releases:
executionClient: '{{ .Values.executionClient }}'
consensusClient: '{{ .Values.consensusClient }}'
network: '{{ .Values.network }}'
mode: '{{ .Values.mode }}'
executionStorageSize: '{{ index .Values "executionStorageSize" | default "" }}'
consensusStorageSize: '{{ index .Values "consensusStorageSize" | default "" }}'

# Ethereum node (execution and consensus clients)
# Uses the external ethereum-helm-charts/ethereum-node umbrella chart
Expand Down Expand Up @@ -58,24 +61,23 @@ releases:
{{- end }}
{{- if ne .Values.mode "archive" }}
- --full
{{- else if eq (.Values.pruneKind | default "") "before" }}
# Partial archive: prune state history before block N.
# Receipts/bodies are pruned to the same cutoff via the
# pre-merge presets where applicable; otherwise reth
# uses the same `before` value.
{{- else if eq (index .Values "pruneKind" | default "") "before" }}
# Partial archive: keep state, receipts and bodies from
# the requested block forward. Reth v2.2.0 and current
# main both support exact .before cutoffs for these
# segments, so avoid the coarser pre-merge preset.
- --prune.account-history.before={{ .Values.pruneBlock }}
- --prune.storage-history.before={{ .Values.pruneBlock }}
{{- if le (int .Values.pruneBlock) 15537394 }}
- --prune.receipts.pre-merge
- --prune.bodies.pre-merge
{{- else }}
- --prune.receipts.before={{ .Values.pruneBlock }}
- --prune.bodies.before={{ .Values.pruneBlock }}
{{- end }}
{{- else if eq (.Values.pruneKind | default "") "distance" }}
# Partial archive: keep last N blocks of history.
{{- else if eq (index .Values "pruneKind" | default "") "distance" }}
# Partial archive: keep last N blocks of state, receipts
# and bodies. Reth interprets .distance as anchored to
# the chain tip at run time.
- --prune.account-history.distance={{ .Values.pruneDistance }}
- --prune.storage-history.distance={{ .Values.pruneDistance }}
- --prune.receipts.distance={{ .Values.pruneDistance }}
- --prune.bodies.distance={{ .Values.pruneDistance }}
{{- end }}
{{- end }}

Expand Down Expand Up @@ -105,7 +107,7 @@ releases:
{{- end }}
persistence:
enabled: true
size: {{ if eq .Values.network "mainnet" }}{{ if eq .Values.mode "archive" }}4500Gi{{ else }}500Gi{{ end }}{{ else }}{{ if eq .Values.mode "archive" }}300Gi{{ else }}100Gi{{ end }}{{ end }}
size: {{ if (index .Values "executionStorageSize" | default "") }}{{ index .Values "executionStorageSize" }}{{ else }}{{ if eq .Values.network "mainnet" }}{{ if eq .Values.mode "archive" }}4500Gi{{ else }}500Gi{{ end }}{{ else }}{{ if eq .Values.mode "archive" }}300Gi{{ else }}100Gi{{ end }}{{ end }}{{ end }}
existingClaim: execution-{{ .Values.executionClient }}-{{ .Values.network }}

# Consensus client (pinned versions — Renovate-tracked)
Expand All @@ -131,7 +133,7 @@ releases:
{{- end }}
persistence:
enabled: true
size: {{ if eq .Values.network "mainnet" }}{{ if eq .Values.mode "archive" }}500Gi{{ else }}200Gi{{ end }}{{ else }}{{ if eq .Values.mode "archive" }}100Gi{{ else }}50Gi{{ end }}{{ end }}
size: {{ if (index .Values "consensusStorageSize" | default "") }}{{ index .Values "consensusStorageSize" }}{{ else }}{{ if eq .Values.network "mainnet" }}{{ if eq .Values.mode "archive" }}500Gi{{ else }}200Gi{{ end }}{{ else }}{{ if eq .Values.mode "archive" }}100Gi{{ else }}50Gi{{ end }}{{ end }}{{ end }}
existingClaim: consensus-{{ .Values.consensusClient }}-{{ .Values.network }}

# Metadata ConfigMap for frontend discovery
Expand Down
42 changes: 21 additions & 21 deletions internal/embed/networks/ethereum/templates/pvc.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
{{- if eq .Release.Name "ethereum-pvcs" }}
{{- /*
PVC sizing is a function of (network, mode). Sizes are estimates with
~30% headroom for chain growth. local-path storage does not pre-allocate,
so these requests primarily document intent and serve as soft caps when
a sized storage class is swapped in later.
PVC sizing is resolved by the CLI and written into values.yaml so partial
archive scopes get smaller requests than full genesis archives. The fallback
keeps older generated values usable.
*/ -}}
{{- $mode := default "full" .Values.mode -}}
{{- $isArchive := eq $mode "archive" -}}
{{- $execSize := "500Gi" -}}
{{- $consensusSize := "200Gi" -}}
{{- if eq .Values.network "mainnet" -}}
{{- if $isArchive -}}
{{- $execSize = "4500Gi" -}}
{{- $consensusSize = "500Gi" -}}
{{- $execSize := index .Values "executionStorageSize" | default "" -}}
{{- $consensusSize := index .Values "consensusStorageSize" | default "" -}}
{{- if or (eq $execSize "") (eq $consensusSize "") -}}
{{- $execSize = "500Gi" -}}
{{- $consensusSize = "200Gi" -}}
{{- if eq .Values.network "mainnet" -}}
{{- if $isArchive -}}
{{- $execSize = "4500Gi" -}}
{{- $consensusSize = "500Gi" -}}
{{- end -}}
{{- else -}}
{{- $execSize = "500Gi" -}}
{{- $consensusSize = "200Gi" -}}
{{- end -}}
{{- else -}}
{{- /* sepolia, hoodi and other testnets */ -}}
{{- if $isArchive -}}
{{- $execSize = "300Gi" -}}
{{- $consensusSize = "100Gi" -}}
{{- else -}}
{{- $execSize = "100Gi" -}}
{{- $consensusSize = "50Gi" -}}
{{- /* sepolia, hoodi and other testnets */ -}}
{{- if $isArchive -}}
{{- $execSize = "300Gi" -}}
{{- $consensusSize = "100Gi" -}}
{{- else -}}
{{- $execSize = "100Gi" -}}
{{- $consensusSize = "50Gi" -}}
{{- end -}}
{{- end -}}
{{- end -}}
---
Expand Down
5 changes: 3 additions & 2 deletions internal/network/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ func Install(cfg *config.Config, u *ui.UI, network string, overrides map[string]
if modeValue == "" {
modeValue = "full"
}
if err := CheckNetworkDiskSpace(u, cfg.DataDir, netValue, modeValue); err != nil {
executionClient := templateData["ExecutionClient"]
if err := CheckNetworkDiskSpace(u, cfg.DataDir, netValue, modeValue, executionClient, archiveScope); err != nil {
return err
}
}
Expand Down Expand Up @@ -176,7 +177,7 @@ func Install(cfg *config.Config, u *ui.UI, network string, overrides map[string]
if network == "ethereum" {
var sb strings.Builder
sb.Write(buf.Bytes())
appendArchiveScopeYAML(&sb, archiveScope)
appendArchiveScopeYAML(&sb, templateData["Network"], templateData["Mode"], templateData["ExecutionClient"], archiveScope)
buf.Reset()
buf.WriteString(sb.String())
}
Expand Down
9 changes: 8 additions & 1 deletion internal/network/picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ func promptCustomBlock(u *ui.UI) (ArchiveScope, error) {
// appendArchiveScopeYAML serializes the resolved scope into a YAML
// fragment for values.yaml, in a form that helmfile reads at deploy time
// to emit per-client prune args.
func appendArchiveScopeYAML(b *strings.Builder, scope ArchiveScope) {
func appendArchiveScopeYAML(b *strings.Builder, network, mode, executionClient string, scope ArchiveScope) {
profile := resolveEthereumStorageProfile(network, mode, executionClient, scope)

b.WriteString("\n# Pruning scope, resolved by `obol network install` from --mode/--since.\n")
b.WriteString("# Edit via the CLI flags rather than this file; helmfile reads these\n")
b.WriteString("# verbatim and emits client-specific prune args at deploy time.\n")
Expand All @@ -233,4 +235,9 @@ func appendArchiveScopeYAML(b *strings.Builder, scope ArchiveScope) {
case "distance":
fmt.Fprintf(b, "pruneKind: \"distance\"\npruneBlock: 0\npruneDistance: %d\n", scope.Distance)
}

b.WriteString("\n# Storage profile derived from the resolved archive scope.\n")
fmt.Fprintf(b, "executionStorageSize: %s\n", profile.ExecutionSize)
fmt.Fprintf(b, "consensusStorageSize: %s\n", profile.ConsensusSize)
fmt.Fprintf(b, "diskRequirementGB: %d\n", profile.DiskRequirementGB)
}
28 changes: 4 additions & 24 deletions internal/network/preflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,6 @@ import (
"github.com/ObolNetwork/obol-stack/internal/ui"
)

// diskSpaceRequirementGB returns the recommended free-disk minimum for
// (network, mode) in gigabytes. Numbers include ~30% headroom for chain
// growth between releases. Sizes are reth-anchored; other clients are in
// the same ballpark.
func diskSpaceRequirementGB(network, mode string) uint64 {
archive := mode == "archive"
switch network {
case "mainnet":
if archive {
return 5000
}
return 700
default:
// sepolia, hoodi, and other testnets
if archive {
return 400
}
return 150
}
}

// freeDiskBytes returns the free disk bytes available at path. Used to
// check whether a network install has room to grow before we let helmfile
// schedule a 4TB PVC that will silently fill the host overnight.
Expand All @@ -46,8 +25,9 @@ func freeDiskBytes(path string) (uint64, error) {
// in non-interactive contexts (no TTY, JSON mode) the prompt auto-accepts
// so scripted installs don't deadlock. The user only blocks the install by
// explicitly declining at an interactive prompt.
func CheckNetworkDiskSpace(u *ui.UI, dataDir, network, mode string) error {
requiredGB := diskSpaceRequirementGB(network, mode)
func CheckNetworkDiskSpace(u *ui.UI, dataDir, network, mode, executionClient string, scope ArchiveScope) error {
profile := resolveEthereumStorageProfile(network, mode, executionClient, scope)
requiredGB := profile.DiskRequirementGB

freeBytes, err := freeDiskBytes(dataDir)
if err != nil {
Expand All @@ -58,7 +38,7 @@ func CheckNetworkDiskSpace(u *ui.UI, dataDir, network, mode string) error {

freeGB := freeBytes / (1024 * 1024 * 1024)

u.Detail("Disk space", fmt.Sprintf("%d GB free at %s (this network needs ~%d GB)", freeGB, dataDir, requiredGB))
u.Detail("Disk space", fmt.Sprintf("%d GB free at %s (this network needs ~%d GB for %s)", freeGB, dataDir, requiredGB, profile.Label))

if freeGB >= requiredGB {
return nil
Expand Down
134 changes: 134 additions & 0 deletions internal/network/storage_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package network

import (
"fmt"
"math"
)

// ethereumStorageProfile is the single source of truth for Ethereum local
// node storage sizing. The profile is intentionally conservative because
// local-path storage does not reserve bytes up front, but sized storage
// classes do enforce these requests.
type ethereumStorageProfile struct {
ExecutionSize string
ConsensusSize string
DiskRequirementGB uint64
Label string
}

func resolveEthereumStorageProfile(network, mode, executionClient string, scope ArchiveScope) ethereumStorageProfile {
if mode != "archive" {
if network == "mainnet" {
return ethereumStorageProfile{
ExecutionSize: "500Gi",
ConsensusSize: "200Gi",
DiskRequirementGB: 700,
Label: "full mainnet",
}
}
return ethereumStorageProfile{
ExecutionSize: "100Gi",
ConsensusSize: "50Gi",
DiskRequirementGB: 150,
Label: "full testnet",
}
}

if network != "mainnet" {
return ethereumStorageProfile{
ExecutionSize: "300Gi",
ConsensusSize: "100Gi",
DiskRequirementGB: 400,
Label: "archive testnet",
}
}

if !partialArchiveClients[executionClient] || scope.Kind == "" || scope.Kind == "all" {
return ethereumStorageProfile{
ExecutionSize: "4500Gi",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just by chance if you spot this @apham0001 or @anadi2311 , do we have archive ELs for our data pipline? (I guess geth), do you know what disk space they take up?

ConsensusSize: "500Gi",
DiskRequirementGB: 5000,
Label: "archive mainnet from genesis",
}
}

switch scope.Kind {
case "before":
if hf := hardforkProfileForBlock(scope.Block); hf != nil {
execGi := roundUpGiB(uint64(math.Ceil(hf.ApproxArchiveSizeTB * 1024 * 1.2)))
return ethereumStorageProfile{
ExecutionSize: formatGi(execGi),
ConsensusSize: "500Gi",
DiskRequirementGB: execGi + 700,
Label: "partial archive mainnet " + hf.Name,
}
}

// A raw block before the oldest known partial-archive preset could
// retain almost all history, so size it as a full archive.
return ethereumStorageProfile{
ExecutionSize: "4500Gi",
ConsensusSize: "500Gi",
DiskRequirementGB: 5000,
Label: "custom archive mainnet block",
}
case "distance":
execGi := executionGiForDistance(scope.Distance)
return ethereumStorageProfile{
ExecutionSize: formatGi(execGi),
ConsensusSize: "500Gi",
DiskRequirementGB: diskRequirementForMainnetArchive(execGi),
Label: "partial archive mainnet distance",
}
default:
return ethereumStorageProfile{
ExecutionSize: "4500Gi",
ConsensusSize: "500Gi",
DiskRequirementGB: 5000,
Label: "archive mainnet",
}
}
}

func hardforkProfileForBlock(block uint64) *Hardfork {
var matched *Hardfork
for i := range MainnetHardforks {
if MainnetHardforks[i].Block <= block {
matched = &MainnetHardforks[i]
continue
}
break
}
return matched
}

func diskRequirementForMainnetArchive(execGi uint64) uint64 {
if execGi >= 4500 {
return 5000
}
return execGi + 700
}

func executionGiForDistance(distance uint64) uint64 {
days := float64(distance*12) / float64(24*60*60)
execGi := uint64(math.Ceil(500 + days*0.82))
if execGi < 500 {
execGi = 500
}
if execGi > 4500 {
execGi = 4500
}
return roundUpGiB(execGi)
}

func roundUpGiB(gib uint64) uint64 {
const step = 100
if gib%step == 0 {
return gib
}
return ((gib / step) + 1) * step
}

func formatGi(gib uint64) string {
return fmt.Sprintf("%dGi", gib)
}
Loading