Add support for caching events to the `AttributeCache` (#17030)

* s/AttributeCache/ClusterStateCache/g in preparation of the event caching getting subsumed

* Add EventCaching support to the ClusterStateCache.

* Review feedback

* Reverted the rename of the Darwin files for now since it was starting to get a bit out of hand

* Apply suggestions from code review

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Review feedback

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/scripts/tools/check_includes_config.py b/scripts/tools/check_includes_config.py
index 36491a3..ccc3de5 100644
--- a/scripts/tools/check_includes_config.py
+++ b/scripts/tools/check_includes_config.py
@@ -103,7 +103,7 @@
 ALLOW: Dict[str, Set[str]] = {
 
     # Not intended for embedded clients (#11705).
-    'src/app/AttributeCache.h': {'list', 'map', 'set', 'vector'},
+    'src/app/ClusterStateCache.h': {'list', 'map', 'set', 'vector', 'queue'},
     'src/app/BufferedReadCallback.h': {'vector'},
 
     # Itself in DENY.
diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn
index d5ee809..51080fa 100644
--- a/src/app/BUILD.gn
+++ b/src/app/BUILD.gn
@@ -53,8 +53,6 @@
 
   sources = [
     "AttributeAccessInterface.cpp",
-    "AttributeCache.cpp",
-    "AttributeCache.h",
     "AttributePathExpandIterator.cpp",
     "AttributePathExpandIterator.h",
     "AttributePathParams.h",
@@ -67,6 +65,8 @@
     "CASESessionManager.h",
     "ChunkedWriteCallback.cpp",
     "ChunkedWriteCallback.h",
+    "ClusterStateCache.cpp",
+    "ClusterStateCache.h",
     "CommandHandler.cpp",
     "CommandResponseHelper.h",
     "CommandSender.cpp",
diff --git a/src/app/AttributeCache.cpp b/src/app/ClusterStateCache.cpp
similarity index 73%
rename from src/app/AttributeCache.cpp
rename to src/app/ClusterStateCache.cpp
index 0970b78..8d8c72a 100644
--- a/src/app/AttributeCache.cpp
+++ b/src/app/ClusterStateCache.cpp
@@ -17,14 +17,15 @@
  */
 
 #include "system/SystemPacketBuffer.h"
-#include <app/AttributeCache.h>
+#include <app/ClusterStateCache.h>
 #include <app/InteractionModelEngine.h>
 #include <tuple>
 
 namespace chip {
 namespace app {
 
-CHIP_ERROR AttributeCache::UpdateCache(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus)
+CHIP_ERROR ClusterStateCache::UpdateCache(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData,
+                                          const StatusIB & aStatus)
 {
     AttributeState state;
     System::PacketBufferHandle handle;
@@ -85,6 +86,7 @@
         {
             mCache[aPath.mEndpointId][aPath.mClusterId].mPendingDataVersion = aPath.mDataVersion;
         }
+
         mLastReportDataPath = aPath;
     }
     else
@@ -106,7 +108,49 @@
     return CHIP_NO_ERROR;
 }
 
-void AttributeCache::OnReportBegin()
+CHIP_ERROR ClusterStateCache::UpdateEventCache(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus)
+{
+    if (apData)
+    {
+        //
+        // If we've already seen this event before, there's no more work to be done.
+        //
+        if (mHighestReceivedEventNumber.HasValue() && aEventHeader.mEventNumber <= mHighestReceivedEventNumber.Value())
+        {
+            return CHIP_NO_ERROR;
+        }
+
+        System::PacketBufferHandle handle = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes);
+
+        System::PacketBufferTLVWriter writer;
+        writer.Init(std::move(handle), false);
+
+        ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), *apData));
+        ReturnErrorOnFailure(writer.Finalize(&handle));
+
+        //
+        // Compact the buffer down to a more reasonably sized packet buffer
+        // if we can.
+        //
+        handle.RightSize();
+
+        EventData eventData;
+        eventData.first  = aEventHeader;
+        eventData.second = std::move(handle);
+
+        mEventDataCache.insert(std::move(eventData));
+
+        mHighestReceivedEventNumber.SetValue(aEventHeader.mEventNumber);
+    }
+    else if (apStatus)
+    {
+        mEventStatusCache[aEventHeader.mPath] = *apStatus;
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+void ClusterStateCache::OnReportBegin()
 {
     mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId);
     mChangedAttributeSet.clear();
@@ -114,7 +158,7 @@
     mCallback.OnReportBegin();
 }
 
-void AttributeCache::CommitPendingDataVersion()
+void ClusterStateCache::CommitPendingDataVersion()
 {
     if (!mLastReportDataPath.IsValidConcreteClusterPath())
     {
@@ -129,7 +173,7 @@
     }
 }
 
-void AttributeCache::OnReportEnd()
+void ClusterStateCache::OnReportEnd()
 {
     CommitPendingDataVersion();
     mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId);
@@ -158,7 +202,112 @@
     mCallback.OnReportEnd();
 }
 
-void AttributeCache::OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus)
+CHIP_ERROR ClusterStateCache::Get(const ConcreteAttributePath & path, TLV::TLVReader & reader)
+{
+    CHIP_ERROR err;
+
+    auto attributeState = GetAttributeState(path.mEndpointId, path.mClusterId, path.mAttributeId, err);
+    ReturnErrorOnFailure(err);
+
+    if (attributeState->Is<StatusIB>())
+    {
+        return CHIP_ERROR_IM_STATUS_CODE_RECEIVED;
+    }
+
+    System::PacketBufferTLVReader bufReader;
+
+    bufReader.Init(attributeState->Get<System::PacketBufferHandle>().Retain());
+    ReturnErrorOnFailure(bufReader.Next());
+
+    reader.Init(bufReader);
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ClusterStateCache::Get(EventNumber eventNumber, TLV::TLVReader & reader)
+{
+    CHIP_ERROR err;
+
+    auto eventData = GetEventData(eventNumber, err);
+    ReturnErrorOnFailure(err);
+
+    System::PacketBufferTLVReader bufReader;
+
+    bufReader.Init(eventData->second.Retain());
+    ReturnErrorOnFailure(bufReader.Next());
+
+    reader.Init(bufReader);
+    return CHIP_NO_ERROR;
+}
+
+ClusterStateCache::EndpointState * ClusterStateCache::GetEndpointState(EndpointId endpointId, CHIP_ERROR & err)
+{
+    auto endpointIter = mCache.find(endpointId);
+    if (endpointIter == mCache.end())
+    {
+        err = CHIP_ERROR_KEY_NOT_FOUND;
+        return nullptr;
+    }
+
+    err = CHIP_NO_ERROR;
+    return &endpointIter->second;
+}
+
+ClusterStateCache::ClusterState * ClusterStateCache::GetClusterState(EndpointId endpointId, ClusterId clusterId, CHIP_ERROR & err)
+{
+    auto endpointState = GetEndpointState(endpointId, err);
+    if (err != CHIP_NO_ERROR)
+    {
+        return nullptr;
+    }
+
+    auto clusterState = endpointState->find(clusterId);
+    if (clusterState == endpointState->end())
+    {
+        err = CHIP_ERROR_KEY_NOT_FOUND;
+        return nullptr;
+    }
+
+    err = CHIP_NO_ERROR;
+    return &clusterState->second;
+}
+
+const ClusterStateCache::AttributeState * ClusterStateCache::GetAttributeState(EndpointId endpointId, ClusterId clusterId,
+                                                                               AttributeId attributeId, CHIP_ERROR & err)
+{
+    auto clusterState = GetClusterState(endpointId, clusterId, err);
+    if (err != CHIP_NO_ERROR)
+    {
+        return nullptr;
+    }
+
+    auto attributeState = clusterState->mAttributes.find(attributeId);
+    if (attributeState == clusterState->mAttributes.end())
+    {
+        err = CHIP_ERROR_KEY_NOT_FOUND;
+        return nullptr;
+    }
+
+    err = CHIP_NO_ERROR;
+    return &attributeState->second;
+}
+
+const ClusterStateCache::EventData * ClusterStateCache::GetEventData(EventNumber eventNumber, CHIP_ERROR & err)
+{
+    EventData compareKey;
+
+    compareKey.first.mEventNumber = eventNumber;
+    auto eventData                = mEventDataCache.find(std::move(compareKey));
+    if (eventData == mEventDataCache.end())
+    {
+        err = CHIP_ERROR_KEY_NOT_FOUND;
+        return nullptr;
+    }
+
+    err = CHIP_NO_ERROR;
+    return &(*eventData);
+}
+
+void ClusterStateCache::OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus)
 {
     //
     // Since the cache itself is a ReadClient::Callback, it may be incorrectly passed in directly when registering with the
@@ -188,28 +337,7 @@
     mCallback.OnAttributeData(aPath, apData ? &dataSnapshot : nullptr, aStatus);
 }
 
