Add support for the latency spin bit (RFC 9000 section 17.4) to Quiche, storing spin state on a per-path basis

Protected by FLAGS_quic_reloadable_flag_quic_enable_spin_bit.

PiperOrigin-RevId: 885955655
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 709670c..ed32c81 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -38,6 +38,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_disable_resumption, true, true, "If true, disable resumption when receiving NRES connection option.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_mtu_discovery_at_server, false, false, "If true, QUIC will default enable MTU discovery at server, with a target of 1450 bytes.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_server_on_wire_ping, true, true, "If true, enable server retransmittable on wire PING.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_spin_bit, false, false, "When true, enable the QUIC latency spin bit (RFC 9000 section 17.3) on 3/4 of QUIC connections.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_version_rfcv2, false, false, "When true, support RFC9369.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enforce_immediate_goaway, false, true, "If true, QUIC will support sending immediate GOAWAYS and will refuse streams above the limit.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enobufs_blocked, true, true, "If true, ENOBUFS socket errors are reported as socket blocked instead of socket failure.")
diff --git a/quiche/quic/core/quic_buffered_packet_store.h b/quiche/quic/core/quic_buffered_packet_store.h
index 3e6a208..6d0b403 100644
--- a/quiche/quic/core/quic_buffered_packet_store.h
+++ b/quiche/quic/core/quic_buffered_packet_store.h
@@ -389,6 +389,8 @@
     return SEND_TO_WRITER;
   }
 
