BDX TransferSession spec implementation (#4473)

* First commit for BDX message utilities

- establish directory for BDX
- add BDXMessageUtils files
- add unit tests for BDXMessageUtils
- edit BUILD.gn files to build BDX directory and tests

* First commit for BDX message utilities

- establish directory for BDX
- add BDXMessageUtils files
- add unit tests for BDXMessageUtils
- edit BUILD.gn files to build BDX directory and tests

* move bdx directory to src/protocols

* fix BufferReader usage, style mistakes

* remove transport/bdx and fix build files

* remove unused header

* adding State files to BDX directory

* First commit for BDX message utilities

- establish directory for BDX
- add BDXMessageUtils files
- add unit tests for BDXMessageUtils
- edit BUILD.gn files to build BDX directory and tests

* move bdx directory to src/protocols

* fix BufferReader usage, style mistakes

* First commit for BDX message utilities

- establish directory for BDX
- add BDXMessageUtils files
- add unit tests for BDXMessageUtils
- edit BUILD.gn files to build BDX directory and tests

* remove transport/bdx and fix build files

* remove unused header

* adding State files to BDX directory

* WIP: integrating BdxMessages back into this branch

* Redesign BdxTransferSession API around StartTransfer, WaitForTransfer

* Delete old files BDXState + BDXMessageUtils

* Implementation for StartTransfer and unit test

* write full exchange test up to first BlockQuery

- add test logic for exchange up to first BlockQuery message (failing
here)
- fix virtual destructor errors
- add helper method for writing BDX messages to packetbuffer

* add methods for verifying transfer control options

also add delegate methods for file descriptor and metadata

* Full overhaul of TransferSession using EmitOutput()

also includes two tests for receiver drive and sender drive

* rename MessageType enum values

* remove reference to kProtocol_StatusReport and add TODO

* remove accidental change to src/BUILD.gn

* fix accidental third party repo changes

* restyling

* fix struct initialization compiler errors

* QEMU compilation fix

* initialize anonymous union to fix -Werror=uninitialized

* replace std::move() calls with Retain(), fix OutputEvent initialization

* remove brace-enclosed initializer lists

* replace Retain() with std::move() when possible

* Address comments from Tennessee:

- lowercase bdx namespace
- remove PrepareBlockAckEOF and combine in PrepareBlockAck
- add test helper for verifying output message type
- change test metadata to TLV format

* add checks for invalid AcceptTransfer data and add tests

* fix build after merge

* add check for transfer timeout, add timestamps to API, add timeout test

* fix build: add SystemPacketBuffer.h include

* change NewWithAvailableSize() -> New()

* add helper test function VerifyNoMoreOutput()

* fix block counter tracking, add test conditions for counters

- also add test helper SendAndVerifyTransferInit

* change OutputEventFlags (bitfield) to OutputEventType (flat enum)

* Update src/protocols/bdx/BdxTransferSession.h

Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>

* use strlen instead of sizeof

* use and enforce metadata with top-level list (path)

* add BdxMessage struct interface for common method calls

* change kTLVType_Path to _List

* addressing many of Tennessee's comments:

- rename remove Output from kOutput_ enums
- add AvailableDataLength() check when writing message to buffer
- fix timeout check
- fix payloadHeader.Encode() call, use headerSize

* remove "State_" prefix from TransferStates enum

- also rename from Idle to Unititalized

* rename TransferStates -> TransferState

* restyling: whitespace

* Use PacketBufBound instead of PacketBufferHandle::New()

* Implement StatusReport handling and sending with tests

* implement AbortTransfer() and update comments related to StatusReport

* combine vendorID and profileID in StatusReport handling

Co-authored-by: Justin Wood <woody@apple.com>
Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>
diff --git a/src/protocols/bdx/BUILD.gn b/src/protocols/bdx/BUILD.gn
index ddc63fb..e8ca3f1 100644
--- a/src/protocols/bdx/BUILD.gn
+++ b/src/protocols/bdx/BUILD.gn
@@ -20,6 +20,8 @@
   sources = [
     "BdxMessages.cpp",
     "BdxMessages.h",
+    "BdxTransferSession.cpp",
+    "BdxTransferSession.h",
   ]
 
   cflags = [ "-Wconversion" ]
@@ -28,5 +30,6 @@
     "${chip_root}/src/lib/core",
     "${chip_root}/src/lib/support",
     "${chip_root}/src/system",
+    "${chip_root}/src/transport/raw",
   ]
 }
diff --git a/src/protocols/bdx/BdxMessages.cpp b/src/protocols/bdx/BdxMessages.cpp
index 7f6b7d6..8d00abb 100644
--- a/src/protocols/bdx/BdxMessages.cpp
+++ b/src/protocols/bdx/BdxMessages.cpp
@@ -35,12 +35,12 @@
 } // namespace
 
 using namespace chip;
-using namespace chip::BDX;
+using namespace chip::bdx;
 using namespace chip::Encoding::LittleEndian;
 
 // WARNING: this function should never return early, since MessageSize() relies on it to calculate
 // the size of the message (even if the message is incomplete or filled out incorrectly).
-BufBound & TransferInit::WriteToBuffer(BufBound & aBuffer) const
+BufBound & TransferInit::DerivedWriteToBuffer(BufBound & aBuffer) const
 {
     uint8_t proposedTransferCtl = 0;
     bool widerange = (StartOffset > std::numeric_limits<uint32_t>::max()) || (MaxLength > std::numeric_limits<uint32_t>::max());
@@ -49,9 +49,9 @@
     proposedTransferCtl = proposedTransferCtl | TransferCtlOptions.Raw();
 
     BitFlags<uint8_t, RangeControlFlags> rangeCtlFlags;
-    rangeCtlFlags.Set(kDefLen, MaxLength > 0);
-    rangeCtlFlags.Set(kStartOffset, StartOffset > 0);
-    rangeCtlFlags.Set(kWiderange, widerange);
+    rangeCtlFlags.Set(kRange_DefLen, MaxLength > 0);
+    rangeCtlFlags.Set(kRange_StartOffset, StartOffset > 0);
+    rangeCtlFlags.Set(kRange_Widerange, widerange);
 
     aBuffer.Put(proposedTransferCtl);
     aBuffer.Put(rangeCtlFlags.Raw());
@@ -94,7 +94,7 @@
     return aBuffer;
 }
 