-CHIP_ERROR AttributeCache::Get(const ConcreteAttributePath & path, TLV::TLVReader & reader)
-{
-    CHIP_ERROR err;
-
-    auto attributeState = GetAttributeState(path.mEndpointId, path.mClusterId, path.mAttributeId, err);
-    ReturnErrorOnFailure(err);
-
-    if (attributeState->Is<StatusIB>())
-    {
-        return CHIP_ERROR_IM_STATUS_CODE_RECEIVED;
-    }
-
-    System::PacketBufferTLVReader bufReader;
-
-    bufReader.Init(attributeState->Get<System::PacketBufferHandle>().Retain());
-    ReturnErrorOnFailure(bufReader.Next());
-
-    reader.Init(bufReader);
-    return CHIP_NO_ERROR;
-}
-
-CHIP_ERROR AttributeCache::GetVersion(EndpointId mEndpointId, ClusterId mClusterId, Optional<DataVersion> & aVersion)
+CHIP_ERROR ClusterStateCache::GetVersion(EndpointId mEndpointId, ClusterId mClusterId, Optional<DataVersion> & aVersion)
 {
     CHIP_ERROR err;
     auto clusterState = GetClusterState(mEndpointId, mClusterId, err);
@@ -218,59 +346,21 @@
     return CHIP_NO_ERROR;
 }
 
-AttributeCache::EndpointState * AttributeCache::GetEndpointState(EndpointId endpointId, CHIP_ERROR & err)
+void ClusterStateCache::OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus)
 {
-    auto endpointIter = mCache.find(endpointId);
-    if (endpointIter == mCache.end())
+    VerifyOrDie(apData != nullptr || apStatus != nullptr);
+
+    TLV::TLVReader dataSnapshot;
+    if (apData)
     {
-        err = CHIP_ERROR_KEY_NOT_FOUND;
-        return nullptr;
+        dataSnapshot.Init(*apData);
     }
 
-    err = CHIP_NO_ERROR;
-    return &endpointIter->second;
+    UpdateEventCache(aEventHeader, apData, apStatus);
+    mCallback.OnEventData(aEventHeader, apData ? &dataSnapshot : nullptr, apStatus);
 }
 
-AttributeCache::ClusterState * AttributeCache::GetClusterState(EndpointId endpointId, ClusterId clusterId, CHIP_ERROR & err)
-{
-    auto endpointState = GetEndpointState(endpointId, err);
-    if (err != CHIP_NO_ERROR)
-    {
-        return nullptr;
-    }
-
-    auto clusterState = endpointState->find(clusterId);
-    if (clusterState == endpointState->end())
-    {
-        err = CHIP_ERROR_KEY_NOT_FOUND;
-        return nullptr;
-    }
-
-    err = CHIP_NO_ERROR;
-    return &clusterState->second;
-}
-
-AttributeCache::AttributeState * AttributeCache::GetAttributeState(EndpointId endpointId, ClusterId clusterId,
-                                                                   AttributeId attributeId, CHIP_ERROR & err)
-{
-    auto clusterState = GetClusterState(endpointId, clusterId, err);
-    if (err != CHIP_NO_ERROR)
-    {
-        return nullptr;
-    }
-
-    auto attributeState = clusterState->mAttributes.find(attributeId);
-    if (attributeState == clusterState->mAttributes.end())
-    {
-        err = CHIP_ERROR_KEY_NOT_FOUND;
-        return nullptr;
-    }
-
-    err = CHIP_NO_ERROR;
-    return &attributeState->second;
-}
-
-CHIP_ERROR AttributeCache::GetStatus(const ConcreteAttributePath & path, StatusIB & status)
+CHIP_ERROR ClusterStateCache::GetStatus(const ConcreteAttributePath & path, StatusIB & status)
 {
     CHIP_ERROR err;
 
@@ -286,7 +376,19 @@
     return CHIP_NO_ERROR;
 }
 
-void AttributeCache::GetSortedFilters(std::vector<std::pair<DataVersionFilter, size_t>> & aVector)
+CHIP_ERROR ClusterStateCache::GetStatus(const ConcreteEventPath & path, StatusIB & status)
+{
+    auto statusIter = mEventStatusCache.find(path);
+    if (statusIter == mEventStatusCache.end())
+    {
+        return CHIP_ERROR_KEY_NOT_FOUND;
+    }
+
+    status = statusIter->second;
+    return CHIP_NO_ERROR;
+}
+
+void ClusterStateCache::GetSortedFilters(std::vector<std::pair<DataVersionFilter, size_t>> & aVector)
 {
     for (auto const & endpointIter : mCache)
     {
@@ -342,9 +444,9 @@
               });
 }
 
