diff --git a/templates/mail/layout.html b/templates/mail/layout.html
index 68f4cca386..ae63b9f367 100644
--- a/templates/mail/layout.html
+++ b/templates/mail/layout.html
@@ -50,7 +50,7 @@
diff --git a/templates/payment/pricing.html b/templates/payment/pricing.html
index b9005edc77..dfc99cfe89 100644
--- a/templates/payment/pricing.html
+++ b/templates/payment/pricing.html
@@ -202,17 +202,21 @@ Free
0€ / mo
+
+ Rate limit per second:
+ {{ formatAddCommas 5 }}
+
Rate limit per minute:
- 10
+ {{ formatAddCommas 20 }}
Requests per day:
- 10,000
+ {{ formatAddCommas 30000 }}
Total requests per month:
- 30,000
+ {{ formatAddCommas 30000 }}
Get started
@@ -225,17 +229,21 @@
Sapphire Popul
59€* / mo
+
+ Rate limit per second:
+ {{ formatAddCommas 10 }}
+
Rate limit per minute:
- 100
+ {{ formatAddCommas 100 }}
Requests per day:
- 100,000
+ {{ formatAddCommas 100000 }}
Total requests per month:
- 500,000
+ {{ formatAddCommas 500000 }}
{{ if .User.Authenticated }}
@@ -266,17 +274,21 @@
Emerald
99€* / mo
+
+ Rate limit per second:
+ {{ formatAddCommas 10 }}
+
Rate limit per minute:
- Unlimited
+ {{ formatAddCommas 600 }}
Requests per day:
- 200,000
+ {{ formatAddCommas 200000 }}
Total requests per month:
- 1,000,000
+ {{ formatAddCommas 1000000 }}
{{ if .User.Authenticated }}
@@ -304,9 +316,13 @@
Diamond
399€* / mo
+
+ Rate limit per second:
+ {{ formatAddCommas 30 }}
+
Rate limit per minute:
- Unlimited
+ {{ formatAddCommas 1200 }}
Requests per day:
@@ -314,7 +330,7 @@ 399€* / mo
Total requests per month:
- 4,000,000
+ {{ formatAddCommas 6000000 }}
{{ if .User.Authenticated }}
@@ -378,6 +394,7 @@
Contact us about your API needs
Sapphire
Emerald
Diamond
+
Enterprise
-
This table displays partial and full withdrawals.
+
This table displays partial and full withdrawals. Only the first 10000 entries of the resultset are shown.
@@ -239,7 +239,7 @@
Address Changes (BLS)
-
This table displays the BLS address changes from 0x00 credentials to 0x01 .
+
This table displays the BLS address changes from 0x00 credentials to 0x01 . Only the first 10000 entries of the resultset are shown.
diff --git a/types/api.go b/types/api.go
index a7a6ecd1bb..86e6f0886d 100644
--- a/types/api.go
+++ b/types/api.go
@@ -715,3 +715,10 @@ type EnsDomainResponse struct {
Address string `json:"address"`
Domain string `json:"domain"`
}
+
+type ApiProposalLuckResponse struct {
+ ProposalLuck *float64 `json:"proposal_luck"` // The proposal luck for the given set of validators as a percentage
+ AverageProposalInterval float64 `json:"average_proposal_interval"` // The average slot interval between proposals for the given set of validators
+ NextProposalEstimateTs *int64 `json:"next_proposal_estimate_ts"` // The estimated timestamp of the next proposal
+ TimeFrameName *string `json:"time_frame_name"` // The timeframe for which the luck is calculated
+}
diff --git a/types/config.go b/types/config.go
index 111dab3479..8a9df8f8a8 100644
--- a/types/config.go
+++ b/types/config.go
@@ -28,8 +28,11 @@ type Config struct {
MaxIdleConns int `yaml:"maxIdleConns" envconfig:"WRITER_DB_MAX_IDLE_CONNS"`
} `yaml:"writerDatabase"`
Bigtable struct {
- Project string `yaml:"project" envconfig:"BIGTABLE_PROJECT"`
- Instance string `yaml:"instance" envconfig:"BIGTABLE_INSTANCE"`
+ Project string `yaml:"project" envconfig:"BIGTABLE_PROJECT"`
+ Instance string `yaml:"instance" envconfig:"BIGTABLE_INSTANCE"`
+ Emulator bool `yaml:"emulator" envconfig:"BIGTABLE_EMULATOR"`
+ EmulatorPort int `yaml:"emulatorPort" envconfig:"BIGTABLE_EMULATOR_PORT"`
+ EmulatorHost string `yaml:"emulatorHost" envconfig:"BIGTABLE_EMULATOR_HOST"`
} `yaml:"bigtable"`
BlobIndexer struct {
S3 struct {
@@ -97,8 +100,12 @@ type Config struct {
Enabled bool `yaml:"enabled" envconfig:"FRONTEND_ENABLED"`
BlobProviderUrl string `yaml:"blobProviderUrl" envconfig:"FRONTEND_BLOB_PROVIDER_URL"`
// Imprint is deprdecated place imprint file into the legal directory
- Imprint string `yaml:"imprint" envconfig:"FRONTEND_IMPRINT"`
- LegalDir string `yaml:"legalDir" envconfig:"FRONTEND_LEGAL"`
+ Imprint string `yaml:"imprint" envconfig:"FRONTEND_IMPRINT"`
+ Legal struct {
+ TermsOfServiceUrl string `yaml:"termsOfServiceUrl" envconfig:"FRONTEND_LEGAL_TERMS_OF_SERVICE_URL"`
+ PrivacyPolicyUrl string `yaml:"privacyPolicyUrl" envconfig:"FRONTEND_LEGAL_PRIVACY_POLICY_URL"`
+ ImprintTemplate string `yaml:"imprintTemplate" envconfig:"FRONTEND_LEGAL_IMPRINT_TEMPLATE"`
+ } `yaml:"legal"`
SiteDomain string `yaml:"siteDomain" envconfig:"FRONTEND_SITE_DOMAIN"`
SiteName string `yaml:"siteName" envconfig:"FRONTEND_SITE_NAME"`
SiteSubtitle string `yaml:"siteSubtitle" envconfig:"FRONTEND_SITE_SUBTITLE"`
@@ -168,9 +175,6 @@ type Config struct {
Timestamp uint64 `yaml:"timestamp" envconfig:"FRONTEND_COUNTDOWN_TIMESTAMP"`
Info string `yaml:"info" envconfig:"FRONTEND_COUNTDOWN_INFO"`
} `yaml:"countdown"`
- Validator struct {
- ShowProposerRewards bool `yaml:"showProposerRewards" envconfig:"FRONTEND_SHOW_PROPOSER_REWARDS"`
- } `yaml:"validator"`
HttpReadTimeout time.Duration `yaml:"httpReadTimeout" envconfig:"FRONTEND_HTTP_READ_TIMEOUT"`
HttpWriteTimeout time.Duration `yaml:"httpWriteTimeout" envconfig:"FRONTEND_HTTP_WRITE_TIMEOUT"`
HttpIdleTimeout time.Duration `yaml:"httpIdleTimeout" envconfig:"FRONTEND_HTTP_IDLE_TIMEOUT"`
@@ -229,3 +233,103 @@ type ServiceMonitoringConfiguration struct {
Name string `yaml:"name" envconfig:"NAME"`
Duration time.Duration `yaml:"duration" envconfig:"DURATION"`
}
+
+type ConfigJsonResponse struct {
+ Data struct {
+ ConfigName string `json:"CONFIG_NAME"`
+ PresetBase string `json:"PRESET_BASE"`
+ TerminalTotalDifficulty string `json:"TERMINAL_TOTAL_DIFFICULTY"`
+ TerminalBlockHash string `json:"TERMINAL_BLOCK_HASH"`
+ TerminalBlockHashActivationEpoch string `json:"TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH"`
+ SafeSlotsToImportOptimistically string `json:"SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY"`
+ MinGenesisActiveValidatorCount string `json:"MIN_GENESIS_ACTIVE_VALIDATOR_COUNT"`
+ MinGenesisTime string `json:"MIN_GENESIS_TIME"`
+ GenesisForkVersion string `json:"GENESIS_FORK_VERSION"`
+ GenesisDelay string `json:"GENESIS_DELAY"`
+ AltairForkVersion string `json:"ALTAIR_FORK_VERSION"`
+ AltairForkEpoch string `json:"ALTAIR_FORK_EPOCH"`
+ BellatrixForkVersion string `json:"BELLATRIX_FORK_VERSION"`
+ BellatrixForkEpoch string `json:"BELLATRIX_FORK_EPOCH"`
+ CapellaForkVersion string `json:"CAPELLA_FORK_VERSION"`
+ CapellaForkEpoch string `json:"CAPELLA_FORK_EPOCH"`
+ SecondsPerSlot string `json:"SECONDS_PER_SLOT"`
+ SecondsPerEth1Block string `json:"SECONDS_PER_ETH1_BLOCK"`
+ MinValidatorWithdrawabilityDelay string `json:"MIN_VALIDATOR_WITHDRAWABILITY_DELAY"`
+ ShardCommitteePeriod string `json:"SHARD_COMMITTEE_PERIOD"`
+ Eth1FollowDistance string `json:"ETH1_FOLLOW_DISTANCE"`
+ SubnetsPerNode string `json:"SUBNETS_PER_NODE"`
+ InactivityScoreBias string `json:"INACTIVITY_SCORE_BIAS"`
+ InactivityScoreRecoveryRate string `json:"INACTIVITY_SCORE_RECOVERY_RATE"`
+ EjectionBalance string `json:"EJECTION_BALANCE"`
+ MinPerEpochChurnLimit string `json:"MIN_PER_EPOCH_CHURN_LIMIT"`
+ ChurnLimitQuotient string `json:"CHURN_LIMIT_QUOTIENT"`
+ ProposerScoreBoost string `json:"PROPOSER_SCORE_BOOST"`
+ DepositChainID string `json:"DEPOSIT_CHAIN_ID"`
+ DepositNetworkID string `json:"DEPOSIT_NETWORK_ID"`
+ DepositContractAddress string `json:"DEPOSIT_CONTRACT_ADDRESS"`
+ MaxCommitteesPerSlot string `json:"MAX_COMMITTEES_PER_SLOT"`
+ TargetCommitteeSize string `json:"TARGET_COMMITTEE_SIZE"`
+ MaxValidatorsPerCommittee string `json:"MAX_VALIDATORS_PER_COMMITTEE"`
+ ShuffleRoundCount string `json:"SHUFFLE_ROUND_COUNT"`
+ HysteresisQuotient string `json:"HYSTERESIS_QUOTIENT"`
+ HysteresisDownwardMultiplier string `json:"HYSTERESIS_DOWNWARD_MULTIPLIER"`
+ HysteresisUpwardMultiplier string `json:"HYSTERESIS_UPWARD_MULTIPLIER"`
+ SafeSlotsToUpdateJustified string `json:"SAFE_SLOTS_TO_UPDATE_JUSTIFIED"`
+ MinDepositAmount string `json:"MIN_DEPOSIT_AMOUNT"`
+ MaxEffectiveBalance string `json:"MAX_EFFECTIVE_BALANCE"`
+ EffectiveBalanceIncrement string `json:"EFFECTIVE_BALANCE_INCREMENT"`
+ MinAttestationInclusionDelay string `json:"MIN_ATTESTATION_INCLUSION_DELAY"`
+ SlotsPerEpoch string `json:"SLOTS_PER_EPOCH"`
+ MinSeedLookahead string `json:"MIN_SEED_LOOKAHEAD"`
+ MaxSeedLookahead string `json:"MAX_SEED_LOOKAHEAD"`
+ EpochsPerEth1VotingPeriod string `json:"EPOCHS_PER_ETH1_VOTING_PERIOD"`
+ SlotsPerHistoricalRoot string `json:"SLOTS_PER_HISTORICAL_ROOT"`
+ MinEpochsToInactivityPenalty string `json:"MIN_EPOCHS_TO_INACTIVITY_PENALTY"`
+ EpochsPerHistoricalVector string `json:"EPOCHS_PER_HISTORICAL_VECTOR"`
+ EpochsPerSlashingsVector string `json:"EPOCHS_PER_SLASHINGS_VECTOR"`
+ HistoricalRootsLimit string `json:"HISTORICAL_ROOTS_LIMIT"`
+ ValidatorRegistryLimit string `json:"VALIDATOR_REGISTRY_LIMIT"`
+ BaseRewardFactor string `json:"BASE_REWARD_FACTOR"`
+ WhistleblowerRewardQuotient string `json:"WHISTLEBLOWER_REWARD_QUOTIENT"`
+ ProposerRewardQuotient string `json:"PROPOSER_REWARD_QUOTIENT"`
+ InactivityPenaltyQuotient string `json:"INACTIVITY_PENALTY_QUOTIENT"`
+ MinSlashingPenaltyQuotient string `json:"MIN_SLASHING_PENALTY_QUOTIENT"`
+ ProportionalSlashingMultiplier string `json:"PROPORTIONAL_SLASHING_MULTIPLIER"`
+ MaxProposerSlashings string `json:"MAX_PROPOSER_SLASHINGS"`
+ MaxAttesterSlashings string `json:"MAX_ATTESTER_SLASHINGS"`
+ MaxAttestations string `json:"MAX_ATTESTATIONS"`
+ MaxDeposits string `json:"MAX_DEPOSITS"`
+ MaxVoluntaryExits string `json:"MAX_VOLUNTARY_EXITS"`
+ InactivityPenaltyQuotientAltair string `json:"INACTIVITY_PENALTY_QUOTIENT_ALTAIR"`
+ MinSlashingPenaltyQuotientAltair string `json:"MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR"`
+ ProportionalSlashingMultiplierAltair string `json:"PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR"`
+ SyncCommitteeSize string `json:"SYNC_COMMITTEE_SIZE"`
+ EpochsPerSyncCommitteePeriod string `json:"EPOCHS_PER_SYNC_COMMITTEE_PERIOD"`
+ MinSyncCommitteeParticipants string `json:"MIN_SYNC_COMMITTEE_PARTICIPANTS"`
+ InactivityPenaltyQuotientBellatrix string `json:"INACTIVITY_PENALTY_QUOTIENT_BELLATRIX"`
+ MinSlashingPenaltyQuotientBellatrix string `json:"MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX"`
+ ProportionalSlashingMultiplierBellatrix string `json:"PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX"`
+ MaxBytesPerTransaction string `json:"MAX_BYTES_PER_TRANSACTION"`
+ MaxTransactionsPerPayload string `json:"MAX_TRANSACTIONS_PER_PAYLOAD"`
+ BytesPerLogsBloom string `json:"BYTES_PER_LOGS_BLOOM"`
+ MaxExtraDataBytes string `json:"MAX_EXTRA_DATA_BYTES"`
+ MaxBlsToExecutionChanges string `json:"MAX_BLS_TO_EXECUTION_CHANGES"`
+ MaxWithdrawalsPerPayload string `json:"MAX_WITHDRAWALS_PER_PAYLOAD"`
+ MaxValidatorsPerWithdrawalsSweep string `json:"MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP"`
+ DomainAggregateAndProof string `json:"DOMAIN_AGGREGATE_AND_PROOF"`
+ TargetAggregatorsPerSyncSubcommittee string `json:"TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE"`
+ SyncCommitteeSubnetCount string `json:"SYNC_COMMITTEE_SUBNET_COUNT"`
+ BlsWithdrawalPrefix string `json:"BLS_WITHDRAWAL_PREFIX"`
+ DomainRandao string `json:"DOMAIN_RANDAO"`
+ DomainVoluntaryExit string `json:"DOMAIN_VOLUNTARY_EXIT"`
+ DomainSyncCommitteeSelectionProof string `json:"DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF"`
+ DomainBeaconAttester string `json:"DOMAIN_BEACON_ATTESTER"`
+ DomainBeaconProposer string `json:"DOMAIN_BEACON_PROPOSER"`
+ DomainDeposit string `json:"DOMAIN_DEPOSIT"`
+ DomainSelectionProof string `json:"DOMAIN_SELECTION_PROOF"`
+ DomainSyncCommittee string `json:"DOMAIN_SYNC_COMMITTEE"`
+ TargetAggregatorsPerCommittee string `json:"TARGET_AGGREGATORS_PER_COMMITTEE"`
+ DomainContributionAndProof string `json:"DOMAIN_CONTRIBUTION_AND_PROOF"`
+ DomainApplicationMask string `json:"DOMAIN_APPLICATION_MASK"`
+ } `json:"data"`
+}
diff --git a/types/exporter.go b/types/exporter.go
index d6fb944d88..99ea9bc86f 100644
--- a/types/exporter.go
+++ b/types/exporter.go
@@ -45,13 +45,20 @@ type FinalityCheckpoints struct {
} `json:"finalized"`
}
+type Slot uint64
+type ValidatorIndex uint64
+
// EpochData is a struct to hold epoch data
type EpochData struct {
Epoch uint64
Validators []*Validator
ValidatorAssignmentes *EpochAssignments
Blocks map[uint64]map[string]*Block
+ FutureBlocks map[uint64]map[string]*Block
EpochParticipationStats *ValidatorParticipation
+ AttestationDuties map[Slot]map[ValidatorIndex][]Slot
+ SyncDuties map[Slot]map[ValidatorIndex]bool
+ Finalized bool
}
// ValidatorParticipation is a struct to hold validator participation data
@@ -60,6 +67,7 @@ type ValidatorParticipation struct {
GlobalParticipationRate float32
VotedEther uint64
EligibleEther uint64
+ Finalized bool
}
// BeaconCommitteItem is a struct to hold beacon committee data
@@ -127,6 +135,8 @@ type Block struct {
ExcessBlobGas uint64
BlobKZGCommitments [][]byte
BlobKZGProofs [][]byte
+ AttestationDuties map[ValidatorIndex]Slot
+ SyncDuties map[ValidatorIndex]bool
}
type SignedBLSToExecutionChange struct {
diff --git a/types/templates.go b/types/templates.go
index 14a8af6b3e..7cc5d12841 100644
--- a/types/templates.go
+++ b/types/templates.go
@@ -52,6 +52,8 @@ type PageData struct {
GlobalNotification template.HTML
AvailableCurrencies []string
MainMenuItems []MainMenuItem
+ TermsOfServiceUrl string
+ PrivacyPolicyUrl string
}
type MainMenuItem struct {
@@ -377,17 +379,16 @@ type ValidatorPageData struct {
ParticipatedSyncCountSlots uint64
MissedSyncCountSlots uint64
OrphanedSyncCountSlots uint64
- UnmissedSyncPercentage float64 // participated/(participated+missed)
- IncomeToday ClElInt64 `json:"incomeToday"`
- Income1d ClElInt64 `json:"income1d"`
- Income7d ClElInt64 `json:"income7d"`
- Income31d ClElInt64 `json:"income31d"`
- IncomeTotal ClElInt64 `json:"incomeTotal"`
- IncomeTotalFormatted template.HTML `json:"incomeTotalFormatted"`
- IncomeProposerFormatted *template.HTML `json:"incomeProposerFormatted"`
- Apr7d ClElFloat64 `json:"apr7d"`
- Apr31d ClElFloat64 `json:"apr31d"`
- Apr365d ClElFloat64 `json:"apr365d"`
+ UnmissedSyncPercentage float64 // participated/(participated+missed)
+ IncomeToday ClElInt64 `json:"incomeToday"`
+ Income1d ClElInt64 `json:"income1d"`
+ Income7d ClElInt64 `json:"income7d"`
+ Income31d ClElInt64 `json:"income31d"`
+ IncomeTotal ClElInt64 `json:"incomeTotal"`
+ IncomeTotalFormatted template.HTML `json:"incomeTotalFormatted"`
+ Apr7d ClElFloat64 `json:"apr7d"`
+ Apr31d ClElFloat64 `json:"apr31d"`
+ Apr365d ClElFloat64 `json:"apr365d"`
SyncLuck float64
SyncEstimate *time.Time
AvgSyncInterval *time.Duration
@@ -1058,7 +1059,6 @@ type ValidatorEarnings struct {
LastDayFormatted template.HTML `json:"lastDayFormatted"`
LastWeekFormatted template.HTML `json:"lastWeekFormatted"`
LastMonthFormatted template.HTML `json:"lastMonthFormatted"`
- ProposerTotalFormatted template.HTML `json:"proposerTotalFormatted"`
TotalFormatted template.HTML `json:"totalFormatted"`
TotalChangeFormatted template.HTML `json:"totalChangeFormatted"`
TotalBalance template.HTML `json:"totalBalance"`
@@ -1686,6 +1686,11 @@ type DepositContractInteraction struct {
Amount []byte
}
+type EpochInfo struct {
+ Finalized bool `db:"finalized"`
+ Participation float64 `db:"globalparticipationrate"`
+}
+
type Eth1TxData struct {
From common.Address
To *common.Address
@@ -1705,10 +1710,7 @@ type Eth1TxData struct {
BlobGasPrice []byte
BlobFee []byte
}
- Epoch struct {
- Finalized bool `db:"finalized"`
- Participation float64 `db:"globalparticipationrate"`
- }
+ Epoch EpochInfo
TypeFormatted string
Type uint8
Nonce uint64
@@ -2088,17 +2090,16 @@ type BroadcastStatusPageData struct {
}
type ValidatorIncomePerformance struct {
- ClIncome1d int64 `db:"cl_performance_1d"`
- ClIncome7d int64 `db:"cl_performance_7d"`
- ClIncome31d int64 `db:"cl_performance_31d"`
- ClIncome365d int64 `db:"cl_performance_365d"`
- ClIncomeTotal int64 `db:"cl_performance_total"`
- ClProposerIncomeTotal int64 `db:"cl_proposer_performance_total"`
- ElIncome1d int64 `db:"el_performance_1d"`
- ElIncome7d int64 `db:"el_performance_7d"`
- ElIncome31d int64 `db:"el_performance_31d"`
- ElIncome365d int64 `db:"el_performance_365d"`
- ElIncomeTotal int64 `db:"el_performance_total"`
+ ClIncome1d int64 `db:"cl_performance_1d"`
+ ClIncome7d int64 `db:"cl_performance_7d"`
+ ClIncome31d int64 `db:"cl_performance_31d"`
+ ClIncome365d int64 `db:"cl_performance_365d"`
+ ClIncomeTotal int64 `db:"cl_performance_total"`
+ ElIncome1d int64 `db:"el_performance_1d"`
+ ElIncome7d int64 `db:"el_performance_7d"`
+ ElIncome31d int64 `db:"el_performance_31d"`
+ ElIncome365d int64 `db:"el_performance_365d"`
+ ElIncomeTotal int64 `db:"el_performance_total"`
}
type ValidatorProposalInfo struct {
diff --git a/utils/eth1.go b/utils/eth1.go
index 905f3ccd14..4a83877b15 100644
--- a/utils/eth1.go
+++ b/utils/eth1.go
@@ -261,14 +261,15 @@ func FormatAddressAsTokenLink(token, address []byte, name string, verified bool,
func FormatHashLong(hash common.Hash) template.HTML {
address := hash.String()
- test := `
-
- %s
- %s
- %s
-
`
if len(address) > 4 {
- return template.HTML(fmt.Sprintf(test, address[:4], address[4:len(address)-4], address[len(address)-4:]))
+ htmlFormat := `
+
+ %s
+ %s
+ %s
+
`
+
+ return template.HTML(fmt.Sprintf(htmlFormat, address[:4], address[4:len(address)-4], address[len(address)-4:]))
}
return template.HTML(address)
@@ -279,10 +280,11 @@ func FormatAddressLong(address string) template.HTML {
return template.HTML(address)
}
address = FixAddressCasing(address)
- test := `
-
%s %s %s `
if len(address) > 4 {
- return template.HTML(fmt.Sprintf(test, address[:6], address[6:len(address)-4], address[len(address)-4:]))
+ htmlFormat := `
+
%s%s %s%s `
+
+ return template.HTML(fmt.Sprintf(htmlFormat, address[:2], address[2:6], address[6:len(address)-4], address[len(address)-4:]))
}
return template.HTML(address)
diff --git a/utils/oauthutils.go b/utils/oauthutils.go
index 89364be64e..8407f678f3 100644
--- a/utils/oauthutils.go
+++ b/utils/oauthutils.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"strings"
"time"
@@ -241,7 +241,7 @@ func AuthorizedAPIMiddleware(next http.Handler) http.Handler {
// as context to the request. By doing this here,
// we can use base.FormValueOrJson(key) without multiple parsings of the same body
if r.Method == "POST" && r.Header.Get("Content-Type") == "application/json" {
- body, err := ioutil.ReadAll(r.Body)
+ body, err := io.ReadAll(r.Body)
if err == nil {
keyVal := make(map[string]interface{})
json.Unmarshal(body, &keyVal)
diff --git a/utils/utils.go b/utils/utils.go
index 99a06db6d0..7bbe59217f 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -18,7 +18,6 @@ import (
"html/template"
"image/color"
"io"
- "io/ioutil"
"log"
"math"
"math/big"
@@ -30,6 +29,7 @@ import (
"regexp"
"runtime"
"sort"
+ "strconv"
"strings"
"time"
"unicode/utf8"
@@ -40,6 +40,7 @@ import (
"gopkg.in/yaml.v3"
"github.com/asaskevich/govalidator"
+ "github.com/carlmjohnson/requests"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/params"
@@ -246,7 +247,7 @@ func GetTemplateFuncs() template.FuncMap {
// IncludeHTML adds html to the page
func IncludeHTML(path string) template.HTML {
- b, err := ioutil.ReadFile(path)
+ b, err := os.ReadFile(path)
if err != nil {
log.Printf("includeHTML - error reading file: %v", err)
return ""
@@ -254,9 +255,9 @@ func IncludeHTML(path string) template.HTML {
return template.HTML(string(b))
}
-func GraffitiToSring(graffiti []byte) string {
+func GraffitiToString(graffiti []byte) string {
s := strings.Map(fixUtf, string(bytes.Trim(graffiti, "\x00")))
- s = strings.Replace(s, "\u0000", "", -1) // rempove 0x00 bytes as it is not supported in postgres
+ s = strings.Replace(s, "\u0000", "", -1) // remove 0x00 bytes as it is not supported in postgres
if !utf8.ValidString(s) {
return "INVALID_UTF8_STRING"
@@ -270,6 +271,22 @@ func FormatGraffitiString(graffiti string) string {
return strings.Map(fixUtf, template.HTMLEscapeString(graffiti))
}
+func HasProblematicUtfCharacters(s string) bool {
+ // Check for null character ('\x00')
+ if utf8.ValidString(s) && utf8.Valid([]byte(s)) {
+ // Check for control characters ('\x01' to '\x1F' and '\x7F')
+ for _, r := range s {
+ if r <= 0x1F || r == 0x7F {
+ return true
+ }
+ }
+ } else {
+ return true // Invalid UTF-8 sequence
+ }
+
+ return false
+}
+
func fixUtf(r rune) rune {
if r == utf8.RuneError {
return -1
@@ -373,6 +390,7 @@ func WaitForCtrlC() {
<-c
}
+/*
func ReadClConfig(clCfg *types.ClChainConfig, name string, cfgPath string) error {
// first check the preset and set all values from it
var cfgBytes []byte
@@ -441,10 +459,16 @@ func ReadClConfig(clCfg *types.ClChainConfig, name string, cfgPath string) error
err = yaml.Unmarshal(cfgBytes, &clCfg)
return err
}
+*/
// ReadConfig will process a configuration
func ReadConfig(cfg *types.Config, path string) error {
+ configPathFromEnv := os.Getenv("BEACONCHAIN_CONFIG")
+
+ if configPathFromEnv != "" { // allow the location of the config file to be passed via env args
+ path = configPathFromEnv
+ }
if strings.HasPrefix(path, "projects/") {
x, err := AccessSecretVersion(path)
if err != nil {
@@ -470,34 +494,166 @@ func ReadConfig(cfg *types.Config, path string) error {
return err
}
- err = ReadClConfig(&cfg.Chain.ClConfig, cfg.Chain.Name, cfg.Chain.ClConfigPath)
- if err != nil {
- return err
- }
+ if cfg.Chain.ClConfigPath == "" {
+ // var prysmParamsConfig *prysmParams.BeaconChainConfig
+ switch cfg.Chain.Name {
+ case "mainnet":
+ err = yaml.Unmarshal([]byte(config.MainnetChainYml), &cfg.Chain.ClConfig)
+ case "prater":
+ err = yaml.Unmarshal([]byte(config.PraterChainYml), &cfg.Chain.ClConfig)
+ case "ropsten":
+ err = yaml.Unmarshal([]byte(config.RopstenChainYml), &cfg.Chain.ClConfig)
+ case "sepolia":
+ err = yaml.Unmarshal([]byte(config.SepoliaChainYml), &cfg.Chain.ClConfig)
+ case "gnosis":
+ err = yaml.Unmarshal([]byte(config.GnosisChainYml), &cfg.Chain.ClConfig)
+ case "holesky":
+ err = yaml.Unmarshal([]byte(config.HoleskyChainYml), &cfg.Chain.ClConfig)
+ default:
+ return fmt.Errorf("tried to set known chain-config, but unknown chain-name")
+ }
+ if err != nil {
+ return err
+ }
+ // err = prysmParams.SetActive(prysmParamsConfig)
+ // if err != nil {
+ // return fmt.Errorf("error setting chainConfig (%v) for prysmParams: %w", cfg.Chain.Name, err)
+ // }
+ } else if cfg.Chain.ClConfigPath == "node" {
+ nodeEndpoint := fmt.Sprintf("http://%s:%s", cfg.Indexer.Node.Host, cfg.Indexer.Node.Port)
- switch cfg.Chain.Name {
- case "mainnet":
- cfg.Chain.ElConfig = params.MainnetChainConfig
- case "prater":
- cfg.Chain.ElConfig = params.GoerliChainConfig
- case "sepolia":
- cfg.Chain.ElConfig = params.SepoliaChainConfig
- case "gnosis":
- // #TODO.patrick
- cfg.Chain.ElConfig = ¶ms.ChainConfig{}
- case "dencun-devnet-8":
- // https://raw.githubusercontent.com/ethpandaops/dencun-testnet/master/network-configs/devnet-8/genesis.json
- // #TODO.patrick
- cfg.Chain.ElConfig = ¶ms.ChainConfig{}
- cfg.Chain.ElConfig.ChainID = new(big.Int).SetInt64(7011893058)
- cfg.Chain.ElConfig.TerminalTotalDifficultyPassed = true
- shanghaiTime := uint64(1692184320)
- cfg.Chain.ElConfig.ShanghaiTime = &shanghaiTime
- cancunTime := uint64(1692186240)
- cfg.Chain.ElConfig.CancunTime = &cancunTime
- default:
- cfg.Chain.ElConfig = ¶ms.ChainConfig{}
+ jr := &types.ConfigJsonResponse{}
+
+ err := requests.
+ URL(nodeEndpoint + "/eth/v1/config/spec").
+ ToJSON(jr).
+ Fetch(context.Background())
+
+ if err != nil {
+ return err
+ }
+
+ chainCfg := types.ClChainConfig{
+ PresetBase: jr.Data.PresetBase,
+ ConfigName: jr.Data.ConfigName,
+ TerminalTotalDifficulty: jr.Data.TerminalTotalDifficulty,
+ TerminalBlockHash: jr.Data.TerminalBlockHash,
+ TerminalBlockHashActivationEpoch: mustParseUint(jr.Data.TerminalBlockHashActivationEpoch),
+ MinGenesisActiveValidatorCount: mustParseUint(jr.Data.MinGenesisActiveValidatorCount),
+ MinGenesisTime: int64(mustParseUint(jr.Data.MinGenesisTime)),
+ GenesisForkVersion: jr.Data.GenesisForkVersion,
+ GenesisDelay: mustParseUint(jr.Data.GenesisDelay),
+ AltairForkVersion: jr.Data.AltairForkVersion,
+ AltairForkEpoch: mustParseUint(jr.Data.AltairForkEpoch),
+ BellatrixForkVersion: jr.Data.BellatrixForkVersion,
+ BellatrixForkEpoch: mustParseUint(jr.Data.BellatrixForkEpoch),
+ CappellaForkVersion: jr.Data.CapellaForkVersion,
+ CappellaForkEpoch: mustParseUint(jr.Data.CapellaForkEpoch),
+ SecondsPerSlot: mustParseUint(jr.Data.SecondsPerSlot),
+ SecondsPerEth1Block: mustParseUint(jr.Data.SecondsPerEth1Block),
+ MinValidatorWithdrawabilityDelay: mustParseUint(jr.Data.MinValidatorWithdrawabilityDelay),
+ ShardCommitteePeriod: mustParseUint(jr.Data.ShardCommitteePeriod),
+ Eth1FollowDistance: mustParseUint(jr.Data.Eth1FollowDistance),
+ InactivityScoreBias: mustParseUint(jr.Data.InactivityScoreBias),
+ InactivityScoreRecoveryRate: mustParseUint(jr.Data.InactivityScoreRecoveryRate),
+ EjectionBalance: mustParseUint(jr.Data.EjectionBalance),
+ MinPerEpochChurnLimit: mustParseUint(jr.Data.MinPerEpochChurnLimit),
+ ChurnLimitQuotient: mustParseUint(jr.Data.ChurnLimitQuotient),
+ ProposerScoreBoost: mustParseUint(jr.Data.ProposerScoreBoost),
+ DepositChainID: mustParseUint(jr.Data.DepositChainID),
+ DepositNetworkID: mustParseUint(jr.Data.DepositNetworkID),
+ DepositContractAddress: jr.Data.DepositContractAddress,
+ MaxCommitteesPerSlot: mustParseUint(jr.Data.MaxCommitteesPerSlot),
+ TargetCommitteeSize: mustParseUint(jr.Data.TargetCommitteeSize),
+ MaxValidatorsPerCommittee: mustParseUint(jr.Data.TargetCommitteeSize),
+ ShuffleRoundCount: mustParseUint(jr.Data.ShuffleRoundCount),
+ HysteresisQuotient: mustParseUint(jr.Data.HysteresisQuotient),
+ HysteresisDownwardMultiplier: mustParseUint(jr.Data.HysteresisDownwardMultiplier),
+ HysteresisUpwardMultiplier: mustParseUint(jr.Data.HysteresisUpwardMultiplier),
+ SafeSlotsToUpdateJustified: mustParseUint(jr.Data.SafeSlotsToUpdateJustified),
+ MinDepositAmount: mustParseUint(jr.Data.MinDepositAmount),
+ MaxEffectiveBalance: mustParseUint(jr.Data.MaxEffectiveBalance),
+ EffectiveBalanceIncrement: mustParseUint(jr.Data.EffectiveBalanceIncrement),
+ MinAttestationInclusionDelay: mustParseUint(jr.Data.MinAttestationInclusionDelay),
+ SlotsPerEpoch: mustParseUint(jr.Data.SlotsPerEpoch),
+ MinSeedLookahead: mustParseUint(jr.Data.MinSeedLookahead),
+ MaxSeedLookahead: mustParseUint(jr.Data.MaxSeedLookahead),
+ EpochsPerEth1VotingPeriod: mustParseUint(jr.Data.EpochsPerEth1VotingPeriod),
+ SlotsPerHistoricalRoot: mustParseUint(jr.Data.SlotsPerHistoricalRoot),
+ MinEpochsToInactivityPenalty: mustParseUint(jr.Data.MinEpochsToInactivityPenalty),
+ EpochsPerHistoricalVector: mustParseUint(jr.Data.EpochsPerHistoricalVector),
+ EpochsPerSlashingsVector: mustParseUint(jr.Data.EpochsPerSlashingsVector),
+ HistoricalRootsLimit: mustParseUint(jr.Data.HistoricalRootsLimit),
+ ValidatorRegistryLimit: mustParseUint(jr.Data.ValidatorRegistryLimit),
+ BaseRewardFactor: mustParseUint(jr.Data.BaseRewardFactor),
+ WhistleblowerRewardQuotient: mustParseUint(jr.Data.WhistleblowerRewardQuotient),
+ ProposerRewardQuotient: mustParseUint(jr.Data.ProposerRewardQuotient),
+ InactivityPenaltyQuotient: mustParseUint(jr.Data.InactivityPenaltyQuotient),
+ MinSlashingPenaltyQuotient: mustParseUint(jr.Data.MinSlashingPenaltyQuotient),
+ ProportionalSlashingMultiplier: mustParseUint(jr.Data.ProportionalSlashingMultiplier),
+ MaxProposerSlashings: mustParseUint(jr.Data.MaxProposerSlashings),
+ MaxAttesterSlashings: mustParseUint(jr.Data.MaxAttesterSlashings),
+ MaxAttestations: mustParseUint(jr.Data.MaxAttestations),
+ MaxDeposits: mustParseUint(jr.Data.MaxDeposits),
+ MaxVoluntaryExits: mustParseUint(jr.Data.MaxVoluntaryExits),
+ InvactivityPenaltyQuotientAltair: mustParseUint(jr.Data.InactivityPenaltyQuotientAltair),
+ MinSlashingPenaltyQuotientAltair: mustParseUint(jr.Data.MinSlashingPenaltyQuotientAltair),
+ ProportionalSlashingMultiplierAltair: mustParseUint(jr.Data.ProportionalSlashingMultiplierAltair),
+ SyncCommitteeSize: mustParseUint(jr.Data.SyncCommitteeSize),
+ EpochsPerSyncCommitteePeriod: mustParseUint(jr.Data.EpochsPerSyncCommitteePeriod),
+ MinSyncCommitteeParticipants: mustParseUint(jr.Data.MinSyncCommitteeParticipants),
+ InvactivityPenaltyQuotientBellatrix: mustParseUint(jr.Data.InactivityPenaltyQuotientBellatrix),
+ MinSlashingPenaltyQuotientBellatrix: mustParseUint(jr.Data.MinSlashingPenaltyQuotientBellatrix),
+ ProportionalSlashingMultiplierBellatrix: mustParseUint(jr.Data.ProportionalSlashingMultiplierBellatrix),
+ MaxBytesPerTransaction: mustParseUint(jr.Data.MaxBytesPerTransaction),
+ MaxTransactionsPerPayload: mustParseUint(jr.Data.MaxTransactionsPerPayload),
+ BytesPerLogsBloom: mustParseUint(jr.Data.BytesPerLogsBloom),
+ MaxExtraDataBytes: mustParseUint(jr.Data.MaxExtraDataBytes),
+ MaxWithdrawalsPerPayload: mustParseUint(jr.Data.MaxWithdrawalsPerPayload),
+ MaxValidatorsPerWithdrawalSweep: mustParseUint(jr.Data.MaxValidatorsPerWithdrawalsSweep),
+ MaxBlsToExecutionChange: mustParseUint(jr.Data.MaxBlsToExecutionChanges),
+ }
+
+ cfg.Chain.ClConfig = chainCfg
+
+ type GenesisResponse struct {
+ Data struct {
+ GenesisTime string `json:"genesis_time"`
+ GenesisValidatorsRoot string `json:"genesis_validators_root"`
+ GenesisForkVersion string `json:"genesis_fork_version"`
+ } `json:"data"`
+ }
+
+ gtr := &GenesisResponse{}
+
+ err = requests.
+ URL(nodeEndpoint + "/eth/v1/beacon/genesis").
+ ToJSON(gtr).
+ Fetch(context.Background())
+
+ if err != nil {
+ return err
+ }
+
+ cfg.Chain.GenesisTimestamp = mustParseUint(gtr.Data.GenesisTime)
+ cfg.Chain.GenesisValidatorsRoot = gtr.Data.GenesisValidatorsRoot
+
+ logger.Infof("loaded chain config from node with genesis time %s", gtr.Data.GenesisTime)
+
+ } else {
+ f, err := os.Open(cfg.Chain.ClConfigPath)
+ if err != nil {
+ return fmt.Errorf("error opening Chain Config file %v: %w", cfg.Chain.ClConfigPath, err)
+ }
+ var chainConfig *types.ClChainConfig
+ decoder := yaml.NewDecoder(f)
+ err = decoder.Decode(&chainConfig)
+ if err != nil {
+ return fmt.Errorf("error decoding Chain Config file %v: %v", cfg.Chain.ClConfigPath, err)
+ }
+ cfg.Chain.ClConfig = *chainConfig
}
+ cfg.Chain.Name = cfg.Chain.ClConfig.ConfigName
if cfg.Chain.GenesisTimestamp == 0 {
switch cfg.Chain.Name {
@@ -507,10 +663,12 @@ func ReadConfig(cfg *types.Config, path string) error {
cfg.Chain.GenesisTimestamp = 1616508000
case "sepolia":
cfg.Chain.GenesisTimestamp = 1655733600
+ case "zhejiang":
+ cfg.Chain.GenesisTimestamp = 1675263600
case "gnosis":
cfg.Chain.GenesisTimestamp = 1638993340
- case "dencun-devnet-8":
- cfg.Chain.GenesisTimestamp = 1692182400
+ case "holesky":
+ cfg.Chain.GenesisTimestamp = 1694786400
default:
return fmt.Errorf("tried to set known genesis-timestamp, but unknown chain-name")
}
@@ -524,10 +682,12 @@ func ReadConfig(cfg *types.Config, path string) error {
cfg.Chain.GenesisValidatorsRoot = "0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb"
case "sepolia":
cfg.Chain.GenesisValidatorsRoot = "0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078"
+ case "zhejiang":
+ cfg.Chain.GenesisValidatorsRoot = "0x53a92d8f2bb1d85f62d16a156e6ebcd1bcaba652d0900b2c2f387826f3481f6f"
case "gnosis":
cfg.Chain.GenesisValidatorsRoot = "0xf5dcb5564e829aab27264b9becd5dfaa017085611224cb3036f573368dbb9d47"
- case "dencun-devnet-8":
- cfg.Chain.GenesisValidatorsRoot = "0x6079c0b803059b77610f0198ce4b4c459cf43afd4992359084894f719d40faba"
+ case "holesky":
+ cfg.Chain.GenesisValidatorsRoot = "0x9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1"
default:
return fmt.Errorf("tried to set known genesis-validators-root, but unknown chain-name")
}
@@ -540,20 +700,6 @@ func ReadConfig(cfg *types.Config, path string) error {
cfg.Chain.DomainVoluntaryExit = "0x04000000"
}
- if cfg.Chain.ClConfig.DepositChainID != cfg.Chain.ElConfig.ChainID.Uint64() {
- logrus.Fatalf("cfg.Chain.ClConfig.DepositChainID != cfg.Chain.ElConfig.ChainID.Uint64(): %v != %v", cfg.Chain.ClConfig.DepositChainID, cfg.Chain.ElConfig.ChainID.Uint64())
- }
-
- // #TODO.patrick
- if cfg.Chain.ClConfig.ConfigName == "dencun-devnet-8" {
- if cfg.Chain.ClConfig.MinSlashingPenaltyQuotientAltair != 64 {
- logrus.Fatal("cfg.Chain.ClConfig.MinSlashingPenaltyQuotientAltair != 64: %v", cfg.Chain.ClConfig.MinSlashingPenaltyQuotientAltair)
- }
- if cfg.Chain.ClConfig.GenesisForkVersion != "0x10109396" {
- logrus.Fatal("cfg.Chain.ClConfig.GenesisForkVersion != 0x10109396: %v", cfg.Chain.ClConfig.GenesisForkVersion)
- }
- }
-
logrus.WithFields(logrus.Fields{
"genesisTimestamp": cfg.Chain.GenesisTimestamp,
"genesisValidatorsRoot": cfg.Chain.GenesisValidatorsRoot,
@@ -566,6 +712,20 @@ func ReadConfig(cfg *types.Config, path string) error {
return nil
}
+func mustParseUint(str string) uint64 {
+
+ if str == "" {
+ return 0
+ }
+
+ nbr, err := strconv.ParseUint(str, 10, 64)
+ if err != nil {
+ logrus.Fatalf("fatal error parsing uint %s: %v", str, err)
+ }
+
+ return nbr
+}
+
func readConfigFile(cfg *types.Config, path string) error {
if path == "" {
return yaml.Unmarshal([]byte(config.DefaultConfigYml), cfg)
@@ -625,6 +785,7 @@ var withdrawalCredentialsRE = regexp.MustCompile("^(0x)?00[0-9a-fA-F]{62}$")
var withdrawalCredentialsAddressRE = regexp.MustCompile("^(0x)?010000000000000000000000[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}$")
// IsValidEth1Address verifies whether a string represents a valid eth1-address.
func IsValidEth1Address(s string) bool {
@@ -641,6 +802,11 @@ func IsValidEth1Tx(s string) bool {
return !zeroHashRE.MatchString(s) && eth1TxRE.MatchString(s)
}
+// IsValidEth1Tx verifies whether a string represents a valid eth1-tx-hash.
+func IsHash(s string) bool {
+ return hashRE.MatchString(s)
+}
+
// IsValidWithdrawalCredentials verifies whether a string represents valid withdrawal credentials.
func IsValidWithdrawalCredentials(s string) bool {
return withdrawalCredentialsRE.MatchString(s) || withdrawalCredentialsAddressRE.MatchString(s)
@@ -868,7 +1034,7 @@ func ValidateReCAPTCHA(recaptchaResponse string) (bool, error) {
return false, err
}
defer req.Body.Close()
- body, err := ioutil.ReadAll(req.Body) // Read the response from Google
+ body, err := io.ReadAll(req.Body) // Read the response from Google
if err != nil {
return false, err
}
@@ -929,7 +1095,7 @@ func TryFetchContractMetadata(address []byte) (*types.ContractMetadata, error) {
// }
// if resp.StatusCode == 200 {
-// body, err := ioutil.ReadAll(resp.Body)
+// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return nil, err
// }
@@ -1004,7 +1170,7 @@ func getABIFromEtherscan(address []byte) (*types.ContractMetadata, error) {
return nil, fmt.Errorf("StatusCode: '%d', Status: '%s'", resp.StatusCode, resp.Status)
}
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
@@ -1345,7 +1511,7 @@ func GetRemainingScheduledSync(validatorCount int, stats types.SyncCommitteesSta
// - `validators` : the validators to add the stats for
// - `syncDutiesHistory` : the sync duties history of all queried validators
// - `stats` : the stats object to add the stats to, if nil a new stats object is created
-func AddSyncStats(validators []uint64, syncDutiesHistory map[uint64][]*types.ValidatorSyncParticipation, stats *types.SyncCommitteesStats) types.SyncCommitteesStats {
+func AddSyncStats(validators []uint64, syncDutiesHistory map[uint64]map[uint64]*types.ValidatorSyncParticipation, stats *types.SyncCommitteesStats) types.SyncCommitteesStats {
if stats == nil {
stats = &types.SyncCommitteesStats{}
}
@@ -1470,7 +1636,7 @@ type HttpReqHttpError struct {
}
func (err *HttpReqHttpError) Error() string {
- return fmt.Sprintf("urls: %s, status: %d, body: %s", err.Url, err.StatusCode, err.Body)
+ return fmt.Sprintf("error response: url: %s, status: %d, body: %s", err.Url, err.StatusCode, err.Body)
}
func HttpReq(ctx context.Context, method, url string, params, result interface{}) error {
@@ -1479,7 +1645,7 @@ func HttpReq(ctx context.Context, method, url string, params, result interface{}
if params != nil {
paramsJSON, err := json.Marshal(params)
if err != nil {
- return fmt.Errorf("error marhsaling params for request: %w, url: %v", err, url)
+ return fmt.Errorf("error marshaling params for request: %w, url: %v", err, url)
}
req, err = http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(paramsJSON))
if err != nil {
@@ -1514,3 +1680,16 @@ func HttpReq(ctx context.Context, method, url string, params, result interface{}
}
return nil
}
+
+func ReverseString(s string) string {
+ runes := []rune(s)
+ for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
+ runes[i], runes[j] = runes[j], runes[i]
+ }
+ return string(runes)
+}
+
+func GetCurrentFuncName() string {
+ pc, _, _, _ := runtime.Caller(1)
+ return runtime.FuncForPC(pc).Name()
+}