+  bool NextSpinBitToSend() override { return false; }
+
   // QuicStreamFrameDataProducer
   WriteStreamDataResult WriteStreamData(QuicStreamId /*id*/,
                                         QuicStreamOffset offset,
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 4286a92..18619fd 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -120,6 +120,12 @@
 // but doesn't allow multiple RTTs of user delay in the hope of using ECN.
 const uint8_t kEcnPtoLimit = 2;
 
+// Constant representing a 7/8 probability of enabling the spin bit for each
+// direction of communication. Since the spin bit only works when both sides
+// choose to enable it, this results in a 3/4 probability that any given
+// connection will have the spin bit enabled, per guidance in RFC 9000.
+const uint8_t kSpinDefaultProbability = 223;
+
 // When the clearer goes out of scope, the coalesced packet gets cleared.
 class ScopedCoalescedPacketClearer {
  public:
@@ -229,7 +235,8 @@
       perspective_(perspective),
       owns_writer_(owns_writer),
       can_truncate_connection_ids_(perspective == Perspective::IS_SERVER),
-      store_one_dcid_(GetQuicReloadableFlag(quic_one_dcid)) {
+      store_one_dcid_(GetQuicReloadableFlag(quic_one_dcid)),
+      spin_bit_enabled_(false) {
   QUICHE_DCHECK(perspective_ == Perspective::IS_CLIENT ||
                 default_path_.self_address.IsInitialized());
 
@@ -1227,6 +1234,39 @@
 }
 
 bool QuicConnection::OnPacketHeader(const QuicPacketHeader& header) {
+  if (spin_bit_enabled_ && header.form == IETF_QUIC_SHORT_HEADER_PACKET) {
+    QUIC_CODE_COUNT(quic_enable_spin_bit);
+    QuicPacketNumber largest_observed =
+        uber_received_packet_manager_.GetLargestObserved(
+            ENCRYPTION_FORWARD_SECURE);
+    if (!largest_observed.IsInitialized() ||
+        header.packet_number > largest_observed) {
+      PathState* absl_nonnull path = &default_path_;
+
+      if (perspective_ == Perspective::IS_CLIENT) {
+        if ((header.destination_connection_id ==
+             alternative_path_.client_connection_id) &&
+            (alternative_path_.client_connection_id !=
+             default_path_.client_connection_id)) {
+          path = &alternative_path_;
+        }
+      } else {
+        if ((header.destination_connection_id ==
+             alternative_path_.server_connection_id) &&
+            (alternative_path_.server_connection_id !=
+             default_path_.server_connection_id)) {
+          path = &alternative_path_;
+        }
+      }
+
+      if (perspective_ == Perspective::IS_SERVER) {
+        path->next_spin_bit = header.spin_bit;
+      } else {
+        path->next_spin_bit = !header.spin_bit;
+      }
+    }
+  }
+
   if (debug_visitor_ != nullptr) {
     debug_visitor_->OnPacketHeader(header, clock_->ApproximateNow(),
                                    last_received_packet_info_.decrypted_level);
@@ -7248,6 +7288,7 @@
   stateless_reset_token.reset();
   ecn_marked_packet_acked = false;
   ecn_pto_count = 0;
+  next_spin_bit = false;
 }
 
 QuicConnection::PathState::PathState(PathState&& other) {
@@ -7604,4 +7645,18 @@
 
 #undef ENDPOINT  // undef for jumbo builds
 
+bool QuicConnection::ShouldEnableSpinBit() const {
+  // Whether this connection will use the spin bit depends on (1) the flag
+  // being enabled and, if so, (2) a coin flip to generate probabilistic
+  // participation.
+  if (!GetQuicReloadableFlag(quic_enable_spin_bit)) {
+    return false;
+  }
+
+  QUIC_RELOADABLE_FLAG_COUNT(quic_enable_spin_bit);
+  uint8_t r;
+  random_generator_->RandBytes(&r, 1);
+  return (r < kSpinDefaultProbability);
+}
+
 }  // namespace quic
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index 4decc34..366b036 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -815,6 +815,9 @@
   QuicByteCount GetFlowControlSendWindowSize(QuicStreamId id) override {
     return visitor_->GetFlowControlSendWindowSize(id);
   }
+  bool NextSpinBitToSend() override {
+    return spin_bit_enabled_ && default_path_.next_spin_bit;
+  }
   QuicPacketBuffer GetPacketBuffer() override;
   void OnSerializedPacket(SerializedPacket packet) override;
   void OnUnrecoverableError(QuicErrorCode error,
@@ -1518,6 +1521,10 @@
     return framer_.process_reset_stream_at();
   }
 
+  // Returns true if the spin bit should be enabled per the RFC 9000 spin
+  // participation guidance.
+  bool ShouldEnableSpinBit() const;
+
  protected:
   // Calls cancel() on all the alarms owned by this connection.
   void CancelAllAlarms();
@@ -1660,6 +1667,8 @@
     // How many total PTOs have fired since the connection started sending ECN
     // on this path, but before an ECN-marked packet has been acked.
     uint8_t ecn_pto_count = 0;
+    // The value of the spin bit for the next packet to be sent on this path.
+    bool next_spin_bit = false;
   };
 
   using QueuedPacketList = std::list<SerializedPacket>;
@@ -2568,6 +2577,8 @@
   const bool quic_fix_timeouts_ : 1 = GetQuicReloadableFlag(quic_fix_timeouts);
   bool quic_close_on_idle_timeout_ : 1 =
       GetQuicReloadableFlag(quic_close_on_idle_timeout);
+  // True if spin bit is enabled for this connection.
+  bool spin_bit_enabled_ : 1 = ShouldEnableSpinBit();
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index e7b0832..802923b 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -26,6 +26,7 @@
 #include "quiche/quic/core/frames/quic_path_response_frame.h"
 #include "quiche/quic/core/frames/quic_reset_stream_at_frame.h"
 #include "quiche/quic/core/frames/quic_rst_stream_frame.h"
+#include "quiche/quic/core/quic_buffered_packet_store.h"
 #include "quiche/quic/core/quic_connection_id.h"
 #include "quiche/quic/core/quic_constants.h"
 #include "quiche/quic/core/quic_error_codes.h"
@@ -614,8 +615,8 @@
         alarm_factory_(new TestAlarmFactory()),
         peer_framer_(SupportedVersions(version()), QuicTime::Zero(),
                      Perspective::IS_SERVER, connection_id_.length()),
-        peer_creator_(connection_id_, &peer_framer_,
-                      /*delegate=*/nullptr),
+        peer_creator_delegate_(&buffer_allocator_),
+        peer_creator_(connection_id_, &peer_framer_, &peer_creator_delegate_),
         writer_(
             new TestPacketWriter(version(), &clock_, Perspective::IS_CLIENT)),
         connection_(connection_id_, kSelfAddress, kPeerAddress, helper_.get(),
@@ -749,6 +750,25 @@
     }
   }
 
+  void ProcessPacketWithSpinBit(bool spin_bit, uint64_t pkn) {
+    QuicPacketHeader header =
+        ConstructPacketHeader(pkn, ENCRYPTION_FORWARD_SECURE);
+    header.spin_bit = spin_bit;
+    QuicFrames frames;
+    frames.push_back(QuicFrame(QuicPingFrame()));
+    std::unique_ptr<QuicPacket> packet(ConstructPacket(header, frames));
+    char buffer[kMaxOutgoingPacketSize];
+    size_t encrypted_length = peer_framer_.EncryptPayload(
+        ENCRYPTION_FORWARD_SECURE, header.packet_number, *packet, buffer,
+        kMaxOutgoingPacketSize);
+    connection_.ProcessUdpPacket(
+        kSelfAddress, kPeerAddress,
+        QuicReceivedPacket(buffer, encrypted_length, clock_.Now(), false));
+    if (connection_.GetSendAlarm()->IsSet()) {
+      connection_.GetSendAlarm()->Fire();
+    }
+  }
+
   void ProcessReceivedPacket(const QuicSocketAddress& self_address,
                              const QuicSocketAddress& peer_address,
                              const QuicReceivedPacket& packet) {
@@ -758,6 +778,26 @@
     }
   }
 
+  void ProcessPacketWithSpinBitAndAddress(
+      bool spin_bit, uint64_t pkn, const QuicSocketAddress& peer_address) {
+    QuicPacketHeader header =
+        ConstructPacketHeader(pkn, ENCRYPTION_FORWARD_SECURE);
+    header.spin_bit = spin_bit;
+    QuicFrames frames;
+    frames.push_back(QuicFrame(QuicPingFrame()));
+    std::unique_ptr<QuicPacket> packet(ConstructPacket(header, frames));
+    char buffer[kMaxOutgoingPacketSize];
+    size_t encrypted_length = peer_framer_.EncryptPayload(
+        ENCRYPTION_FORWARD_SECURE, header.packet_number, *packet, buffer,
+        kMaxOutgoingPacketSize);
+    connection_.ProcessUdpPacket(
+        kSelfAddress, peer_address,
+        QuicReceivedPacket(buffer, encrypted_length, clock_.Now(), false));
+    if (connection_.GetSendAlarm()->IsSet()) {
+      connection_.GetSendAlarm()->Fire();
+    }
+  }
+
   QuicFrame MakeCryptoFrame() const {
     if (VersionIsIetfQuic(connection_.transport_version())) {
       return QuicFrame(new QuicCryptoFrame(crypto_frame_));
@@ -1560,6 +1600,7 @@
   std::unique_ptr<TestConnectionHelper> helper_;
   std::unique_ptr<TestAlarmFactory> alarm_factory_;
   QuicFramer peer_framer_;
+  PacketCollector peer_creator_delegate_;
   QuicPacketCreator peer_creator_;
   std::unique_ptr<TestPacketWriter> writer_;
   TestConnection connection_;
@@ -18181,6 +18222,113 @@
                              ENCRYPTION_FORWARD_SECURE);
 }
 
+TEST_P(QuicConnectionTest, DisabledSpinBit) {
+  const int iterations = 10;
+
+  SetQuicReloadableFlag(quic_enable_spin_bit, false);
+
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  EXPECT_CALL(*send_algorithm_, EnableECT1()).WillRepeatedly(Return(false));
+  EXPECT_CALL(*send_algorithm_, EnableECT0()).WillRepeatedly(Return(false));
+
+  QuicConfig config;
+  connection_.SetFromConfig(config);
+  bool saw_enabled = false;
+  for (int i = 0; i < iterations; i++) {
+    saw_enabled |= QuicConnectionPeer::GetSpinBitEnabled(&connection_);
+  }
+  EXPECT_FALSE(saw_enabled);
+}
+
+TEST_P(QuicConnectionTest, EnabledSpinBit) {
+  SetQuicReloadableFlag(quic_enable_spin_bit, true);
+  ON_CALL(random_generator_, RandBytes(_, 1))
+      .WillByDefault([](void* data, size_t len) {
+        ASSERT_EQ(1u, len);
+        *static_cast<uint8_t*>(data) = 0x00;
+      });
+
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  EXPECT_CALL(*send_algorithm_, EnableECT1()).WillRepeatedly(Return(false));
+  EXPECT_CALL(*send_algorithm_, EnableECT0()).WillRepeatedly(Return(false));
+
+  QuicConfig config;
+  connection_.SetFromConfig(config);
+  EXPECT_FALSE(QuicConnectionPeer::GetSpinBitEnabled(&connection_));
+}
+
+TEST_P(QuicConnectionTest, ClientSpinsSpinBit) {
+  SetQuicReloadableFlag(quic_enable_spin_bit, true);
+  set_perspective(Perspective::IS_CLIENT);
+  // Skip test for non-IETF versions,
+  if (!version().IsIetfQuic()) {
+    return;
+  }
+  QuicConfig config;
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  EXPECT_CALL(*send_algorithm_, EnableECT1()).WillOnce(Return(false));
+  EXPECT_CALL(*send_algorithm_, EnableECT0()).WillOnce(Return(false));
+  connection_.SetFromConfig(config);
+  QuicConnectionPeer::SetSpinBitEnabled(&connection_, true);
+  ASSERT_TRUE(QuicConnectionPeer::GetSpinBitEnabled(&connection_));
+
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  peer_framer_.SetEncrypter(
+      ENCRYPTION_FORWARD_SECURE,
+      std::make_unique<TaggingEncrypter>(ENCRYPTION_FORWARD_SECURE));
+  connection_.OnHandshakeComplete();
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+
+  const int kPacketCount = 4;
+  bool next_spin = false;
+  QuicStreamOffset offset = 0;
+
+  for (int i = 0; i < kPacketCount; i++) {
+    ProcessPacketWithSpinBit(next_spin, i + 1);
+    connection_.SendStreamDataWithString(3, "foo", offset, NO_FIN);
+    EXPECT_EQ(!next_spin, writer_->last_packet_header().spin_bit);
+    next_spin = !next_spin;
+    offset += 3;
+  }
+}
+
+TEST_P(QuicConnectionTest, ServerReflectsSpinBit) {
+  SetQuicReloadableFlag(quic_enable_spin_bit, true);
+  set_perspective(Perspective::IS_SERVER);
+  // Skip test for non-IETF versions,
+  if (!version().IsIetfQuic()) {
+    return;
+  }
+  QuicConfig config;
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  EXPECT_CALL(*send_algorithm_, EnableECT1()).WillOnce(Return(false));
+  EXPECT_CALL(*send_algorithm_, EnableECT0()).WillOnce(Return(false));
+  connection_.SetFromConfig(config);
+  QuicConnectionPeer::SetSpinBitEnabled(&connection_, true);
+  ASSERT_TRUE(QuicConnectionPeer::GetSpinBitEnabled(&connection_));
+
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  peer_framer_.SetEncrypter(
+      ENCRYPTION_FORWARD_SECURE,
+      std::make_unique<TaggingEncrypter>(ENCRYPTION_FORWARD_SECURE));
+  connection_.OnHandshakeComplete();
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+
+  const int kPacketCount = 4;
+  bool next_spin = false;
+  QuicStreamOffset offset = 0;
+
+  for (int i = 0; i < kPacketCount; i++) {
+    ProcessPacketWithSpinBit(next_spin, i + 1);
+    connection_.SendStreamDataWithString(3, "foo", offset, NO_FIN);
+    EXPECT_EQ(next_spin, writer_->last_packet_header().spin_bit);
+    next_spin = !next_spin;
+    offset += 3;
+  }
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_framer.cc b/quiche/quic/core/quic_framer.cc
index 253c1b0..1be4f2a 100644
--- a/quiche/quic/core/quic_framer.cc
+++ b/quiche/quic/core/quic_framer.cc
@@ -2006,8 +2006,10 @@
   } else {
     type = static_cast<uint8_t>(
         FLAGS_FIXED_BIT | (current_key_phase_bit_ ? FLAGS_KEY_PHASE_BIT : 0) |
+        (header.spin_bit ? FLAGS_SPIN_BIT : 0) |
         PacketNumberLengthToOnWireValue(header.packet_number_length));
   }
+
   return writer->WriteUInt8(type);
 }
 