-CHIP_ERROR AttributeCache::OnUpdateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder,
-                                                         const Span<AttributePathParams> & aAttributePaths,
-                                                         bool & aEncodedDataVersionList)
+CHIP_ERROR ClusterStateCache::OnUpdateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder,
+                                                            const Span<AttributePathParams> & aAttributePaths,
+                                                            bool & aEncodedDataVersionList)
 {
     CHIP_ERROR err = CHIP_NO_ERROR;
     TLV::TLVWriter backup;
diff --git a/src/app/AttributeCache.h b/src/app/ClusterStateCache.h
similarity index 65%
rename from src/app/AttributeCache.h
rename to src/app/ClusterStateCache.h
index 3a602b1..720766e 100644
--- a/src/app/AttributeCache.h
+++ b/src/app/ClusterStateCache.h
@@ -29,13 +29,14 @@
 #include <lib/support/Variant.h>
 #include <list>
 #include <map>
+#include <queue>
 #include <set>
 #include <vector>
 
 namespace chip {
 namespace app {
 /*
- * This implements an attribute cache designed to aggregate attribute data received by a client
+ * This implements a cluster state cache designed to aggregate both attribute and event data received by a client
  * from either read or subscribe interactions and keep it resident and available for clients to
  * query at any time while the cache is active.
  *
@@ -46,9 +47,11 @@
  * state being determined by the associated ReadClient's path set.
  *
  * The cache provides a number of getters and helper functions to iterate over the topology
- * of the received data which is organized by endpoint, cluster and attribute ID. These permit greater
+ * of the received data which is organized by endpoint, cluster and attribute ID (for attributes). These permit greater
  * flexibility when dealing with interactions that use wildcards heavily.
  *
+ * For events, functions that permit iteration over the cached events sorted by event number are provided.
+ *
  * The data is stored internally in the cache as TLV. This permits re-use of the existing cluster objects
  * to de-serialize the state on-demand.
  *
@@ -61,7 +64,7 @@
  * 2. The same cache cannot be used by multiple subscribe/read interactions at the same time.
  *
  */
-class AttributeCache : protected ReadClient::Callback
+class ClusterStateCache : protected ReadClient::Callback
 {
 public:
     class Callback : public ReadClient::Callback
@@ -70,31 +73,40 @@
         /*
          * Called anytime an attribute value has changed in the cache
          */
-        virtual void OnAttributeChanged(AttributeCache * cache, const ConcreteAttributePath & path){};
+        virtual void OnAttributeChanged(ClusterStateCache * cache, const ConcreteAttributePath & path){};
 
         /*
          * Called anytime any attribute in a cluster has changed in the cache
          */
-        virtual void OnClusterChanged(AttributeCache * cache, EndpointId endpointId, ClusterId clusterId){};
+        virtual void OnClusterChanged(ClusterStateCache * cache, EndpointId endpointId, ClusterId clusterId){};
 
         /*
          * Called anytime an endpoint was added to the cache
          */
-        virtual void OnEndpointAdded(AttributeCache * cache, EndpointId endpointId){};
+        virtual void OnEndpointAdded(ClusterStateCache * cache, EndpointId endpointId){};
     };
 
-    AttributeCache(Callback & callback) : mCallback(callback), mBufferedReader(*this) {}
+    ClusterStateCache(Callback & callback, Optional<EventNumber> highestReceivedEventNumber = Optional<EventNumber>::Missing()) :
+        mCallback(callback), mBufferedReader(*this)
+    {
+        mHighestReceivedEventNumber = highestReceivedEventNumber;
+    }
+
+    void SetHighestReceivedEventNumber(EventNumber highestReceivedEventNumber)
+    {
+        mHighestReceivedEventNumber.SetValue(highestReceivedEventNumber);
+    }
 
     /*
-     * When registering as a callback to the ReadClient, the AttributeCache cannot not be passed as a callback
+     * When registering as a callback to the ReadClient, the ClusterStateCache cannot not be passed as a callback
      * directly. Instead, utilize this method below to correctly set up the callback chain such that
      * the buffered reader is the first callback in the chain before calling into cache subsequently.
      */
     ReadClient::Callback & GetBufferedCallback() { return mBufferedReader; }
 
     /*
-     * Retrieve the value of an attribute from the cache (if present) given a concrete path and decode
-     * is using DataModel::Decode into the in-out argument 'value'.
+     * Retrieve the value of an attribute from the cache (if present) given a concrete path by decoding
+     * it using DataModel::Decode into the in-out argument 'value'.
      *
      * For some types of attributes, the value for the attribute is directly backed by the underlying TLV buffer
      * and has pointers into that buffer. (e.g octet strings, char strings and lists).  This buffer only remains
@@ -227,6 +239,69 @@
     CHIP_ERROR GetVersion(EndpointId mEndpointId, ClusterId mClusterId, Optional<DataVersion> & aVersion);
 
     /*
+     * Retrieve the value of an event from the cache given an EventNumber by decoding
+     * it using DataModel::Decode into the in-out argument 'value'.
+     *
+     * This should be used in conjunction with the ForEachEvent() iterator function to
+     * retrieve the EventHeader (and corresponding metadata information for the event) along with its EventNumber.
+     *
+     * For some types of events, the values for the fields in the event are directly backed by the underlying TLV buffer
+     * and have pointers into that buffer. (e.g octet strings, char strings and lists). Unlike its attribute counterpart,
+     * these pointers are stable and will not change until a call to `ClearEventCache` happens.
+     *
+     * The template parameter EventObjectTypeT is generally expected to be a
+     * ClusterName::Events::EventName::DecodableType, but any
+     * object that can be decoded using the DataModel::Decode machinery will work.
+     *
+     * Notable return values:
+     *      - If the provided attribute object's Cluster and Event IDs don't match those of the event in the cache,
+     *        a CHIP_ERROR_SCHEMA_MISMATCH shall be returned.
+     *
+     *      - If event doesn't exist in the cache, CHIP_ERROR_KEY_NOT_FOUND
+     *        shall be returned.
+     */
+
+    template <typename EventObjectTypeT>
+    CHIP_ERROR Get(EventNumber eventNumber, EventObjectTypeT & value)
+    {
+        TLV::TLVReader reader;
+        CHIP_ERROR err;
+
+        auto * eventData = GetEventData(eventNumber, err);
+        ReturnErrorOnFailure(err);
+
+        if (eventData->first.mPath.mClusterId != value.GetClusterId() || eventData->first.mPath.mEventId != value.GetEventId())
+        {
+            return CHIP_ERROR_SCHEMA_MISMATCH;
+        }
+
+        ReturnErrorOnFailure(Get(eventNumber, reader));
+        return DataModel::Decode(reader, value);
+    }
+
+    /*
+     * Retrieve the data of an event by updating a in-out TLVReader to be positioned
+     * right at the structure that encapsulates the event payload.
+     *
+     * Notable return values:
+     *      - If no event with a matching eventNumber exists in the cache, CHIP_ERROR_KEY_NOT_FOUND
+     *        shall be returned.
+     *
+     */
+    CHIP_ERROR Get(EventNumber eventNumber, TLV::TLVReader & reader);
+
+    /*
+     * Retrieve the StatusIB for a specific event from the event status cache (if one exists).
+     * Otherwise, a CHIP_ERROR_KEY_NOT_FOUND error will be returned.
+     *
+     * This is to be used with the `ForEachEventStatus` iterator function.
+     *
+     * NOTE: Receipt of a StatusIB does not affect any pre-existing or future event data entries in the cache (and vice-versa).
+     *
+     */
+    CHIP_ERROR GetStatus(const ConcreteEventPath & path, StatusIB & status);
+
+    /*
      * Execute an iterator function that is called for every attribute
      * in a given endpoint and cluster. The function when invoked is provided a concrete attribute path
      * to every attribute that matches in the cache.
@@ -319,6 +394,85 @@
         return CHIP_NO_ERROR;
     }
 
+    /*
+     * Execute an iterator function that is called for every event in the event data cache that satisfies the following
+     * conditions:
+     *      - It matches the provided path filter
+     *      - Its event number is greater than or equal to the provided minimum event number filter.
+     *
+     * Each filter argument can be omitted from the match criteria above by passing in an empty EventPathParams() and/or
+     * a minimum event filter of 0.
+     *
+     * This iterator is called in increasing order from the event with the lowest event number to the highest.
+     *
+     * The function is passed a const reference to the EventHeader associated with that event.
+     *
+     * The iterator is expected to have this signature:
+     *      CHIP_ERROR IteratorFunc(const EventHeader & eventHeader);
+     *
+     * Notable return values:
+     *      - If func returns an error, that will result in termination of any further iteration over events
+     *        and that error shall be returned back up to the original call to this function.
+     *
+     */
+    template <typename IteratorFunc>
+    CHIP_ERROR ForEachEventData(IteratorFunc func, EventPathParams pathFilter = EventPathParams(),
+                                EventNumber minEventNumberFilter = 0)
+    {
+        for (const auto & item : mEventDataCache)
+        {
+            if (pathFilter.IsEventPathSupersetOf(item.first.mPath) && item.first.mEventNumber >= minEventNumberFilter)
+            {
+                ReturnErrorOnFailure(func(item.first));
+            }
+        }
+
+        return CHIP_NO_ERROR;
+    }
+
+    /*
+     * Execute an iterator function that is called for every StatusIB in the event status cache.
+     *
+     * The iterator is expected to have this signature:
+     *      CHIP_ERROR IteratorFunc(const ConcreteEventPath & eventPath, const StatusIB & statusIB);
+     *
+     * Notable return values:
+     *      - If func returns an error, that will result in termination of any further iteration over events
+     *        and that error shall be returned back up to the original call to this function.
+     *
+     * NOTE: Receipt of a StatusIB does not affect any pre-existing event data entries in the cache (and vice-versa).
+     *
+     */
+    template <typename IteratorFunc>
+    CHIP_ERROR ForEachEventStatus(IteratorFunc func)
+    {
+        for (const auto & item : mEventStatusCache)
+        {
+            ReturnErrorOnFailure(func(item.first, item.second));
+        }
+    }
+
+    /*
+     * Clear out the event data and status caches.
+     *
+     * By default, this will not clear out any internally tracked event counters, specifically:
+     *   - the highest event number seen so far. This is used in reads/subscribe requests to express to the receiving
+     *    server to not send events that the client has already seen so far.
+     *
+     * That can be over-ridden by passing in 'true' to `resetTrackedEventCounters`.
+     *
+     */
+    void ClearEventCache(bool resetTrackedEventCounters = false)
+    {
+        mEventDataCache.clear();
+        if (resetTrackedEventCounters)
+        {
+            mHighestReceivedEventNumber.ClearValue();
+        }
+
+        mEventStatusCache.clear();
+    }
+
 private:
     using AttributeState = Variant<System::PacketBufferHandle, StatusIB>;
     // mPendingDataVersion represents a tentative data version for a cluster that we have gotten some reports for.
@@ -342,6 +496,21 @@
             return x.mEndpointId < y.mEndpointId || x.mClusterId < y.mClusterId;
         }
     };
+
+    using EventData = std::pair<EventHeader, System::PacketBufferHandle>;
+
+    //
+    // This is a custom comparator for use with the std::set<EventData> below. Uniqueness
+    // is determined solely by the event number associated with each event.
+    //
+    struct EventDataCompare
+    {
+        bool operator()(const EventData & lhs, const EventData & rhs) const
+        {
+            return (lhs.first.mEventNumber < rhs.first.mEventNumber);
+        }
+    };
+
     /*
      * These functions provide a way to index into the cached state with different sub-sets of a path, returning
      * appropriate slices of the data as requested.
@@ -356,7 +525,9 @@
      */
     EndpointState * GetEndpointState(EndpointId endpointId, CHIP_ERROR & err);
     ClusterState * GetClusterState(EndpointId endpointId, ClusterId clusterId, CHIP_ERROR & err);
-    AttributeState * GetAttributeState(EndpointId endpointId, ClusterId clusterId, AttributeId attributeId, CHIP_ERROR & err);
+    const AttributeState * GetAttributeState(EndpointId endpointId, ClusterId clusterId, AttributeId attributeId, CHIP_ERROR & err);
+
+    const EventData * GetEventData(EventNumber number, CHIP_ERROR & err);
 
     /*
      * Updates the state of an attribute in the cache given a reader. If the reader is null, the state is updated
@@ -364,18 +535,24 @@
      */
     CHIP_ERROR UpdateCache(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus);
 
+    /*
+     * If apData is not null, updates the cached event set with the specified event header + payload.
+     * If apData is null and apStatus is not null, the StatusIB is stored in the event status cache.
+     *
+     * Storage of either of these do not affect pre-existing data for the other events in the cache.
+     *
+     */
+    CHIP_ERROR UpdateEventCache(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus);
+
     //
     // ReadClient::Callback
     //
     void OnReportBegin() override;
     void OnReportEnd() override;
     void OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) override;
