diff --git a/node-api/handlers/proof/merkle/validator_balance.go b/node-api/handlers/proof/merkle/validator_balance.go index d9315c1d34..f9ffe6f6fe 100644 --- a/node-api/handlers/proof/merkle/validator_balance.go +++ b/node-api/handlers/proof/merkle/validator_balance.go @@ -56,9 +56,8 @@ func ProveBalanceInState( leafOffset := validatorIndex / BalancesPerLeaf // Calculate the generalized index for the target validator's balance leaf. - // The offset multiplication is bounded by the number of validators, so - // converting to int is safe on 64-bit architectures. - gIndex := zeroBalanceGIndexState + int(leafOffset) // #nosec G115 + // int conversion is safe on 64-bit architectures: (2^40-1)*8 < 2^43 < 2^63. + gIndex := zeroBalanceGIndexState + int(leafOffset) // #nosec G115 -- offset bounded by caller. balanceProof, err := stateProofTree.Prove(gIndex) if err != nil { @@ -82,6 +81,10 @@ func ProveBalanceInBlock( bsm types.BeaconStateMarshallable, allBalances []uint64, ) ([]common.Root, common.Root, common.Root, error) { + if err := validateValidatorIndexBound(validatorIndex); err != nil { + return nil, common.Root{}, common.Root{}, err + } + forkVersion := bsm.GetForkVersion() // 1. Proof inside the state. diff --git a/node-api/handlers/proof/merkle/validator_credentials.go b/node-api/handlers/proof/merkle/validator_credentials.go index d1dd47fb1f..f93051bb7a 100644 --- a/node-api/handlers/proof/merkle/validator_credentials.go +++ b/node-api/handlers/proof/merkle/validator_credentials.go @@ -49,10 +49,8 @@ func ProveWithdrawalCredentialsInState( return nil, common.Root{}, err } - // Calculate the generalized index for the target validator. The offset - // multiplication is bounded by (2^40-1)*8 < 2^43 < 2^63, so converting to - // int is safe on 64-bit architectures. - gIndex := zeroWithdrawalGIndexState + int(validatorOffset) // #nosec G115 + // int conversion is safe on 64-bit architectures: (2^40-1)*8 < 2^43 < 2^63. + gIndex := zeroWithdrawalGIndexState + int(validatorOffset) // #nosec G115 -- offset bounded by caller. withdrawalProof, err := stateProofTree.Prove(gIndex) if err != nil { @@ -75,6 +73,10 @@ func ProveWithdrawalCredentialsInBlock( bbh *ctypes.BeaconBlockHeader, bsm types.BeaconStateMarshallable, ) ([]common.Root, common.Root, error) { + if err := validateValidatorIndexBound(validatorIndex); err != nil { + return nil, common.Root{}, err + } + forkVersion := bsm.GetForkVersion() // Calculate the validator-specific offset. diff --git a/node-api/handlers/proof/merkle/validator_index_bounds.go b/node-api/handlers/proof/merkle/validator_index_bounds.go new file mode 100644 index 0000000000..3946dd0a2c --- /dev/null +++ b/node-api/handlers/proof/merkle/validator_index_bounds.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +// +// Copyright (C) 2025, Berachain Foundation. All rights reserved. +// Use of this software is governed by the Business Source License included +// in the LICENSE file of this repository and at www.mariadb.com/bsl11. +// +// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY +// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER +// VERSIONS OF THE LICENSED WORK. +// +// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF +// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF +// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE). +// +// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +// AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +// TITLE. + +package merkle + +import ( + "fmt" + + "github.com/berachain/beacon-kit/primitives/constants" + "github.com/berachain/beacon-kit/primitives/math" +) + +// validateValidatorIndexBound returns an error if validatorIndex is at or +// above the registry limit. +func validateValidatorIndexBound(validatorIndex math.U64) error { + if validatorIndex.Unwrap() >= constants.ValidatorsRegistryLimit { + return fmt.Errorf( + "validator index %d exceeds registry limit %d", + validatorIndex, uint64(constants.ValidatorsRegistryLimit), + ) + } + return nil +} diff --git a/node-api/handlers/proof/merkle/validator_index_bounds_test.go b/node-api/handlers/proof/merkle/validator_index_bounds_test.go new file mode 100644 index 0000000000..ae53e3585c --- /dev/null +++ b/node-api/handlers/proof/merkle/validator_index_bounds_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BUSL-1.1 +// +// Copyright (C) 2025, Berachain Foundation. All rights reserved. +// Use of this software is governed by the Business Source License included +// in the LICENSE file of this repository and at www.mariadb.com/bsl11. +// +// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY +// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER +// VERSIONS OF THE LICENSED WORK. +// +// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF +// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF +// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE). +// +// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +// AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +// TITLE. + +package merkle_test + +import ( + "testing" + + ctypes "github.com/berachain/beacon-kit/consensus-types/types" + "github.com/berachain/beacon-kit/node-api/handlers/proof/merkle" + "github.com/berachain/beacon-kit/node-api/handlers/proof/merkle/mock" + "github.com/berachain/beacon-kit/primitives/common" + "github.com/berachain/beacon-kit/primitives/constants" + "github.com/berachain/beacon-kit/primitives/math" + "github.com/berachain/beacon-kit/primitives/version" + "github.com/stretchr/testify/require" +) + +// TestProveInBlock_RejectsOutOfBoundsValidatorIndex asserts a shared invariant +// that a validator index at or above the registry limit must be rejected. +func TestProveInBlock_RejectsOutOfBoundsValidatorIndex(t *testing.T) { + t.Parallel() + + bs := mock.NewBeaconStateWith( + 4, + ctypes.Validators{&ctypes.Validator{}}, + 0, + common.ExecutionAddress{}, + version.Electra(), + ) + bs.Balances = []uint64{32000000000} + bbh := ctypes.NewBeaconBlockHeader( + 4, 0, common.Root{1, 2, 3}, bs.HashTreeRoot(), common.Root{3, 2, 1}, + ) + + provers := []struct { + name string + fn func(math.U64) error + }{ + {"ProveValidatorPubkeyInBlock", func(idx math.U64) error { + _, _, err := merkle.ProveValidatorPubkeyInBlock(idx, bbh, bs) + return err + }}, + {"ProveWithdrawalCredentialsInBlock", func(idx math.U64) error { + _, _, err := merkle.ProveWithdrawalCredentialsInBlock(idx, bbh, bs) + return err + }}, + {"ProveBalanceInBlock", func(idx math.U64) error { + _, _, _, err := merkle.ProveBalanceInBlock(idx, bbh, bs, bs.Balances) + return err + }}, + } + + indices := []struct { + name string + validatorIndex math.U64 + }{ + {"at registry limit", math.U64(constants.ValidatorsRegistryLimit)}, + {"above registry limit", math.U64(constants.ValidatorsRegistryLimit) + 1}, + } + + for _, p := range provers { + t.Run(p.name, func(t *testing.T) { + for _, idx := range indices { + t.Run(idx.name, func(t *testing.T) { + err := p.fn(idx.validatorIndex) + require.ErrorContains(t, err, "exceeds registry limit") + }) + } + }) + } +} diff --git a/node-api/handlers/proof/merkle/validator_pubkey.go b/node-api/handlers/proof/merkle/validator_pubkey.go index eccdbc7be4..f2d56247c0 100644 --- a/node-api/handlers/proof/merkle/validator_pubkey.go +++ b/node-api/handlers/proof/merkle/validator_pubkey.go @@ -47,6 +47,10 @@ func ProveValidatorPubkeyInBlock( bbh *ctypes.BeaconBlockHeader, bsm types.BeaconStateMarshallable, ) ([]common.Root, common.Root, error) { + if err := validateValidatorIndexBound(validatorIndex); err != nil { + return nil, common.Root{}, err + } + forkVersion := bsm.GetForkVersion() // Calculate the validator-specific offset. @@ -96,7 +100,8 @@ func ProveValidatorPubkeyInState( } // Determine the correct gIndex based on the fork version. - gIndex := int(validatorOffset) // #nosec G115 -- max validator offset is 8 * (2^40 - 1). + // int conversion is safe on 64-bit architectures: (2^40-1)*8 < 2^43 < 2^63. + gIndex := int(validatorOffset) // #nosec G115 -- offset bounded by caller. zeroValidatorPubkeyGIndexState, err := GetZeroValidatorPubkeyGIndexState(forkVersion) if err != nil { return nil, common.Root{}, err