@@ -2440,8 +2442,10 @@
         header->packet_number_length =
             GetShortHeaderPacketNumberLength(header->type_byte);
       }
+      header->spin_bit = (header->type_byte & FLAGS_SPIN_BIT) > 0;
       return true;
     }
+
     if (header->long_packet_type == RETRY) {
       if (!version().IsIetfQuic()) {
         set_detailed_error("RETRY not supported in this version.");
diff --git a/quiche/quic/core/quic_framer_test.cc b/quiche/quic/core/quic_framer_test.cc
index 2885f04..86583b6 100644
--- a/quiche/quic/core/quic_framer_test.cc
+++ b/quiche/quic/core/quic_framer_test.cc
@@ -15014,6 +15014,59 @@
   }
 }
 
+TEST_P(QuicFramerTest, SpinBit) {
+  if (!framer_.version().IsIetfQuic()) {
+    return;
+  }
+  SetDecrypterLevel(ENCRYPTION_FORWARD_SECURE);
+  QuicFramerPeer::SetLargestPacketNumber(&framer_, kPacketNumber - 2);
+
+  // clang-format off
+  unsigned char packet[] = {
+    // type (short header, 1 byte packet number, spin bit on)
+    0x40 | FLAGS_SPIN_BIT,
+    // connection_id
+    0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
+    // packet number
+    0x78,
+    // padding
+    0x00, 0x00, 0x00,
+  };
+  // clang-format on
+
+  QuicEncryptedPacket encrypted(AsChars(packet), ABSL_ARRAYSIZE(packet), false);
+  EXPECT_TRUE(framer_.ProcessPacket(encrypted));
+  EXPECT_THAT(framer_.error(), IsQuicNoError());
+  ASSERT_TRUE(visitor_.header_.get());
+  EXPECT_EQ(FramerTestConnectionId(),
+            visitor_.header_->destination_connection_id);
+  EXPECT_FALSE(visitor_.header_->reset_flag);
+  EXPECT_FALSE(visitor_.header_->version_flag);
+  EXPECT_EQ(PACKET_1BYTE_PACKET_NUMBER, visitor_.header_->packet_number_length);
+  EXPECT_EQ(kPacketNumber, visitor_.header_->packet_number);
+  EXPECT_TRUE(visitor_.header_->spin_bit);
+}
+
+TEST_P(QuicFramerTest, BuildPacketWithSpinBit) {
+  if (!framer_.version().IsIetfQuic()) {
+    return;
+  }
+  QuicFramerPeer::SetPerspective(&framer_, Perspective::IS_CLIENT);
+  QuicPacketHeader header;
+  header.destination_connection_id = FramerTestConnectionId();
+  header.reset_flag = false;
+  header.version_flag = false;
+  header.packet_number_length = PACKET_1BYTE_PACKET_NUMBER;
+  header.packet_number = kPacketNumber;
+  header.spin_bit = true;
+
+  QuicFrames frames = {QuicFrame(QuicPingFrame())};
+
+  std::unique_ptr<QuicPacket> data(BuildDataPacket(header, frames));
+  ASSERT_TRUE(data != nullptr);
+  EXPECT_TRUE(data->data()[0] & FLAGS_SPIN_BIT);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_packet_creator.cc b/quiche/quic/core/quic_packet_creator.cc
index 45f78f6..137a4e4 100644
--- a/quiche/quic/core/quic_packet_creator.cc
+++ b/quiche/quic/core/quic_packet_creator.cc
@@ -1895,6 +1895,9 @@
   header->retry_token = GetRetryToken();
   header->length_length = GetLengthLength();
   header->remaining_packet_length = 0;
+
+  header->spin_bit = delegate_->NextSpinBitToSend();
+
   if (!HasIetfLongHeader()) {
     return;
   }
diff --git a/quiche/quic/core/quic_packet_creator.h b/quiche/quic/core/quic_packet_creator.h
index 8fff612..737e32b 100644
--- a/quiche/quic/core/quic_packet_creator.h
+++ b/quiche/quic/core/quic_packet_creator.h
@@ -79,6 +79,9 @@
     // serialized.
     virtual SerializedPacketFate GetSerializedPacketFate(
         bool is_mtu_discovery, EncryptionLevel encryption_level) = 0;
+
+    // Return the spin bit value to send in the next short header packet.
+    virtual bool NextSpinBitToSend() = 0;
   };
 
   // Interface which gets callbacks from the QuicPacketCreator at interesting