-    void OnError(CHIP_ERROR aError) override { mCallback.OnError(aError); }
+    void OnError(CHIP_ERROR aError) override { return mCallback.OnError(aError); }
 
-    void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus) override
-    {
-        mCallback.OnEventData(aEventHeader, apData, apStatus);
-    }
+    void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus) override;
 
     void OnDone() override
     {
@@ -407,6 +584,10 @@
     std::set<ConcreteAttributePath> mChangedAttributeSet;
     std::set<AttributePathParams, Comparator> mRequestPathSet; // wildcard attribute request path only
     std::vector<EndpointId> mAddedEndpoints;
+
+    std::set<EventData, EventDataCompare> mEventDataCache;
+    Optional<EventNumber> mHighestReceivedEventNumber;
+    std::map<ConcreteEventPath, StatusIB> mEventStatusCache;
     BufferedReadCallback mBufferedReader;
     ConcreteClusterPath mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId);
 };
diff --git a/src/app/ConcreteEventPath.h b/src/app/ConcreteEventPath.h
index 92dc185..3909b84 100644
--- a/src/app/ConcreteEventPath.h
+++ b/src/app/ConcreteEventPath.h
@@ -45,6 +45,12 @@
 
     bool operator!=(const ConcreteEventPath & aOther) const { return !(*this == aOther); }
 
+    bool operator<(const ConcreteEventPath & path) const
+    {
+        return (mEndpointId < path.mEndpointId) || ((mEndpointId == path.mEndpointId) && (mClusterId < path.mClusterId)) ||
+            ((mEndpointId == path.mEndpointId) && (mClusterId == path.mClusterId) && (mEventId < path.mEventId));
+    }
+
     EventId mEventId = 0;
 };
 } // namespace app
diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn
index 907bb22..bb9f276 100644
--- a/src/app/tests/BUILD.gn
+++ b/src/app/tests/BUILD.gn
@@ -100,7 +100,7 @@
   #
   if (chip_device_platform != "nrfconnect") {
     test_sources += [ "TestBufferedReadCallback.cpp" ]
-    test_sources += [ "TestAttributeCache.cpp" ]
+    test_sources += [ "TestClusterStateCache.cpp" ]
   }
 
   cflags = [ "-Wconversion" ]
diff --git a/src/app/tests/TestAttributeCache.cpp b/src/app/tests/TestClusterStateCache.cpp
similarity index 96%
rename from src/app/tests/TestAttributeCache.cpp
rename to src/app/tests/TestClusterStateCache.cpp
index 6c10e12..0bca990 100644
--- a/src/app/tests/TestAttributeCache.cpp
+++ b/src/app/tests/TestClusterStateCache.cpp
@@ -23,7 +23,7 @@
 #include "system/SystemPacketBuffer.h"
 #include "system/TLVPacketBufferBackingStore.h"
 #include <app-common/zap-generated/cluster-objects.h>
-#include <app/AttributeCache.h>
+#include <app/ClusterStateCache.h>
 #include <app/data-model/DecodableList.h>
 #include <app/data-model/Decode.h>
 #include <app/tests/AppTestContext.h>
@@ -270,7 +270,7 @@
     callback->OnReportEnd();
 }
 
-class CacheValidator : public AttributeCache::Callback
+class CacheValidator : public ClusterStateCache::Callback
 {
 public:
     CacheValidator(AttributeInstructionListType & instructionList, ForwardedDataCallbackValidator & dataCallbackValidator);
@@ -301,7 +301,7 @@
         }
     }
 