-CHIP_ERROR TransferInit::Parse(System::PacketBufferHandle aBuffer)
+CHIP_ERROR TransferInit::DerivedParse(System::PacketBufferHandle aBuffer)
 {
     CHIP_ERROR err = CHIP_NO_ERROR;
     uint8_t proposedTransferCtl;
@@ -111,9 +111,9 @@
     rangeCtlFlags.SetRaw(rangeCtl);
 
     StartOffset = 0;
-    if (rangeCtlFlags.Has(kStartOffset))
+    if (rangeCtlFlags.Has(kRange_StartOffset))
     {
-        if (rangeCtlFlags.Has(kWiderange))
+        if (rangeCtlFlags.Has(kRange_Widerange))
         {
             SuccessOrExit(bufReader.Read64(&StartOffset).StatusCode());
         }
@@ -125,9 +125,9 @@
     }
 
     MaxLength = 0;
-    if (rangeCtlFlags.Has(kDefLen))
+    if (rangeCtlFlags.Has(kRange_DefLen))
     {
-        if (rangeCtlFlags.Has(kWiderange))
+        if (rangeCtlFlags.Has(kRange_Widerange))
         {
             SuccessOrExit(bufReader.Read64(&MaxLength).StatusCode());
         }
@@ -164,7 +164,7 @@
     return err;
 }
 
-size_t TransferInit::MessageSize() const
+size_t TransferInit::DerivedMessageSize() const
 {
     BufBound emptyBuf(nullptr, 0);
     return WriteToBuffer(emptyBuf).Needed();
@@ -196,7 +196,7 @@
 
 // WARNING: this function should never return early, since MessageSize() relies on it to calculate
 // the size of the message (even if the message is incomplete or filled out incorrectly).
-BufBound & SendAccept::WriteToBuffer(BufBound & aBuffer) const
+BufBound & SendAccept::DerivedWriteToBuffer(BufBound & aBuffer) const
 {
     uint8_t transferCtl = 0;
 
@@ -213,7 +213,7 @@
     return aBuffer;
 }
 
-CHIP_ERROR SendAccept::Parse(System::PacketBufferHandle aBuffer)
+CHIP_ERROR SendAccept::DerivedParse(System::PacketBufferHandle aBuffer)
 {
     CHIP_ERROR err      = CHIP_NO_ERROR;
     uint8_t transferCtl = 0;
@@ -247,7 +247,7 @@
     return err;
 }
 
-size_t SendAccept::MessageSize() const
+size_t SendAccept::DerivedMessageSize() const
 {
     BufBound emptyBuf(nullptr, 0);
     return WriteToBuffer(emptyBuf).Needed();
@@ -272,7 +272,7 @@
 
 // WARNING: this function should never return early, since MessageSize() relies on it to calculate
 // the size of the message (even if the message is incomplete or filled out incorrectly).
-BufBound & ReceiveAccept::WriteToBuffer(BufBound & aBuffer) const
+BufBound & ReceiveAccept::DerivedWriteToBuffer(BufBound & aBuffer) const
 {
     uint8_t transferCtl = 0;
     bool widerange      = (StartOffset > std::numeric_limits<uint32_t>::max()) || (Length > std::numeric_limits<uint32_t>::max());
@@ -281,9 +281,9 @@
     transferCtl = transferCtl | TransferCtlFlags.Raw();
 
     BitFlags<uint8_t, RangeControlFlags> rangeCtlFlags;
-    rangeCtlFlags.Set(kDefLen, Length > 0);
-    rangeCtlFlags.Set(kStartOffset, StartOffset > 0);
-    rangeCtlFlags.Set(kWiderange, widerange);
+    rangeCtlFlags.Set(kRange_DefLen, Length > 0);
+    rangeCtlFlags.Set(kRange_StartOffset, StartOffset > 0);
+    rangeCtlFlags.Set(kRange_Widerange, widerange);
 
     aBuffer.Put(transferCtl);
     aBuffer.Put(rangeCtlFlags.Raw());
@@ -320,7 +320,7 @@
     return aBuffer;
 }
 
-CHIP_ERROR ReceiveAccept::Parse(System::PacketBufferHandle aBuffer)
+CHIP_ERROR ReceiveAccept::DerivedParse(System::PacketBufferHandle aBuffer)
 {
     CHIP_ERROR err          = CHIP_NO_ERROR;
     uint8_t transferCtl     = 0;
@@ -340,9 +340,9 @@
     rangeCtlFlags.SetRaw(rangeCtl);
 
     StartOffset = 0;
-    if (rangeCtlFlags.Has(kStartOffset))
+    if (rangeCtlFlags.Has(kRange_StartOffset))
     {
-        if (rangeCtlFlags.Has(kWiderange))
+        if (rangeCtlFlags.Has(kRange_Widerange))
         {
             SuccessOrExit(bufReader.Read64(&StartOffset).StatusCode());
         }
@@ -354,9 +354,9 @@
     }
 
     Length = 0;
-    if (rangeCtlFlags.Has(kDefLen))
+    if (rangeCtlFlags.Has(kRange_DefLen))
     {
-        if (rangeCtlFlags.Has(kWiderange))
+        if (rangeCtlFlags.Has(kRange_Widerange))
         {
             SuccessOrExit(bufReader.Read64(&Length).StatusCode());
         }
@@ -387,7 +387,7 @@
     return err;
 }
 
-size_t ReceiveAccept::MessageSize() const
+size_t ReceiveAccept::DerivedMessageSize() const
 {
     BufBound emptyBuf(nullptr, 0);
     return WriteToBuffer(emptyBuf).Needed();
@@ -413,19 +413,19 @@
 
 // WARNING: this function should never return early, since MessageSize() relies on it to calculate
 // the size of the message (even if the message is incomplete or filled out incorrectly).
-BufBound & CounterMessage::WriteToBuffer(BufBound & aBuffer) const
+BufBound & CounterMessage::DerivedWriteToBuffer(BufBound & aBuffer) const
 {
     return aBuffer.Put32(BlockCounter);
 }
 
-CHIP_ERROR CounterMessage::Parse(System::PacketBufferHandle aBuffer)
+CHIP_ERROR CounterMessage::DerivedParse(System::PacketBufferHandle aBuffer)
 {
     uint8_t * bufStart = aBuffer->Start();
     Reader bufReader(bufStart, aBuffer->DataLength());
     return bufReader.Read32(&BlockCounter).StatusCode();
 }
 
-size_t CounterMessage::MessageSize() const
+size_t CounterMessage::DerivedMessageSize() const
 {
     BufBound emptyBuf(nullptr, 0);
     return WriteToBuffer(emptyBuf).Needed();
@@ -438,7 +438,7 @@
 
 // WARNING: this function should never return early, since MessageSize() relies on it to calculate
 // the size of the message (even if the message is incomplete or filled out incorrectly).
-BufBound & DataBlock::WriteToBuffer(BufBound & aBuffer) const
+BufBound & DataBlock::DerivedWriteToBuffer(BufBound & aBuffer) const
 {
     aBuffer.Put32(BlockCounter);
     if (Data != nullptr)
@@ -448,7 +448,7 @@
     return aBuffer;
 }
 
-CHIP_ERROR DataBlock::Parse(System::PacketBufferHandle aBuffer)
+CHIP_ERROR DataBlock::DerivedParse(System::PacketBufferHandle aBuffer)
 {
     CHIP_ERROR err     = CHIP_NO_ERROR;
     uint8_t * bufStart = aBuffer->Start();
@@ -476,7 +476,7 @@
     return err;
 }
 
-size_t DataBlock::MessageSize() const
+size_t DataBlock::DerivedMessageSize() const
 {
     BufBound emptyBuf(nullptr, 0);
     return WriteToBuffer(emptyBuf).Needed();
diff --git a/src/protocols/bdx/BdxMessages.h b/src/protocols/bdx/BdxMessages.h
index 061888b..79920e7 100644
--- a/src/protocols/bdx/BdxMessages.h
+++ b/src/protocols/bdx/BdxMessages.h
@@ -28,65 +28,111 @@
 #include <support/BufBound.h>
 #include <support/CodeUtils.h>
 #include <system/SystemPacketBuffer.h>
-
 namespace chip {
-namespace BDX {
+namespace bdx {
+
+enum MessageType : uint8_t
+{
+    kBdxMsg_SendInit      = 0x01,
+    kBdxMsg_SendAccept    = 0x02,
+    kBdxMsg_ReceiveInit   = 0x04,
+    kBdxMsg_ReceiveAccept = 0x05,
+    kBdxMsg_BlockQuery    = 0x10,
+    kBdxMsg_Block         = 0x11,
+    kBdxMsg_BlockEOF      = 0x12,
+    kBdxMsg_BlockAck      = 0x13,
+    kBdxMsg_BlockAckEOF   = 0x14,
+};
+
+enum StatusCode : uint16_t
+{
+    kStatus_None                       = 0x0000,
+    kStatus_Overflow                   = 0x0011,
+    kStatus_LengthTooLarge             = 0x0012,
+    kStatus_LengthTooShort             = 0x0013,
+    kStatus_LengthMismatch             = 0x0014,
+    kStatus_LengthRequired             = 0x0015,
+    kStatus_BadMessageContents         = 0x0016,
+    kStatus_BadBlockCounter            = 0x0017,
+    kStatus_TransferFailedUnknownError = 0x001F,
+    kStatus_ServerBadState             = 0x0020,
+    kStatus_FailureToSend              = 0x0021,
+    kStatus_TransferMethodNotSupported = 0x0050,
+    kStatus_FileDesignatorUnknown      = 0x0051,
+    kStatus_StartOffsetNotSupported    = 0x0052,
+    kStatus_VersionNotSupported        = 0x0053,
+    kStatus_Unknown                    = 0x005F,
+};
 
 enum TransferControlFlags : uint8_t
 {
     // first 4 bits reserved for version
-    kSenderDrive   = (1U << 4),
-    kReceiverDrive = (1U << 5),
-    kAsync         = (1U << 6),
+    kControl_SenderDrive   = (1U << 4),
+    kControl_ReceiverDrive = (1U << 5),
+    kControl_Async         = (1U << 6),
 };
 
 enum RangeControlFlags : uint8_t
 {
-    kDefLen      = (1U),
-    kStartOffset = (1U << 1),
-    kWiderange   = (1U << 4),
+    kRange_DefLen      = (1U),
+    kRange_StartOffset = (1U << 1),
+    kRange_Widerange   = (1U << 4),
 };
 
-/*
- * A structure for representing a SendInit or ReceiveInit message (both contain
- * identical parameters).
+/**
+ * @brief
+ *   Interface for defining methods that apply to all BDX messages.
  */
-struct TransferInit
+struct BdxMessage
 {
     /**
      * @brief
+     *  Parse data from an PacketBuffer into a BdxMessage struct.
+     *
+     *  Note that this may store pointers that point into the passed PacketBuffer,
+     *  so it is essential that the underlying PacketBuffer is not modified until after this
+     *  struct is no longer needed.
+     *
+     * @param[in] aBuffer A PacketBufferHandle with a refernce to the PacketBuffer containing the data.
+     *
+     * @return CHIP_ERROR Return an error if the message format is invalid and/or can't be parsed
+     */
+    CHECK_RETURN_VALUE
+    CHIP_ERROR Parse(System::PacketBufferHandle aBuffer) { return DerivedParse(std::move(aBuffer)); }
+
+    /**
+     * @brief
      *  Write the message fields to a buffer using the provided BufBound.
      *
      *  It is up to the caller to use BufBound::Fit() to verify that the write was
      *  successful. This method will also not check for correctness or completeness for
      *  any of the fields - it is the caller's responsibility to ensure that the fields
-     *  have been filled out adequately.
+     *  align with BDX specifications.
      *
      * @param aBuffer A BufBound object that will be used to write the message
      */
-    BufBound & WriteToBuffer(BufBound & aBuffer) const;
+    BufBound & WriteToBuffer(BufBound & aBuffer) const { return DerivedWriteToBuffer(aBuffer); }
 
     /**
      * @brief
-     *  Parse data from an PacketBuffer into a struct instance.
-     *
-     *  Note that this struct will store pointers that point into the passed PacketBuffer,
-     *  so it is essential that the PacketBuffer is not modified until after this
-     *  struct is no longer needed.
-     *
-     * @param[in] aBuffer Pointer to a PacketBuffer containing the data.
-     *
-     * @return CHIP_ERROR Any error that occurs when trying to read the message
+     *  Returns the size of buffer needed to write the message.
      */
-    CHECK_RETURN_VALUE
-    CHIP_ERROR Parse(System::PacketBufferHandle aBuffer);
+    virtual size_t MessageSize() const { return DerivedMessageSize(); }
 
-    /**
-     * @brief
-     *  Returns the size of buffer needed to write this message.
-     */
-    size_t MessageSize() const;
+    virtual ~BdxMessage() = default;
 
+private:
+    virtual CHIP_ERROR DerivedParse(System::PacketBufferHandle aBuffer) = 0;
+    virtual BufBound & DerivedWriteToBuffer(BufBound & aBuffer) const   = 0;
+    virtual size_t DerivedMessageSize() const                           = 0;
+};
+
+/*
+ * A structure for representing a SendInit or ReceiveInit message (both contain
+ * identical parameters).
+ */
+struct TransferInit : public BdxMessage
+{
     /**
      * @brief
      *  Equality check method.
@@ -102,16 +148,21 @@
     uint64_t StartOffset  = 0; ///< Proposed start offset of data. 0 for no offset
     uint64_t MaxLength    = 0; ///< Proposed max length of data in transfer, 0 for indefinite
 
-    // File designator (required)
+    // File designator (required) and additional metadata (optional, TLV format)
+    // WARNING: there is no guarantee at any point that these pointers will point to valid memory. The Buffer field should be used
+    // to hold a reference to the PacketBuffer containing the data in order to ensure the data is not freed.
     const uint8_t * FileDesignator = nullptr;
     uint16_t FileDesLength         = 0; ///< Length of file designator string (not including null-terminator)
-
-    // Additional metadata (optional, TLV format)
-    const uint8_t * Metadata = nullptr;
-    uint16_t MetadataLength  = 0;
+    const uint8_t * Metadata       = nullptr;
+    uint16_t MetadataLength        = 0;
 
     // Retain ownership of the packet buffer so that the FileDesignator and Metadata pointers remain valid.
     System::PacketBufferHandle Buffer;
+
+private:
+    CHIP_ERROR DerivedParse(System::PacketBufferHandle aBuffer) override;
+    BufBound & DerivedWriteToBuffer(BufBound & aBuffer) const override;
+    size_t DerivedMessageSize() const override;
 };
 
 using SendInit    = TransferInit;
@@ -120,44 +171,10 @@
 /*
  * A structure for representing a SendAccept message.
  */
-struct SendAccept
+struct SendAccept : public BdxMessage
 {
     /**
      * @brief
-     *  Write the message fields to a buffer using the provided BufBound.
-     *
-     *  It is up to the caller to use BufBound::Fit() to verify that the write was
-     *  successful. This method will also not check for correctness or completeness for
-     *  any of the fields - it is the caller's responsibility to ensure that the fields
-     *  have been filled out adequately.
-     *
-     * @param aBuffer A BufBound object that will be used to write the message
-     */
-    BufBound & WriteToBuffer(BufBound & aBuffer) const;
-
-    /**
-     * @brief
-     *  Parse data from an PacketBuffer into a struct instance
-     *
-     *  Note that this struct will store pointers that point into the passed PacketBuffer,
-     *  so it is essential that the PacketBuffer is not modified until after this
-     *  struct is no longer needed.
-     *
-     * @param[in] aBuffer Pointer to a PacketBuffer containing the data
-     *
-     * @return CHIP_ERROR Any error that occurs when trying to read the message
-     */
-    CHECK_RETURN_VALUE
-    CHIP_ERROR Parse(System::PacketBufferHandle aBuffer);
-
-    /**
-     * @brief
-     *  Returns the size of buffer needed to write this message.
-     */
-    size_t MessageSize() const;
-
-    /**
-     * @brief
      *  Equality check method.
      */
     bool operator==(const SendAccept &) const;
@@ -169,56 +186,27 @@
     uint16_t MaxBlockSize = 0; ///< Chosen max block size to use in transfer (required)
 
     // Additional metadata (optional, TLV format)
-    // WARNING: there is no guarantee at any point that this pointer will point to valid memory.
-    // It is up to the caller to ensure that the memory pointed to here has not been freed.
-    uint8_t * Metadata      = nullptr;
-    uint16_t MetadataLength = 0;
+    // WARNING: there is no guarantee at any point that this pointer will point to valid memory. The Buffer field should be used to
+    // hold a reference to the PacketBuffer containing the data in order to ensure the data is not freed.
+    const uint8_t * Metadata = nullptr;
+    uint16_t MetadataLength  = 0;
 
     // Retain ownership of the packet buffer so that the FileDesignator and Metadata pointers remain valid.
     System::PacketBufferHandle Buffer;
+
+private:
+    CHIP_ERROR DerivedParse(System::PacketBufferHandle aBuffer) override;
+    BufBound & DerivedWriteToBuffer(BufBound & aBuffer) const override;
+    size_t DerivedMessageSize() const override;
 };
 
 /**
  * A structure for representing ReceiveAccept messages.
  */
-struct ReceiveAccept
+struct ReceiveAccept : public BdxMessage
 {
     /**
      * @brief
-     *  Write the message fields to a buffer using the provided BufBound.
-     *
-     *  It is up to the caller to use BufBound::Fit() to verify that the write was
-     *  successful. This method will also not check for correctness or completeness for
-     *  any of the fields - it is the caller's responsibility to ensure that the fields
-     *  have been filled out adequately.
-     *
-     * @param aBuffer A BufBound object that will be used to write the message
-     */
-    BufBound & WriteToBuffer(BufBound & aBuffer) const;
-
-    /**
-     * @brief
-     *  Parse data from an PacketBuffer into a struct instance
-     *
-     *  Note that this struct will store pointers that point into the passed PacketBuffer,
-     *  so it is essential that the PacketBuffer is not modified until after this
-     *  struct is no longer needed.
-     *
-     * @param[in] aBuffer Pointer to a PacketBuffer containing the data
-     *
-     * @return CHIP_ERROR Any error that occurs when trying to read the message
-     */
-    CHECK_RETURN_VALUE
-    CHIP_ERROR Parse(System::PacketBufferHandle aBuffer);
-
-    /**
-     * @brief
-     *  Returns the size of buffer needed to write this message.
-     */
-    size_t MessageSize() const;
-
-    /**
-     * @brief
      *  Equality check method.
      */
     bool operator==(const ReceiveAccept &) const;
@@ -233,61 +221,38 @@
     uint64_t Length       = 0; ///< Length of transfer. 0 if length is indefinite.
 
     // Additional metadata (optional, TLV format)
-    // WARNING: there is no guarantee at any point that this pointer will point to valid memory.
-    // It is up to the caller to ensure that the memory pointed to here has not been freed.
-    uint8_t * Metadata      = nullptr;
-    uint16_t MetadataLength = 0;
+    // WARNING: there is no guarantee at any point that this pointer will point to valid memory. The Buffer field should be used to
+    // hold a reference to the PacketBuffer containing the data in order to ensure the data is not freed.
+    const uint8_t * Metadata = nullptr;
+    uint16_t MetadataLength  = 0;
 
     // Retain ownership of the packet buffer so that the FileDesignator and Metadata pointers remain valid.
     System::PacketBufferHandle Buffer;
+
+private:
+    CHIP_ERROR DerivedParse(System::PacketBufferHandle aBuffer) override;
+    BufBound & DerivedWriteToBuffer(BufBound & aBuffer) const override;
+    size_t DerivedMessageSize() const override;
 };
 
 /**
  * A struct for representing messages contiaining just a counter field. Can be used to
  * represent BlockQuery, BlockAck, and BlockAckEOF.
  */
-struct CounterMessage
+struct CounterMessage : public BdxMessage
 {
     /**
      * @brief
-     *  Write the message fields to a buffer using the provided BufBound.
-     *
-     *  It is up to the caller to use BufBound::Fit() to verify that the write was
-     *  successful. This method will also not check for correctness or completeness for
-     *  any of the fields - it is the caller's responsibility to ensure that the fields
-     *  have been filled out adequately.
-     *
-     * @param aBuffer A BufBound object that will be used to write the message
-     */
-    BufBound & WriteToBuffer(BufBound & aBuffer) const;
-
-    /**
-     * @brief
-     *  Parse data from an PacketBuffer into a struct instance
-     *
-     * @param[in] aBuffer Pointer to a PacketBuffer containing the data
-     *
-     * @return CHIP_ERROR Any error that occurs when trying to read the message
-     */
-    CHECK_RETURN_VALUE
-    CHIP_ERROR Parse(System::PacketBufferHandle aBuffer);
-
-    /**
-     * @brief
-     *  Returns the size of buffer needed to write this message.
-     */
-    size_t MessageSize() const;
-
-    /**
-     * @brief
      *  Equality check method.
      */
     bool operator==(const CounterMessage &) const;
 
     uint32_t BlockCounter = 0;
 
-    // Retain ownership of the packet buffer so that the FileDesignator and Metadata pointers remain valid.
-    System::PacketBufferHandle Buffer;
+private:
+    CHIP_ERROR DerivedParse(System::PacketBufferHandle aBuffer) override;
+    BufBound & DerivedWriteToBuffer(BufBound & aBuffer) const override;
+    size_t DerivedMessageSize() const override;
 };
 
 using BlockQuery  = CounterMessage;
@@ -297,61 +262,32 @@
 /**
  * A struct that represents a message containing actual data (Block, BlockEOF).
  */
-struct DataBlock
+struct DataBlock : public BdxMessage
 {
     /**
      * @brief
-     *  Write the message fields to a buffer using the provided BufBound.
-     *
-     *  It is up to the caller to use BufBound::Fit() to verify that the write was
-     *  successful. This method will also not check for correctness or completeness for
-     *  any of the fields - it is the caller's responsibility to ensure that the fields
-     *  have been filled out adequately.
-     *
-     * @param aBuffer A BufBound object that will be used to write the message
-     */
-    BufBound & WriteToBuffer(BufBound & aBuffer) const;
-
-    /**
-     * @brief
-     *  Parse data from an PacketBuffer into a struct instance
-     *
-     *  Note that this struct will store pointers that point into the passed PacketBuffer,
-     *  so it is essential that the PacketBuffer is not modified until after this
-     *  struct is no longer needed.
-     *
-     * @param[in] aBuffer Pointer to a PacketBuffer containing the data
-     *
-     * @return CHIP_ERROR Any error that occurs when trying to read the message
-     */
-    CHECK_RETURN_VALUE
-    CHIP_ERROR Parse(System::PacketBufferHandle aBuffer);
-
-    /**
-     * @brief
-     *  Returns the size of buffer needed to write this message.
-     */
-    size_t MessageSize() const;
-
-    /**
-     * @brief
      *  Equality check method.
      */
     bool operator==(const DataBlock &) const;
 
     uint32_t BlockCounter = 0;
 
-    // WARNING: there is no guarantee at any point that this pointer will point to valid memory.
-    // It is up to the caller to ensure that the memory pointed to here has not been freed.
-    uint8_t * Data      = nullptr;
-    uint16_t DataLength = 0;
+    // WARNING: there is no guarantee at any point that this pointer will point to valid memory. The Buffer field should be used to
+    // hold a reference to the PacketBuffer containing the data in order to ensure the data is not freed.
+    const uint8_t * Data = nullptr;
+    uint16_t DataLength  = 0;
 
     // Retain ownership of the packet buffer so that the FileDesignator and Metadata pointers remain valid.
     System::PacketBufferHandle Buffer;
+
+private:
+    CHIP_ERROR DerivedParse(System::PacketBufferHandle aBuffer) override;
+    BufBound & DerivedWriteToBuffer(BufBound & aBuffer) const override;
+    size_t DerivedMessageSize() const override;
 };
 
 using Block    = DataBlock;
 using BlockEOF = DataBlock;
 
-} // namespace BDX
+} // namespace bdx
 } // namespace chip
diff --git a/src/protocols/bdx/BdxTransferSession.cpp b/src/protocols/bdx/BdxTransferSession.cpp
new file mode 100644
index 0000000..3427dbd
--- /dev/null
+++ b/src/protocols/bdx/BdxTransferSession.cpp
@@ -0,0 +1,934 @@
+/**
+ *    @file
+ *      Implementation for the TransferSession class.
+ *      // TODO: Support Asynchronous mode. Currently, only Synchronous mode is supported.
+ */
+
+#include <protocols/bdx/BdxTransferSession.h>
+
+#include <protocols/Protocols.h>
+#include <protocols/bdx/BdxMessages.h>
+#include <protocols/common/Constants.h>
+#include <support/BufferReader.h>
+#include <support/CodeUtils.h>
+#include <support/ReturnMacros.h>
+#include <system/SystemPacketBuffer.h>
+
+namespace {
+constexpr uint8_t kBdxVersion         = 0;         ///< The version of this implementation of the BDX spec
+constexpr size_t kStatusReportMinSize = 2 + 4 + 2; ///< 16 bits for GeneralCode, 32 bits for ProtocolId, 16 bits for ProtocolCode
+
+/**
+ * @brief
+ *   Allocate a new PacketBuffer and write data from a BDX message struct.
+ */
+CHIP_ERROR WriteToPacketBuffer(const ::chip::bdx::BdxMessage & msgStruct, ::chip::System::PacketBufferHandle & msgBuf)
+{
+    size_t msgDataSize = msgStruct.MessageSize();
+    ::chip::System::PacketBufBound bbuf(msgDataSize);
+    if (bbuf.IsNull())
+    {
+        return CHIP_ERROR_NO_MEMORY;
+    }
+    msgStruct.WriteToBuffer(bbuf);
+    msgBuf = bbuf.Finalize();
+    if (msgBuf.IsNull())
+    {
+        return CHIP_ERROR_NO_MEMORY;
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR AttachHeader(uint16_t protocolId, uint8_t msgType, ::chip::System::PacketBufferHandle & msgBuf)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    ::chip::PayloadHeader payloadHeader;
+
+    payloadHeader.SetMessageType(protocolId, msgType);
+
+    uint16_t headerSize              = payloadHeader.EncodeSizeBytes();
+    uint16_t actualEncodedHeaderSize = 0;
+
+    VerifyOrExit(msgBuf->EnsureReservedSize(headerSize), err = CHIP_ERROR_NO_MEMORY);
+
+    msgBuf->SetStart(msgBuf->Start() - headerSize);
+    err = payloadHeader.Encode(msgBuf->Start(), headerSize, &actualEncodedHeaderSize);
+    SuccessOrExit(err);
+    VerifyOrExit(headerSize == actualEncodedHeaderSize, err = CHIP_ERROR_INTERNAL);
+
+exit:
+    return err;
+}
+} // anonymous namespace
+
+namespace chip {
+namespace bdx {
+
+TransferSession::TransferSession()
+{
+    mSuppportedXferOpts.SetRaw(0);
+}
+
+void TransferSession::PollOutput(OutputEvent & event, uint64_t curTimeMs)
+{
+    event = OutputEvent(kNone);
+
+    if (mShouldInitTimeoutStart)
+    {
+        mTimeoutStartTimeMs     = curTimeMs;
+        mShouldInitTimeoutStart = false;
+    }
+
+    if (mAwaitingResponse && ((curTimeMs - mTimeoutStartTimeMs) >= mTimeoutMs))
+    {
+        event             = OutputEvent(kTransferTimeout);
+        mState            = kErrorState;
+        mAwaitingResponse = false;
+        return;
+    }
+
+    switch (mPendingOutput)
+    {
+    case kNone:
+        event = OutputEvent(kNone);
+        break;
+    case kInternalError:
+        event = OutputEvent::StatusReportEvent(kInternalError, mStatusReportData);
+        break;
+    case kStatusReceived:
+        event = OutputEvent::StatusReportEvent(kStatusReceived, mStatusReportData);
+        break;
+    case kMsgToSend:
+        event               = OutputEvent(kMsgToSend);
+        event.MsgData       = std::move(mPendingMsgHandle);
+        mTimeoutStartTimeMs = curTimeMs;
+        break;
+    case kInitReceived:
+        event = OutputEvent::TransferInitEvent(mTransferRequestData, std::move(mPendingMsgHandle));
+        break;
+    case kAcceptReceived:
+        event = OutputEvent::TransferAcceptEvent(mTransferAcceptData, std::move(mPendingMsgHandle));
+        break;
+    case kQueryReceived:
+        event = OutputEvent(kQueryReceived);
+        break;
+    case kBlockReceived:
+        event = OutputEvent::BlockDataEvent(mBlockEventData, std::move(mPendingMsgHandle));
+        break;
+    case kAckReceived:
+        event = OutputEvent(kAckReceived);
+        break;
+    case kAckEOFReceived:
+        event = OutputEvent(kAckEOFReceived);
+        break;
+    default:
+        event = OutputEvent(kNone);
+        break;
+    }
+
+    // If there's no other pending output but an error occured or was received, then continue to output the error.
+    // This ensures that when the TransferSession encounters an error and needs to send a StatusReport, both a kMsgToSend and a
+    // kInternalError output event will be emitted.
+    if (event.EventType == kNone && mState == kErrorState)
+    {
+        event = OutputEvent::StatusReportEvent(kInternalError, mStatusReportData);
+    }
+
+    mPendingOutput = kNone;
+}
+
+CHIP_ERROR TransferSession::StartTransfer(TransferRole role, const TransferInitData & initData, uint32_t timeoutMs)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    MessageType msgType;
+    TransferInit initMsg;
+
+    VerifyOrExit(mState == kUnitialized, err = CHIP_ERROR_INCORRECT_STATE);
+
+    mRole      = role;
+    mTimeoutMs = timeoutMs;
+
+    // Set transfer parameters. They may be overridden later by an Accept message
+    mSuppportedXferOpts.SetRaw(initData.TransferCtlFlagsRaw);
+    mMaxSupportedBlockSize = initData.MaxBlockSize;
+    mStartOffset           = initData.StartOffset;
+    mTransferLength        = initData.Length;
+
+    // Prepare TransferInit message
+    initMsg.TransferCtlOptions.SetRaw(initData.TransferCtlFlagsRaw);
+    initMsg.Version        = kBdxVersion;
+    initMsg.MaxBlockSize   = mMaxSupportedBlockSize;
+    initMsg.StartOffset    = mStartOffset;
+    initMsg.MaxLength      = mTransferLength;
+    initMsg.FileDesignator = initData.FileDesignator;
+    initMsg.FileDesLength  = initData.FileDesLength;
+    initMsg.Metadata       = initData.Metadata;
+    initMsg.MetadataLength = initData.MetadataLength;
+
+    err = WriteToPacketBuffer(initMsg, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    msgType = (mRole == kRole_Sender) ? kBdxMsg_SendInit : kBdxMsg_ReceiveInit;
+    err     = AttachHeader(Protocols::kProtocol_BDX, msgType, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    mState            = kAwaitingAccept;
+    mAwaitingResponse = true;
+
+    mPendingOutput = kMsgToSend;
+
+exit:
+    return err;
+}
+
+CHIP_ERROR TransferSession::WaitForTransfer(TransferRole role, BitFlags<uint8_t, TransferControlFlags> xferControlOpts,
+                                            uint16_t maxBlockSize, uint32_t timeoutMs)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+
+    VerifyOrExit(mState == kUnitialized, err = CHIP_ERROR_INCORRECT_STATE);
+
+    // Used to determine compatibility with any future TransferInit parameters
+    mRole                  = role;
+    mTimeoutMs             = timeoutMs;
+    mSuppportedXferOpts    = xferControlOpts;
+    mMaxSupportedBlockSize = maxBlockSize;
+
+    mState = kAwaitingInitMsg;
+
+exit:
+    return err;
+}
+
+CHIP_ERROR TransferSession::AcceptTransfer(const TransferAcceptData & acceptData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    System::PacketBufferHandle outMsgBuf;
+    BitFlags<uint8_t, TransferControlFlags> proposedControlOpts;
+
+    VerifyOrExit(mState == kNegotiateTransferParams, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(mPendingOutput == kNone, err = CHIP_ERROR_INCORRECT_STATE);
+
+    // Don't allow a Control method that wasn't supported by the initiator
+    // MaxBlockSize can't be larger than the proposed value
+    proposedControlOpts.SetRaw(mTransferRequestData.TransferCtlFlagsRaw);
+    VerifyOrExit(proposedControlOpts.Has(acceptData.ControlMode), err = CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrExit(acceptData.MaxBlockSize <= mTransferRequestData.MaxBlockSize, err = CHIP_ERROR_INVALID_ARGUMENT);
+
+    mTransferMaxBlockSize = acceptData.MaxBlockSize;
+
+    if (mRole == kRole_Sender)
+    {
+        mStartOffset    = acceptData.StartOffset;
+        mTransferLength = acceptData.Length;
+
+        ReceiveAccept acceptMsg;
+        acceptMsg.TransferCtlFlags.Set(acceptData.ControlMode);
+        acceptMsg.Version        = mTransferVersion;
+        acceptMsg.MaxBlockSize   = acceptData.MaxBlockSize;
+        acceptMsg.StartOffset    = acceptData.StartOffset;
+        acceptMsg.Length         = acceptData.Length;
+        acceptMsg.Metadata       = acceptData.Metadata;
+        acceptMsg.MetadataLength = acceptData.MetadataLength;
+
+        err = WriteToPacketBuffer(acceptMsg, mPendingMsgHandle);
+        SuccessOrExit(err);
+
+        err = AttachHeader(Protocols::kProtocol_BDX, kBdxMsg_ReceiveAccept, mPendingMsgHandle);
+        SuccessOrExit(err);
+    }
+    else
+    {
+        SendAccept acceptMsg;
+        acceptMsg.TransferCtlFlags.Set(acceptData.ControlMode);
+        acceptMsg.Version        = mTransferVersion;
+        acceptMsg.MaxBlockSize   = acceptData.MaxBlockSize;
+        acceptMsg.Metadata       = acceptData.Metadata;
+        acceptMsg.MetadataLength = acceptData.MetadataLength;
+
+        err = WriteToPacketBuffer(acceptMsg, mPendingMsgHandle);
+        SuccessOrExit(err);
+
+        err = AttachHeader(Protocols::kProtocol_BDX, kBdxMsg_SendAccept, mPendingMsgHandle);
+        SuccessOrExit(err);
+    }
+
+    mPendingOutput = kMsgToSend;
+
+    mState = kTransferInProgress;
+
+    if ((mRole == kRole_Receiver && mControlMode == kControl_SenderDrive) ||
+        (mRole == kRole_Sender && mControlMode == kControl_ReceiverDrive))
+    {
+        mAwaitingResponse = true;
+    }
+
+exit:
+    return err;
+}
+
+CHIP_ERROR TransferSession::PrepareBlockQuery()
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    BlockQuery queryMsg;
+
+    VerifyOrExit(mState == kTransferInProgress, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(mRole == kRole_Receiver, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(mPendingOutput == kNone, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(!mAwaitingResponse, err = CHIP_ERROR_INCORRECT_STATE);
+
+    queryMsg.BlockCounter = mNextQueryNum;
+
+    err = WriteToPacketBuffer(queryMsg, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    err = AttachHeader(Protocols::kProtocol_BDX, kBdxMsg_BlockQuery, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    mPendingOutput = kMsgToSend;
+
+    mAwaitingResponse = true;
+    mLastQueryNum     = mNextQueryNum++;
+
+exit:
+    return err;
+}
+
+CHIP_ERROR TransferSession::PrepareBlock(const BlockData & inData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    DataBlock blockMsg;
+    MessageType msgType;
+
+    VerifyOrExit(mState == kTransferInProgress, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(mRole == kRole_Sender, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(mPendingOutput == kNone, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(!mAwaitingResponse, err = CHIP_ERROR_INCORRECT_STATE);
+
+    // Verify non-zero data is provided and is no longer than MaxBlockSize (BlockEOF may contain 0 length data)
+    VerifyOrExit((inData.Data != nullptr) && (inData.Length <= mTransferMaxBlockSize), err = CHIP_ERROR_INVALID_ARGUMENT);
+
+    blockMsg.BlockCounter = mNextBlockNum;
+    blockMsg.Data         = inData.Data;
+    blockMsg.DataLength   = inData.Length;
+
+    err = WriteToPacketBuffer(blockMsg, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    msgType = inData.IsEof ? kBdxMsg_BlockEOF : kBdxMsg_Block;
+    err     = AttachHeader(Protocols::kProtocol_BDX, msgType, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    mPendingOutput = kMsgToSend;
+
+    if (msgType == kBdxMsg_BlockEOF)
+    {
+        mState = kAwaitingEOFAck;
+    }
+
+    mAwaitingResponse = true;
+    mLastBlockNum     = mNextBlockNum++;
+
+exit:
+    return err;
+}
+
+CHIP_ERROR TransferSession::PrepareBlockAck()
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    CounterMessage ackMsg;
+    MessageType msgType;
+
+    VerifyOrExit(mRole == kRole_Receiver, err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit((mState == kTransferInProgress) || (mState == kReceivedEOF), err = CHIP_ERROR_INCORRECT_STATE);
+    VerifyOrExit(mPendingOutput == kNone, err = CHIP_ERROR_INCORRECT_STATE);
+
+    ackMsg.BlockCounter = mLastBlockNum;
+    msgType             = (mState == kReceivedEOF) ? kBdxMsg_BlockAckEOF : kBdxMsg_BlockAck;
+
+    err = WriteToPacketBuffer(ackMsg, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    err = AttachHeader(Protocols::kProtocol_BDX, msgType, mPendingMsgHandle);
+    SuccessOrExit(err);
+
+    if (mState == kTransferInProgress)
+    {
+        if (mControlMode == kControl_SenderDrive)
+        {
+            // In Sender Drive, a BlockAck is implied to also be a query for the next Block, so expect to receive a Block
+            // message.
+            mLastQueryNum     = ackMsg.BlockCounter + 1;
+            mAwaitingResponse = true;
+        }
+    }
+    else if (mState == kReceivedEOF)
+    {
+        mState            = kTransferDone;
+        mAwaitingResponse = false;
+    }
+
+    mPendingOutput = kMsgToSend;
+
+exit:
+    return err;
+}
+
+CHIP_ERROR TransferSession::AbortTransfer(StatusCode reason)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+
+    VerifyOrExit((mState != kUnitialized) && (mState != kTransferDone) && (mState != kErrorState),
+                 err = CHIP_ERROR_INCORRECT_STATE);
+
+    PrepareStatusReport(reason);
+
+exit:
+    return err;
+}
+
+void TransferSession::Reset()
+{
+    mPendingOutput = kNone;
+    mState         = kUnitialized;
+    mSuppportedXferOpts.SetRaw(0);
+    mTransferVersion       = 0;
+    mMaxSupportedBlockSize = 0;
+    mStartOffset           = 0;
+    mTransferLength        = 0;
+    mTransferMaxBlockSize  = 0;
+
+    mPendingMsgHandle = nullptr;
+
+    mNumBytesProcessed = 0;
+    mLastBlockNum      = 0;
+    mNextBlockNum      = 0;
+    mLastQueryNum      = 0;
+    mNextQueryNum      = 0;
+
+    mTimeoutMs              = 0;
+    mTimeoutStartTimeMs     = 0;
+    mShouldInitTimeoutStart = true;
+    mAwaitingResponse       = false;
+}
+
+CHIP_ERROR TransferSession::HandleMessageReceived(System::PacketBufferHandle msg, uint64_t curTimeMs)
+{
+    CHIP_ERROR err      = CHIP_NO_ERROR;
+    uint16_t headerSize = 0;
+    PayloadHeader payloadHeader;
+
+    VerifyOrExit(!msg.IsNull(), err = CHIP_ERROR_INVALID_ARGUMENT);
+
+    err = payloadHeader.Decode(msg->Start(), msg->DataLength(), &headerSize);
+    SuccessOrExit(err);
+
+    msg->ConsumeHead(headerSize);
+
+    if (payloadHeader.GetProtocolID() == Protocols::kProtocol_BDX)
+    {
+        err = HandleBdxMessage(payloadHeader, std::move(msg));
+        SuccessOrExit(err);
+
+        mTimeoutStartTimeMs = curTimeMs;
+    }
+    else if (payloadHeader.GetProtocolID() == Protocols::kProtocol_Protocol_Common &&
+             payloadHeader.GetMessageType() == static_cast<uint8_t>(Protocols::Common::MsgType::StatusReport))
+    {
+        err = HandleStatusReportMessage(payloadHeader, std::move(msg));
+        SuccessOrExit(err);
+    }
+    else
+    {
+        err = CHIP_ERROR_INVALID_MESSAGE_TYPE;
+    }
+
+exit:
+    return err;
+}
+
+// Return CHIP_ERROR only if there was a problem decoding the message. Otherwise, call PrepareStatusReport().
+CHIP_ERROR TransferSession::HandleBdxMessage(PayloadHeader & header, System::PacketBufferHandle msg)
+{
+    CHIP_ERROR err      = CHIP_NO_ERROR;
+    MessageType msgType = static_cast<MessageType>(header.GetMessageType());
+
+    VerifyOrExit(!msg.IsNull(), err = CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrExit(mPendingOutput == kNone, err = CHIP_ERROR_INCORRECT_STATE);
+
+    switch (msgType)
+    {
+    case kBdxMsg_SendInit:
+    case kBdxMsg_ReceiveInit:
+        HandleTransferInit(msgType, std::move(msg));
+        break;
+    case kBdxMsg_SendAccept:
+        HandleSendAccept(std::move(msg));
+        break;
+    case kBdxMsg_ReceiveAccept:
+        HandleReceiveAccept(std::move(msg));
+        break;
+    case kBdxMsg_BlockQuery:
+        HandleBlockQuery(std::move(msg));
+        break;
+    case kBdxMsg_Block:
+        HandleBlock(std::move(msg));
+        break;
+    case kBdxMsg_BlockEOF:
+        HandleBlockEOF(std::move(msg));
+        break;
+    case kBdxMsg_BlockAck:
+        HandleBlockAck(std::move(msg));
+        break;
+    case kBdxMsg_BlockAckEOF:
+        HandleBlockAckEOF(std::move(msg));
+        break;
+    default:
+        err = CHIP_ERROR_INVALID_MESSAGE_TYPE;
+        break;
+    }
+
+exit:
+    return err;
+}
+
+/**
+ * @brief
+ *   Parse a StatusReport message and prepare to emit an OutputEvent with the message data.
+ *
+ *   NOTE: BDX does not currently expect to ever use a "Success" general code, so it will be treated as an error along with any
+ *         other code.
+ */
+CHIP_ERROR TransferSession::HandleStatusReportMessage(PayloadHeader & header, System::PacketBufferHandle msg)
+{
+    VerifyOrReturnError(!msg.IsNull(), CHIP_ERROR_INVALID_ARGUMENT);
+
+    mState            = kErrorState;
+    mAwaitingResponse = false;
+
+    uint16_t generalCode  = 0;
+    uint32_t protocolId   = 0;
+    uint16_t protocolCode = 0;
+    Encoding::LittleEndian::Reader reader(msg->Start(), msg->DataLength());
+    ReturnErrorOnFailure(reader.Read16(&generalCode).Read32(&protocolId).Read16(&protocolCode).StatusCode());
+    VerifyOrReturnError((protocolId == Protocols::kProtocol_BDX), CHIP_ERROR_INVALID_MESSAGE_TYPE);
+
+    mStatusReportData.StatusCode = protocolCode;
+
+    mPendingOutput = kStatusReceived;
+
+    return CHIP_NO_ERROR;
+}
+
+void TransferSession::HandleTransferInit(MessageType msgType, System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    TransferInit transferInit;
+
+    VerifyOrExit(mState == kAwaitingInitMsg, PrepareStatusReport(kStatus_ServerBadState));
+
+    if (mRole == kRole_Sender)
+    {
+        VerifyOrExit(msgType == kBdxMsg_ReceiveInit, PrepareStatusReport(kStatus_ServerBadState));
+    }
+    else
+    {
+        VerifyOrExit(msgType == kBdxMsg_SendInit, PrepareStatusReport(kStatus_ServerBadState));
+    }
+
+    err = transferInit.Parse(msgData.Retain());
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+
+    ResolveTransferControlOptions(transferInit.TransferCtlOptions);
+    mTransferVersion      = ::chip::min(kBdxVersion, transferInit.Version);
+    mTransferMaxBlockSize = ::chip::min(mMaxSupportedBlockSize, transferInit.MaxBlockSize);
+
+    // Accept for now, they may be changed or rejected by the peer if this is a ReceiveInit
+    mStartOffset    = transferInit.StartOffset;
+    mTransferLength = transferInit.MaxLength;
+
+    // Store the Request data to share with the caller for verification
+    mTransferRequestData.TransferCtlFlagsRaw = transferInit.TransferCtlOptions.Raw(),
+    mTransferRequestData.MaxBlockSize        = transferInit.MaxBlockSize;
+    mTransferRequestData.StartOffset         = transferInit.StartOffset;
+    mTransferRequestData.Length              = transferInit.MaxLength;
+    mTransferRequestData.FileDesignator      = transferInit.FileDesignator;
+    mTransferRequestData.FileDesLength       = transferInit.FileDesLength;
+    mTransferRequestData.Metadata            = transferInit.Metadata;
+    mTransferRequestData.MetadataLength      = transferInit.MetadataLength;
+
+    mPendingMsgHandle = std::move(msgData);
+    mPendingOutput    = kInitReceived;
+
+    mState = kNegotiateTransferParams;
+
+exit:
+    return;
+}
+
+void TransferSession::HandleReceiveAccept(System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    ReceiveAccept rcvAcceptMsg;
+
+    VerifyOrExit(mRole == kRole_Receiver, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mState == kAwaitingAccept, PrepareStatusReport(kStatus_ServerBadState));
+
+    err = rcvAcceptMsg.Parse(msgData.Retain());
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+
+    // Verify that Accept parameters are compatible with the original proposed parameters
+    err = VerifyProposedMode(rcvAcceptMsg.TransferCtlFlags);
+    SuccessOrExit(err);
+
+    mTransferMaxBlockSize = rcvAcceptMsg.MaxBlockSize;
+    mStartOffset          = rcvAcceptMsg.StartOffset;
+    mTransferLength       = rcvAcceptMsg.Length;
+
+    // Note: if VerifyProposedMode() returned with no error, then mControlMode must match the proposed mode in the ReceiveAccept
+    // message
+    mTransferAcceptData.ControlMode    = mControlMode;
+    mTransferAcceptData.MaxBlockSize   = rcvAcceptMsg.MaxBlockSize;
+    mTransferAcceptData.StartOffset    = rcvAcceptMsg.StartOffset;
+    mTransferAcceptData.Length         = rcvAcceptMsg.Length;
+    mTransferAcceptData.Metadata       = rcvAcceptMsg.Metadata;
+    mTransferAcceptData.MetadataLength = rcvAcceptMsg.MetadataLength;
+
+    mPendingMsgHandle = std::move(msgData);
+    mPendingOutput    = kAcceptReceived;
+
+    mAwaitingResponse = (mControlMode == kControl_SenderDrive);
+    mState            = kTransferInProgress;
+
+exit:
+    return;
+}
+
+void TransferSession::HandleSendAccept(System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    SendAccept sendAcceptMsg;
+
+    VerifyOrExit(mRole == kRole_Sender, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mState == kAwaitingAccept, PrepareStatusReport(kStatus_ServerBadState));
+
+    err = sendAcceptMsg.Parse(msgData.Retain());
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+
+    // Verify that Accept parameters are compatible with the original proposed parameters
+    err = VerifyProposedMode(sendAcceptMsg.TransferCtlFlags);
+    SuccessOrExit(err);
+
+    // Note: if VerifyProposedMode() returned with no error, then mControlMode must match the proposed mode in the SendAccept
+    // message
+    mTransferMaxBlockSize = sendAcceptMsg.MaxBlockSize;
+
+    mTransferAcceptData.ControlMode    = mControlMode;
+    mTransferAcceptData.MaxBlockSize   = sendAcceptMsg.MaxBlockSize;
+    mTransferAcceptData.StartOffset    = mStartOffset;    // Not included in SendAccept msg, so use member
+    mTransferAcceptData.Length         = mTransferLength; // Not included in SendAccept msg, so use member
+    mTransferAcceptData.Metadata       = sendAcceptMsg.Metadata;
+    mTransferAcceptData.MetadataLength = sendAcceptMsg.MetadataLength;
+
+    mPendingMsgHandle = std::move(msgData);
+    mPendingOutput    = kAcceptReceived;
+
+    mAwaitingResponse = (mControlMode == kControl_ReceiverDrive);
+    mState            = kTransferInProgress;
+
+exit:
+    return;
+}
+
+void TransferSession::HandleBlockQuery(System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    BlockQuery query;
+
+    VerifyOrExit(mRole == kRole_Sender, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mState == kTransferInProgress, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mAwaitingResponse, PrepareStatusReport(kStatus_ServerBadState));
+
+    err = query.Parse(std::move(msgData));
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+
+    VerifyOrExit(query.BlockCounter == mNextBlockNum, PrepareStatusReport(kStatus_BadBlockCounter));
+
+    mPendingOutput = kQueryReceived;
+
+    mAwaitingResponse = false;
+    mLastQueryNum     = query.BlockCounter;
+
+exit:
+    return;
+}
+
+void TransferSession::HandleBlock(System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    Block blockMsg;
+
+    VerifyOrExit(mRole == kRole_Receiver, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mState == kTransferInProgress, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mAwaitingResponse, PrepareStatusReport(kStatus_ServerBadState));
+
+    err = blockMsg.Parse(msgData.Retain());
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+
+    VerifyOrExit(blockMsg.BlockCounter == mLastQueryNum, PrepareStatusReport(kStatus_BadBlockCounter));
+    VerifyOrExit((blockMsg.DataLength > 0) && (blockMsg.DataLength <= mTransferMaxBlockSize),
+                 PrepareStatusReport(kStatus_BadMessageContents));
+
+    if (IsTransferLengthDefinite())
+    {
+        VerifyOrExit(mNumBytesProcessed + blockMsg.DataLength <= mTransferLength, PrepareStatusReport(kStatus_LengthMismatch));
+    }
+
+    mBlockEventData.Data   = blockMsg.Data;
+    mBlockEventData.Length = blockMsg.DataLength;
+    mBlockEventData.IsEof  = false;
+
+    mPendingMsgHandle = std::move(msgData);
+    mPendingOutput    = kBlockReceived;
+
+    mNumBytesProcessed += blockMsg.DataLength;
+    mLastBlockNum = blockMsg.BlockCounter;
+
+    mAwaitingResponse = false;
+
+exit:
+    return;
+}
+
+void TransferSession::HandleBlockEOF(System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    BlockEOF blockEOFMsg;
+
+    VerifyOrExit(mRole == kRole_Receiver, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mState == kTransferInProgress, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mAwaitingResponse, PrepareStatusReport(kStatus_ServerBadState));
+
+    err = blockEOFMsg.Parse(msgData.Retain());
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+
+    VerifyOrExit(blockEOFMsg.BlockCounter == mLastQueryNum, PrepareStatusReport(kStatus_BadBlockCounter));
+    VerifyOrExit(blockEOFMsg.DataLength <= mTransferMaxBlockSize, PrepareStatusReport(kStatus_BadMessageContents));
+
+    mBlockEventData.Data   = blockEOFMsg.Data;
+    mBlockEventData.Length = blockEOFMsg.DataLength;
+    mBlockEventData.IsEof  = true;
+
+    mPendingMsgHandle = std::move(msgData);
+    mPendingOutput    = kBlockReceived;
+
+    mNumBytesProcessed += blockEOFMsg.DataLength;
+    mLastBlockNum = blockEOFMsg.BlockCounter;
+
+    mAwaitingResponse = false;
+    mState            = kReceivedEOF;
+
+exit:
+    return;
+}
+
+void TransferSession::HandleBlockAck(System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    BlockAck ackMsg;
+
+    VerifyOrExit(mRole == kRole_Sender, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mState == kTransferInProgress, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mAwaitingResponse, PrepareStatusReport(kStatus_ServerBadState));
+
+    err = ackMsg.Parse(std::move(msgData));
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+    VerifyOrExit(ackMsg.BlockCounter == mLastBlockNum, PrepareStatusReport(kStatus_BadBlockCounter));
+
+    mPendingOutput = kAckReceived;
+
+    // In Receiver Drive, the Receiver can send a BlockAck to indicate receipt of the message and reset the timeout.
+    // In this case, the Sender should wait to receive a BlockQuery next.
+    mAwaitingResponse = (mControlMode == kControl_ReceiverDrive);
+
+exit:
+    return;
+}
+
+void TransferSession::HandleBlockAckEOF(System::PacketBufferHandle msgData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    BlockAckEOF ackMsg;
+
+    VerifyOrExit(mRole == kRole_Sender, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mState == kAwaitingEOFAck, PrepareStatusReport(kStatus_ServerBadState));
+    VerifyOrExit(mAwaitingResponse, PrepareStatusReport(kStatus_ServerBadState));
+
+    err = ackMsg.Parse(std::move(msgData));
+    VerifyOrExit(err == CHIP_NO_ERROR, PrepareStatusReport(kStatus_BadMessageContents));
+    VerifyOrExit(ackMsg.BlockCounter == mLastBlockNum, PrepareStatusReport(kStatus_BadBlockCounter));
+
+    mPendingOutput = kAckEOFReceived;
+
+    mAwaitingResponse = false;
+
+    mState = kTransferDone;
+
+exit:
+    return;
+}
+
+void TransferSession::ResolveTransferControlOptions(const BitFlags<uint8_t, TransferControlFlags> & proposed)
+{
+    // Must specify at least one synchronous option
+    if (!proposed.Has(kControl_SenderDrive) && !proposed.Has(kControl_ReceiverDrive))
+    {
+        PrepareStatusReport(kStatus_TransferMethodNotSupported);
+        return;
+    }
+
+    // Ensure there are options supported by both nodes. Async gets priority.
+    // If there is only one common option, choose that one. Otherwise the application must pick.
+    BitFlags<uint8_t, TransferControlFlags> commonOpts;
+    commonOpts.SetRaw(proposed.Raw() & mSuppportedXferOpts.Raw());
+    if (commonOpts.Raw() == 0)
+    {
+        PrepareStatusReport(kStatus_TransferMethodNotSupported);
+    }
+    else if (commonOpts.HasOnly(kControl_Async))
+    {
+        mControlMode = kControl_Async;
+    }
+    else if (commonOpts.HasOnly(kControl_ReceiverDrive))
+    {
+        mControlMode = kControl_ReceiverDrive;
+    }
+    else if (commonOpts.HasOnly(kControl_SenderDrive))
+    {
+        mControlMode = kControl_SenderDrive;
+    }
+}
+
+CHIP_ERROR TransferSession::VerifyProposedMode(const BitFlags<uint8_t, TransferControlFlags> & proposed)
+{
+    TransferControlFlags mode;
+
+    // Must specify only one mode in Accept messages
+    if (proposed.HasOnly(kControl_Async))
+    {
+        mode = kControl_Async;
+    }
+    else if (proposed.HasOnly(kControl_ReceiverDrive))
+    {
+        mode = kControl_ReceiverDrive;
+    }
+    else if (proposed.HasOnly(kControl_SenderDrive))
+    {
+        mode = kControl_SenderDrive;
+    }
+    else
+    {
+        PrepareStatusReport(kStatus_BadMessageContents);
+        return CHIP_ERROR_INTERNAL;
+    }
+
+    // Verify the proposed mode is supported by this instance
+    if (mSuppportedXferOpts.Has(mode))
+    {
+        mControlMode = mode;
+    }
+    else
+    {
+        PrepareStatusReport(kStatus_TransferMethodNotSupported);
+        return CHIP_ERROR_INTERNAL;
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+void TransferSession::PrepareStatusReport(StatusCode code)
+{
+    mStatusReportData.StatusCode = code;
+
+    System::PacketBufBound bbuf(kStatusReportMinSize);
+    VerifyOrReturn(!bbuf.IsNull());
+
+    bbuf.Put16(static_cast<uint16_t>(Protocols::Common::StatusCode::Failure));
+    bbuf.Put32(Protocols::kProtocol_BDX);
+    bbuf.Put16(mStatusReportData.StatusCode);
+
+    mPendingMsgHandle = bbuf.Finalize();
+    if (mPendingMsgHandle.IsNull())
+    {
+        mPendingOutput = kInternalError;
+    }
+    else
+    {
+        CHIP_ERROR err = AttachHeader(Protocols::kProtocol_Protocol_Common,
+                                      static_cast<uint8_t>(Protocols::Common::MsgType::StatusReport), mPendingMsgHandle);
+        VerifyOrReturn(err == CHIP_NO_ERROR);
+
+        mPendingOutput = kMsgToSend;
+    }
+
+    mState            = kErrorState;
+    mAwaitingResponse = false; // Prevent triggering timeout
+}
+
+bool TransferSession::IsTransferLengthDefinite()
+{
+    return (mTransferLength > 0);
+}
+
+TransferSession::OutputEvent TransferSession::OutputEvent::TransferInitEvent(TransferInitData data, System::PacketBufferHandle msg)
+{
+    OutputEvent event(kInitReceived);
+    event.MsgData          = std::move(msg);
+    event.transferInitData = data;
+    return event;
+}
+
+/**
+ * @brief
+ *   Convenience method for constructing an OutputEvent with TransferAcceptData that does not contain Metadata
+ */
+TransferSession::OutputEvent TransferSession::OutputEvent::TransferAcceptEvent(TransferAcceptData data)
+{
+    OutputEvent event(kAcceptReceived);
+    event.transferAcceptData = data;
+    return event;
+}
+/**
+ * @brief
+ *   Convenience method for constructing an OutputEvent with TransferAcceptData that contains Metadata
+ */
+TransferSession::OutputEvent TransferSession::OutputEvent::TransferAcceptEvent(TransferAcceptData data,
+                                                                               System::PacketBufferHandle msg)
+{
+    OutputEvent event = TransferAcceptEvent(data);
+    event.MsgData     = std::move(msg);
+    return event;
+}
+
+TransferSession::OutputEvent TransferSession::OutputEvent::BlockDataEvent(BlockData data, System::PacketBufferHandle msg)
+{
+    OutputEvent event(kBlockReceived);
+    event.MsgData   = std::move(msg);
+    event.blockdata = data;
+    return event;
+}
+
+/**
+ * @brief
+ *   Convenience method for constructing an event with kInternalError or kOutputStatusReceived
+ */
+TransferSession::OutputEvent TransferSession::OutputEvent::StatusReportEvent(OutputEventType type, StatusReportData data)
+{
+    OutputEvent event(type);
+    event.statusData = data;
+    return event;
+}
+
+} // namespace bdx
+} // namespace chip
diff --git a/src/protocols/bdx/BdxTransferSession.h b/src/protocols/bdx/BdxTransferSession.h
new file mode 100644
index 0000000..e3b5831
--- /dev/null
+++ b/src/protocols/bdx/BdxTransferSession.h
@@ -0,0 +1,331 @@
+/**
+ *    @file
+ *      This file defines a TransferSession state machine that contains the main logic governing a Bulk Data Transfer session. It
+ *      provides APIs for starting a transfer or preparing to receive a transfer request, providing input to be processed, and
+ *      accessing output data (including messages to be sent, message data received by the TransferSession, or state information).
+ */
+
+#pragma once
+
+#include <core/CHIPError.h>
+#include <protocols/bdx/BdxMessages.h>
+#include <system/SystemPacketBuffer.h>
+#include <transport/raw/MessageHeader.h>
+
+namespace chip {
+namespace bdx {
+
+enum TransferRole : uint8_t
+{
+    kRole_Receiver = 0,
+    kRole_Sender   = 1,
+};
+
+class DLL_EXPORT TransferSession
+{
+public:
+    enum OutputEventType : uint16_t
+    {
+        kNone = 0,
+        kMsgToSend,
+        kInitReceived,
+        kAcceptReceived,
+        kBlockReceived,
+        kQueryReceived,
+        kAckReceived,
+        kAckEOFReceived,
+        kStatusReceived,
+        kInternalError,
+        kTransferTimeout
+    };
+
+    struct TransferInitData
+    {
+        uint8_t TransferCtlFlagsRaw = 0;
+
+        uint16_t MaxBlockSize = 0;
+        uint64_t StartOffset  = 0;
+        uint64_t Length       = 0;
+
+        const uint8_t * FileDesignator = nullptr;
+        uint16_t FileDesLength         = 0;
+
+        // Additional metadata (optional, TLV format)
+        const uint8_t * Metadata = nullptr;
+        uint16_t MetadataLength  = 0;
+    };
+
+    struct TransferAcceptData
+    {
+        TransferControlFlags ControlMode;
+
+        uint16_t MaxBlockSize = 0;
+        uint64_t StartOffset  = 0; ///< Not used for SendAccept message
+        uint64_t Length       = 0; ///< Not used for SendAccept message
+
+        // Additional metadata (optional, TLV format)
+        const uint8_t * Metadata = nullptr;
+        uint16_t MetadataLength  = 0;
+    };
+
+    struct StatusReportData
+    {
+        uint16_t StatusCode;
+    };
+
+    struct BlockData
+    {
+        const uint8_t * Data = nullptr;
+        uint16_t Length      = 0;
+        bool IsEof           = false;
+    };
+
+    /**
+     * @brief
+     *   All output data processed by the TransferSession object will be passed to the caller using this struct via PollOutput().
+     *
+     *   NOTE: Some sub-structs may contain pointers to data in a PacketBuffer. In this case, the MsgData field MUST be populated
+     *         with a PacketBufferHandle that encapsulates the respective PacketBuffer, in order to ensure valid memory access.
+     */
+    struct OutputEvent
+    {
+        OutputEventType EventType;
+        System::PacketBufferHandle MsgData;
+        union
+        {
+            TransferInitData transferInitData;
+            TransferAcceptData transferAcceptData;
+            BlockData blockdata;
+            StatusReportData statusData;
+        };
+
+        OutputEvent() : EventType(kNone) { statusData = { kStatus_None }; }
+        OutputEvent(OutputEventType type) : EventType(type) { statusData = { kStatus_None }; }
+
+        static OutputEvent TransferInitEvent(TransferInitData data, System::PacketBufferHandle msg);
+        static OutputEvent TransferAcceptEvent(TransferAcceptData data);
+        static OutputEvent TransferAcceptEvent(TransferAcceptData data, System::PacketBufferHandle msg);
+        static OutputEvent BlockDataEvent(BlockData data, System::PacketBufferHandle msg);
+        static OutputEvent StatusReportEvent(OutputEventType type, StatusReportData data);
+    };
+
+    /**
+     * @brief
+     *   Indicates the presence of pending output and includes any data for the caller to take action on.
+     *
+     *   This method should be called frequently in order to be notified about any messages received. It should also be called after
+     *   most other methods in order to notify the user of any message that needs to be sent, or errors that occurred internally.
+     *
+     *   It is possible that consecutive calls to this method may emit different outputs depending on the state of the
+     *   TransferSession object.
+     *
+     *   Note that if the type outputted is kMsgToSend, it is assumed that the message will be send immediately, and the
+     *   session timeout timer will begin at curTimeMs.
+     *
+     *   See OutputEventType for all possible output event types.
+     *
+     * @param event     Reference to an OutputEvent struct that will be filled out with any pending output data
+     * @param curTimeMs Current time indicated by the number of milliseconds since some epoch defined by the platform
+     */
+    void PollOutput(OutputEvent & event, uint64_t curTimeMs);
+
+    /**
+     * @brief
+     *   Initializes the TransferSession object and prepares a TransferInit message (emitted via PollOutput()).
+     *
+     *   A TransferSession object must be initialized with either StartTransfer() or WaitForTransfer().
+     *
+     * @param role      Inidcates whether this object will be sending or receiving data
+     * @param initData  Data for initializing this object and for populating a TransferInit message
+     *                  The role parameter will determine whether to populate a ReceiveInit or SendInit
+     * @param timeoutMs The amount of time to wait for a response before considering the transfer failed (milliseconds)
+     * @param curTimeMs The current time since epoch in milliseconds. Needed to set a start time for the transfer timeout.
+     *
+     * @return CHIP_ERROR Result of initialization and preparation of a TransferInit message. May also indicate if the
+     *                    TransferSession object is unable to handle this request.
+     */
+    CHIP_ERROR StartTransfer(TransferRole role, const TransferInitData & initData, uint32_t timeoutMs);
+
+    /**
+     * @brief
+     *   Initialize the TransferSession object and prepare to receive a TransferInit message at some point.
+     *
+     *   A TransferSession object must be initialized with either StartTransfer() or WaitForTransfer().
+     *
+     * @param role            Inidcates whether this object will be sending or receiving data
+     * @param xferControlOpts Indicates all supported control modes. Used to respond to a TransferInit message
+     * @param maxBlockSize    The max Block size that this object supports.
+     * @param timeoutMs       The amount of time to wait for a response before considering the transfer failed (milliseconds)
+     *
+     * @return CHIP_ERROR Result of initialization. May also indicate if the TransferSession object is unable to handle this
+     *                    request.
+     */
+    CHIP_ERROR WaitForTransfer(TransferRole role, BitFlags<uint8_t, TransferControlFlags> xferControlOpts, uint16_t maxBlockSize,
+                               uint32_t timeoutMs);
+
+    /**
+     * @brief
+     *   Indicate that all transfer parameters are acceptable and prepare a SendAccept or ReceiveAccept message (depending on role).
+     *
+     * @param acceptData Data used to populate an Accept message (some fields may differ from the original Init message)
+     *
+     * @return CHIP_ERROR Result of preparation of an Accept message. May also indicate if the TransferSession object is unable to
+     *                    handle this request.
+     */
+    CHIP_ERROR AcceptTransfer(const TransferAcceptData & acceptData);
+
+    /**
+     * @brief
+     *   Reject a TransferInit message. Use Reset() to prepare this object for another transfer.
+     *
+     * @param reason A StatusCode indicating the reason for rejecting the transfer
+     *
+     * @return CHIP_ERROR The result of the preparation of a StatusReport message. May also indicate if the TransferSession object
+     *                    is unable to handle this request.
+     */
+    CHIP_ERROR RejectTransfer(StatusCode reason);
+
+    /**
+     * @brief
+     *   Prepare a BlockQuery message. The Block counter will be populated automatically.
+     *
+     * @return CHIP_ERROR The result of the preparation of a BlockQuery message. May also indicate if the TransferSession object
+     *                    is unable to handle this request.
+     */
+    CHIP_ERROR PrepareBlockQuery();
+
+    /**
+     * @brief
+     *   Prepare a Block message. The Block counter will be populated automatically.
+     *
+     * @param inData Contains data for filling out the Block message
+     *
+     * @return CHIP_ERROR The result of the preparation of a Block message. May also indicate if the TransferSession object
+     *                    is unable to handle this request.
+     */
+    CHIP_ERROR PrepareBlock(const BlockData & inData);
+
+    /**
+     * @brief
+     *   Prepare a BlockAck message. The Block counter will be populated automatically.
+     *
+     * @return CHIP_ERROR The result of the preparation of a BlockAck message. May also indicate if the TransferSession object
+     *                    is unable to handle this request.
+     */
+    CHIP_ERROR PrepareBlockAck();
+
+    /**
+     * @brief
+     *   Prematurely end a transfer with a StatusReport. Must still call Reset() to prepare the TransferSession for another
+     *   transfer.
+     *
+     * @param reason The StatusCode reason for ending the transfer.
+     *
+     * @return CHIP_ERROR May return an error if there is no transfer in progress.
+     */
+    CHIP_ERROR AbortTransfer(StatusCode reason);
+
+    /**
+     * @brief
+     *   Reset all TransferSession parameters. The TransferSession object must then be re-initialized with StartTransfer() or
+     *   WaitForTransfer().
+     */
+    void Reset();
+
+    /**
+     * @brief
+     *   Process a message intended for this TransferSession object.
+     *
+     * @param msg       A PacketBufferHandle pointing to the message buffer to process. May be BDX or StatusReport protocol.
+     * @param curTimeMs Current time indicated by the number of milliseconds since some epoch defined by the platform
+     *
+     * @return CHIP_ERROR Indicates any problems in decoding the message, or if the message is not of the BDX or StatusReport
+     *                    protocols.
+     */
+    CHIP_ERROR HandleMessageReceived(System::PacketBufferHandle msg, uint64_t curTimeMs);
+
+    TransferControlFlags GetControlMode() const { return mControlMode; }
+    uint64_t GetStartOffset() const { return mStartOffset; }
+    uint64_t GetTransferLength() const { return mTransferLength; }
+    uint16_t GetTransferBlockSize() const { return mTransferMaxBlockSize; }
+
+    TransferSession();
+
+private:
+    enum TransferState : uint8_t
+    {
+        kUnitialized,
+        kAwaitingInitMsg,
+        kAwaitingAccept,
+        kNegotiateTransferParams,
+        kTransferInProgress,
+        kAwaitingEOFAck,
+        kReceivedEOF,
+        kTransferDone,
+        kErrorState,
+    };
+
+    // Incoming message handlers
+    CHIP_ERROR HandleBdxMessage(PayloadHeader & header, System::PacketBufferHandle msg);
+    CHIP_ERROR HandleStatusReportMessage(PayloadHeader & header, System::PacketBufferHandle msg);
+    void HandleTransferInit(MessageType msgType, System::PacketBufferHandle msgData);
+    void HandleReceiveAccept(System::PacketBufferHandle msgData);
+    void HandleSendAccept(System::PacketBufferHandle msgData);
+    void HandleBlockQuery(System::PacketBufferHandle msgData);
+    void HandleBlock(System::PacketBufferHandle msgData);
+    void HandleBlockEOF(System::PacketBufferHandle msgData);
+    void HandleBlockAck(System::PacketBufferHandle msgData);
+    void HandleBlockAckEOF(System::PacketBufferHandle msgData);
+
+    /**
+     * @brief
+     *   Used when handling a TransferInit message. Determines if there are any compatible Transfer control modes between the two
+     *   transfer peers.
+     */
+    void ResolveTransferControlOptions(const BitFlags<uint8_t, TransferControlFlags> & proposed);
+
+    /**
+     * @brief
+     *   Used when handling an Accept message. Verifies that the chosen control mode is compatible with the orignal supported modes.
+     */
+    CHIP_ERROR VerifyProposedMode(const BitFlags<uint8_t, TransferControlFlags> & proposed);
+
+    void PrepareStatusReport(StatusCode code);
+    bool IsTransferLengthDefinite();
+
+    OutputEventType mPendingOutput = kNone;
+    TransferState mState           = kUnitialized;
+    TransferRole mRole;
+
+    // Indicate supported options pre- transfer accept
+    BitFlags<uint8_t, TransferControlFlags> mSuppportedXferOpts;
+    uint16_t mMaxSupportedBlockSize = 0;
+
+    // Used to govern transfer once it has been accepted
+    TransferControlFlags mControlMode;
+    uint8_t mTransferVersion       = 0;
+    uint64_t mStartOffset          = 0; ///< 0 represents no offset
+    uint64_t mTransferLength       = 0; ///< 0 represents indefinite length
+    uint16_t mTransferMaxBlockSize = 0;
+
+    System::PacketBufferHandle mPendingMsgHandle;
+    StatusReportData mStatusReportData;
+    TransferInitData mTransferRequestData;
+    TransferAcceptData mTransferAcceptData;
+    BlockData mBlockEventData;
+
+    uint32_t mNumBytesProcessed = 0;
+
+    uint32_t mLastBlockNum = 0;
+    uint32_t mNextBlockNum = 0;
+    uint32_t mLastQueryNum = 0;
+    uint32_t mNextQueryNum = 0;
+
+    uint32_t mTimeoutMs          = 0;
+    uint64_t mTimeoutStartTimeMs = 0;
+    bool mShouldInitTimeoutStart = true;
+    bool mAwaitingResponse       = false;
+};
+
+} // namespace bdx
+} // namespace chip
diff --git a/src/protocols/bdx/tests/BUILD.gn b/src/protocols/bdx/tests/BUILD.gn
index cc5e88f..f489fb7 100644
--- a/src/protocols/bdx/tests/BUILD.gn
+++ b/src/protocols/bdx/tests/BUILD.gn
@@ -22,7 +22,10 @@
 chip_test_suite("tests") {
   output_name = "libBDXTests"
 
-  test_sources = [ "TestBdxMessages.cpp" ]
+  test_sources = [
+    "TestBdxMessages.cpp",
+    "TestBdxTransferSession.cpp",
+  ]
 
   public_deps = [
     "${chip_root}/src/lib/core",
diff --git a/src/protocols/bdx/tests/TestBdxMessages.cpp b/src/protocols/bdx/tests/TestBdxMessages.cpp
index 3c0f37c..cfc4859 100644
--- a/src/protocols/bdx/tests/TestBdxMessages.cpp
+++ b/src/protocols/bdx/tests/TestBdxMessages.cpp
@@ -8,7 +8,7 @@
 #include <limits>
 
 using namespace chip;
-using namespace chip::BDX;
+using namespace chip::bdx;
 
 /**
  * Helper method for testing that WriteToBuffer() and Parse() are successful, and that the parsed message
@@ -42,7 +42,7 @@
     TransferInit testMsg;
 
     testMsg.TransferCtlOptions.SetRaw(0);
-    testMsg.TransferCtlOptions.Set(kReceiverDrive, true);
+    testMsg.TransferCtlOptions.Set(kControl_ReceiverDrive, true);
     testMsg.Version = 1;
 
     // Make sure MaxLength is greater than UINT32_MAX to test widerange being set
@@ -68,7 +68,7 @@
 
     testMsg.Version = 1;
     testMsg.TransferCtlFlags.SetRaw(0);
-    testMsg.TransferCtlFlags.Set(kReceiverDrive, true);
+    testMsg.TransferCtlFlags.Set(kControl_ReceiverDrive, true);
     testMsg.MaxBlockSize = 256;
 
     uint8_t fakeData[5]    = { 7, 6, 5, 4, 3 };
@@ -84,7 +84,7 @@
 
     testMsg.Version = 1;
     testMsg.TransferCtlFlags.SetRaw(0);
-    testMsg.TransferCtlFlags.Set(kReceiverDrive, true);
+    testMsg.TransferCtlFlags.Set(kControl_ReceiverDrive, true);
 
     // Make sure Length is greater than UINT32_MAX to test widerange being set
     testMsg.Length = static_cast<uint64_t>(std::numeric_limits<uint32_t>::max()) + 1;
diff --git a/src/protocols/bdx/tests/TestBdxTransferSession.cpp b/src/protocols/bdx/tests/TestBdxTransferSession.cpp
new file mode 100644
index 0000000..7d8b8c0
--- /dev/null
+++ b/src/protocols/bdx/tests/TestBdxTransferSession.cpp
@@ -0,0 +1,768 @@
+#include <protocols/Protocols.h>
+#include <protocols/bdx/BdxMessages.h>
+#include <protocols/bdx/BdxTransferSession.h>
+
+#include <string.h>
+
+#include <nlunit-test.h>
+
+#include <core/CHIPTLV.h>
+#include <protocols/common/Constants.h>
+#include <support/BufferReader.h>
+#include <support/CodeUtils.h>
+#include <support/ReturnMacros.h>
+#include <support/UnitTestRegistration.h>
+#include <system/SystemPacketBuffer.h>
+
+using namespace ::chip;
+using namespace ::chip::bdx;
+
+namespace {
+// Use this as a timestamp if not needing to test BDX timeouts.
+constexpr uint64_t kNoAdvanceTime = 0;
+
+const uint64_t tlvStrTag  = TLV::ContextTag(4);
+const uint64_t tlvListTag = TLV::ProfileTag(7777, 8888);
+} // anonymous namespace
+
+// Helper method for generating a complete TLV structure with a list containing a single tag and string
+CHIP_ERROR WriteChipTLVString(uint8_t * buf, uint32_t bufLen, const char * data, uint32_t & written)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    written        = 0;
+    TLV::TLVWriter writer;
+    writer.Init(buf, bufLen);
+
+    {
+        TLV::TLVWriter listWriter;
+        err = writer.OpenContainer(tlvListTag, TLV::kTLVType_List, listWriter);
+        SuccessOrExit(err);
+        err = listWriter.PutString(tlvStrTag, data);
+        SuccessOrExit(err);
+        err = writer.CloseContainer(listWriter);
+        SuccessOrExit(err);
+    }
+
+    err = writer.Finalize();
+    SuccessOrExit(err);
+    written = writer.GetLengthWritten();
+
+exit:
+    return err;
+}
+
+// Helper method: read a TLV structure with a single tag and string and verify it matches expected string.
+CHIP_ERROR ReadAndVerifyTLVString(nlTestSuite * inSuite, void * inContext, const uint8_t * dataStart, uint32_t len,
+                                  const char * expected, uint16_t expectedLen)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    TLV::TLVReader reader;
+    char tmp[64]        = { 0 };
+    uint32_t readLength = 0;
+    VerifyOrExit(sizeof(tmp) > len, err = CHIP_ERROR_INTERNAL);
+
+    reader.Init(dataStart, len);
+    err = reader.Next();
+
+    VerifyOrExit(reader.GetTag() == tlvListTag, err = CHIP_ERROR_INTERNAL);
+
+    // Metadata must have a top-level list
+    {
+        TLV::TLVReader listReader;
+        err = reader.OpenContainer(listReader);
+        SuccessOrExit(err);
+
+        err = listReader.Next();
+        SuccessOrExit(err);
+
+        VerifyOrExit(listReader.GetTag() == tlvStrTag, err = CHIP_ERROR_INTERNAL);
+        readLength = listReader.GetLength();
+        VerifyOrExit(readLength == expectedLen, err = CHIP_ERROR_INTERNAL);
+        err = listReader.GetString(tmp, sizeof(tmp));
+        SuccessOrExit(err);
+        VerifyOrExit(!memcmp(expected, tmp, readLength), err = CHIP_ERROR_INTERNAL);
+
+        err = reader.CloseContainer(listReader);
+        SuccessOrExit(err);
+    }
+
+exit:
+    return err;
+}
+
+// Helper method for verifying that a PacketBufferHandle contains a valid BDX header and message type matches expected.
+void VerifyBdxMessageType(nlTestSuite * inSuite, void * inContext, const System::PacketBufferHandle & msg, MessageType expected)
+{
+    CHIP_ERROR err      = CHIP_NO_ERROR;
+    uint16_t headerSize = 0;
+    PayloadHeader payloadHeader;
+
+    if (msg.IsNull())
+    {
+        NL_TEST_ASSERT(inSuite, false);
+        return;
+    }
+
+    err = payloadHeader.Decode(msg->Start(), msg->DataLength(), &headerSize);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    NL_TEST_ASSERT(inSuite, payloadHeader.GetProtocolID() == Protocols::kProtocol_BDX);
+    NL_TEST_ASSERT(inSuite, payloadHeader.GetMessageType() == expected);
+}
+
+// Helper method for verifying that a PacketBufferHandle contains a valid StatusReport message and contains a specific StatusCode.
+void VerifyStatusReport(nlTestSuite * inSuite, void * inContext, const System::PacketBufferHandle & msg, StatusCode code)
+{
+    CHIP_ERROR err      = CHIP_NO_ERROR;
+    uint16_t headerSize = 0;
+    PayloadHeader payloadHeader;
+    uint16_t generalCode  = 0;
+    uint32_t protocolId   = 0;
+    uint16_t protocolCode = 0;
+
+    if (msg.IsNull())
+    {
+        NL_TEST_ASSERT(inSuite, false);
+        return;
+    }
+
+    err = payloadHeader.Decode(msg->Start(), msg->DataLength(), &headerSize);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    NL_TEST_ASSERT(inSuite, payloadHeader.GetProtocolID() == Protocols::kProtocol_Protocol_Common);
+    NL_TEST_ASSERT(inSuite, payloadHeader.GetMessageType() == static_cast<uint8_t>(Protocols::Common::MsgType::StatusReport));
+
+    Encoding::LittleEndian::Reader reader(msg->Start() + headerSize, msg->DataLength());
+    err = reader.Read16(&generalCode).Read32(&protocolId).Read16(&protocolCode).StatusCode();
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    NL_TEST_ASSERT(inSuite, generalCode == static_cast<uint16_t>(Protocols::Common::StatusCode::Failure));
+    NL_TEST_ASSERT(inSuite, protocolId == Protocols::kProtocol_BDX);
+    NL_TEST_ASSERT(inSuite, protocolCode == code);
+}
+
+void VerifyNoMoreOutput(nlTestSuite * inSuite, void * inContext, TransferSession & transferSession)
+{
+    TransferSession::OutputEvent event;
+    transferSession.PollOutput(event, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, event.EventType == TransferSession::kNone);
+}
+
+// Helper method for initializing two TransferSession objects, generating a TransferInit message, and passing it to a responding
+// TransferSession.
+void SendAndVerifyTransferInit(nlTestSuite * inSuite, void * inContext, TransferSession::OutputEvent & outEvent, uint32_t timeoutMs,
+                               TransferSession & initiator, TransferRole initiatorRole, TransferSession::TransferInitData initData,
+                               TransferSession & responder, BitFlags<uint8_t, TransferControlFlags> & responderControlOpts,
+                               uint16_t responderMaxBlock)
+{
+    CHIP_ERROR err              = CHIP_NO_ERROR;
+    TransferRole responderRole  = (initiatorRole == kRole_Sender) ? kRole_Receiver : kRole_Sender;
+    MessageType expectedInitMsg = (initiatorRole == kRole_Sender) ? kBdxMsg_SendInit : kBdxMsg_ReceiveInit;
+
+    // Initializer responder to wait for transfer
+    err = responder.WaitForTransfer(responderRole, responderControlOpts, responderMaxBlock, timeoutMs);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    VerifyNoMoreOutput(inSuite, inContext, responder);
+
+    // Verify initiator outputs respective Init message (depending on role) after StartTransfer()
+    err = initiator.StartTransfer(initiatorRole, initData, timeoutMs);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    initiator.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kMsgToSend);
+    VerifyBdxMessageType(inSuite, inContext, outEvent.MsgData, expectedInitMsg);
+    VerifyNoMoreOutput(inSuite, inContext, initiator);
+
+    // Verify that all parsed TransferInit fields match what was sent by the initiator
+    err = responder.HandleMessageReceived(std::move(outEvent.MsgData), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    responder.PollOutput(outEvent, kNoAdvanceTime);
+    VerifyNoMoreOutput(inSuite, inContext, responder);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kInitReceived);
+    NL_TEST_ASSERT(inSuite, outEvent.transferInitData.TransferCtlFlagsRaw == initData.TransferCtlFlagsRaw);
+    NL_TEST_ASSERT(inSuite, outEvent.transferInitData.MaxBlockSize == initData.MaxBlockSize);
+    NL_TEST_ASSERT(inSuite, outEvent.transferInitData.StartOffset == initData.StartOffset);
+    NL_TEST_ASSERT(inSuite, outEvent.transferInitData.Length == initData.Length);
+    NL_TEST_ASSERT(inSuite, outEvent.transferInitData.FileDesignator != nullptr);
+    NL_TEST_ASSERT(inSuite, outEvent.transferInitData.FileDesLength == initData.FileDesLength);
+    if (outEvent.EventType == TransferSession::kInitReceived && outEvent.transferInitData.FileDesignator != nullptr)
+    {
+        NL_TEST_ASSERT(
+            inSuite,
+            !memcmp(initData.FileDesignator, outEvent.transferInitData.FileDesignator, outEvent.transferInitData.FileDesLength));
+    }
+    if (outEvent.transferInitData.Metadata != nullptr)
+    {
+        NL_TEST_ASSERT(inSuite, outEvent.transferInitData.MetadataLength == initData.MetadataLength);
+        if (outEvent.transferInitData.MetadataLength == initData.MetadataLength)
+        {
+            // Only check that metadata buffers match. The OutputEvent can still be inspected when this function returns to parse
+            // the metadata and verify that it matches.
+            NL_TEST_ASSERT(
+                inSuite, !memcmp(initData.Metadata, outEvent.transferInitData.Metadata, outEvent.transferInitData.MetadataLength));
+        }
+        else
+        {
+            NL_TEST_ASSERT(inSuite, false); // Metadata length mismatch
+        }
+    }
+}
+
+// Helper method for sending an Accept message and verifying that the received parameters match what was sent.
+// This function assumes that the acceptData struct contains transfer parameters that are valid responses to the original
+// TransferInit message (for example, MaxBlockSize should be <= the TransferInit MaxBlockSize). If such parameters are invalid, the
+// receiver should emit a StatusCode event instead.
+//
+// The acceptSender is the node that is sending the Accept message (not necessarily the same node that will send Blocks).
+void SendAndVerifyAcceptMsg(nlTestSuite * inSuite, void * inContext, TransferSession::OutputEvent & outEvent,
+                            TransferSession & acceptSender, TransferRole acceptSenderRole,
+                            TransferSession::TransferAcceptData acceptData, TransferSession & acceptReceiver,
+                            TransferSession::TransferInitData initData)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+
+    // If the node sending the Accept message is also the one that will send Blocks, then this should be a ReceiveAccept message.
+    MessageType expectedMsg = (acceptSenderRole == kRole_Sender) ? kBdxMsg_ReceiveAccept : kBdxMsg_SendAccept;
+
+    err = acceptSender.AcceptTransfer(acceptData);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+
+    // Verify Sender emits ReceiveAccept message for sending
+    acceptSender.PollOutput(outEvent, kNoAdvanceTime);
+    VerifyNoMoreOutput(inSuite, inContext, acceptSender);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kMsgToSend);
+    VerifyBdxMessageType(inSuite, inContext, outEvent.MsgData, expectedMsg);
+
+    // Pass Accept message to acceptReceiver
+    err = acceptReceiver.HandleMessageReceived(std::move(outEvent.MsgData), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+
+    // Verify received ReceiveAccept.
+    // Client may want to inspect TransferControl, MaxBlockSize, StartOffset, Length, and Metadata, and may choose to reject the
+    // Transfer at this point.
+    acceptReceiver.PollOutput(outEvent, kNoAdvanceTime);
+    VerifyNoMoreOutput(inSuite, inContext, acceptReceiver);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kAcceptReceived);
+    NL_TEST_ASSERT(inSuite, outEvent.transferAcceptData.ControlMode == acceptData.ControlMode);
+    NL_TEST_ASSERT(inSuite, outEvent.transferAcceptData.MaxBlockSize == acceptData.MaxBlockSize);
+    NL_TEST_ASSERT(inSuite, outEvent.transferAcceptData.StartOffset == acceptData.StartOffset);
+    NL_TEST_ASSERT(inSuite, outEvent.transferAcceptData.Length == acceptData.Length);
+    if (outEvent.transferAcceptData.Metadata != nullptr)
+    {
+        NL_TEST_ASSERT(inSuite, outEvent.transferAcceptData.MetadataLength == acceptData.MetadataLength);
+        if (outEvent.transferAcceptData.MetadataLength == acceptData.MetadataLength)
+        {
+            // Only check that metadata buffers match. The OutputEvent can still be inspected when this function returns to parse
+            // the metadata and verify that it matches.
+            NL_TEST_ASSERT(
+                inSuite,
+                !memcmp(acceptData.Metadata, outEvent.transferAcceptData.Metadata, outEvent.transferAcceptData.MetadataLength));
+        }
+        else
+        {
+            NL_TEST_ASSERT(inSuite, false); // Metadata length mismatch
+        }
+    }
+
+    // Verify that MaxBlockSize was set appropriately
+    NL_TEST_ASSERT(inSuite, acceptReceiver.GetTransferBlockSize() <= initData.MaxBlockSize);
+}
+
+// Helper method for preparing a sending a BlockQuery message between two TransferSession objects.
+void SendAndVerifyQuery(nlTestSuite * inSuite, void * inContext, TransferSession & queryReceiver, TransferSession & querySender,
+                        TransferSession::OutputEvent & outEvent)
+{
+    // Verify that querySender emits BlockQuery message
+    CHIP_ERROR err = querySender.PrepareBlockQuery();
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    querySender.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kMsgToSend);
+    VerifyBdxMessageType(inSuite, inContext, outEvent.MsgData, kBdxMsg_BlockQuery);
+    VerifyNoMoreOutput(inSuite, inContext, querySender);
+
+    // Pass BlockQuery to queryReceiver and verify queryReceiver emits QueryReceived event
+    err = queryReceiver.HandleMessageReceived(std::move(outEvent.MsgData), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    queryReceiver.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kQueryReceived);
+    VerifyNoMoreOutput(inSuite, inContext, queryReceiver);
+}
+
+// Helper method for preparing a sending a Block message between two TransferSession objects. The sender refers to the node that is
+// sending Blocks. Uses a static counter incremented with each call. Also verifies that block data received matches what was sent.
+void SendAndVerifyArbitraryBlock(nlTestSuite * inSuite, void * inContext, TransferSession & sender, TransferSession & receiver,
+                                 TransferSession::OutputEvent & outEvent, bool isEof)
+{
+    CHIP_ERROR err           = CHIP_NO_ERROR;
+    static uint8_t dataCount = 0;
+    uint16_t maxBlockSize    = sender.GetTransferBlockSize();
+
+    NL_TEST_ASSERT(inSuite, maxBlockSize > 0);
+    System::PacketBufferHandle fakeDataBuf = System::PacketBufferHandle::New(maxBlockSize);
+    if (fakeDataBuf.IsNull())
+    {
+        NL_TEST_ASSERT(inSuite, false);
+        return;
+    }
+
+    uint8_t * fakeBlockData = fakeDataBuf->Start();
+    fakeBlockData[0]        = dataCount++;
+
+    TransferSession::BlockData blockData;
+    blockData.Data   = fakeBlockData;
+    blockData.Length = maxBlockSize;
+    blockData.IsEof  = isEof;
+
+    MessageType expected = isEof ? kBdxMsg_BlockEOF : kBdxMsg_Block;
+
+    // Provide Block data and verify sender emits Block message
+    err = sender.PrepareBlock(blockData);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    sender.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kMsgToSend);
+    VerifyBdxMessageType(inSuite, inContext, outEvent.MsgData, expected);
+    VerifyNoMoreOutput(inSuite, inContext, sender);
+
+    // Pass Block message to receiver and verify matching Block is received
+    err = receiver.HandleMessageReceived(std::move(outEvent.MsgData), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    receiver.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kBlockReceived);
+    NL_TEST_ASSERT(inSuite, outEvent.blockdata.Data != nullptr);
+    if (outEvent.EventType == TransferSession::kBlockReceived && outEvent.blockdata.Data != nullptr)
+    {
+        NL_TEST_ASSERT(inSuite, !memcmp(fakeBlockData, outEvent.blockdata.Data, outEvent.blockdata.Length));
+    }
+    VerifyNoMoreOutput(inSuite, inContext, receiver);
+}
+
+// Helper method for sending a BlockAck or BlockAckEOF, depending on the state of the receiver.
+void SendAndVerifyBlockAck(nlTestSuite * inSuite, void * inContext, TransferSession & ackReceiver, TransferSession & ackSender,
+                           TransferSession::OutputEvent & outEvent, bool expectEOF)
+{
+    TransferSession::OutputEventType expectedEventType =
+        expectEOF ? TransferSession::kAckEOFReceived : TransferSession::kAckReceived;
+    MessageType expectedMsgType = expectEOF ? kBdxMsg_BlockAckEOF : kBdxMsg_BlockAck;
+
+    // Verify PrepareBlockAck() outputs message to send
+    CHIP_ERROR err = ackSender.PrepareBlockAck();
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    ackSender.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kMsgToSend);
+    VerifyBdxMessageType(inSuite, inContext, outEvent.MsgData, expectedMsgType);
+    VerifyNoMoreOutput(inSuite, inContext, ackSender);
+
+    // Pass BlockAck to ackReceiver and verify it was received
+    err = ackReceiver.HandleMessageReceived(std::move(outEvent.MsgData), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    ackReceiver.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == expectedEventType);
+    VerifyNoMoreOutput(inSuite, inContext, ackReceiver);
+}
+
+// Test a full transfer using a responding receiver and an initiating sender, receiver drive.
+void TestInitiatingReceiverReceiverDrive(nlTestSuite * inSuite, void * inContext)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    TransferSession::OutputEvent outEvent;
+    TransferSession initiatingReceiver;
+    TransferSession respondingSender;
+    uint32_t numBlocksSent = 0;
+
+    // Chosen arbitrarily for this test
+    uint32_t numBlockSends        = 10;
+    uint16_t proposedBlockSize    = 128;
+    uint16_t testSmallerBlockSize = 64;
+    uint64_t proposedOffset       = 64;
+    uint64_t proposedLength       = 0;
+    uint32_t timeoutMs            = 1000 * 24;
+
+    // Chosen specifically for this test
+    TransferControlFlags driveMode = kControl_ReceiverDrive;
+
+    // ReceiveInit parameters
+    TransferSession::TransferInitData initOptions;
+    initOptions.TransferCtlFlagsRaw = driveMode;
+    initOptions.MaxBlockSize        = proposedBlockSize;
+    char testFileDes[9]             = { "test.txt" };
+    initOptions.FileDesLength       = static_cast<uint16_t>(strlen(testFileDes));
+    initOptions.FileDesignator      = reinterpret_cast<uint8_t *>(testFileDes);
+
+    // Initialize respondingSender and pass ReceiveInit message
+    BitFlags<uint8_t, TransferControlFlags> senderOpts;
+    senderOpts.Set(driveMode);
+
+    SendAndVerifyTransferInit(inSuite, inContext, outEvent, timeoutMs, initiatingReceiver, kRole_Receiver, initOptions,
+                              respondingSender, senderOpts, proposedBlockSize);
+
+    // Test metadata for Accept message
+    uint8_t tlvBuf[64]    = { 0 };
+    char metadataStr[11]  = { "hi_dad.txt" };
+    uint32_t bytesWritten = 0;
+    err                   = WriteChipTLVString(tlvBuf, sizeof(tlvBuf), metadataStr, bytesWritten);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    uint16_t metadataSize = static_cast<uint16_t>(bytesWritten & 0x0000FFFF);
+
+    // Compose ReceiveAccept parameters struct and give to respondingSender
+    TransferSession::TransferAcceptData acceptData;
+    acceptData.ControlMode    = respondingSender.GetControlMode();
+    acceptData.StartOffset    = proposedOffset;
+    acceptData.Length         = proposedLength;
+    acceptData.MaxBlockSize   = testSmallerBlockSize;
+    acceptData.Metadata       = tlvBuf;
+    acceptData.MetadataLength = metadataSize;
+
+    SendAndVerifyAcceptMsg(inSuite, inContext, outEvent, respondingSender, kRole_Sender, acceptData, initiatingReceiver,
+                           initOptions);
+
+    // Verify that MaxBlockSize was chosen correctly
+    NL_TEST_ASSERT(inSuite, respondingSender.GetTransferBlockSize() == testSmallerBlockSize);
+    NL_TEST_ASSERT(inSuite, respondingSender.GetTransferBlockSize() == initiatingReceiver.GetTransferBlockSize());
+
+    // Verify parsed TLV metadata matches the original
+    err =
+        ReadAndVerifyTLVString(inSuite, inContext, outEvent.transferAcceptData.Metadata, outEvent.transferAcceptData.MetadataLength,
+                               metadataStr, static_cast<uint16_t>(strlen(metadataStr)));
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+
+    // Test BlockQuery -> Block -> BlockAck
+    SendAndVerifyQuery(inSuite, inContext, respondingSender, initiatingReceiver, outEvent);
+    SendAndVerifyArbitraryBlock(inSuite, inContext, respondingSender, initiatingReceiver, outEvent, false);
+    numBlocksSent++;
+
+    // Test only one block can be prepared at a time, without receiving a response to the first
+    System::PacketBufferHandle fakeBuf = System::PacketBufferHandle::New(testSmallerBlockSize);
+    TransferSession::BlockData prematureBlock;
+    if (fakeBuf.IsNull())
+    {
+        NL_TEST_ASSERT(inSuite, false);
+        return;
+    }
+    prematureBlock.Data   = fakeBuf->Start();
+    prematureBlock.Length = testSmallerBlockSize;
+    prematureBlock.IsEof  = false;
+    err                   = respondingSender.PrepareBlock(prematureBlock);
+    NL_TEST_ASSERT(inSuite, err != CHIP_NO_ERROR);
+    VerifyNoMoreOutput(inSuite, inContext, respondingSender);
+
+    // Test Ack -> Query -> Block
+    SendAndVerifyBlockAck(inSuite, inContext, respondingSender, initiatingReceiver, outEvent, false);
+
+    // Test multiple Blocks sent and received (last Block is BlockEOF)
+    while (numBlocksSent < numBlockSends)
+    {
+        bool isEof = (numBlocksSent == numBlockSends - 1);
+
+        SendAndVerifyQuery(inSuite, inContext, respondingSender, initiatingReceiver, outEvent);
+        SendAndVerifyArbitraryBlock(inSuite, inContext, respondingSender, initiatingReceiver, outEvent, isEof);
+
+        numBlocksSent++;
+    }
+
+    // Verify last block was BlockEOF, then verify response BlockAckEOF message
+    NL_TEST_ASSERT(inSuite, outEvent.blockdata.IsEof == true);
+    SendAndVerifyBlockAck(inSuite, inContext, respondingSender, initiatingReceiver, outEvent, true);
+}
+
+// Partial transfer test using Sender Drive to specifically test Block -> BlockAck -> Block sequence
+void TestInitiatingSenderSenderDrive(nlTestSuite * inSuite, void * inContext)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    TransferSession::OutputEvent outEvent;
+    TransferSession initiatingSender;
+    TransferSession respondingReceiver;
+
+    TransferControlFlags driveMode = kControl_SenderDrive;
+
+    // Chosen arbitrarily for this test
+    uint16_t transferBlockSize = 10;
+    uint32_t timeoutMs         = 1000 * 24;
+
+    // Initialize respondingReceiver
+    BitFlags<uint8_t, TransferControlFlags> receiverOpts;
+    receiverOpts.Set(driveMode);
+
+    // Test metadata for TransferInit message
+    uint8_t tlvBuf[64]    = { 0 };
+    char metadataStr[11]  = { "hi_dad.txt" };
+    uint32_t bytesWritten = 0;
+    err                   = WriteChipTLVString(tlvBuf, sizeof(tlvBuf), metadataStr, bytesWritten);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    uint16_t metadataSize = static_cast<uint16_t>(bytesWritten & 0x0000FFFF);
+
+    // Initialize struct with TransferInit parameters
+    TransferSession::TransferInitData initOptions;
+    initOptions.TransferCtlFlagsRaw = driveMode;
+    initOptions.MaxBlockSize        = transferBlockSize;
+    char testFileDes[9]             = { "test.txt" };
+    initOptions.FileDesLength       = static_cast<uint16_t>(strlen(testFileDes));
+    initOptions.FileDesignator      = reinterpret_cast<uint8_t *>(testFileDes);
+    initOptions.Metadata            = tlvBuf;
+    initOptions.MetadataLength      = metadataSize;
+
+    SendAndVerifyTransferInit(inSuite, inContext, outEvent, timeoutMs, initiatingSender, kRole_Sender, initOptions,
+                              respondingReceiver, receiverOpts, transferBlockSize);
+
+    // Verify parsed TLV metadata matches the original
+    err = ReadAndVerifyTLVString(inSuite, inContext, outEvent.transferInitData.Metadata, outEvent.transferInitData.MetadataLength,
+                                 metadataStr, static_cast<uint16_t>(strlen(metadataStr)));
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+
+    // Compose SendAccept parameters struct and give to respondingSender
+    uint16_t proposedBlockSize = transferBlockSize;
+    TransferSession::TransferAcceptData acceptData;
+    acceptData.ControlMode    = respondingReceiver.GetControlMode();
+    acceptData.MaxBlockSize   = proposedBlockSize;
+    acceptData.StartOffset    = 0; // not used in SendAccept
+    acceptData.Length         = 0; // not used in SendAccept
+    acceptData.Metadata       = nullptr;
+    acceptData.MetadataLength = 0;
+
+    SendAndVerifyAcceptMsg(inSuite, inContext, outEvent, respondingReceiver, kRole_Receiver, acceptData, initiatingSender,
+                           initOptions);
+
+    // Test multiple Block -> BlockAck -> Block
+    for (int i = 0; i < 3; i++)
+    {
+        SendAndVerifyArbitraryBlock(inSuite, inContext, initiatingSender, respondingReceiver, outEvent, false);
+        SendAndVerifyBlockAck(inSuite, inContext, initiatingSender, respondingReceiver, outEvent, false);
+    }
+
+    SendAndVerifyArbitraryBlock(inSuite, inContext, initiatingSender, respondingReceiver, outEvent, true);
+    SendAndVerifyBlockAck(inSuite, inContext, initiatingSender, respondingReceiver, outEvent, true);
+}
+
+// Test that calls to AcceptTransfer() with bad parameters result in an error.
+void TestBadAcceptMessageFields(nlTestSuite * inSuite, void * inContext)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    TransferSession::OutputEvent outEvent;
+    TransferSession initiatingReceiver;
+    TransferSession respondingSender;
+
+    uint16_t maxBlockSize          = 64;
+    TransferControlFlags driveMode = kControl_ReceiverDrive;
+    uint64_t commonLength          = 0;
+    uint64_t commonOffset          = 0;
+    uint32_t timeoutMs             = 1000 * 24;
+
+    // Initialize struct with TransferInit parameters
+    TransferSession::TransferInitData initOptions;
+    initOptions.TransferCtlFlagsRaw = driveMode;
+    initOptions.MaxBlockSize        = maxBlockSize;
+    initOptions.StartOffset         = commonOffset;
+    initOptions.Length              = commonLength;
+    char testFileDes[9]             = { "test.txt" }; // arbitrary file designator
+    initOptions.FileDesLength       = static_cast<uint16_t>(strlen(testFileDes));
+    initOptions.FileDesignator      = reinterpret_cast<uint8_t *>(testFileDes);
+    initOptions.Metadata            = nullptr;
+    initOptions.MetadataLength      = 0;
+
+    // Responder parameters
+    BitFlags<uint8_t, TransferControlFlags> responderControl;
+    responderControl.Set(driveMode);
+
+    SendAndVerifyTransferInit(inSuite, inContext, outEvent, timeoutMs, initiatingReceiver, kRole_Receiver, initOptions,
+                              respondingSender, responderControl, maxBlockSize);
+
+    // Verify AcceptTransfer() returns error for choosing larger max block size
+    TransferSession::TransferAcceptData acceptData;
+    acceptData.ControlMode  = driveMode;
+    acceptData.MaxBlockSize = static_cast<uint16_t>(maxBlockSize + 1); // invalid if larger than proposed
+    acceptData.StartOffset  = commonOffset;
+    acceptData.Length       = commonLength;
+    err                     = respondingSender.AcceptTransfer(acceptData);
+    NL_TEST_ASSERT(inSuite, err != CHIP_NO_ERROR);
+
+    // Verify AcceptTransfer() returns error for choosing unsupported transfer control mode
+    TransferSession::TransferAcceptData acceptData2;
+    acceptData2.ControlMode  = (driveMode == kControl_ReceiverDrive) ? kControl_SenderDrive : kControl_ReceiverDrive;
+    acceptData2.MaxBlockSize = maxBlockSize;
+    acceptData2.StartOffset  = commonOffset;
+    acceptData2.Length       = commonLength;
+    err                      = respondingSender.AcceptTransfer(acceptData2);
+    NL_TEST_ASSERT(inSuite, err != CHIP_NO_ERROR);
+}
+
+// Test that a TransferSession will emit kTransferTimeout if the specified timeout is exceeded while waiting for a response.
+void TestTimeout(nlTestSuite * inSuite, void * inContext)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    TransferSession initiator;
+    TransferSession::OutputEvent outEvent;
+
+    uint32_t timeoutMs   = 24;
+    uint64_t startTimeMs = 100;
+    uint64_t endTimeMs   = 124;
+
+    // Initialize struct with arbitrary TransferInit parameters
+    TransferSession::TransferInitData initOptions;
+    initOptions.TransferCtlFlagsRaw = kControl_ReceiverDrive;
+    initOptions.MaxBlockSize        = 64;
+    initOptions.StartOffset         = 0;
+    initOptions.Length              = 0;
+    char testFileDes[9]             = { "test.txt" }; // arbitrary file designator
+    initOptions.FileDesLength       = static_cast<uint16_t>(strlen(testFileDes));
+    initOptions.FileDesignator      = reinterpret_cast<uint8_t *>(testFileDes);
+    initOptions.Metadata            = nullptr;
+    initOptions.MetadataLength      = 0;
+
+    TransferRole role = kRole_Receiver;
+
+    // Verify initiator outputs respective Init message (depending on role) after StartTransfer()
+    err = initiator.StartTransfer(role, initOptions, timeoutMs);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+
+    // First PollOutput() should output the TransferInit message
+    initiator.PollOutput(outEvent, startTimeMs);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kMsgToSend);
+    MessageType expectedInitMsg = (role == kRole_Sender) ? kBdxMsg_SendInit : kBdxMsg_ReceiveInit;
+    VerifyBdxMessageType(inSuite, inContext, outEvent.MsgData, expectedInitMsg);
+
+    // Second PollOutput() with no call to HandleMessageReceived() should result in a timeout.
+    initiator.PollOutput(outEvent, endTimeMs);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kTransferTimeout);
+}
+
+// Test that sending the same block twice (with same block counter) results in a StatusReport message with BadBlockCounter. Also
+// test that receiving the StatusReport ends the transfer on the other node.
+void TestDuplicateBlockError(nlTestSuite * inSuite, void * inContext)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    TransferSession::OutputEvent outEvent;
+    TransferSession::OutputEvent eventWithBlock;
+    TransferSession initiatingReceiver;
+    TransferSession respondingSender;
+
+    uint8_t fakeData[64] = { 0 };
+    uint8_t fakeDataLen  = sizeof(fakeData);
+    uint16_t blockSize   = sizeof(fakeData);
+
+    // Chosen arbitrarily for this test
+    uint64_t proposedOffset = 64;
+    uint64_t proposedLength = 0;
+    uint32_t timeoutMs      = 1000 * 24;
+
+    // Chosen specifically for this test
+    TransferControlFlags driveMode = kControl_ReceiverDrive;
+
+    // ReceiveInit parameters
+    TransferSession::TransferInitData initOptions;
+    initOptions.TransferCtlFlagsRaw = driveMode;
+    initOptions.MaxBlockSize        = blockSize;
+    char testFileDes[9]             = { "test.txt" };
+    initOptions.FileDesLength       = static_cast<uint16_t>(strlen(testFileDes));
+    initOptions.FileDesignator      = reinterpret_cast<uint8_t *>(testFileDes);
+
+    // Initialize respondingSender and pass ReceiveInit message
+    BitFlags<uint8_t, TransferControlFlags> senderOpts;
+    senderOpts.Set(driveMode);
+
+    SendAndVerifyTransferInit(inSuite, inContext, outEvent, timeoutMs, initiatingReceiver, kRole_Receiver, initOptions,
+                              respondingSender, senderOpts, blockSize);
+
+    // Compose ReceiveAccept parameters struct and give to respondingSender
+    TransferSession::TransferAcceptData acceptData;
+    acceptData.ControlMode    = respondingSender.GetControlMode();
+    acceptData.StartOffset    = proposedOffset;
+    acceptData.Length         = proposedLength;
+    acceptData.MaxBlockSize   = blockSize;
+    acceptData.Metadata       = nullptr;
+    acceptData.MetadataLength = 0;
+
+    SendAndVerifyAcceptMsg(inSuite, inContext, outEvent, respondingSender, kRole_Sender, acceptData, initiatingReceiver,
+                           initOptions);
+
+    SendAndVerifyQuery(inSuite, inContext, respondingSender, initiatingReceiver, outEvent);
+
+    TransferSession::BlockData blockData;
+    blockData.Data   = fakeData;
+    blockData.Length = fakeDataLen;
+    blockData.IsEof  = false;
+
+    // Provide Block data and verify sender emits Block message
+    err = respondingSender.PrepareBlock(blockData);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    respondingSender.PollOutput(eventWithBlock, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, eventWithBlock.EventType == TransferSession::kMsgToSend);
+    VerifyBdxMessageType(inSuite, inContext, eventWithBlock.MsgData, kBdxMsg_Block);
+    VerifyNoMoreOutput(inSuite, inContext, respondingSender);
+    System::PacketBufferHandle blockCopy =
+        System::PacketBufferHandle::NewWithData(eventWithBlock.MsgData->Start(), eventWithBlock.MsgData->DataLength());
+
+    // Pass Block message to receiver and verify matching Block is received
+    err = initiatingReceiver.HandleMessageReceived(std::move(eventWithBlock.MsgData), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    initiatingReceiver.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kBlockReceived);
+    NL_TEST_ASSERT(inSuite, outEvent.blockdata.Data != nullptr);
+    VerifyNoMoreOutput(inSuite, inContext, initiatingReceiver);
+
+    SendAndVerifyQuery(inSuite, inContext, respondingSender, initiatingReceiver, outEvent);
+
+    // Verify receiving same Block twice fails and results in StatusReport event, and then InternalError event
+    err = initiatingReceiver.HandleMessageReceived(std::move(blockCopy), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    initiatingReceiver.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kMsgToSend);
+    System::PacketBufferHandle statusReportMsg = outEvent.MsgData.Retain();
+    VerifyStatusReport(inSuite, inContext, std::move(outEvent.MsgData), kStatus_BadBlockCounter);
+
+    // All subsequent PollOutput() calls should return kInternalError
+    for (int i = 0; i < 5; ++i)
+    {
+        initiatingReceiver.PollOutput(outEvent, kNoAdvanceTime);
+        NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kInternalError);
+        NL_TEST_ASSERT(inSuite, outEvent.statusData.StatusCode == kStatus_BadBlockCounter);
+    }
+
+    err = respondingSender.HandleMessageReceived(std::move(statusReportMsg), kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR);
+    respondingSender.PollOutput(outEvent, kNoAdvanceTime);
+    NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kStatusReceived);
+    NL_TEST_ASSERT(inSuite, outEvent.statusData.StatusCode == kStatus_BadBlockCounter);
+
+    // All subsequent PollOutput() calls should return kInternalError
+    for (int i = 0; i < 5; ++i)
+    {
+        respondingSender.PollOutput(outEvent, kNoAdvanceTime);
+        NL_TEST_ASSERT(inSuite, outEvent.EventType == TransferSession::kInternalError);
+        NL_TEST_ASSERT(inSuite, outEvent.statusData.StatusCode == kStatus_BadBlockCounter);
+    }
+}
+
+// Test Suite
+
+/**
+ *  Test Suite that lists all the test functions.
+ */
+// clang-format off
+static const nlTest sTests[] =
+{
+    NL_TEST_DEF("TestInitiatingReceiverReceiverDrive", TestInitiatingReceiverReceiverDrive),
+    NL_TEST_DEF("TestInitiatingSenderSenderDrive", TestInitiatingSenderSenderDrive),
+    NL_TEST_DEF("TestBadAcceptMessageFields", TestBadAcceptMessageFields),
+    NL_TEST_DEF("TestTimeout", TestTimeout),
+    NL_TEST_DEF("TestDuplicateBlockError", TestDuplicateBlockError),
+    NL_TEST_SENTINEL()
+};
+// clang-format on
+
+// clang-format off
+static nlTestSuite sSuite =
+{
+    "Test-CHIP-TransferSession",
+    &sTests[0],
+    nullptr,
+    nullptr
+};
+// clang-format on
+
+/**
+ *  Main
+ */
+int TestBdxTransferSession()
+{
+    // Run test suit against one context
+    nlTestRunner(&sSuite, nullptr);
+
+    return (nlTestRunnerStats(&sSuite));
+}
+
+CHIP_REGISTER_TEST_SUITE(TestBdxTransferSession)