diff --git a/quiche/quic/core/quic_packet_creator_test.cc b/quiche/quic/core/quic_packet_creator_test.cc
index 62e7c6d..f4c3262 100644
--- a/quiche/quic/core/quic_packet_creator_test.cc
+++ b/quiche/quic/core/quic_packet_creator_test.cc
@@ -56,28 +56,36 @@
 }
 
 // Run tests with combinations of {ParsedQuicVersion,
-// ToggleVersionSerialization}.
+// ToggleVersionSerialization, and spin bit}.
 struct TestParams {
-  TestParams(ParsedQuicVersion version, bool version_serialization)
-      : version(version), version_serialization(version_serialization) {}
+  TestParams(ParsedQuicVersion version, bool version_serialization,
+             bool spin_bit)
+      : version(version),
+        version_serialization(version_serialization),
+        spin_bit(spin_bit) {}
 
   ParsedQuicVersion version;
   bool version_serialization;
+  bool spin_bit;
 };
 
 // Used by ::testing::PrintToStringParamName().
 std::string PrintToString(const TestParams& p) {
   return absl::StrCat(ParsedQuicVersionToString(p.version), "_",
-                      (p.version_serialization ? "Include" : "No"), "Version");
+                      (p.version_serialization ? "Include" : "No"), "Version",
+                      "_", (p.spin_bit ? "Spin" : "NoSpin"));
 }
 
 // Constructs various test permutations.
 std::vector<TestParams> GetTestParams() {
   std::vector<TestParams> params;
   ParsedQuicVersionVector all_supported_versions = AllSupportedVersions();
-  for (size_t i = 0; i < all_supported_versions.size(); ++i) {
-    params.push_back(TestParams(all_supported_versions[i], true));
-    params.push_back(TestParams(all_supported_versions[i], false));
+  for (const auto& version : all_supported_versions) {
+    for (bool version_serialization : {true, false}) {
+      for (bool spin_bit : {true, false}) {
+        params.push_back(TestParams(version, version_serialization, spin_bit));
+      }
+    }
   }
   return params;
 }
@@ -146,6 +154,8 @@
         .WillRepeatedly(Return(QuicPacketBuffer()));
     EXPECT_CALL(delegate_, GetSerializedPacketFate(_, _))
         .WillRepeatedly(Return(SEND_TO_WRITER));
+    EXPECT_CALL(delegate_, NextSpinBitToSend())
+        .WillRepeatedly(Return(GetParam().spin_bit));
     creator_.SetEncrypter(
         ENCRYPTION_INITIAL,
         std::make_unique<TaggingEncrypter>(ENCRYPTION_INITIAL));
@@ -2558,6 +2568,7 @@
               (override));
   MOCK_METHOD(SerializedPacketFate, GetSerializedPacketFate,
               (bool, EncryptionLevel), (override));