-    void DecodeAttribute(const AttributeInstruction & instruction, const ConcreteAttributePath & path, AttributeCache * cache)
+    void DecodeAttribute(const AttributeInstruction & instruction, const ConcreteAttributePath & path, ClusterStateCache * cache)
     {
         CHIP_ERROR err;
         bool gotStatus = false;
@@ -401,9 +401,10 @@
         }
     }
 
-    void DecodeClusterObject(const AttributeInstruction & instruction, const ConcreteAttributePath & path, AttributeCache * cache)
+    void DecodeClusterObject(const AttributeInstruction & instruction, const ConcreteAttributePath & path,
+                             ClusterStateCache * cache)
     {
-        std::list<AttributeCache::AttributeStatus> statusList;
+        std::list<ClusterStateCache::AttributeStatus> statusList;
         NL_TEST_ASSERT(gSuite, cache->Get(path.mEndpointId, path.mClusterId, clusterValue, statusList) == CHIP_NO_ERROR);
 
         if (instruction.mValueType == AttributeInstruction::kData)
@@ -454,7 +455,7 @@
         }
     }
 
-    void OnAttributeChanged(AttributeCache * cache, const ConcreteAttributePath & path) override
+    void OnAttributeChanged(ClusterStateCache * cache, const ConcreteAttributePath & path) override
     {
         StatusIB status;
 
@@ -482,14 +483,14 @@
         }
     }
 
-    void OnClusterChanged(AttributeCache * cache, EndpointId endpointId, ClusterId clusterId) override
+    void OnClusterChanged(ClusterStateCache * cache, EndpointId endpointId, ClusterId clusterId) override
     {
         auto iter = mExpectedClusters.find(std::make_tuple(endpointId, clusterId));
         NL_TEST_ASSERT(gSuite, iter != mExpectedClusters.end());
         mExpectedClusters.erase(iter);
     }
 
-    void OnEndpointAdded(AttributeCache * cache, EndpointId endpointId) override
+    void OnEndpointAdded(ClusterStateCache * cache, EndpointId endpointId) override
     {
         auto iter = mExpectedEndpoints.find(endpointId);
         NL_TEST_ASSERT(gSuite, iter != mExpectedEndpoints.end());
@@ -538,7 +539,7 @@
 {
     ForwardedDataCallbackValidator dataCallbackValidator;
     CacheValidator client(list, dataCallbackValidator);
-    AttributeCache cache(client);
+    ClusterStateCache cache(client);
     DataSeriesGenerator generator(&cache.GetBufferedCallback(), list);
     generator.Generate(dataCallbackValidator);
 }
@@ -622,7 +623,7 @@
 
 nlTestSuite theSuite =
 {
-    "TestAttributeCache",
+    "TestClusterStateCache",
     &sTests[0],
     TestContext::InitializeAsync,
     TestContext::Finalize
@@ -631,7 +632,7 @@
 }
 // clang-format on
 
-int TestAttributeCache()
+int TestClusterStateCache()
 {
     TestContext gContext;
     gSuite = &theSuite;
@@ -639,4 +640,4 @@
     return (nlTestRunnerStats(&theSuite));
 }
 
-CHIP_REGISTER_TEST_SUITE(TestAttributeCache)
+CHIP_REGISTER_TEST_SUITE(TestClusterStateCache)
diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp
index 09a712c..b1ce32d 100644
--- a/src/controller/CHIPDeviceController.cpp
+++ b/src/controller/CHIPDeviceController.cpp
@@ -1572,7 +1572,7 @@
     }
 }
 
-// AttributeCache::Callback impl
+// ClusterStateCache::Callback impl
 void DeviceCommissioner::OnDone()
 {
     CHIP_ERROR err;
@@ -1835,7 +1835,7 @@
         {
             readParams.mTimeout = timeout.Value();
         }
-        auto attributeCache = Platform::MakeUnique<app::AttributeCache>(*this);
+        auto attributeCache = Platform::MakeUnique<app::ClusterStateCache>(*this);
         auto readClient     = chip::Platform::MakeUnique<app::ReadClient>(
             engine, proxy->GetExchangeManager(), attributeCache->GetBufferedCallback(), app::ReadClient::InteractionType::Read);
         CHIP_ERROR err = readClient->SendRequest(readParams);
diff --git a/src/controller/CHIPDeviceController.h b/src/controller/CHIPDeviceController.h
index 5b5108e..e09490f 100644
--- a/src/controller/CHIPDeviceController.h
+++ b/src/controller/CHIPDeviceController.h
@@ -28,9 +28,9 @@
 
 #pragma once
 
-#include <app/AttributeCache.h>
 #include <app/CASEClientPool.h>
 #include <app/CASESessionManager.h>
+#include <app/ClusterStateCache.h>
 #include <app/OperationalDeviceProxy.h>
 #include <app/OperationalDeviceProxyPool.h>
 #include <controller/AbstractDnssdDiscoveryController.h>
@@ -280,7 +280,7 @@
                                       public Protocols::UserDirectedCommissioning::InstanceNameResolver,
 #endif
                                       public SessionEstablishmentDelegate,
-                                      public app::AttributeCache::Callback
+                                      public app::ClusterStateCache::Callback
 {
 public:
     DeviceCommissioner();
@@ -549,7 +549,7 @@
     void RegisterPairingDelegate(DevicePairingDelegate * pairingDelegate) { mPairingDelegate = pairingDelegate; }
     DevicePairingDelegate * GetPairingDelegate() const { return mPairingDelegate; }
 
-    // AttributeCache::Callback impl
+    // ClusterStateCache::Callback impl
     void OnDone() override;
 
     // Commissioner will establish new device connections after PASE.
@@ -764,7 +764,7 @@
         nullptr; // Commissioning delegate that issued the PerformCommissioningStep command
     CompletionStatus commissioningCompletionStatus;
 
-    Platform::UniquePtr<app::AttributeCache> mAttributeCache;
+    Platform::UniquePtr<app::ClusterStateCache> mAttributeCache;
     Platform::UniquePtr<app::ReadClient> mReadClient;
     Credentials::AttestationVerificationResult mAttestationResult;
 };
diff --git a/src/controller/tests/BUILD.gn b/src/controller/tests/BUILD.gn
index 2d83326..2c5373e 100644
--- a/src/controller/tests/BUILD.gn
+++ b/src/controller/tests/BUILD.gn
@@ -28,6 +28,7 @@
     test_sources += [ "TestServerCommandDispatch.cpp" ]
     test_sources += [ "TestReadChunking.cpp" ]
     test_sources += [ "TestEventChunking.cpp" ]
+    test_sources += [ "TestEventCaching.cpp" ]
     test_sources += [ "TestWriteChunking.cpp" ]
   }
 
