Add static analyzer to discourage use of panic() (#15075)

* Implement static analysis to prevent panics

* Add nopanic to nogo

* Fix violations and add exclusions

Fix violations and add exclusions for all

* Changelog fragment

* Use pass.Report instead of pass.Reportf

* Remove strings.ToLower for checking init method name

* Add exclusion for herumi init

* Move api/client/beacon template function to init and its own file

* Fix nopanic testcase
This commit is contained in:
Preston Van Loon
2025-03-19 13:04:15 -05:00
committed by GitHub
parent 16d5abd21b
commit 2aa52fb56a
74 changed files with 329 additions and 104 deletions

View File

@@ -197,6 +197,7 @@ nogo(
"//tools/analyzers/logruswitherror:go_default_library",
"//tools/analyzers/maligned:go_default_library",
"//tools/analyzers/nop:go_default_library",
"//tools/analyzers/nopanic:go_default_library",
"//tools/analyzers/properpermissions:go_default_library",
"//tools/analyzers/recursivelock:go_default_library",
"//tools/analyzers/shadowpredecl:go_default_library",

View File

@@ -7,6 +7,7 @@ go_library(
"doc.go",
"health.go",
"log.go",
"template.go",
],
importpath = "github.com/prysmaticlabs/prysm/v5/api/client/beacon",
visibility = ["//visibility:public"],

View File

@@ -11,7 +11,6 @@ import (
"regexp"
"sort"
"strconv"
"text/template"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
@@ -64,23 +63,6 @@ func IdFromSlot(s primitives.Slot) StateOrBlockId {
return StateOrBlockId(strconv.FormatUint(uint64(s), 10))
}
// idTemplate is used to create template functions that can interpolate StateOrBlockId values.
func idTemplate(ts string) func(StateOrBlockId) string {
t := template.Must(template.New("").Parse(ts))
f := func(id StateOrBlockId) string {
b := bytes.NewBuffer(nil)
err := t.Execute(b, struct{ Id string }{Id: string(id)})
if err != nil {
panic(fmt.Sprintf("invalid idTemplate: %s", ts))
}
return b.String()
}
// run the template to ensure that it is valid
// this should happen load time (using package scoped vars) to ensure runtime errors aren't possible
_ = f(IdGenesis)
return f
}
// RenderGetBlockPath formats a block id into a path for the GetBlock API endpoint.
func RenderGetBlockPath(id StateOrBlockId) string {
return path.Join(getSignedBlockPath, string(id))
@@ -114,8 +96,6 @@ func (c *Client) GetBlock(ctx context.Context, blockId StateOrBlockId) ([]byte,
return b, nil
}
var getBlockRootTpl = idTemplate(getBlockRootPath)
// GetBlockRoot retrieves the hash_tree_root of the BeaconBlock for the given block id.
// Block identifier can be one of: "head" (canonical head in node's view), "genesis", "finalized",
// <slot>, <hex encoded blockRoot with 0x prefix>. Variables of type StateOrBlockId are exported by this package
@@ -138,8 +118,6 @@ func (c *Client) GetBlockRoot(ctx context.Context, blockId StateOrBlockId) ([32]
return bytesutil.ToBytes32(rs), nil
}
var getForkTpl = idTemplate(getForkForStatePath)
// GetFork queries the Beacon Node API for the Fork from the state identified by stateId.
// Block identifier can be one of: "head" (canonical head in node's view), "genesis", "finalized",
// <slot>, <hex encoded blockRoot with 0x prefix>. Variables of type StateOrBlockId are exported by this package

View File

@@ -0,0 +1,34 @@
package beacon
import (
"bytes"
"fmt"
"text/template"
)
type templateFn func(StateOrBlockId) string
var getBlockRootTpl templateFn
var getForkTpl templateFn
func init() {
// idTemplate is used to create template functions that can interpolate StateOrBlockId values.
idTemplate := func(ts string) func(StateOrBlockId) string {
t := template.Must(template.New("").Parse(ts))
f := func(id StateOrBlockId) string {
b := bytes.NewBuffer(nil)
err := t.Execute(b, struct{ Id string }{Id: string(id)})
if err != nil {
panic(fmt.Sprintf("invalid idTemplate: %s", ts))
}
return b.String()
}
// run the template to ensure that it is valid
// this should happen load time (using package scoped vars) to ensure runtime errors aren't possible
_ = f(IdGenesis)
return f
}
getBlockRootTpl = idTemplate(getBlockRootPath)
getForkTpl = idTemplate(getForkForStatePath)
}

View File

@@ -154,7 +154,7 @@ retry:
continue retry
}
if sub == nil {
panic("event: ResubscribeFunc returned nil subscription and no error")
panic("event: ResubscribeFunc returned nil subscription and no error") // lint:nopanic -- This should never happen.
}
return sub
case <-s.unsub:

View File

@@ -35,7 +35,7 @@ func (s *Store) LastArchivedRoot(ctx context.Context) [32]byte {
_, blockRoot = bkt.Cursor().Last()
return nil
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
panic(err)
panic(err) // lint:nopanic -- View never returns an error.
}
return bytesutil.ToBytes32(blockRoot)
@@ -53,7 +53,7 @@ func (s *Store) ArchivedPointRoot(ctx context.Context, slot primitives.Slot) [32
blockRoot = bucket.Get(bytesutil.SlotToBytesBigEndian(slot))
return nil
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
panic(err)
panic(err) // lint:nopanic -- View never returns an error.
}
return bytesutil.ToBytes32(blockRoot)
@@ -69,7 +69,7 @@ func (s *Store) HasArchivedPoint(ctx context.Context, slot primitives.Slot) bool
exists = iBucket.Get(bytesutil.SlotToBytesBigEndian(slot)) != nil
return nil
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
panic(err)
panic(err) // lint:nopanic -- View never returns an error.
}
return exists
}

View File

@@ -185,7 +185,7 @@ func (s *Store) HasBlock(ctx context.Context, blockRoot [32]byte) bool {
exists = bkt.Get(blockRoot[:]) != nil
return nil
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
panic(err)
panic(err) // lint:nopanic -- View never returns an error.
}
return exists
}