+  MOCK_METHOD(bool, NextSpinBitToSend, (), (override));
 
   void SetCanWriteAnything() {
     EXPECT_CALL(*this, ShouldGeneratePacket(_, _)).WillRepeatedly(Return(true));
@@ -2691,6 +2702,7 @@
         .WillRepeatedly(Return(QuicPacketBuffer()));
     EXPECT_CALL(delegate_, GetSerializedPacketFate(_, _))
         .WillRepeatedly(Return(SEND_TO_WRITER));
+    EXPECT_CALL(delegate_, NextSpinBitToSend()).WillRepeatedly(Return(false));
     EXPECT_CALL(delegate_, GetFlowControlSendWindowSize(_))
         .WillRepeatedly(Return(std::numeric_limits<QuicByteCount>::max()));
     creator_.SetEncrypter(
diff --git a/quiche/quic/core/quic_packets.cc b/quiche/quic/core/quic_packets.cc
index 8064434..cfa2df2 100644
--- a/quiche/quic/core/quic_packets.cc
+++ b/quiche/quic/core/quic_packets.cc
@@ -166,9 +166,10 @@
       type_byte(0),
       destination_connection_id_included(CONNECTION_ID_PRESENT),
       source_connection_id_included(CONNECTION_ID_ABSENT),
-      reset_flag(false),
       version_flag(false),
+      reset_flag(false),
       has_possible_stateless_reset_token(false),
+      spin_bit(false),
       version(UnsupportedQuicVersion()),
       source_connection_id(EmptyQuicConnectionId()),
       remaining_packet_length(0),
@@ -255,7 +256,8 @@
        << absl::BytesToHexString(
               absl::string_view(header.nonce->data(), header.nonce->size()));
   }
-  os << ", packet_number: " << header.packet_number << " }\n";
+  os << ", packet_number: " << header.packet_number
+     << ", spin_bit: " << header.spin_bit << " }\n";
   return os;
 }
 