diff --git a/src/controller/tests/TestEventCaching.cpp b/src/controller/tests/TestEventCaching.cpp
new file mode 100644
index 0000000..ee6dc6c
--- /dev/null
+++ b/src/controller/tests/TestEventCaching.cpp
@@ -0,0 +1,410 @@
+/*
+ *
+ *    Copyright (c) 2022 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include "app-common/zap-generated/ids/Attributes.h"
+#include "app-common/zap-generated/ids/Clusters.h"
+#include "app/ClusterStateCache.h"
+#include "app/ConcreteAttributePath.h"
+#include "protocols/interaction_model/Constants.h"
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/AppBuildConfig.h>
+#include <app/AttributeAccessInterface.h>
+#include <app/BufferedReadCallback.h>
+#include <app/CommandHandlerInterface.h>
+#include <app/EventLogging.h>
+#include <app/InteractionModelEngine.h>
+#include <app/data-model/Decode.h>
+#include <app/tests/AppTestContext.h>
+#include <app/util/DataModelHandler.h>
+#include <app/util/attribute-storage.h>
+#include <controller/InvokeInteraction.h>
+#include <lib/support/ErrorStr.h>
+#include <lib/support/TimeUtils.h>
+#include <lib/support/UnitTestRegistration.h>
+#include <lib/support/UnitTestUtils.h>
+#include <lib/support/logging/CHIPLogging.h>
+#include <messaging/tests/MessagingContext.h>
+#include <nlunit-test.h>
+
+using namespace chip;
+using namespace chip::app::Clusters;
+
+namespace {
+
+static uint8_t gDebugEventBuffer[4096];
+static uint8_t gInfoEventBuffer[4096];
+static uint8_t gCritEventBuffer[4096];
+static chip::app::CircularEventBuffer gCircularEventBuffer[3];
+
+class TestContext : public chip::Test::AppContext
+{
+public:
+    static int InitializeAsync(void * context)
+    {
+        if (AppContext::InitializeAsync(context) != SUCCESS)
+            return FAILURE;
+
+        auto * ctx = static_cast<TestContext *>(context);
+
+        chip::app::LogStorageResources logStorageResources[] = {
+            { &gDebugEventBuffer[0], sizeof(gDebugEventBuffer), chip::app::PriorityLevel::Debug },
+            { &gInfoEventBuffer[0], sizeof(gInfoEventBuffer), chip::app::PriorityLevel::Info },
+            { &gCritEventBuffer[0], sizeof(gCritEventBuffer), chip::app::PriorityLevel::Critical },
+        };
+
+        chip::app::EventManagement::CreateEventManagement(&ctx->GetExchangeManager(),
+                                                          sizeof(logStorageResources) / sizeof(logStorageResources[0]),
+                                                          gCircularEventBuffer, logStorageResources, nullptr, 0, nullptr);
+
+        return SUCCESS;
+    }
+
+    static int Finalize(void * context)
+    {
+        chip::app::EventManagement::DestroyEventManagement();
+
+        if (AppContext::Finalize(context) != SUCCESS)
+            return FAILURE;
+
+        return SUCCESS;
+    }
+};
+
+nlTestSuite * gSuite = nullptr;
+
+//
+// The generated endpoint_config for the controller app has Endpoint 1
+// already used in the fixed endpoint set of size 1. Consequently, let's use the next
+// number higher than that for our dynamic test endpoint.
+//
+constexpr EndpointId kTestEndpointId = 2;
+
+class TestReadEvents
+{
+public:
+    TestReadEvents() {}
+    static void TestBasicCaching(nlTestSuite * apSuite, void * apContext);
+
+private:
+};
+
+//clang-format off
+DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrs)
+DECLARE_DYNAMIC_ATTRIBUTE_LIST_END();
+
+DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters)
+DECLARE_DYNAMIC_CLUSTER(TestCluster::Id, testClusterAttrs, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END;
+
+DECLARE_DYNAMIC_ENDPOINT(testEndpoint, testEndpointClusters);
+
+//clang-format on
+
+class TestReadCallback : public app::ClusterStateCache::Callback
+{
+public:
+    TestReadCallback() : mClusterCacheAdapter(*this) {}
+    void OnDone() {}
+
+    app::ClusterStateCache mClusterCacheAdapter;
+};
+
+namespace {
+
+void GenerateEvents(nlTestSuite * apSuite, chip::EventNumber & firstEventNumber, chip::EventNumber & lastEventNumber)
+{
+    CHIP_ERROR err                 = CHIP_NO_ERROR;
+    static uint8_t generationCount = 0;
+
+    TestCluster::Events::TestEvent::Type content;
+
+    for (int i = 0; i < 5; i++)
+    {
+        content.arg1 = static_cast<uint8_t>(generationCount++);
+        NL_TEST_ASSERT(apSuite, (err = app::LogEvent(content, kTestEndpointId, lastEventNumber)) == CHIP_NO_ERROR);
+        if (i == 0)
+        {
+            firstEventNumber = lastEventNumber;
+        }
+    }
+}
+
+} // namespace
+
+/*
+ * This validates event caching by forcing a bunch of events to get generated, then reading them back
+ * and upon completion of that operation, iterating over any events that have accumulated in the cache
+ * and validating their contents.
+ *
+ * It then proceeds to do another round of generation and re-do the read, validating that the old and new
+ * events are present in the cache.
+ *
+ */
+void TestReadEvents::TestBasicCaching(nlTestSuite * apSuite, void * apContext)
+{
+    TestContext & ctx                    = *static_cast<TestContext *>(apContext);
+    auto sessionHandle                   = ctx.GetSessionBobToAlice();
+    app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
+
+    // Initialize the ember side server logic
+    InitDataModelHandler(&ctx.GetExchangeManager());
+
+    // Register our fake dynamic endpoint.
+    DataVersion dataVersionStorage[ArraySize(testEndpointClusters)];
+    emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage));
+
+    chip::EventNumber firstEventNumber;
+    chip::EventNumber lastEventNumber;
+
+    GenerateEvents(apSuite, firstEventNumber, lastEventNumber);
+
+    app::EventPathParams eventPath;
+    eventPath.mEndpointId = kTestEndpointId;
+    eventPath.mClusterId  = app::Clusters::TestCluster::Id;
+    app::ReadPrepareParams readParams(sessionHandle);
+
+    readParams.mpEventPathParamsList    = &eventPath;
+    readParams.mEventPathParamsListSize = 1;
+    readParams.mEventNumber             = firstEventNumber;
+
+    TestReadCallback readCallback;
+
+    {
+        app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mClusterCacheAdapter.GetBufferedCallback(),
+                                   app::ReadClient::InteractionType::Read);
+
+        NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR);
+
+        ctx.DrainAndServiceIO();
+
+        uint8_t generationCount = 0;
+        readCallback.mClusterCacheAdapter.ForEachEventData(
+            [&apSuite, &readCallback, &generationCount](const app::EventHeader & header) {
+                NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == TestCluster::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEventId == TestCluster::Events::TestEvent::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId);
+
+                TestCluster::Events::TestEvent::DecodableType eventData;
+                NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) == CHIP_NO_ERROR);
+
+                NL_TEST_ASSERT(apSuite, eventData.arg1 == generationCount);
+                generationCount++;
+                return CHIP_NO_ERROR;
+            });
+
+        NL_TEST_ASSERT(apSuite, generationCount == 5);
+
+        //
+        // Re-run the iterator but pass in a path filter: EP*/TestCluster/EID*
+        //
+        generationCount = 0;
+        readCallback.mClusterCacheAdapter.ForEachEventData(
+            [&apSuite, &readCallback, &generationCount](const app::EventHeader & header) {
+                NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == TestCluster::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEventId == TestCluster::Events::TestEvent::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId);
+
+                TestCluster::Events::TestEvent::DecodableType eventData;
+                NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) == CHIP_NO_ERROR);
+
+                NL_TEST_ASSERT(apSuite, eventData.arg1 == generationCount);
+                generationCount++;
+                return CHIP_NO_ERROR;
+            },
+            app::EventPathParams(kInvalidEndpointId, TestCluster::Id, kInvalidEventId));
+
+        NL_TEST_ASSERT(apSuite, generationCount == 5);
+
+        //
+        // Re-run the iterator but pass in a path filter: EP*/TestCluster/TestEvent
+        //
+        generationCount = 0;
+        readCallback.mClusterCacheAdapter.ForEachEventData(
+            [&apSuite, &readCallback, &generationCount](const app::EventHeader & header) {
+                NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == TestCluster::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEventId == TestCluster::Events::TestEvent::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId);
+
+                TestCluster::Events::TestEvent::DecodableType eventData;
+                NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) == CHIP_NO_ERROR);
+
+                NL_TEST_ASSERT(apSuite, eventData.arg1 == generationCount);
+                generationCount++;
+                return CHIP_NO_ERROR;
+            },
+            app::EventPathParams(kInvalidEndpointId, TestCluster::Id, TestCluster::Events::TestEvent::Id));
+
+        NL_TEST_ASSERT(apSuite, generationCount == 5);
+
+        //
+        // Re-run the iterator but pass in a min event number filter (EventNumber = 1). We should only receive 4 events.
+        //
+        generationCount = 1;
+        readCallback.mClusterCacheAdapter.ForEachEventData(
+            [&apSuite, &readCallback, &generationCount](const app::EventHeader & header) {
+                NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == TestCluster::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEventId == TestCluster::Events::TestEvent::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId);
+
+                TestCluster::Events::TestEvent::DecodableType eventData;
+                NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) == CHIP_NO_ERROR);
+
+                NL_TEST_ASSERT(apSuite, eventData.arg1 == generationCount);
+                generationCount++;
+                return CHIP_NO_ERROR;
+            },
+            app::EventPathParams(), 1);
+
+        NL_TEST_ASSERT(apSuite, generationCount == 5);
+
+        //
+        // Re-run the iterator but pass in a min event number filter (EventNumber = 1) AND a path filter. We should only receive 4
+        // events.
+        //
+        generationCount = 1;
+        readCallback.mClusterCacheAdapter.ForEachEventData(
+            [&apSuite, &readCallback, &generationCount](const app::EventHeader & header) {
+                NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == TestCluster::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEventId == TestCluster::Events::TestEvent::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId);
+
+                TestCluster::Events::TestEvent::DecodableType eventData;
+                NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) == CHIP_NO_ERROR);
+
+                NL_TEST_ASSERT(apSuite, eventData.arg1 == generationCount);
+                generationCount++;
+                return CHIP_NO_ERROR;
+            },
+            app::EventPathParams(kInvalidEndpointId, TestCluster::Id, kInvalidEventId), 1);
+
+        NL_TEST_ASSERT(apSuite, generationCount == 5);
+    }
+
+    //
+    // Generate more events.
+    //
+    GenerateEvents(apSuite, firstEventNumber, lastEventNumber);
+
+    {
+        app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mClusterCacheAdapter.GetBufferedCallback(),
+                                   app::ReadClient::InteractionType::Read);
+
+        NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR);
+
+        ctx.DrainAndServiceIO();
+
+        //
+        // Validate that we still have all 5 of the old events we received, as well as the new ones that just got generated.
+        // This also ensures that we don't receive duplicate events in the `ForEachEventData` call below.
+        //
+        uint8_t generationCount = 0;
+        readCallback.mClusterCacheAdapter.ForEachEventData(
+            [&apSuite, &readCallback, &generationCount](const app::EventHeader & header) {
+                NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == TestCluster::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEventId == TestCluster::Events::TestEvent::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId);
+
+                TestCluster::Events::TestEvent::DecodableType eventData;
+                NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) == CHIP_NO_ERROR);
+
+                NL_TEST_ASSERT(apSuite, eventData.arg1 == generationCount);
+                generationCount++;
+
+                return CHIP_NO_ERROR;
+            });
+
+        NL_TEST_ASSERT(apSuite, generationCount == 10);
+
+        readCallback.mClusterCacheAdapter.ClearEventCache();
+        generationCount = 0;
+        readCallback.mClusterCacheAdapter.ForEachEventData([&generationCount](const app::EventHeader & header) {
+            generationCount++;
+            return CHIP_NO_ERROR;
+        });
+
+        NL_TEST_ASSERT(apSuite, generationCount == 0);
+    }
+
+    //
+    // Clear out the event cache and set its highest received event number to a non zero value. Validate that
+    // we don't receive events lower than that value.
+    //
+    {
+        app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mClusterCacheAdapter.GetBufferedCallback(),
+                                   app::ReadClient::InteractionType::Read);
+
+        readCallback.mClusterCacheAdapter.ClearEventCache();
+        readCallback.mClusterCacheAdapter.SetHighestReceivedEventNumber(3);
+
+        NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR);
+
+        ctx.DrainAndServiceIO();
+
+        uint8_t generationCount = 4;
+        readCallback.mClusterCacheAdapter.ForEachEventData(
+            [&apSuite, &readCallback, &generationCount](const app::EventHeader & header) {
+                NL_TEST_ASSERT(apSuite, header.mPath.mClusterId == TestCluster::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEventId == TestCluster::Events::TestEvent::Id);
+                NL_TEST_ASSERT(apSuite, header.mPath.mEndpointId == kTestEndpointId);
+
+                TestCluster::Events::TestEvent::DecodableType eventData;
+                NL_TEST_ASSERT(apSuite, readCallback.mClusterCacheAdapter.Get(header.mEventNumber, eventData) == CHIP_NO_ERROR);
+
+                NL_TEST_ASSERT(apSuite, eventData.arg1 == generationCount);
+                generationCount++;
+
+                return CHIP_NO_ERROR;
+            });
+
+        NL_TEST_ASSERT(apSuite, generationCount == 10);
+    }
+
+    NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+
+    emberAfClearDynamicEndpoint(0);
+}
+
+// clang-format off
+const nlTest sTests[] =
+{
+    NL_TEST_DEF("TestBasicCaching", TestReadEvents::TestBasicCaching),
+    NL_TEST_SENTINEL()
+};
+
+// clang-format on
+
+// clang-format off
+nlTestSuite sSuite =
+{
+    "TestEventCaching",
+    &sTests[0],
+    TestContext::InitializeAsync,
+    TestContext::Finalize
+};
+// clang-format on
+
+} // namespace
+
+int TestEventCaching()
+{
+    TestContext gContext;
+    gSuite = &sSuite;
+    nlTestRunner(&sSuite, &gContext);
+    return (nlTestRunnerStats(&sSuite));
+}
+
+CHIP_REGISTER_TEST_SUITE(TestEventCaching)
diff --git a/src/controller/tests/data_model/TestRead.cpp b/src/controller/tests/data_model/TestRead.cpp
index 3bb43c7..295ad93 100644
--- a/src/controller/tests/data_model/TestRead.cpp
+++ b/src/controller/tests/data_model/TestRead.cpp
@@ -18,7 +18,7 @@
 
 #include "transport/SecureSession.h"
 #include <app-common/zap-generated/cluster-objects.h>