View File

@@ -20,7 +20,7 @@ func (s *Store) DepositContractAddress(ctx context.Context) ([]byte, error) {
addr = chainInfo.Get(depositContractAddressKey)
return nil
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
panic(err)
panic(err) // lint:nopanic -- View never returns an error.
}
return addr, nil
}

View File

@@ -407,7 +407,7 @@ func (s *Store) HasState(ctx context.Context, blockRoot [32]byte) bool {
return nil
})
if err != nil {
panic(err)
panic(err) // lint:nopanic -- View never returns an error.
}
return hasState
}

View File

@@ -158,7 +158,7 @@ func trim(queue *cache.FIFO, maxSize uint64) {
for s := uint64(len(queue.ListKeys())); s > maxSize; s-- {
// #nosec G104 popProcessNoopFunc never returns an error
if _, err := queue.Pop(popProcessNoopFunc); err != nil { // This never returns an error, but we'll handle anyway for sanity.
panic(err)
panic(err) // lint:nopanic -- popProcessNoopFunc never returns an error.
}
}
}

View File

@@ -57,7 +57,7 @@ func (*FaultyExecutionChain) ChainStartEth1Data() *ethpb.Eth1Data {
func (*FaultyExecutionChain) PreGenesisState() state.BeaconState {
s, err := state_native.InitializeFromProtoUnsafePhase0(&ethpb.BeaconState{})
if err != nil {
panic("could not initialize state")
panic("could not initialize state") // lint:nopanic -- test code.
}
return s
}

View File

@@ -440,7 +440,7 @@ func (b *BeaconNode) Start() {
log.WithField("times", i-1).Info("Already shutting down, interrupt more to panic")
}
}
panic("Panic closing the beacon node")
panic("Panic closing the beacon node") // lint:nopanic -- Panic is requested by user.
}()
// Wait for stop channel to be closed.
@@ -706,7 +706,7 @@ func (b *BeaconNode) registerP2P(cliCtx *cli.Context) error {
func (b *BeaconNode) fetchP2P() p2p.P2P {
var p *p2p.Service
if err := b.services.FetchService(&p); err != nil {
panic(err)
panic(err) // lint:nopanic -- This could panic application start if the services are misconfigured.
}
return p
}
@@ -714,7 +714,7 @@ func (b *BeaconNode) fetchP2P() p2p.P2P {
func (b *BeaconNode) fetchBuilderService() *builder.Service {
var s *builder.Service
if err := b.services.FetchService(&s); err != nil {
panic(err)
panic(err) // lint:nopanic -- This could panic application start if the services are misconfigured.
}
return s
}
@@ -1018,13 +1018,13 @@ func (b *BeaconNode) registerPrometheusService(_ *cli.Context) error {
var additionalHandlers []prometheus.Handler
var p *p2p.Service
if err := b.services.FetchService(&p); err != nil {
panic(err)
panic(err) // lint:nopanic -- This could panic application start if the services are misconfigured.
}
additionalHandlers = append(additionalHandlers, prometheus.Handler{Path: "/p2p", Handler: p.InfoHandler})
var c *blockchain.Service
if err := b.services.FetchService(&c); err != nil {
panic(err)
panic(err) // lint:nopanic -- This could panic application start if the services are misconfigured.
}
service := prometheus.NewService(

View File

@@ -1,3 +1,4 @@
// lint:nopanic -- Mock / test code, panic is allowed.
package mock
import (

View File

@@ -28,10 +28,10 @@ func (m *PoolMock) InsertBLSToExecChange(change *eth.SignedBLSToExecutionChange)
// MarkIncluded --
func (*PoolMock) MarkIncluded(_ *eth.SignedBLSToExecutionChange) {
panic("implement me")
panic("implement me") // lint:nopanic -- mock / test code.
}
// ValidatorExists --
func (*PoolMock) ValidatorExists(_ primitives.ValidatorIndex) bool {
panic("implement me")
panic("implement me") // lint:nopanic -- mock / test code.
}

View File

@@ -40,10 +40,10 @@ func (*PoolMock) ConvertToElectra() {}
// MarkIncludedAttesterSlashing --
func (*PoolMock) MarkIncludedAttesterSlashing(_ ethpb.AttSlashing) {
panic("implement me")
panic("implement me") // lint:nopanic -- Test / mock code.
}
// MarkIncludedProposerSlashing --
func (*PoolMock) MarkIncludedProposerSlashing(_ *ethpb.ProposerSlashing) {
panic("implement me")
panic("implement me") // lint:nopanic -- Test / mock code.
}

View File

@@ -28,5 +28,5 @@ func (m *PoolMock) InsertVoluntaryExit(exit *eth.SignedVoluntaryExit) {
// MarkIncluded --
func (*PoolMock) MarkIncluded(_ *eth.SignedVoluntaryExit) {
panic("implement me")
panic("implement me") // lint:nopanic -- Mock / test code.
}

View File

@@ -37,5 +37,5 @@ func (m *MockBlocker) Block(_ context.Context, b []byte) (interfaces.ReadOnlySig
// Blobs --
func (m *MockBlocker) Blobs(_ context.Context, _ string, _ []uint64) ([]*blocks.VerifiedROBlob, *core.RpcError) {
panic("implement me")
panic("implement me") // lint:nopanic -- Test code.
}

View File

@@ -178,7 +178,7 @@ func (e *epochBoundaryState) delete(blockRoot [32]byte) error {
func trim(queue *cache.FIFO, maxSize uint64) {
for s := uint64(len(queue.ListKeys())); s > maxSize; s-- {
if _, err := queue.Pop(popProcessNoopFunc); err != nil { // This never returns an error, but we'll handle anyway for sanity.
panic(err)
panic(err) // lint:nopanic -- Never returns an error.
}
}
}

View File

@@ -1,3 +1,4 @@
// lint:nopanic -- Mock code, OK to panic.
package mock
import (

View File

@@ -192,7 +192,7 @@ func (s *Service) Start() {
if errors.Is(s.ctx.Err(), context.Canceled) {
return
}
panic(err)
panic(err) // lint:nopanic -- Unexpected error. This should probably be surfaced with a returned error.
}
log.WithField("slot", s.cfg.Chain.HeadSlot()).Info("Synced up to")
s.markSynced()

View File

@@ -181,12 +181,12 @@ func (s *Service) subscribe(topic string, validator wrappedVal, handle subHandle
_, e, err := forks.RetrieveForkDataFromDigest(digest, genRoot[:])
if err != nil {
// Impossible condition as it would mean digest does not exist.
panic(err)
panic(err) // lint:nopanic -- Impossible condition.
}
base := p2p.GossipTopicMappings(topic, e)
if base == nil {
// Impossible condition as it would mean topic does not exist.
panic(fmt.Sprintf("%s is not mapped to any message in GossipTopicMappings", topic))
panic(fmt.Sprintf("%s is not mapped to any message in GossipTopicMappings", topic)) // lint:nopanic -- Impossible condition.
}
return s.subscribeWithBase(s.addDigestToTopic(topic, digest), validator, handle)
}
@@ -497,13 +497,13 @@ func (s *Service) subscribeWithParameters(
// Retrieve the epoch of the fork corresponding to the digest.
_, epoch, err := forks.RetrieveForkDataFromDigest(digest, genesisValidatorsRoot[:])
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Impossible condition.
}
// Retrieve the base protobuf message.
base := p2p.GossipTopicMappings(topicFormat, epoch)
if base == nil {
panic(fmt.Sprintf("%s is not mapped to any message in GossipTopicMappings", topicFormat))
panic(fmt.Sprintf("%s is not mapped to any message in GossipTopicMappings", topicFormat)) // lint:nopanic -- Impossible condition.
}
// Retrieve the genesis time.

View File

@@ -60,6 +60,7 @@ func NewTmpDir(prefix string) (string, error) {
// `@go_sdk//:files` in its `data` -- this will make sure the whole toolchain
// gets pulled into the sandbox as well. Generally, this function should only
// be called in init().
// lint:nopanic -- This method is only used for testing.
func SetGoEnv() {
gobin, err := Runfile("bin/go")
if err != nil {

View File

@@ -10,7 +10,7 @@ import (
func New(size int) *lru.Cache {
cache, err := lru.New(size)
if err != nil {
panic(fmt.Errorf("lru new failed: %w", err))
panic(fmt.Errorf("lru new failed: %w", err)) // lint:nopanic -- This should never panic.
}
return cache
}
@@ -20,7 +20,7 @@ func New(size int) *lru.Cache {
func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) *lru.Cache {
cache, err := lru.NewWithEvict(size, onEvicted)
if err != nil {
panic(fmt.Errorf("lru new with evict failed: %w", err))
panic(fmt.Errorf("lru new with evict failed: %w", err)) // lint:nopanic -- This should never panic.
}
return cache
}

3
changelog/pvl_nopanic.md Normal file
View File

@@ -0,0 +1,3 @@
### Added
- Added a static analyzer to discourage use of panic() in Prysm

View File

@@ -180,7 +180,7 @@ func before(ctx *cli.Context) error {
f := joonix.NewFormatter()
if err := joonix.DisableTimestampFormat(f); err != nil {
panic(err)
panic(err) // lint:nopanic -- This shouldn't happen, but crashing immediately at startup is OK.
}
logrus.SetFormatter(f)
@@ -250,7 +250,7 @@ func main() {
defer func() {
if x := recover(); x != nil {
log.Errorf("Runtime panic: %v\n%v", x, string(runtimeDebug.Stack()))
panic(x)
panic(x) // lint:nopanic -- This is just resurfacing the original panic.
}
}()

View File

@@ -69,7 +69,7 @@ func main() {
case "fluentd":
f := joonix.NewFormatter()
if err := joonix.DisableTimestampFormat(f); err != nil {
panic(err)
panic(err) // lint:nopanic -- This shouldn't happen, but crashing immediately at startup is OK.
}
logrus.SetFormatter(f)
case "json":
@@ -94,7 +94,7 @@ func main() {
defer func() {
if x := recover(); x != nil {
log.Errorf("Runtime panic: %v\n%v", x, string(runtimeDebug.Stack()))
panic(x)
panic(x) // lint:nopanic -- This is just resurfacing the original panic.
}
}()

View File

@@ -90,7 +90,7 @@ func newClient(beaconEndpoints []string, tcpPort, quicPort uint) (*client, error
func (c *client) Close() {
if err := c.host.Close(); err != nil {
panic(err)
panic(err) // lint:nopanic -- The client is closing anyway...
}
}
@@ -193,7 +193,7 @@ func (c *client) initializeMockChainService(ctx context.Context) (*mockChain, er
func ipAddr() net.IP {
ip, err := network.ExternalIP()
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Only returns an error when network interfaces are not available. This is a requirement for the application anyway.
}
return net.ParseIP(ip)
}

View File

@@ -14,7 +14,7 @@ func (c *client) connectToPeers(ctx context.Context, peerMultiaddrs ...string) e
}
addrInfos, err := peer.AddrInfosFromP2pAddrs(peers...)
if err != nil {
panic(err)
return err
}
for _, info := range addrInfos {
if info.ID == c.host.ID() {

View File

@@ -163,7 +163,7 @@ func main() {
case "fluentd":
f := joonix.NewFormatter()
if err := joonix.DisableTimestampFormat(f); err != nil {
panic(err)
panic(err) // lint:nopanic -- This shouldn't happen, but crashing immediately at startup is OK.
}
logrus.SetFormatter(f)
case "json":
@@ -208,7 +208,7 @@ func main() {
defer func() {
if x := recover(); x != nil {
log.Errorf("Runtime panic: %v\n%v", x, string(runtimeDebug.Stack()))
panic(x)
panic(x) // lint:nopanic -- This is just resurfacing the original panic.
}
}()

View File

@@ -34,11 +34,11 @@ func WrapFlags(flags []cli.Flag) []cli.Flag {
f = altsrc.NewPathFlag(t)
case *cli.Int64Flag:
// Int64Flag does not work. See https://github.com/prysmaticlabs/prysm/issues/6478
panic(fmt.Sprintf("unsupported flag type %T", f))
panic(fmt.Sprintf("unsupported flag type %T", f)) // lint:nopanic -- This is evaluated at application start.
case *cli.IntSliceFlag:
f = altsrc.NewIntSliceFlag(t)
default:
panic(fmt.Sprintf("cannot convert type %T", f))
panic(fmt.Sprintf("cannot convert type %T", f)) // lint:nopanic -- This is evaluated at application start.
}
wrapped = append(wrapped, f)
}

View File

@@ -33,7 +33,7 @@ func (b *BeaconChainConfig) Copy() *BeaconChainConfig {
defer cfgrw.RUnlock()
config, ok := deepcopy.Copy(*b).(BeaconChainConfig)
if !ok {
panic("somehow deepcopy produced a BeaconChainConfig that is not of the same type as the original")
panic("somehow deepcopy produced a BeaconChainConfig that is not of the same type as the original") // lint:nopanic -- Impossible scenario.
}
return &config
}

View File

@@ -23,7 +23,7 @@ func OverrideBeaconConfig(c *BeaconChainConfig) {
func (b *BeaconChainConfig) Copy() *BeaconChainConfig {
config, ok := deepcopy.Copy(*b).(BeaconChainConfig)
if !ok {
panic("somehow deepcopy produced a BeaconChainConfig that is not of the same type as the original")
panic("somehow deepcopy produced a BeaconChainConfig that is not of the same type as the original") // lint:nopanic -- This would only panic with an application misconfiguration and it should fail right away.
}
return &config
}

View File

@@ -52,7 +52,7 @@ func newConfigset(configs ...*BeaconChainConfig) *configset {
}
for _, c := range configs {
if err := r.add(c); err != nil {
panic(err)
panic(err) // lint:nopanic -- This would only panic with an application misconfiguration and it should fail right away.
}
}
return r

View File

@@ -490,6 +490,7 @@ func (b *SignedBeaconBlock) MarshalSSZTo(dst []byte) ([]byte, error) {
// of fastssz's SizeSSZ() interface function to avoid panicking.
// Changing the signature causes very problematic issues with wealdtech deps.
// For the time being panicking is preferable.
// lint:nopanic -- Panic warning is communicated in godoc commentary.
func (b *SignedBeaconBlock) SizeSSZ() int {
pb, err := b.Proto()
if err != nil {
@@ -530,7 +531,7 @@ func (b *SignedBeaconBlock) SizeSSZ() int {
}
}
// UnmarshalSSZ unmarshals the signed beacon block from its relevant ssz form.
// UnmarshalSSZ unmarshals the sitime/slots/slottime.gogned beacon block from its relevant ssz form.
// nolint:gocognit
func (b *SignedBeaconBlock) UnmarshalSSZ(buf []byte) error {
var newBlock *SignedBeaconBlock
@@ -884,6 +885,7 @@ func (b *BeaconBlock) MarshalSSZTo(dst []byte) ([]byte, error) {
// of fastssz's SizeSSZ() interface function to avoid panicking.
// Changing the signature causes very problematic issues with wealdtech deps.
// For the time being panicking is preferable.
// lint:nopanic -- Panic is communicated in godoc.
func (b *BeaconBlock) SizeSSZ() int {
pb, err := b.Proto()
if err != nil {

View File

@@ -1,3 +1,5 @@
// package mock
// lint:nopanic -- This is test / mock code, allowed to panic.
package mock
import (

View File

@@ -27,7 +27,7 @@ func MaxEpoch(a, b Epoch) Epoch {
func (e Epoch) Mul(x uint64) Epoch {
res, err := e.SafeMul(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -44,7 +44,7 @@ func (e Epoch) SafeMul(x uint64) (Epoch, error) {
func (e Epoch) Div(x uint64) Epoch {
res, err := e.SafeDiv(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -61,7 +61,7 @@ func (e Epoch) SafeDiv(x uint64) (Epoch, error) {
func (e Epoch) Add(x uint64) Epoch {
res, err := e.SafeAdd(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -90,7 +90,7 @@ func (e Epoch) SafeAddEpoch(x Epoch) (Epoch, error) {
func (e Epoch) Sub(x uint64) Epoch {
res, err := e.SafeSub(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -107,7 +107,7 @@ func (e Epoch) SafeSub(x uint64) (Epoch, error) {
func (e Epoch) Mod(x uint64) Epoch {
res, err := e.SafeMod(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}

View File

@@ -19,7 +19,7 @@ type Slot uint64
func (s Slot) Mul(x uint64) Slot {
res, err := s.SafeMul(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -48,7 +48,7 @@ func (s Slot) SafeMulSlot(x Slot) (Slot, error) {
func (s Slot) Div(x uint64) Slot {
res, err := s.SafeDiv(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -77,7 +77,7 @@ func (s Slot) SafeDivSlot(x Slot) (Slot, error) {
func (s Slot) Add(x uint64) Slot {
res, err := s.SafeAdd(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -106,7 +106,7 @@ func (s Slot) SafeAddSlot(x Slot) (Slot, error) {
func (s Slot) Sub(x uint64) Slot {
res, err := s.SafeSub(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}
@@ -143,7 +143,7 @@ func (s Slot) SafeSubSlot(x Slot) (Slot, error) {
func (s Slot) Mod(x uint64) Slot {
res, err := s.SafeMod(x)
if err != nil {
panic(err.Error())
panic(err.Error()) // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return res
}

View File

@@ -14,9 +14,10 @@ var _ fssz.Unmarshaler = (*ValidatorIndex)(nil)
type ValidatorIndex uint64
// Div divides validator index by x.
// This method panics if dividing by zero!
func (v ValidatorIndex) Div(x uint64) ValidatorIndex {
if x == 0 {
panic("divbyzero")
panic("divbyzero") // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return ValidatorIndex(uint64(v) / x)
}
@@ -27,9 +28,10 @@ func (v ValidatorIndex) Add(x uint64) ValidatorIndex {
}
// Sub subtracts x from the validator index.
// This method panics if causing an underflow!
func (v ValidatorIndex) Sub(x uint64) ValidatorIndex {
if uint64(v) < x {
panic("underflow")
panic("underflow") // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return ValidatorIndex(uint64(v) - x)
}

View File

@@ -27,11 +27,12 @@ func (pq priorityQueue) Swap(i, j int) {
pq[j].index = j
}
// Push a LeakyBucket to priorityQueue
func (pq *priorityQueue) Push(x interface{}) {
n := len(*pq)
b, ok := x.(*LeakyBucket)
if !ok {
panic(fmt.Sprintf("%T", x))
panic(fmt.Sprintf("%T", x)) // lint:nopanic -- This method should be improved. High risk for misuse!
}
b.index = n
*pq = append(*pq, b)

View File

@@ -3,6 +3,7 @@ package herumi
import "github.com/herumi/bls-eth-go-binary/bls"
// Init allows the required curve orders and appropriate sub-groups to be initialized.
// lint:nopanic -- This method is called at init time only.
func Init() {
if err := bls.Init(bls.BLS12_381); err != nil {
panic(err)

View File

@@ -13,7 +13,7 @@ func hashParallel(inputList [][32]byte, outputList [][32]byte, wg *sync.WaitGrou
defer wg.Done()
err := gohashtree.Hash(outputList, inputList)
if err != nil {
panic(err)
panic(err) // lint:nopanic -- This should never panic.
}
}
@@ -27,7 +27,7 @@ func VectorizedSha256(inputList [][32]byte) [][32]byte {
if len(inputList) < minSliceSizeToParallelize {
err := gohashtree.Hash(outputList, inputList)
if err != nil {
panic(err)
panic(err) // lint:nopanic -- This should never panic.
}
return outputList
}
@@ -40,7 +40,7 @@ func VectorizedSha256(inputList [][32]byte) [][32]byte {
}
err := gohashtree.Hash(outputList[n*groupSize:], inputList[n*2*groupSize:])
if err != nil {
panic(err)
panic(err) // lint:nopanic -- This should never panic.
}
wg.Wait()
return outputList

View File

@@ -130,7 +130,7 @@ func EncryptKey(key *Key, password string, scryptN, scryptP int) ([]byte, error)
authArray := []byte(password)
salt := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
panic("reading from crypto/rand failed: " + err.Error())
panic("reading from crypto/rand failed: " + err.Error()) // lint:nopanic -- This should never happen.
}
derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen)

View File

@@ -57,7 +57,7 @@ func (_ *source) Uint64() (val uint64) {
lock.RLock()
defer lock.RUnlock()
if err := binary.Read(rand.Reader, binary.BigEndian, &val); err != nil {
panic(err)
panic(err) // lint:nopanic -- Panic risk is communicated in the godoc commentary.
}
return
}

View File

@@ -35,7 +35,7 @@ func (f fieldType) Size() int {
case typeBytes4:
return 4
default:
panic("can't determine size for unrecognizedtype ")
panic("can't determine size for unrecognizedtype ") // lint:nopanic -- Impossible field type.
}
}

View File

@@ -68,9 +68,10 @@ func Depth(v uint64) (out uint8) {
}
// Merkleize with log(N) space allocation
// This method will panic when count > limit.
func Merkleize(hasher Hasher, count, limit uint64, leaf func(i uint64) []byte) (out [32]byte) {
if count > limit {
panic("merkleizing list that is too large, over limit")
panic("merkleizing list that is too large, over limit") // lint:nopanic -- Panic is communicated in godoc commentary.
}
if limit == 0 {
return

View File

@@ -109,9 +109,10 @@ func IsPowerOf2(n uint64) bool {
// PowerOf2 returns an integer that is the provided
// exponent of 2. Can only return powers of 2 till 63,
// after that it overflows
// This method will panic if `n` is greater than 63.
func PowerOf2(n uint64) uint64 {
if n >= 64 {
panic("integer overflow")
panic("integer overflow") // lint:nopanic -- Panic is communicated in the godoc commentary.
}
return 1 << n
}

View File

@@ -10,7 +10,7 @@ import (
func IPAddr() net.IP {
ip, err := ExternalIP()
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Only panics if a network interface is not available. This is a requirement to run the application anyway.
}
return net.ParseIP(ip)
}

View File

@@ -214,5 +214,13 @@
"external/com_github_ethereum_go_ethereum/.*": "Unsafe third party code",
"rules_go_work-.*": "Third party code"
}
},
"nopanic": {
"exclude_files": {
"validator/web/site_data.go": "generated code",
".*/.*_test\\.go": "Tests are OK",
".*/main\\.go": "main methods are OK",
"external/.*": "Third party code"
}
}
}

View File

@@ -212,7 +212,7 @@ func defaultDepositContractAllocation(contractAddress string) depositAllocation
}
codeBytes, err := hexutil.Decode(DepositContractCode)
if err != nil {
panic(err)
panic(err) // lint:nopanic -- The deposit contract code is hardcoded and checked in tests.
}
return depositAllocation{
Address: common.HexToAddress(contractAddress),

View File

@@ -104,7 +104,7 @@ func SetBenchmarkConfig() (func(), error) {
undo, err := params.SetActiveWithUndo(c)
return func() {
if err := undo(); err != nil {
panic(err)
panic(err) // lint:nopanic -- Test code / impossible scenario.
}
}, err
}

View File

@@ -321,7 +321,7 @@ func RandomBlobTx(rpc *rpc.Client, f *filler.Filler, sender common.Address, nonc
func New4844Tx(nonce uint64, to *common.Address, gasLimit uint64, chainID, tip, feeCap, value *big.Int, code []byte, blobFeeCap *big.Int, blobData []byte, al types.AccessList) *types.Transaction {
blobs, comms, proofs, versionedHashes, err := EncodeBlobs(blobData)
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Test code.
}
tx := types.NewTx(&types.BlobTx{
ChainID: uint256.MustFromBig(chainID),
@@ -427,7 +427,7 @@ func randomAddress() common.Address {
b := make([]byte, 20)
_, err := mathRand.Read(b) // #nosec G404
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Test code.
}
return common.BytesToAddress(b)
case 3:

View File

@@ -1,3 +1,4 @@
// lint:nopanic -- Test tooling / code.
package components
import (

View File

@@ -11,7 +11,7 @@ import (
func InitForkCfg(start, end int, c *params.BeaconChainConfig) *params.BeaconChainConfig {
c = c.Copy()
if end < start {
panic("end fork is less than the start fork")
panic("end fork is less than the start fork") // lint:nopanic -- test code.
}
if start >= version.Altair {
c.AltairForkEpoch = 0

View File

@@ -1,3 +1,4 @@
// lint:nopanic -- Test tooling / code.
package simulator
import (

View File

@@ -56,6 +56,7 @@ func (mr *MockPrysmChainClientMockRecorder) ValidatorCount(arg0, arg1, arg2 any)
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatorCount", reflect.TypeOf((*MockPrysmChainClient)(nil).ValidatorCount), arg0, arg1, arg2)
}
// ValidatorPerformance mocks base method.
func (m *MockPrysmChainClient) ValidatorPerformance(arg0 context.Context, arg1 *ethpb.ValidatorPerformanceRequest) (*ethpb.ValidatorPerformanceResponse, error) {
m.ctrl.T.Helper()
@@ -70,5 +71,3 @@ func (mr *MockPrysmChainClientMockRecorder) ValidatorPerformance(arg0, arg1 any)
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatorCount", reflect.TypeOf((*MockPrysmChainClient)(nil).ValidatorPerformance), arg0, arg1)
}

View File

@@ -75,6 +75,8 @@ func (s *SlotIntervalTicker) Done() {
}
// NewSlotTicker starts and returns a new SlotTicker instance.
// This method panics if genesis time is zero.
// lint:nopanic -- Communicated panic in godoc commentary.
func NewSlotTicker(genesisTime time.Time, secondsPerSlot uint64) *SlotTicker {
if genesisTime.IsZero() {
panic("zero genesis time")
@@ -89,6 +91,8 @@ func NewSlotTicker(genesisTime time.Time, secondsPerSlot uint64) *SlotTicker {
// NewSlotTickerWithOffset starts and returns a SlotTicker instance that allows a offset of time from genesis,
// entering a offset greater than secondsPerSlot is not allowed.
// This method will panic if genesis time is zero or the offset is less than seconds per slot.
// lint:nopanic -- Communicated panic in godoc commentary.
func NewSlotTickerWithOffset(genesisTime time.Time, offset time.Duration, secondsPerSlot uint64) *SlotTicker {
if genesisTime.Unix() == 0 {
panic("zero genesis time")
@@ -176,6 +180,8 @@ func (s *SlotIntervalTicker) startWithIntervals(
// several offsets of time from genesis,
// Caller is responsible to input the intervals in increasing order and none bigger or equal than
// SecondsPerSlot
// This method will panic if genesis time is zero, intervals is 0 length, or offsets are invalid.
// lint:nopanic -- Communicated panic in godoc commentary.
func NewSlotTickerWithIntervals(genesisTime time.Time, intervals []time.Duration) *SlotIntervalTicker {
if genesisTime.Unix() == 0 {
panic("zero genesis time")

View File

@@ -126,7 +126,7 @@ func EpochStart(epoch primitives.Epoch) (primitives.Slot, error) {
func UnsafeEpochStart(epoch primitives.Epoch) primitives.Slot {
es, err := EpochStart(epoch)
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Unsafe is implied and communicated in the godoc commentary.
}
return es
}

View File

@@ -0,0 +1,26 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["analyzer.go"],
importpath = "github.com/prysmaticlabs/prysm/v5/tools/analyzers/nopanic",
visibility = ["//visibility:public"],
deps = [
"@org_golang_x_tools//go/analysis:go_default_library",
"@org_golang_x_tools//go/analysis/passes/inspect:go_default_library",
"@org_golang_x_tools//go/ast/inspector:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["analyzer_test.go"],
data = glob(["testdata/**"]) + [
"@go_sdk//:files",
],
embed = [":go_default_library"],
deps = [
"//build/bazel:go_default_library",
"@org_golang_x_tools//go/analysis/analysistest:go_default_library",
],
)

View File

@@ -0,0 +1,86 @@
package nopanic
import (
"errors"
"go/ast"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
const Doc = "Tool to discourage the use of panic(), except in init functions"
var errNoPanic = errors.New("panic() should not be used, except in rare situations or init functions")
// Analyzer runs static analysis.
var Analyzer = &analysis.Analyzer{
Name: "nopanic",
Doc: Doc,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspection, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
if !ok {
return nil, errors.New("analyzer is not type *inspector.Inspector")
}
nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
inspection.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) bool {
switch stmt := n.(type) {
case *ast.CallExpr:
if isPanic(stmt) && !hasExclusion(pass, stack) {
pass.Report(analysis.Diagnostic{
Pos: n.Pos(),
End: n.End(),
Message: errNoPanic.Error(),
})
return false
}
}
return true
})
return nil, nil
}
// isPanic returns true if the method name is exactly "panic", case insensitive.
func isPanic(call *ast.CallExpr) bool {
i, ok := call.Fun.(*ast.Ident)
return ok && strings.ToLower(i.Name) == "panic"
}
// hasExclusion looks at the ast stack and if any node in the stack has the magic words "lint:nopanic"
// then this node is considered excluded. This allows exclusions to be placed at the function or package level.
// This method also excludes init functions.
func hasExclusion(pass *analysis.Pass, stack []ast.Node) bool {
if len(stack) < 2 {
return false
}
// The first value in the stack is always the file, then the second value would be a package level function.
// Init functions are always package level.
if fd, ok := stack[1].(*ast.FuncDecl); ok && fd.Name.Name == "init" {
return true
}
// Build a comment map and scan the comments of this node stack.
cm := ast.NewCommentMap(pass.Fset, stack[0], stack[0].(*ast.File).Comments)
for _, n := range stack {
for _, cmt := range cm[n] {
for _, l := range cmt.List {
if strings.Contains(l.Text, "lint:nopanic") {
return true
}
}
}
}
return false
}

View File

@@ -0,0 +1,20 @@
package nopanic
import (
"testing"
"github.com/prysmaticlabs/prysm/v5/build/bazel"
"golang.org/x/tools/go/analysis/analysistest"
)
func init() {
if bazel.BuiltWithBazel() {
bazel.SetGoEnv()
}
}
func TestAnalyzer(t *testing.T) {
testdata := bazel.TestDataPath(t)
analysistest.TestData = func() string { return testdata }
analysistest.Run(t, testdata, Analyzer)
}

View File

@@ -0,0 +1,26 @@
package testdata
func A() error {
panic("feeling cute, let's panic!") // want "panic\\(\\) should not be used, except in rare situations or init functions"
}
func B(foo interface{}) error {
if foo == nil {
panic("impossible condition: foo is nil") //lint:nopanic -- This is validated by the caller.
}
if _, ok := foo.(string); !ok {
panic("foo should not be a string!!") // want "panic\\(\\) should not be used, except in rare situations or init functions"
}
return nil
}
//lint:nopanic -- This is method is really safe ;)
func C(foo interface{}) error {
if foo == nil {
panic("impossible condition: foo is nil")
}
return nil
}

View File

@@ -0,0 +1,7 @@
package testdata
func init() {
if false {
panic("this should never happen")
}
}

View File

@@ -0,0 +1,8 @@
// package testdata
//
// lint:nopanic -- This package is so safe that you can trust it...
package testdata
func dive() {
panic("BELLY FLOP!!!!")
}

View File

@@ -7,6 +7,7 @@
*
* Usage: Run bootnode --help for flag options.
*/
// lint:nopanic -- This tool is OK to panic.
package main
import (

View File

@@ -1,5 +1,7 @@
// This binary is a simple rest API endpoint to calculate
// the ENR value of a node given its private key,ip address and port.
//
// lint:nopanic -- Tooling allowed to panic.
package main
import (

View File

@@ -37,7 +37,7 @@ func (v *votes) Insert(blk interfaces.ReadOnlyBeaconBlock) {
e1d := blk.Body().Eth1Data()
htr, err := e1d.HashTreeRoot()
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Panic is OK for this tool.
}
v.hashes[bytesutil.ToBytes32(e1d.BlockHash)]++
v.roots[bytesutil.ToBytes32(e1d.DepositRoot)]++

View File

@@ -2,6 +2,7 @@
// merges them into one profile
//
// Copied, with minor changes, from https://github.com/wadey/gocovmerge under BSD-2-Clause License
// lint:nopanic -- Tooling that is allowed to panic.
package main
import (

View File

@@ -1,3 +1,6 @@
// package mock
//
// lint:nopanic -- Test / mock code, allowed to panic.
package mock
import (

View File

@@ -148,7 +148,7 @@ func (c beaconApiChainClient) ValidatorBalances(ctx context.Context, in *ethpb.L
}
// TODO: Implement me
panic("beaconApiChainClient.ValidatorBalances is not implemented. To use a fallback client, pass a fallback client as the last argument of NewBeaconApiChainClientWithFallback.")
return nil, errors.New("beaconApiChainClient.ValidatorBalances is not implemented. To use a fallback client, pass a fallback client as the last argument of NewBeaconApiChainClientWithFallback.")
}
func (c beaconApiChainClient) Validators(ctx context.Context, in *ethpb.ListValidatorsRequest) (*ethpb.Validators, error) {

View File

@@ -100,7 +100,7 @@ func (c *beaconApiNodeClient) Peers(ctx context.Context, in *empty.Empty) (*ethp
}
// TODO: Implement me
panic("beaconApiNodeClient.Peers is not implemented. To use a fallback client, pass a fallback client as the last argument of NewBeaconApiNodeClientWithFallback.")
return nil, errors.New("beaconApiNodeClient.Peers is not implemented. To use a fallback client, pass a fallback client as the last argument of NewBeaconApiNodeClientWithFallback.")
}
func (c *beaconApiNodeClient) IsHealthy(ctx context.Context) bool {

View File

@@ -146,7 +146,7 @@ func (v *ValidatorService) Start() {
BufferItems: 64, // number of keys per Get buffer.
})
if err != nil {
panic(err)
panic(err) // lint:nopanic -- Only errors on misconfiguration of config values.
}
aggregatedSlotCommitteeIDCache := lruwrpr.New(int(params.BeaconConfig().MaxCommitteesPerSlot))

View File

@@ -27,7 +27,7 @@ func (*Store) SaveEIPImportBlacklistedPublicKeys(_ context.Context, _ [][fieldpa
// SigningRootAtTargetEpoch is implemented only to satisfy the interface.
func (*Store) SigningRootAtTargetEpoch(_ context.Context, _ [fieldparams.BLSPubkeyLength]byte, _ primitives.Epoch) ([]byte, error) {
panic("not implemented")
return nil, errors.New("not implemented")
}
// LowestSignedTargetEpoch returns the lowest signed target epoch for a public key, a boolean indicating if it exists and an error.

View File

@@ -14,12 +14,12 @@ import (
// HighestSignedProposal is implemented only to satisfy the interface.
func (*Store) HighestSignedProposal(_ context.Context, _ [fieldparams.BLSPubkeyLength]byte) (primitives.Slot, bool, error) {
panic("not implemented")
return 0, false, errors.New("not implemented")
}
// LowestSignedProposal is implemented only to satisfy the interface.
func (*Store) LowestSignedProposal(_ context.Context, _ [fieldparams.BLSPubkeyLength]byte) (primitives.Slot, bool, error) {
panic("not implemented")
return 0, false, errors.New("not implemented")
}
// ProposalHistoryForPubKey returns the proposal history for a given public key.
@@ -45,7 +45,7 @@ func (s *Store) ProposalHistoryForPubKey(_ context.Context, publicKey [fieldpara
// ProposalHistoryForSlot is implemented only to satisfy the interface.
func (*Store) ProposalHistoryForSlot(_ context.Context, _ [fieldparams.BLSPubkeyLength]byte, _ primitives.Slot) ([fieldparams.RootLength]byte, bool, bool, error) {
panic("not implemented")
return [32]byte{}, false, false, errors.New("not implemented")
}
// SaveProposalHistoryForSlot checks if the incoming proposal is valid regarding EIP-3076 minimal slashing protection.

View File

@@ -162,7 +162,7 @@ func (c *ValidatorClient) Start() {
log.WithField("times", i-1).Info("Already shutting down, interrupt more to panic.")
}
}
panic("Panic closing the validator client")
panic("Panic closing the validator client") // lint:nopanic -- Panic is requested by user.
}()
// Wait for stop channel to be closed.