@@ -604,6 +606,7 @@
          form == other.form && long_packet_type == other.long_packet_type &&
          possible_stateless_reset_token ==
              other.possible_stateless_reset_token &&
+         spin_bit == other.spin_bit &&
          retry_token_length_length == other.retry_token_length_length &&
          retry_token == other.retry_token &&
          length_length == other.length_length &&
diff --git a/quiche/quic/core/quic_packets.h b/quiche/quic/core/quic_packets.h
index a73184e..31b4e85 100644
--- a/quiche/quic/core/quic_packets.h
+++ b/quiche/quic/core/quic_packets.h
@@ -134,15 +134,18 @@
   uint8_t type_byte;
   QuicConnectionIdIncluded destination_connection_id_included;
   QuicConnectionIdIncluded source_connection_id_included;
-  // TODO(martinduke): Compress these into bitfields.
-  // This is only used for Google QUIC.
-  bool reset_flag;
   // For Google QUIC, version flag in packets from the server means version
   // negotiation packet. For IETF QUIC, version flag means long header.
   bool version_flag;
+  // --- bitfield-able bools in the first cacheline go here ---
+  // This is only used for Google QUIC.
+  bool reset_flag : 1;
   // Indicates whether |possible_stateless_reset_token| contains a valid value
   // parsed from the packet buffer. IETF QUIC only, always false for GQUIC.
