This table displays the deposits made to the Ethereum staking depos
Consensus Layer
- {{ if not .Deposits.Eth2Deposits }}
-
No beacon chain deposits found for this validator. It takes around {{ .InclusionDelay }} hours for a deposit to be processed by the beacon chain.
+ {{ if and (not .Deposits.Eth2Deposits) (not .Deposits.PendingEth2Deposits) }}
+
No beacon chain deposits found for this validator. It will appear once your validator has left the pending deposit queue.
{{ else }}
-
This table displays the deposits received and processed by the beacon chain.
+
Execution Layer deposits must be recognized by the beacon chain before validator balances are updated. Both processed and scheduled deposits for the beacon chain will appear here.
@@ -572,7 +191,6 @@
This table displays the deposits received and processed by the beac
Time
Withdrawal Credential
Amount
-
Signature
@@ -596,6 +214,26 @@
This table displays the deposits received and processed by the beac
{{ end }}
+
+ {{ range $i, $deposit := .Deposits.PendingEth2Deposits }}
+
")
+ for i := uint64(0); i < committeeBits.Len(); i++ {
+ if committeeBits.BitAt(i) {
+ h += template.HTML(fmt.Sprintf("%d ", i))
+ }
+ }
+ h += template.HTML("
")
+ return h
+}
+
func FormatBitlist(b []byte) template.HTML {
p := bitfield.Bitlist(b)
return formatBits(p.BytesNoTrim(), int(p.Len()))
@@ -1078,9 +1124,8 @@ func FormatValidatorTags(tags []string) template.HTML {
// FormatValidator will return html formatted text for a validator
func FormatValidator(validator uint64) template.HTML {
- return template.HTML(fmt.Sprintf("%v", validator, validator))
+ return template.HTML(fmt.Sprintf("%v", validator, validator))
}
-
func FormatValidatorWithName(validator interface{}, name string) template.HTML {
var validatorRead string
var validatorLink string
@@ -1103,7 +1148,11 @@ func FormatValidatorWithName(validator interface{}, name string) template.HTML {
}
// FormatValidatorInt64 will return html formatted text for a validator (for an int64 validator-id)
+// Returns "-" if the validator index is less than 0
func FormatValidatorInt64(validator int64) template.HTML {
+ if validator < 0 {
+ return "-"
+ }
return FormatValidator(uint64(validator))
}
diff --git a/utils/format_test.go b/utils/format_test.go
new file mode 100644
index 0000000000..7de0c02db3
--- /dev/null
+++ b/utils/format_test.go
@@ -0,0 +1,40 @@
+// Copyright (C) 2025 Bitfly GmbH
+//
+// This file is part of Beaconchain Dashboard.
+//
+// Beaconchain Dashboard is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Beaconchain Dashboard is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Beaconchain Dashboard. If not, see .
+
+package utils
+
+import "testing"
+
+func TestBeginningOfSetWithdrawalCredentials(t *testing.T) {
+ tests := []struct {
+ version int
+ expected string
+ }{
+ {0, "000000000000000000000000"},
+ {1, "010000000000000000000000"},
+ {2, "020000000000000000000000"},
+ {10, "0a0000000000000000000000"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.expected, func(t *testing.T) {
+ v := BeginningOfSetWithdrawalCredentials(tt.version)
+ if v != tt.expected {
+ t.Errorf("wrong beginning of set withdrawal credentials for version %v: %v expected %v", tt.version, v, tt.expected)
+ }
+ })
+ }
+}
diff --git a/utils/session.go b/utils/session.go
index 108cca2071..43b2f09266 100644
--- a/utils/session.go
+++ b/utils/session.go
@@ -101,6 +101,10 @@ func InitSessionStore(secret string) {
sessionManager.Cookie.SameSite = http.SameSiteLaxMode
sessionManager.Cookie.Secure = true
sessionManager.Cookie.Domain = Config.Frontend.SessionCookieDomain
+ sessionManager.ErrorFunc = func(writer http.ResponseWriter, request *http.Request, err error) {
+ logger.Errorf("error in session LoadAndSave middleware: %v ", err)
+ http.Error(writer, "Internal server error", http.StatusInternalServerError)
+ }
if Config.Frontend.SessionCookieDeriveDomainFromRequest {
logger.Infof("deriving cookie.domain from request")
diff --git a/utils/utils.go b/utils/utils.go
index 35313ff243..86c4b07fe1 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -50,8 +50,8 @@ import (
"github.com/kelseyhightower/envconfig"
"github.com/lib/pq"
"github.com/mvdan/xurls"
- "github.com/prysmaticlabs/prysm/v3/beacon-chain/core/signing"
- prysm_params "github.com/prysmaticlabs/prysm/v3/config/params"
+ "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing"
+ prysm_params "github.com/prysmaticlabs/prysm/v5/config/params"
"github.com/shopspring/decimal"
"github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode"
@@ -92,6 +92,7 @@ func GetTemplateFuncs() template.FuncMap {
"formatElCurrency": FormatElCurrency,
"formatClCurrency": FormatClCurrency,
"formatEffectiveBalance": FormatEffectiveBalance,
+ "formatBlockNumber": FormatBlockNumber,
"formatBlockStatus": FormatBlockStatus,
"formatBlockSlot": FormatBlockSlot,
"formatSlotToTimestamp": FormatSlotToTimestamp,
@@ -107,9 +108,12 @@ func GetTemplateFuncs() template.FuncMap {
"formatEth1TxHash": FormatEth1TxHash,
"formatGraffiti": FormatGraffiti,
"formatHash": FormatHash,
+ "formatDepositStatus": FormatDepositStatus,
+ "formatConsolidationStatus": FormatConsolidationStatus,
"formatWithdawalCredentials": FormatWithdawalCredentials,
"formatAddressToWithdrawalCredentials": FormatAddressToWithdrawalCredentials,
"formatBitlist": FormatBitlist,
+ "formatCommitteeBitList": FormatCommitteeBitList,
"formatBitvectorValidators": formatBitvectorValidators,
"formatParticipation": FormatParticipation,
"formatIncome": FormatIncome,
@@ -452,6 +456,14 @@ func ReadConfig(cfg *types.Config, path string) error {
err = yaml.Unmarshal([]byte(config.GnosisChainYml), &cfg.Chain.ClConfig)
case "holesky":
err = yaml.Unmarshal([]byte(config.HoleskyChainYml), &cfg.Chain.ClConfig)
+ case "hoodi":
+ err = yaml.Unmarshal([]byte(config.HoodiChainYml), &cfg.Chain.ClConfig)
+ case "mekong":
+ err = yaml.Unmarshal([]byte(config.MekongChainYml), &cfg.Chain.ClConfig)
+ case "pectra-devnet-5":
+ err = yaml.Unmarshal([]byte(config.PectraDevnet5ChainYml), &cfg.Chain.ClConfig)
+ case "pectra-devnet-6":
+ err = yaml.Unmarshal([]byte(config.PectraDevnet6ChainYml), &cfg.Chain.ClConfig)
default:
return fmt.Errorf("tried to set known chain-config, but unknown chain-name: %v (path: %v)", cfg.Chain.Name, cfg.Chain.ClConfigPath)
}
@@ -494,6 +506,8 @@ func ReadConfig(cfg *types.Config, path string) error {
CappellaForkEpoch: mustParseUint(jr.Data.CapellaForkEpoch),
DenebForkVersion: jr.Data.DenebForkVersion,
DenebForkEpoch: mustParseUint(jr.Data.DenebForkEpoch),
+ ElectraForkVersion: jr.Data.ElectraForkVersion,
+ ElectraForkEpoch: mustParseUint(jr.Data.ElectraForkEpoch),
SecondsPerSlot: mustParseUint(jr.Data.SecondsPerSlot),
SecondsPerEth1Block: mustParseUint(jr.Data.SecondsPerEth1Block),
MinValidatorWithdrawabilityDelay: mustParseUint(jr.Data.MinValidatorWithdrawabilityDelay),
@@ -558,6 +572,15 @@ func ReadConfig(cfg *types.Config, path string) error {
MaxWithdrawalsPerPayload: mustParseUint(jr.Data.MaxWithdrawalsPerPayload),
MaxValidatorsPerWithdrawalSweep: mustParseUint(jr.Data.MaxValidatorsPerWithdrawalsSweep),
MaxBlsToExecutionChange: mustParseUint(jr.Data.MaxBlsToExecutionChanges),
+ // Electra
+ MinPerEpochChurnLimitElectra: mustParseUint(jr.Data.MinPerEpochChurnLimitElectra),
+ MaxPerEpochActivationExitChurnLimit: mustParseUint(jr.Data.MaxPerEpochActivationExitChurnLimit),
+ BlobSidecarSubnetCountElectra: mustParseUint(jr.Data.BlobSidecarSubnetCountElectra),
+ MaxBlobsPerBlockElectra: mustParseUint(jr.Data.MaxBlobsPerBlockElectra),
+ MaxRequestBlobSidecarsElectra: mustParseUint(jr.Data.MaxRequestBlobSidecarsElectra),
+ MinActivationBalance: mustParseUint(jr.Data.MinActivationBalance),
+ MaxPendingDepositsPerEpoch: mustParseUint(jr.Data.MaxPendingDepositsPerEpoch),
+ MaxEffectiveBalanceElectra: mustParseUint(jr.Data.MaxEffectiveBalanceElectra),
}
if jr.Data.AltairForkEpoch == "" {
@@ -572,6 +595,9 @@ func ReadConfig(cfg *types.Config, path string) error {
if jr.Data.DenebForkEpoch == "" {
chainCfg.DenebForkEpoch = 18446744073709551615
}
+ if jr.Data.ElectraForkEpoch == "" {
+ chainCfg.DenebForkEpoch = 18446744073709551615
+ }
cfg.Chain.ClConfig = chainCfg
@@ -632,6 +658,14 @@ func ReadConfig(cfg *types.Config, path string) error {
err = yaml.Unmarshal([]byte(config.GnosisChainYml), &minimalCfg)
case "holesky":
err = yaml.Unmarshal([]byte(config.HoleskyChainYml), &minimalCfg)
+ case "hoodi":
+ err = yaml.Unmarshal([]byte(config.HoodiChainYml), &minimalCfg)
+ case "mekong":
+ err = yaml.Unmarshal([]byte(config.MekongChainYml), &minimalCfg)
+ case "pectra-devnet-5":
+ err = yaml.Unmarshal([]byte(config.PectraDevnet5ChainYml), &cfg.Chain.ClConfig)
+ case "pectra-devnet-6":
+ err = yaml.Unmarshal([]byte(config.PectraDevnet6ChainYml), &cfg.Chain.ClConfig)
default:
return fmt.Errorf("tried to set known chain-config, but unknown chain-name: %v (path: %v)", cfg.Chain.Name, cfg.Chain.ElConfigPath)
}
@@ -679,6 +713,8 @@ func ReadConfig(cfg *types.Config, path string) error {
cfg.Chain.GenesisTimestamp = 1638993340
case "holesky":
cfg.Chain.GenesisTimestamp = 1695902400
+ case "hoodi":
+ cfg.Chain.GenesisTimestamp = 1742213400
default:
return fmt.Errorf("tried to set known genesis-timestamp, but unknown chain-name")
}
@@ -698,6 +734,8 @@ func ReadConfig(cfg *types.Config, path string) error {
cfg.Chain.GenesisValidatorsRoot = "0xf5dcb5564e829aab27264b9becd5dfaa017085611224cb3036f573368dbb9d47"
case "holesky":
cfg.Chain.GenesisValidatorsRoot = "0x9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
+ case "hoodi":
+ cfg.Chain.GenesisValidatorsRoot = "0x212f13fc4df078b6cb7db228f1c8307566dcecf900867401a92023d7ba99cb5f"
default:
return fmt.Errorf("tried to set known genesis-validators-root, but unknown chain-name")
}
@@ -754,6 +792,8 @@ func ReadConfig(cfg *types.Config, path string) error {
cfg.Chain.Id = 5
case "holesky":
cfg.Chain.Id = 17000
+ case "hoodi":
+ cfg.Chain.Id = 560048
case "sepolia":
cfg.Chain.Id = 11155111
case "gnosis":
@@ -802,6 +842,29 @@ func mustParseUint(str string) uint64 {
return nbr
}
+func GetMaxEffectiveBalance(currentEpoch uint64) uint64 {
+ if Config.Chain.ClConfig.ElectraForkEpoch >= currentEpoch {
+ return Config.Chain.ClConfig.MaxEffectiveBalanceElectra
+ }
+ return Config.Chain.ClConfig.MaxEffectiveBalance
+}
+
+func GetMaxEffectiveBalanceByWithdrawalCredentials(withCred []byte) uint64 {
+ if len(withCred) == 0 {
+ return 0
+ }
+ switch withCred[0] {
+ case 0x00, 0x01:
+ // phase0, capella
+ return Config.Chain.ClConfig.MaxEffectiveBalance
+ case 0x02:
+ // electra
+ return Config.Chain.ClConfig.MaxEffectiveBalanceElectra
+ default:
+ return 0
+ }
+}
+
func readConfigFile(cfg *types.Config, path string) error {
if path == "" {
return yaml.Unmarshal([]byte(config.DefaultConfigYml), cfg)
@@ -858,7 +921,7 @@ func IsApiRequest(r *http.Request) bool {
var eth1AddressRE = regexp.MustCompile("^(0x)?[0-9a-fA-F]{40}$")
var withdrawalCredentialsRE = regexp.MustCompile("^(0x)?00[0-9a-fA-F]{62}$")
-var withdrawalCredentialsAddressRE = regexp.MustCompile("^(0x)?" + BeginningOfSetWithdrawalCredentials + "[0-9a-fA-F]{40}$")
+var withdrawalCredentialsAddressRE = regexp.MustCompile("^(0x)?(?:0[12]0{22})[0-9A-Fa-f]{40}$")
var eth1TxRE = regexp.MustCompile("^(0x)?[0-9a-fA-F]{64}$")
var zeroHashRE = regexp.MustCompile("^(0x)?0+$")
var hashRE = regexp.MustCompile("^(0x)?[0-9a-fA-F]{96}$")
@@ -1823,3 +1886,42 @@ func GetMaxAllowedDayRangeValidatorStats(validatorAmount int) int {
return math.MaxInt
}
}
+
+func FormatDepositStatus(queuedAtEpoch, processedAtEpoch int64) template.HTML {
+ if queuedAtEpoch == -2 && processedAtEpoch == -2 {
+ return `Processed`
+ }
+ if queuedAtEpoch == -1 && processedAtEpoch == -1 {
+ return `Pending`
+ }
+
+ if queuedAtEpoch >= 0 && processedAtEpoch == -1 {
+ return `Queued`
+ }
+
+ if queuedAtEpoch >= 0 && processedAtEpoch >= 0 {
+ return `Processed`
+ }
+
+ return ""
+}
+
+func FormatConsolidationStatus(queuedAtEpoch, processedAtEpoch int64, consolidationType string) template.HTML {
+ if consolidationType == "Credentials Update" {
+ return `Processed`
+ }
+
+ if queuedAtEpoch == -1 && processedAtEpoch == -1 {
+ return `Pending`
+ }
+
+ if queuedAtEpoch >= 0 && processedAtEpoch == -1 {
+ return `Queued`
+ }
+
+ if queuedAtEpoch >= 0 && processedAtEpoch >= 0 {
+ return template.HTML(fmt.Sprintf(`Processed`, processedAtEpoch))
+ }
+
+ return ""
+}
diff --git a/utils/utils_test.go b/utils/utils_test.go
index fb5e0f7a74..d3a7c5c6df 100644
--- a/utils/utils_test.go
+++ b/utils/utils_test.go
@@ -26,3 +26,42 @@ func TestIsValidUrl(t *testing.T) {
}
}
}
+
+func TestIsValidWithdrawalCredentials(t *testing.T) {
+ tests := []struct {
+ cred string
+ valid bool
+ }{
+ // real world examples (sepolia)
+ {"0x020000000000000000000000332e43696a505ef45b9319973785f837ce5267b9", true}, // prefixed
+ {"020000000000000000000000332e43696a505ef45b9319973785f837ce5267b9", true},
+ {"0x020000000000000000000000388ea662ef2c223ec0b047d41bf3c0f362142ad5", true}, // prefixed
+ {"020000000000000000000000388ea662ef2c223ec0b047d41bf3c0f362142ad5", true},
+ {"0x01000000000000000000000025c4a76e7d118705e7ea2e9b7d8c59930d8acd3b", true}, // prefixed
+ {"01000000000000000000000025c4a76e7d118705e7ea2e9b7d8c59930d8acd3b", true},
+
+ // valid but not real world examples
+ {"0x000000000000000000000000332e43696a505ef45b9319973785f837ce5267b9", true},
+ {"0x010000000000000000000000332e43696a505ef45b9319973785f837ce5267b9", true},
+
+ // invalid examples
+ {"0x030000000000000000000000332e43696a505ef45b9319973785f837ce5267b9", false}, // wrong version
+ {"0x010000000000000000000004332e43696a505ef45b9319973785f837ce5267b9", false}, // not enough 0 padding
+ {"0x010000000000000000000000332e43696a505ef45b9319973785f837ce5267b", false}, // not enough bytes
+ {"0x010000000000000000000000332e43696a505ef45b9319973785f83HALLO0000", false}, // invalid characters
+ {"0x332e43696a505ef45b9319973785f837ce5267b96", false}, // just an address (with prefix)
+ {"332e43696a505ef45b9319973785f837ce5267b96", false}, // just an address (without prefix)
+ {"0000000000000000000000332e43696a505ef45b9319973785f837ce5267b9", false}, // just padding and address, no versioning at all
+ {"dsasxfafsafass", false}, // random string
+ {"", false}, // empty string
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.cred, func(t *testing.T) {
+ v := IsValidWithdrawalCredentials(tt.cred)
+ if v != tt.valid {
+ t.Errorf("wrong withdrawal credentials validation for %v", tt.cred)
+ }
+ })
+ }
+}