-#include <app/AttributeCache.h>
+#include <app/ClusterStateCache.h>
 #include <app/InteractionModelEngine.h>
 #include <app/tests/AppTestContext.h>
 #include <app/util/mock/Constants.h>
@@ -221,7 +221,7 @@
 
 TestReadInteraction gTestReadInteraction;
 
-class MockInteractionModelApp : public chip::app::AttributeCache::Callback
+class MockInteractionModelApp : public chip::app::ClusterStateCache::Callback
 {
 public:
     void OnEventData(const chip::app::EventHeader & aEventHeader, chip::TLV::TLVReader * apData,
@@ -361,7 +361,7 @@
     responseDirective = kSendDataResponse;
 
     MockInteractionModelApp delegate;
-    chip::app::AttributeCache cache(delegate);
+    chip::app::ClusterStateCache cache(delegate);
     auto * engine = chip::app::InteractionModelEngine::GetInstance();
     err           = engine->Init(&ctx.GetExchangeManager());
     NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
@@ -388,7 +388,7 @@
     // Expect no versions would be cached.
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams1[3];
@@ -424,7 +424,7 @@
     // previous test. Expect cache E2C2 version
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams2[2];
@@ -457,7 +457,7 @@
     // path intersects with previous cached data version Expect no E2C3 attributes in report, only E3C2A1 attribute in report
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams1[3];
@@ -494,7 +494,7 @@
     // intersects with previous cached data version Expect no C1 attributes in report, only E3C2A2 attribute in report
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams2[2];
@@ -529,7 +529,7 @@
     // report, and invalidate the cached pending and committed data version since no wildcard attributes exists in mRequestPathSet.
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams1[3];
@@ -566,7 +566,7 @@
     // cache any committed data version. Expect E2C3A1, E2C3A2 and E3C2A2 attribute in report
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams1[3];
@@ -603,7 +603,7 @@
     // Expect E2C3A* attributes in report, and E3C2A2 attribute in report and cache latest data version
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams2[2];
@@ -636,7 +636,7 @@
     // E2C3A* attributes in report, and E3C2A2 attribute in report
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
         chip::app::AttributePathParams attributePathParams2[2];
@@ -676,7 +676,7 @@
     // changed in server. Expect E1C2A* and C2C3A* and E2C2A* attributes in report, and cache their versions
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
 
@@ -720,7 +720,7 @@
     // filter with only C2. Expect E1C2A*, E2C2A* attributes(7 attributes) in report,
     {
         testId++;
-        ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId);
+        ChipLogProgress(DataManagement, "\t -- Running Read with ClusterStateCache Test ID %d", testId);
         app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(),
                                    cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
 
diff --git a/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer.mm b/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer.mm
index 6b06037..9a7df6d 100644
--- a/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer.mm
+++ b/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer.mm
@@ -53,7 +53,7 @@
 }
 
 static CHIP_ERROR AppendAttibuteValueToArray(
-    const chip::app::ConcreteAttributePath & path, chip::app::AttributeCache * cache, NSMutableArray * array)
+    const chip::app::ConcreteAttributePath & path, chip::app::ClusterStateCache * cache, NSMutableArray * array)
 {
     chip::TLV::TLVReader reader;
     CHIP_ERROR err = cache->Get(path, reader);
diff --git a/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer_Internal.h b/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer_Internal.h
index 9e58fc1..cb7bf68 100644
--- a/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer_Internal.h
+++ b/src/darwin/Framework/CHIP/CHIPAttributeCacheContainer_Internal.h
@@ -20,13 +20,13 @@
 #import "CHIPAttributeCacheContainer.h"
 #import "CHIPDeviceControllerOverXPC.h"
 
-#include <app/AttributeCache.h>
+#include <app/ClusterStateCache.h>
 
 NS_ASSUME_NONNULL_BEGIN
 
 @interface CHIPAttributeCacheContainer ()
 
-@property (atomic, readwrite, nullable) chip::app::AttributeCache * cppAttributeCache;
+@property (atomic, readwrite, nullable) chip::app::ClusterStateCache * cppAttributeCache;
 @property (nonatomic, readwrite) uint64_t deviceId;
 @property (nonatomic, readwrite, weak, nullable) CHIPDeviceControllerXPCConnection * xpcConnection;
 @property (nonatomic, readwrite, strong, nullable) id<NSCopying> xpcControllerId;
diff --git a/src/darwin/Framework/CHIP/CHIPDevice.mm b/src/darwin/Framework/CHIP/CHIPDevice.mm
index a46ec0d..b8800ac 100644
--- a/src/darwin/Framework/CHIP/CHIPDevice.mm
+++ b/src/darwin/Framework/CHIP/CHIPDevice.mm
@@ -27,9 +27,9 @@
 #include "lib/core/CHIPError.h"
 #include "lib/core/DataModelTypes.h"
 
-#include <app/AttributeCache.h>
 #include <app/AttributePathParams.h>
 #include <app/BufferedReadCallback.h>
+#include <app/ClusterStateCache.h>
 #include <app/InteractionModelEngine.h>
 #include <app/ReadClient.h>
 #include <app/util/error-mapping.h>
@@ -238,7 +238,7 @@
 
 namespace {
 
-class SubscriptionCallback final : public AttributeCache::Callback {
+class SubscriptionCallback final : public ClusterStateCache::Callback {
 public:
     SubscriptionCallback(dispatch_queue_t queue, ReportCallback reportCallback,
         SubscriptionEstablishedHandler _Nullable subscriptionEstablishedHandler)
@@ -263,7 +263,7 @@
 
     // We need to exist to get a ReadClient, so can't take this as a constructor argument.
     void AdoptReadClient(std::unique_ptr<ReadClient> aReadClient) { mReadClient = std::move(aReadClient); }
-    void AdoptAttributeCache(std::unique_ptr<AttributeCache> aAttributeCache) { mAttributeCache = std::move(aAttributeCache); }
+    void AdoptAttributeCache(std::unique_ptr<ClusterStateCache> aAttributeCache) { mAttributeCache = std::move(aAttributeCache); }
 
 private:
     void OnReportBegin() override;
@@ -306,7 +306,7 @@
     //    error callback, but not both, by tracking whether we have a queued-up
     //    deletion.
     std::unique_ptr<ReadClient> mReadClient;
-    std::unique_ptr<AttributeCache> mAttributeCache;
+    std::unique_ptr<ClusterStateCache> mAttributeCache;
     bool mHaveQueuedDeletion = false;
     void (^mOnDoneHandler)(void) = nil;
 };
@@ -341,7 +341,7 @@
 
     std::unique_ptr<SubscriptionCallback> callback;
     std::unique_ptr<ReadClient> readClient;
-    std::unique_ptr<AttributeCache> attributeCache;
+    std::unique_ptr<ClusterStateCache> attributeCache;
     if (attributeCacheContainer) {
         __weak CHIPAttributeCacheContainer * weakPtr = attributeCacheContainer;
         callback = std::make_unique<SubscriptionCallback>(queue, reportHandler, subscriptionEstablishedHandler, ^{
@@ -350,7 +350,7 @@
                 container.cppAttributeCache = nullptr;
             }
         });
-        attributeCache = std::make_unique<AttributeCache>(*callback.get());
+        attributeCache = std::make_unique<ClusterStateCache>(*callback.get());
         readClient = std::make_unique<ReadClient>(InteractionModelEngine::GetInstance(), device->GetExchangeManager(),
             attributeCache->GetBufferedCallback(), ReadClient::InteractionType::Subscribe);
     } else {
@@ -378,7 +378,7 @@
 
     if (attributeCacheContainer) {
         attributeCacheContainer.cppAttributeCache = attributeCache.get();
-        // AttributeCache will be deleted when OnDone is called or an error is encountered as well.
+        // ClusterStateCache will be deleted when OnDone is called or an error is encountered as well.
         callback->AdoptAttributeCache(std::move(attributeCache));
     }
     // Callback and ReadClient will be deleted when OnDone is called or an error is