-  bool has_possible_stateless_reset_token;
+  bool has_possible_stateless_reset_token : 1;
+  // Latency spin bit on the short packet header (RFC 9000 Section 17.4)
+  bool spin_bit : 1;
+  // -- end bitfield-able bools in the first cacheline --
 
   // There are 8 bytes still available in the first cacheline.  Start with long
   // header stuff.
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index bb5e3e8..dcf16b5 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -649,9 +649,10 @@
   // bit to 0, allowing to distinguish Google QUIC packets from short header
   // packets.
   FLAGS_DEMULTIPLEXING_BIT = 1 << 3,
-  // Bits 4 and 5: Reserved bits for short header.
+  // Bits 4: Reserved bit for short header.
   FLAGS_SHORT_HEADER_RESERVED_1 = 1 << 4,
-  FLAGS_SHORT_HEADER_RESERVED_2 = 1 << 5,
+  // Bit 5: the spin bit.
+  FLAGS_SPIN_BIT = 1 << 5,
   // Bit 6: the 'QUIC' bit.
   FLAGS_FIXED_BIT = 1 << 6,
   // Bit 7: Indicates the header is long or short header.
diff --git a/quiche/quic/test_tools/quic_connection_peer.cc b/quiche/quic/test_tools/quic_connection_peer.cc
index 1e549f9..818576e 100644
--- a/quiche/quic/test_tools/quic_connection_peer.cc
+++ b/quiche/quic/test_tools/quic_connection_peer.cc
@@ -629,5 +629,16 @@
   return connection->active_migration_disabled_;
 }
 
+// static
+void QuicConnectionPeer::SetSpinBitEnabled(QuicConnection* connection,
+                                           bool enabled) {
+  connection->spin_bit_enabled_ = enabled;
+}
+
+// static
+bool QuicConnectionPeer::GetSpinBitEnabled(QuicConnection* connection) {
+  return connection->spin_bit_enabled_;
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/test_tools/quic_connection_peer.h b/quiche/quic/test_tools/quic_connection_peer.h
index d7b9378..0016642 100644
--- a/quiche/quic/test_tools/quic_connection_peer.h
+++ b/quiche/quic/test_tools/quic_connection_peer.h
@@ -258,6 +258,10 @@
   static uint64_t GetPeerReorderingThreshold(QuicConnection* connection);
 
   static bool ConnectionMigrationDisabled(QuicConnection* connection);
+
+  static void SetSpinBitEnabled(QuicConnection* connection, bool enabled);
+
+  static bool GetSpinBitEnabled(QuicConnection* connection);
 };
 
 }  // namespace test
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h
index c040b85..2e1149a 100644
--- a/quiche/quic/test_tools/quic_test_utils.h
+++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -1433,6 +1433,7 @@
               (override));
   MOCK_METHOD(SerializedPacketFate, GetSerializedPacketFate,
               (bool, EncryptionLevel), (override));
+  MOCK_METHOD(bool, NextSpinBitToSend, (), (override));
 };
 
 class MockSessionNotifier : public SessionNotifierInterface {