Skip to content
25 changes: 23 additions & 2 deletions modules/network/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,29 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M
return nil, sdkerr.Wrapf(sdkerrors.ErrNotFound, "validator index not found for %s", msg.ConsensusAddress)
}

// todo (Alex): we need to set a limit to not have validators attest old blocks. Also make sure that this relates with
// the retention period for pruning
// Enforce attestation height upper bound to prevent storage exhaustion
// from future-height spam.
currentHeight := ctx.BlockHeight()
maxFutureHeight := currentHeight + 1
if msg.Height > maxFutureHeight {
return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "attestation height %d exceeds max allowed height %d", msg.Height, maxFutureHeight)
}

// Enforce attestation height lower bound: reject heights that fall below
// the PruneAfter retention window. Attesting pruned/about-to-be-pruned
// heights wastes storage and serves no purpose. This uses the same epoch
// calculation as PruneOldBitmaps so the two stay aligned.
params := k.GetParams(ctx)
minHeight := int64(1)
if params.PruneAfter > 0 && params.EpochLength > 0 {
currentEpoch := uint64(currentHeight) / params.EpochLength
if currentEpoch > params.PruneAfter {
minHeight = int64((currentEpoch - params.PruneAfter) * params.EpochLength)
}
}
if msg.Height < minHeight {
return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "attestation height %d is below retention window (min %d)", msg.Height, minHeight)
}
bitmap, err := k.GetAttestationBitmap(ctx, msg.Height)
if err != nil && !errors.Is(err, collections.ErrNotFound) {
return nil, sdkerr.Wrap(err, "get attestation bitmap")
Expand Down
89 changes: 89 additions & 0 deletions modules/network/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,95 @@ func TestAttestVotePayloadValidation(t *testing.T) {
}
}

func TestAttestHeightBounds(t *testing.T) {
myValAddr := sdk.ValAddress("validator1")
// With DefaultParams: EpochLength=1, PruneAfter=15
// At blockHeight=100: currentEpoch=100, minHeight=(100-7)*1=93

specs := map[string]struct {
blockHeight int64
attestH int64
expErr error
}{
"future height rejected": {
blockHeight: 100,
attestH: 200,
expErr: sdkerrors.ErrInvalidRequest,
},
"two-ahead rejected": {
blockHeight: 100,
attestH: 102,
expErr: sdkerrors.ErrInvalidRequest,
},
"current height accepted": {
blockHeight: 100,
attestH: 100,
},
"next height accepted": {
blockHeight: 100,
attestH: 101,
},
"stale height rejected": {
blockHeight: 100,
attestH: 1,
expErr: sdkerrors.ErrInvalidRequest,
},
"below retention window rejected": {
blockHeight: 100,
attestH: 84, // minHeight = 85
expErr: sdkerrors.ErrInvalidRequest,
},
"at retention boundary accepted": {
blockHeight: 100,
attestH: 93, // exactly minHeight
},
"early chain no stale rejection": {
blockHeight: 16,
attestH: 1,
},
}

for name, spec := range specs {
t.Run(name, func(t *testing.T) {
sk := NewMockStakingKeeper()
cdc := moduletestutil.MakeTestEncodingConfig().Codec
keys := storetypes.NewKVStoreKeys(types.StoreKey)
logger := log.NewTestLogger(t)
cms := integration.CreateMultiStore(keys, logger)
authority := authtypes.NewModuleAddress("gov")
keeper := NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), sk, nil, nil, authority.String())
server := msgServer{Keeper: keeper}
ctx := sdk.NewContext(cms, cmtproto.Header{
ChainID: "test-chain",
Time: time.Now().UTC(),
Height: spec.blockHeight,
}, false, logger).WithContext(t.Context())

require.NoError(t, keeper.SetParams(ctx, types.DefaultParams()))

// Setup: add attester and build index map
require.NoError(t, keeper.SetAttesterSetMember(ctx, myValAddr.String()))
require.NoError(t, keeper.BuildValidatorIndexMap(ctx))

msg := &types.MsgAttest{
Authority: myValAddr.String(),
ConsensusAddress: myValAddr.String(),
Height: spec.attestH,
Vote: make([]byte, MinVoteLen),
}

rsp, err := server.Attest(ctx, msg)
if spec.expErr != nil {
require.ErrorIs(t, err, spec.expErr)
require.Nil(t, rsp)
return
}
require.NoError(t, err)
require.NotNil(t, rsp)
})
}
}

var _ types.StakingKeeper = &MockStakingKeeper{}

type MockStakingKeeper struct {
Expand Down
2 changes: 1 addition & 1 deletion modules/network/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var (
DefaultEpochLength = uint64(1) // Changed from 10 to 1 to allow attestations on every block
DefaultQuorumFraction = math.LegacyNewDecWithPrec(667, 3) // 2/3
DefaultMinParticipation = math.LegacyNewDecWithPrec(5, 1) // 1/2
DefaultPruneAfter = uint64(7)
DefaultPruneAfter = uint64(15) // also used as number of blocks for attestations to land
DefaultSignMode = SignMode_SIGN_MODE_CHECKPOINT
)

Expand Down
Loading