Add a dedicated class to represent receive timestamps

We currently use QuicTime for this purpose.  While that is a correct time for timestamps generated locally, it is not correct for those received from the peer, despite QuicFramer trying to pretend that is the case (by rebasing those with respect to the local framer's creation time).

This CL introduces a new class, QuicReceiveTime, to represent both local and remote timestamps.  It moves the `creation_time` subtraction from the framer to the receive packet manager, as a part of this process.  It also removes an overflow-prone `creation_time` addition on the incoming acks side.

PiperOrigin-RevId: 1001326952
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 2d56fab..cf9b469 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -367,6 +367,7 @@
     "quic/core/quic_ping_manager.h",
     "quic/core/quic_process_packet_interface.h",
     "quic/core/quic_queue_alarm_factory.h",
+    "quic/core/quic_receive_time.h",
     "quic/core/quic_received_packet_manager.h",
     "quic/core/quic_sent_packet_manager.h",
     "quic/core/quic_server_id.h",
diff --git a/build/source_list.gni b/build/source_list.gni
index 7b8c3a1..04c126b 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -367,6 +367,7 @@
     "src/quiche/quic/core/quic_ping_manager.h",
     "src/quiche/quic/core/quic_process_packet_interface.h",
     "src/quiche/quic/core/quic_queue_alarm_factory.h",
+    "src/quiche/quic/core/quic_receive_time.h",
     "src/quiche/quic/core/quic_received_packet_manager.h",
     "src/quiche/quic/core/quic_sent_packet_manager.h",
     "src/quiche/quic/core/quic_server_id.h",
diff --git a/build/source_list.json b/build/source_list.json
index 79230dd..1b0d59d 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -366,6 +366,7 @@
     "quiche/quic/core/quic_ping_manager.h",
     "quiche/quic/core/quic_process_packet_interface.h",
     "quiche/quic/core/quic_queue_alarm_factory.h",
+    "quiche/quic/core/quic_receive_time.h",
     "quiche/quic/core/quic_received_packet_manager.h",
     "quiche/quic/core/quic_sent_packet_manager.h",
     "quiche/quic/core/quic_server_id.h",
diff --git a/quiche/quic/core/chlo_extractor.cc b/quiche/quic/core/chlo_extractor.cc
index 7a9689b..a8e0e88 100644
--- a/quiche/quic/core/chlo_extractor.cc
+++ b/quiche/quic/core/chlo_extractor.cc
@@ -61,7 +61,7 @@
                        QuicTime::Delta ack_delay_time) override;
   bool OnAckRange(QuicPacketNumber start, QuicPacketNumber end) override;
   bool OnAckTimestamp(QuicPacketNumber packet_number,
-                      QuicTime timestamp) override;
+                      QuicReceiveTime timestamp) override;
   bool OnAckFrameEnd(QuicPacketNumber start,
                      const std::optional<QuicEcnCounts>& ecn_counts) override;
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override;
@@ -226,7 +226,7 @@
 }
 
 bool ChloFramerVisitor::OnAckTimestamp(QuicPacketNumber /*packet_number*/,
-                                       QuicTime /*timestamp*/) {
+                                       QuicReceiveTime /*timestamp*/) {
   return true;
 }
 
diff --git a/quiche/quic/core/congestion_control/bandwidth_sampler.cc b/quiche/quic/core/congestion_control/bandwidth_sampler.cc
index 1af1172..d5fef80 100644
--- a/quiche/quic/core/congestion_control/bandwidth_sampler.cc
+++ b/quiche/quic/core/congestion_control/bandwidth_sampler.cc
@@ -6,11 +6,13 @@
 
 #include <algorithm>
 #include <cstddef>
+#include <optional>
 #include <ostream>
 
 #include "quiche/quic/core/congestion_control/send_algorithm_interface.h"
 #include "quiche/quic/core/quic_bandwidth.h"
 #include "quiche/quic/core/quic_packet_number.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_unacked_packet_map.h"
@@ -197,11 +199,10 @@
     // `sent_time` here.  Fortunately, since setting A_0 time to `sent_time`
     // inherently underestimates bandwidth, the resulting sample should be fine
     // without the anti-overestimation effect of receive timestamps.
-    last_acked_packet_receive_time_ = QuicTime::Zero();
+    last_acked_packet_receive_time_ = std::nullopt;
     if (overestimate_avoidance_) {
       recent_ack_points_.Clear();
-      recent_ack_points_.Update(sent_time, QuicTime::Zero(),
-                                total_bytes_acked_);
+      recent_ack_points_.Update(sent_time, std::nullopt, total_bytes_acked_);
       a0_candidates_.clear();
       a0_candidates_.push_back(recent_ack_points_.MostRecentPoint());
     }
@@ -478,12 +479,13 @@
   // bandwidth sample.  Those generally should not be trusted for increasing
   // the bandwidth estimate to avoid "speculative ACK"-style attacks, but they
   // do work as a reasonable signal to compute an upper bound.
-  const QuicTime recv_time1 = acked_packet.receive_timestamp;
-  const QuicTime recv_time0 = a0.receive_time;
-  if (recv_time0.IsInitialized() && recv_time1.IsInitialized() &&
-      recv_time0 < recv_time1) {
+  const std::optional<QuicReceiveTime> recv_time1 =
+      acked_packet.receive_timestamp;
+  const std::optional<QuicReceiveTime> recv_time0 = a0.receive_time;
+  if (recv_time0.has_value() && recv_time1.has_value() &&
+      *recv_time0 < *recv_time1) {
     const QuicBandwidth recv_rate = QuicBandwidth::FromBytesAndTimeDelta(
-        total_bytes_acked_ - a0.total_bytes_acked, recv_time1 - recv_time0);
+        total_bytes_acked_ - a0.total_bytes_acked, *recv_time1 - *recv_time0);
     sample.bandwidth = std::min(sample.bandwidth, recv_rate);
   }
 
diff --git a/quiche/quic/core/congestion_control/bandwidth_sampler.h b/quiche/quic/core/congestion_control/bandwidth_sampler.h
index d8f433b..e11e6bf 100644
--- a/quiche/quic/core/congestion_control/bandwidth_sampler.h
+++ b/quiche/quic/core/congestion_control/bandwidth_sampler.h
@@ -6,6 +6,7 @@
 #define QUICHE_QUIC_CORE_CONGESTION_CONTROL_BANDWIDTH_SAMPLER_H_
 
 #include <cstdint>
+#include <optional>
 #include <ostream>
 
 #include "quiche/quic/core/congestion_control/send_algorithm_interface.h"
@@ -13,6 +14,7 @@
 #include "quiche/quic/core/packet_number_indexed_queue.h"
 #include "quiche/quic/core/quic_bandwidth.h"
 #include "quiche/quic/core/quic_packet_number.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_unacked_packet_map.h"
@@ -394,13 +396,13 @@
   // AckPoint represents a point on the ack line.
   struct QUICHE_EXPORT AckPoint {
     QuicTime ack_time = QuicTime::Zero();
-    QuicTime receive_time = QuicTime::Zero();
+    std::optional<QuicReceiveTime> receive_time = std::nullopt;
     QuicByteCount total_bytes_acked = 0;
 
     friend QUICHE_EXPORT std::ostream& operator<<(std::ostream& os,
                                                   const AckPoint& ack_point) {
-      if (ack_point.receive_time.IsInitialized()) {
-        return os << ack_point.ack_time << "(" << ack_point.receive_time
+      if (ack_point.receive_time.has_value()) {
+        return os << ack_point.ack_time << "(" << *ack_point.receive_time
                   << "):" << ack_point.total_bytes_acked;
       }
       return os << ack_point.ack_time << ":" << ack_point.total_bytes_acked;
@@ -410,7 +412,7 @@
   // RecentAckPoints maintains the most recent 2 ack points at distinct times.
   class QUICHE_EXPORT RecentAckPoints {
    public:
-    void Update(QuicTime ack_time, QuicTime receive_time,
+    void Update(QuicTime ack_time, std::optional<QuicReceiveTime> receive_time,
                 QuicByteCount total_bytes_acked) {
       QUICHE_DCHECK_GE(total_bytes_acked, ack_points_[1].total_bytes_acked);
 
@@ -486,7 +488,7 @@
 
     // The value of |last_acked_packet_receive_time_| at the time the packet was
     // sent.
-    QuicTime last_acked_packet_receive_time() const {
+    std::optional<QuicReceiveTime> last_acked_packet_receive_time() const {
       return last_acked_packet_receive_time_;
     }
 
@@ -533,7 +535,8 @@
     QuicByteCount total_bytes_sent_at_last_acked_packet_ = 0;
     QuicTime last_acked_packet_sent_time_ = QuicTime::Zero();
     QuicTime last_acked_packet_ack_time_ = QuicTime::Zero();
-    QuicTime last_acked_packet_receive_time_ = QuicTime::Zero();
+    std::optional<QuicReceiveTime> last_acked_packet_receive_time_ =
+        std::nullopt;
     SendTimeState send_time_state_;
   };
 
@@ -585,9 +588,9 @@
   // The time at which the most recent packet was acknowledged.
   QuicTime last_acked_packet_ack_time_ = QuicTime::Zero();
 
-  // The peer-specified receive time of the most recent packet.  Zero if receive
-  // timestamps are not supported.
-  QuicTime last_acked_packet_receive_time_ = QuicTime::Zero();
+  // The peer-specified receive time of the most recent packet.  std::nullopt if
+  // receive timestamps are not supported.
+  std::optional<QuicReceiveTime> last_acked_packet_receive_time_ = std::nullopt;
 
   // The most recently sent packet.
   QuicPacketNumber last_sent_packet_;
diff --git a/quiche/quic/core/congestion_control/bandwidth_sampler_test.cc b/quiche/quic/core/congestion_control/bandwidth_sampler_test.cc
index 2a34327..7326cc6 100644
--- a/quiche/quic/core/congestion_control/bandwidth_sampler_test.cc
+++ b/quiche/quic/core/congestion_control/bandwidth_sampler_test.cc
@@ -6,11 +6,13 @@
 
 #include <algorithm>
 #include <cstdint>
+#include <optional>
 #include <set>
 #include <string>
 
 #include "absl/base/nullability.h"
 #include "quiche/quic/core/quic_bandwidth.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/platform/api/quic_expect_bug.h"
@@ -37,7 +39,7 @@
       BandwidthSampler* absl_nonnull sampler, QuicTime ack_time,
       QuicPacketNumber packet_number) {
     // `bytes_acked` is not used by the BandwidthSampler.
-    AckedPacket packet(packet_number, /*bytes_acked=*/0, QuicTime::Zero());
+    AckedPacket packet(packet_number, /*bytes_acked=*/0, std::nullopt);
     return sampler->OnPacketAcknowledged(ack_time, packet);
   }
 };
@@ -100,8 +102,9 @@
                     HAS_RETRANSMITTABLE_DATA);
   }
 
-  BandwidthSample AckPacketInner(uint64_t packet_number,
-                                 QuicTime receive_time = QuicTime::Zero()) {
+  BandwidthSample AckPacketInner(
+      uint64_t packet_number,
+      std::optional<QuicReceiveTime> receive_time = std::nullopt) {
     QuicByteCount size = BandwidthSamplerPeer::GetPacketSize(
         sampler_, QuicPacketNumber(packet_number));
     bytes_in_flight_ -= size;
@@ -117,8 +120,9 @@
     return bandwidth_sample;
   }
 
-  AckedPacket MakeAckedPacket(uint64_t packet_number,
-                              QuicTime receive_time) const {
+  AckedPacket MakeAckedPacket(
+      uint64_t packet_number,
+      std::optional<QuicReceiveTime> receive_time) const {
     QuicByteCount size = BandwidthSamplerPeer::GetPacketSize(
         sampler_, QuicPacketNumber(packet_number));
     return AckedPacket(QuicPacketNumber(packet_number), size, receive_time);
@@ -131,8 +135,9 @@
   }
 
   // Acknowledge receipt of a packet and expect it to be not app-limited.
-  QuicBandwidth AckPacket(uint64_t packet_number,
-                          QuicTime receive_time = QuicTime::Zero()) {
+  QuicBandwidth AckPacket(
+      uint64_t packet_number,
+      std::optional<QuicReceiveTime> receive_time = std::nullopt) {
     BandwidthSample sample = AckPacketInner(packet_number, receive_time);
     return sample.bandwidth;
   }
@@ -143,7 +148,7 @@
     AckedPacketVector acked_packets;
     for (auto it = acked_packet_numbers.begin();
          it != acked_packet_numbers.end(); ++it) {
-      acked_packets.push_back(MakeAckedPacket(*it, QuicTime::Zero()));
+      acked_packets.push_back(MakeAckedPacket(*it, std::nullopt));
       bytes_in_flight_ -= acked_packets.back().bytes_acked;
     }
 
@@ -609,7 +614,7 @@
   clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(10));
   BandwidthSampler::CongestionEventSample sample = sampler_.OnCongestionEvent(
       clock_.Now(),
-      {AckedPacket(QuicPacketNumber(1), kRegularPacketSize, clock_.Now())}, {},
+      {AckedPacket(QuicPacketNumber(1), kRegularPacketSize, std::nullopt)}, {},
       max_bandwidth_, est_bandwidth_upper_bound_, round_trip_count_);
   EXPECT_EQ(0u, sampler_.total_bytes_acked());
   EXPECT_EQ(QuicBandwidth::Zero(), sample.sample_max_bandwidth);
@@ -925,7 +930,7 @@
       QuicTimeDelta::FromMilliseconds(10);
   constexpr QuicBandwidth kExpectedBandwidth =
       QuicBandwidth::FromBytesPerSecond(kRegularPacketSize * 100);
-  QuicTime current_peer_clock = QuicTime::Zero() + 12345 * kTimeBetweenPackets;
+  QuicTimeDelta current_peer_clock = 12345 * kTimeBetweenPackets;
 
   // We need to be in the middle of a flight of packets in order to compute a
   // rate based on receive timestamps, otherwise the A_0 value will be missing.
@@ -938,7 +943,8 @@
     SendPacket(i);
     clock_.AdvanceTime(kTimeBetweenPackets);
     current_peer_clock = current_peer_clock + kTimeBetweenPackets;
-    QuicBandwidth current_sample = AckPacket(i - 1, current_peer_clock);
+    QuicBandwidth current_sample =
+        AckPacket(i - 1, QuicReceiveTime::FromPeerTime(current_peer_clock));
     EXPECT_EQ(kExpectedBandwidth, current_sample);
   }
 
@@ -948,7 +954,8 @@
     SendPacket(i);
     clock_.AdvanceTime(kTimeBetweenPackets);
     current_peer_clock = current_peer_clock + 2 * kTimeBetweenPackets;
-    QuicBandwidth current_sample = AckPacket(i - 1, current_peer_clock);
+    QuicBandwidth current_sample =
+        AckPacket(i - 1, QuicReceiveTime::FromPeerTime(current_peer_clock));
     if (i > 21) {
       // i=21 is the transitional packet, and thus has to be skipped.
       EXPECT_EQ(0.5 * kExpectedBandwidth, current_sample);
diff --git a/quiche/quic/core/congestion_control/bbr2_simulator_test.cc b/quiche/quic/core/congestion_control/bbr2_simulator_test.cc
index 0efed58..30e3096 100644
--- a/quiche/quic/core/congestion_control/bbr2_simulator_test.cc
+++ b/quiche/quic/core/congestion_control/bbr2_simulator_test.cc
@@ -1724,7 +1724,7 @@
       sender_unacked_map()->GetLeastUnacked();
   for (auto& info : *sender_unacked_map()) {
     acked_packets.emplace_back(acked_packet_number++, info.bytes_sent,
-                               SimulatedNow());
+                               std::nullopt);
   }
 
   // Advance time significantly so the OnCongestionEvent enters PROBE_REFILL.
@@ -1742,8 +1742,9 @@
   now = now + params.RTT();
   sender_->OnCongestionEvent(
       /*rtt_updated=*/true, kDefaultMaxPacketSize, now,
-      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize, now)}, {}, 0,
-      0);
+      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize,
+                   std::nullopt)},
+      {}, 0, 0);
   ASSERT_EQ(ProbePhase::PROBE_UP, sender_->ExportDebugState().probe_bw.phase);
 
   // Send 2 packets and lose the first one(50% loss) to exit PROBE_UP.
@@ -1755,7 +1756,8 @@
   now = now + params.RTT();
   sender_->OnCongestionEvent(
       /*rtt_updated=*/true, 2 * kDefaultMaxPacketSize, now,
-      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize, now)},
+      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize,
+                   std::nullopt)},
       {LostPacket(next_packet_number - 2, kDefaultMaxPacketSize)}, 0, 0);
 
   QuicByteCount inflight_hi = sender_->ExportDebugState().inflight_hi;
@@ -1836,7 +1838,7 @@
   // Pretend the first lost packet number is acked.
   now = now + params.RTT() * 0.5;
   AckedPacketVector acked_packets;
-  acked_packets.emplace_back(lost_packets[0].packet_number, 0, now);
+  acked_packets.emplace_back(lost_packets[0].packet_number, 0, std::nullopt);
   acked_packets.back().spurious_loss = true;
   EXPECT_EQ(sender_unacked_map()->bytes_in_flight(), 0);
   sender_->OnCongestionEvent(false, sender_unacked_map()->bytes_in_flight(),
@@ -1965,8 +1967,8 @@
 
   // Ack 1 & 2.
   AckedPacketVector acked = {
-      AckedPacket(QuicPacketNumber(1), /*bytes_acked=*/0, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(2), /*bytes_acked=*/0, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(1), /*bytes_acked=*/0, std::nullopt),
+      AckedPacket(QuicPacketNumber(2), /*bytes_acked=*/0, std::nullopt),
   };
   now = now + QuicTime::Delta::FromMilliseconds(2000);
   sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
@@ -1981,7 +1983,7 @@
 
   // Ack 3.
   acked = {
-      AckedPacket(QuicPacketNumber(3), /*bytes_acked=*/0, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(3), /*bytes_acked=*/0, std::nullopt),
   };
   now = now + QuicTime::Delta::FromMilliseconds(2000);
   sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
@@ -1994,10 +1996,10 @@
 
   // Ack 4-7.
   acked = {
-      AckedPacket(QuicPacketNumber(4), /*bytes_acked=*/0, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(5), /*bytes_acked=*/0, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(6), /*bytes_acked=*/767, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(7), /*bytes_acked=*/1350, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(4), /*bytes_acked=*/0, std::nullopt),
+      AckedPacket(QuicPacketNumber(5), /*bytes_acked=*/0, std::nullopt),
+      AckedPacket(QuicPacketNumber(6), /*bytes_acked=*/767, std::nullopt),
+      AckedPacket(QuicPacketNumber(7), /*bytes_acked=*/1350, std::nullopt),
   };
   now = now + QuicTime::Delta::FromMilliseconds(2000);
   sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
diff --git a/quiche/quic/core/congestion_control/bbr3_simulator_test.cc b/quiche/quic/core/congestion_control/bbr3_simulator_test.cc
index 8e85c51..2f6a561 100644
--- a/quiche/quic/core/congestion_control/bbr3_simulator_test.cc
+++ b/quiche/quic/core/congestion_control/bbr3_simulator_test.cc
@@ -1598,7 +1598,7 @@
       sender_unacked_map()->GetLeastUnacked();
   for (auto& info : *sender_unacked_map()) {
     acked_packets.emplace_back(acked_packet_number++, info.bytes_sent,
-                               SimulatedNow());
+                               std::nullopt);
   }
 
   // Advance time significantly so the OnCongestionEvent enters PROBE_REFILL.
@@ -1616,8 +1616,9 @@
   now = now + params.RTT();
   sender_->OnCongestionEvent(
       /*rtt_updated=*/true, kDefaultMaxPacketSize, now,
-      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize, now)}, {}, 0,
-      0);
+      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize,
+                   std::nullopt)},
+      {}, 0, 0);
   ASSERT_EQ(ProbePhase::PROBE_UP, sender_->ExportDebugState().probe_bw.phase);
 
   // Send 2 packets and lose the first one(50% loss) to exit PROBE_UP.
@@ -1629,7 +1630,8 @@
   now = now + params.RTT();
   sender_->OnCongestionEvent(
       /*rtt_updated=*/true, 2 * kDefaultMaxPacketSize, now,
-      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize, now)},
+      {AckedPacket(next_packet_number - 1, kDefaultMaxPacketSize,
+                   std::nullopt)},
       {LostPacket(next_packet_number - 2, kDefaultMaxPacketSize)}, 0, 0);
 
   QuicByteCount inflight_hi = sender_->ExportDebugState().inflight_hi;
@@ -1710,7 +1712,7 @@
   // Pretend the first lost packet number is acked.
   now = now + params.RTT() * 0.5;
   AckedPacketVector acked_packets;
-  acked_packets.emplace_back(lost_packets[0].packet_number, 0, now);
+  acked_packets.emplace_back(lost_packets[0].packet_number, 0, std::nullopt);
   acked_packets.back().spurious_loss = true;
   EXPECT_EQ(sender_unacked_map()->bytes_in_flight(), 0);
   sender_->OnCongestionEvent(false, sender_unacked_map()->bytes_in_flight(),
@@ -1839,8 +1841,8 @@
 
   // Ack 1 & 2.
   AckedPacketVector acked = {
-      AckedPacket(QuicPacketNumber(1), /*bytes_acked=*/0, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(2), /*bytes_acked=*/0, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(1), /*bytes_acked=*/0, std::nullopt),
+      AckedPacket(QuicPacketNumber(2), /*bytes_acked=*/0, std::nullopt),
   };
   now = now + QuicTime::Delta::FromMilliseconds(2000);
   sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
@@ -1855,7 +1857,7 @@
 
   // Ack 3.
   acked = {
-      AckedPacket(QuicPacketNumber(3), /*bytes_acked=*/0, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(3), /*bytes_acked=*/0, std::nullopt),
   };
   now = now + QuicTime::Delta::FromMilliseconds(2000);
   sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
@@ -1868,10 +1870,10 @@
 
   // Ack 4-7.
   acked = {
-      AckedPacket(QuicPacketNumber(4), /*bytes_acked=*/0, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(5), /*bytes_acked=*/0, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(6), /*bytes_acked=*/767, QuicTime::Zero()),
-      AckedPacket(QuicPacketNumber(7), /*bytes_acked=*/1350, QuicTime::Zero()),
+      AckedPacket(QuicPacketNumber(4), /*bytes_acked=*/0, std::nullopt),
+      AckedPacket(QuicPacketNumber(5), /*bytes_acked=*/0, std::nullopt),
+      AckedPacket(QuicPacketNumber(6), /*bytes_acked=*/767, std::nullopt),
+      AckedPacket(QuicPacketNumber(7), /*bytes_acked=*/1350, std::nullopt),
   };
   now = now + QuicTime::Delta::FromMilliseconds(2000);
   sender_->OnCongestionEvent(true, bytes_in_flight, now, acked, {}, 0, 0);
diff --git a/quiche/quic/core/congestion_control/general_loss_algorithm_test.cc b/quiche/quic/core/congestion_control/general_loss_algorithm_test.cc
index 1bebb8a..fba5eb6 100644
--- a/quiche/quic/core/congestion_control/general_loss_algorithm_test.cc
+++ b/quiche/quic/core/congestion_control/general_loss_algorithm_test.cc
@@ -108,20 +108,20 @@
   AckedPacketVector packets_acked;
   // No loss on one ack.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, std::vector<uint64_t>{}, 1, 0);
   packets_acked.clear();
   // No loss on two acks.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(3));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(3), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(3), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(3, packets_acked, std::vector<uint64_t>{}, 2, 0);
   packets_acked.clear();
   // Loss on three acks.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(4));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(4), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(4), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(4, packets_acked, {1}, 3, 0);
   EXPECT_EQ(QuicTime::Zero(), loss_algorithm_.GetLossTimeout());
 }
@@ -137,14 +137,14 @@
   AckedPacketVector packets_acked;
   // Nack the first packet 3 times in a single StretchAck.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(3));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(3), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(3), kMaxOutgoingPacketSize, std::nullopt));
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(4));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(4), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(4), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(4, packets_acked, {1});
   EXPECT_EQ(QuicTime::Zero(), loss_algorithm_.GetLossTimeout());
 }
@@ -159,8 +159,8 @@
   AckedPacketVector packets_acked;
   // Nack the first packet 3 times in an AckFrame with three missing packets.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(4));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(4), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(4), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(4, packets_acked, {1});
   EXPECT_EQ(clock_.Now() + 1.25 * rtt_stats_.smoothed_rtt(),
             loss_algorithm_.GetLossTimeout());
@@ -175,8 +175,8 @@
   AckedPacketVector packets_acked;
   // Early retransmit when the final packet gets acked and the first is nacked.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
   EXPECT_EQ(clock_.Now() + 1.25 * rtt_stats_.smoothed_rtt(),
@@ -206,8 +206,7 @@
   // elapsed since the packets were sent.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(kNumSentPackets));
   packets_acked.push_back(AckedPacket(QuicPacketNumber(kNumSentPackets),
-                                      kMaxOutgoingPacketSize,
-                                      QuicTime::Zero()));
+                                      kMaxOutgoingPacketSize, std::nullopt));
   // This simulates a single ack following multiple missing packets with FACK.
   VerifyLosses(kNumSentPackets, packets_acked, {1, 2});
   packets_acked.clear();
@@ -240,8 +239,8 @@
   unacked_packets_.MaybeUpdateLargestAckedOfPacketNumberSpace(
       APPLICATION_DATA, QuicPacketNumber(2));
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, std::vector<uint64_t>{});
   EXPECT_EQ(clock_.Now() + 0.25 * rtt_stats_.smoothed_rtt(),
             loss_algorithm_.GetLossTimeout());
@@ -259,8 +258,8 @@
   unacked_packets_.MaybeUpdateLargestAckedOfPacketNumberSpace(
       APPLICATION_DATA, QuicPacketNumber(2));
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
   EXPECT_EQ(clock_.Now() + 0.25 * rtt_stats_.smoothed_rtt(),
@@ -287,8 +286,8 @@
   unacked_packets_.MaybeUpdateLargestAckedOfPacketNumberSpace(
       APPLICATION_DATA, QuicPacketNumber(2));
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, {1});
 }
 
@@ -309,8 +308,8 @@
   EXPECT_EQ(QuicTime::Zero(), loss_algorithm_.GetLossTimeout());
   // Packet 1 should not be lost until 1/4 RTTs pass.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
   // Expect the timer to be set to 1/4 RTT's in the future.
@@ -341,8 +340,8 @@
   // Acking 4 causes 1 detected lost.
   AckedPacketVector packets_acked;
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(4));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(4), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(4), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(4, packets_acked, std::vector<uint64_t>{1});
   packets_acked.clear();
 
@@ -351,8 +350,8 @@
 
   // Acking 1 such that it was detected lost spuriously.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(1));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(1), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(1), kMaxOutgoingPacketSize, std::nullopt));
   loss_algorithm_.SpuriousLossDetected(unacked_packets_, rtt_stats_,
                                        clock_.Now(), QuicPacketNumber(1),
                                        QuicPacketNumber(4));
@@ -361,8 +360,8 @@
 
   // Verify acking 5 does not cause 2 detected lost.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(5));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(5), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(5), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(5, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
 
@@ -370,8 +369,8 @@
 
   // Acking 6 will causes 2 detected lost.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(6));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(6), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(6), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(6, packets_acked, std::vector<uint64_t>{2});
   packets_acked.clear();
 
@@ -380,8 +379,8 @@
 
   // Acking 2 such that it was detected lost spuriously.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   loss_algorithm_.SpuriousLossDetected(unacked_packets_, rtt_stats_,
                                        clock_.Now(), QuicPacketNumber(2),
                                        QuicPacketNumber(6));
@@ -390,8 +389,8 @@
 
   // Acking 7 will not cause 3 as detected lost.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(7));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(7), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(7), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(7, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
 }
@@ -405,20 +404,20 @@
   AckedPacketVector packets_acked;
   // No loss on one ack.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
   // No loss on two acks.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(3));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(3), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(3), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(3, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
   // Loss on three acks.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(4));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(4), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(4), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(4, packets_acked, {1});
   EXPECT_EQ(QuicTime::Zero(), loss_algorithm_.GetLossTimeout());
   packets_acked.clear();
@@ -427,8 +426,8 @@
 
   // Time threshold loss detection.
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(6));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(6), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(6), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(6, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
   EXPECT_EQ(clock_.Now() + rtt_stats_.smoothed_rtt() +
@@ -447,8 +446,8 @@
 
   AckedPacketVector packets_acked;
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(2));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(2), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(2), kMaxOutgoingPacketSize, std::nullopt));
   VerifyLosses(2, packets_acked, std::vector<uint64_t>{});
   packets_acked.clear();
   EXPECT_EQ(clock_.Now() + rtt_stats_.smoothed_rtt() +
@@ -470,8 +469,8 @@
   // No packet threshold for runt packet.
   AckedPacketVector packets_acked;
   unacked_packets_.RemoveFromInFlight(QuicPacketNumber(7));
-  packets_acked.push_back(AckedPacket(
-      QuicPacketNumber(7), kMaxOutgoingPacketSize, QuicTime::Zero()));
+  packets_acked.push_back(
+      AckedPacket(QuicPacketNumber(7), kMaxOutgoingPacketSize, std::nullopt));
   // Verify no packet is detected lost because packet 7 is a runt.
   VerifyLosses(7, packets_acked, std::vector<uint64_t>{});
   EXPECT_EQ(clock_.Now() + rtt_stats_.smoothed_rtt() +
diff --git a/quiche/quic/core/congestion_control/prague_sender_test.cc b/quiche/quic/core/congestion_control/prague_sender_test.cc
index 3b38eec..0e80956 100644
--- a/quiche/quic/core/congestion_control/prague_sender_test.cc
+++ b/quiche/quic/core/congestion_control/prague_sender_test.cc
@@ -83,7 +83,7 @@
       ++acked_packet_number_;
       acked_packets.push_back(
           AckedPacket(QuicPacketNumber(acked_packet_number_), kDefaultTCPMSS,
-                      QuicTime::Zero()));
+                      std::nullopt));
     }
     sender_.OnCongestionEvent(true, bytes_in_flight_, clock_.Now(),
                               acked_packets, lost_packets, n - ce, ce);
diff --git a/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes_test.cc b/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes_test.cc
index 919233a..675b05f 100644
--- a/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes_test.cc
+++ b/quiche/quic/core/congestion_control/tcp_cubic_sender_bytes_test.cc
@@ -86,7 +86,7 @@
       ++acked_packet_number_;
       acked_packets.push_back(
           AckedPacket(QuicPacketNumber(acked_packet_number_), kDefaultTCPMSS,
-                      QuicTime::Zero()));
+                      std::nullopt));
     }
     sender_->OnCongestionEvent(true, bytes_in_flight_, clock_.Now(),
                                acked_packets, lost_packets, 0, 0);
@@ -791,7 +791,7 @@
   for (uint64_t i = 1; i < max_congestion_window; ++i) {
     acked_packets.clear();
     acked_packets.push_back(
-        AckedPacket(QuicPacketNumber(i), 1350, QuicTime::Zero()));
+        AckedPacket(QuicPacketNumber(i), 1350, std::nullopt));
     sender->OnCongestionEvent(true, sender->GetCongestionWindow(), clock_.Now(),
                               acked_packets, missing_packets, 0, 0);
   }
diff --git a/quiche/quic/core/congestion_control/uber_loss_algorithm_test.cc b/quiche/quic/core/congestion_control/uber_loss_algorithm_test.cc
index 83fb6d0..abb8782 100644
--- a/quiche/quic/core/congestion_control/uber_loss_algorithm_test.cc
+++ b/quiche/quic/core/congestion_control/uber_loss_algorithm_test.cc
@@ -62,7 +62,7 @@
     for (uint64_t acked : packets_acked) {
       unacked_packets_->RemoveFromInFlight(QuicPacketNumber(acked));
       packets_acked_.push_back(AckedPacket(
-          QuicPacketNumber(acked), kMaxOutgoingPacketSize, QuicTime::Zero()));
+          QuicPacketNumber(acked), kMaxOutgoingPacketSize, std::nullopt));
     }
   }
 
diff --git a/quiche/quic/core/frames/quic_ack_frame.cc b/quiche/quic/core/frames/quic_ack_frame.cc
index b274672..7a81f74 100644
--- a/quiche/quic/core/frames/quic_ack_frame.cc
+++ b/quiche/quic/core/frames/quic_ack_frame.cc
@@ -9,6 +9,7 @@
 
 #include "quiche/quic/core/quic_interval.h"
 #include "quiche/quic/core/quic_packet_number.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/platform/api/quic_bug_tracker.h"
 
 namespace quic {
@@ -33,7 +34,7 @@
      << ", ack_delay_time: " << ack_frame.ack_delay_time.ToMicroseconds()
      << ", packets: [ " << ack_frame.packets << " ]"
      << ", received_packets: [ ";
-  for (const std::pair<QuicPacketNumber, QuicTime>& p :
+  for (const std::pair<QuicPacketNumber, QuicReceiveTime>& p :
        ack_frame.received_packet_times) {
     os << p.first << " at " << p.second.ToDebuggingValue() << " ";
   }
diff --git a/quiche/quic/core/frames/quic_frames_test.cc b/quiche/quic/core/frames/quic_frames_test.cc
index e85166a..983116e 100644
--- a/quiche/quic/core/frames/quic_frames_test.cc
+++ b/quiche/quic/core/frames/quic_frames_test.cc
@@ -41,7 +41,7 @@
   frame.packets.Add(QuicPacketNumber(5));
   frame.received_packet_times = {
       {QuicPacketNumber(6),
-       QuicTime::Zero() + QuicTime::Delta::FromMicroseconds(7)}};
+       *QuicReceiveTime::FromPeerTime(QuicTime::Delta::FromMicroseconds(7))}};
   std::ostringstream stream;
   stream << frame;
   EXPECT_EQ(
@@ -63,7 +63,7 @@
   frame.packets.Add(QuicPacketNumber(6));
   frame.received_packet_times = {
       {QuicPacketNumber(7),
-       QuicTime::Zero() + QuicTime::Delta::FromMicroseconds(7)}};
+       *QuicReceiveTime::FromPeerTime(QuicTime::Delta::FromMicroseconds(7))}};
   std::ostringstream stream;
   stream << frame;
   EXPECT_EQ(
@@ -83,7 +83,7 @@
   frame.packets.Add(QuicPacketNumber(6));
   frame.received_packet_times = {
       {QuicPacketNumber(7),
-       QuicTime::Zero() + QuicTime::Delta::FromMicroseconds(7)}};
+       *QuicReceiveTime::FromPeerTime(QuicTime::Delta::FromMicroseconds(7))}};
   std::ostringstream stream;
   stream << frame;
   EXPECT_EQ(
@@ -101,7 +101,7 @@
   frame.packets.AddRange(QuicPacketNumber(4), QuicPacketNumber(501));
   frame.received_packet_times = {
       {QuicPacketNumber(500),
-       QuicTime::Zero() + QuicTime::Delta::FromMicroseconds(7)}};
+       *QuicReceiveTime::FromPeerTime(QuicTime::Delta::FromMicroseconds(7))}};
   std::ostringstream stream;
   stream << frame;
   EXPECT_EQ(
diff --git a/quiche/quic/core/quic_ack_timestamp_list.cc b/quiche/quic/core/quic_ack_timestamp_list.cc
index ff178a0..deb6932 100644
--- a/quiche/quic/core/quic_ack_timestamp_list.cc
+++ b/quiche/quic/core/quic_ack_timestamp_list.cc
@@ -54,8 +54,7 @@
 
 QuicAckTimestampList::QuicAckTimestampList(const QuicAckFrame& ack,
                                            uint32_t max_ack_count,
-                                           uint32_t exponent,
-                                           QuicTime receive_timestamp_basis)
+                                           uint32_t exponent)
     : timestamps_(
           std::min<size_t>(ack.received_packet_times.size(), max_ack_count)) {
   if (exponent > kMaxExponent) {
@@ -68,7 +67,7 @@
   // Since the timestamps are delta-encoded and rounded, we need to keep track
   // of what is the latest timestamp written as it would be computed by the
   // decoder.
-  QuicTime effective_previous_time = QuicTime::Zero();
+  QuicTimeDelta effective_previous_time = QuicTimeDelta::Zero();
 
   for (uint32_t i = 0; i < timestamps_.size(); ++i) {
     // `i` is the index into the output timestamp list; `i_src` is the index
@@ -80,10 +79,6 @@
     const size_t i_src = ack.received_packet_times.size() - i - 1;
     const auto [packet_number, timestamp] = ack.received_packet_times[i_src];
 
-    if (timestamp < receive_timestamp_basis) {
-      error_ = "Packet is received earlier than framer creation time";
-      return;
-    }
     if (packet_number > ack.largest_acked) {
       error_ = "Packet number listed higher than largest_acked";
       return;
@@ -104,7 +99,6 @@
 
     bool should_open_new_range;
     if (i >= 1) {
-      QUICHE_DCHECK(effective_previous_time.IsInitialized());
       const auto [prev_packet_number, prev_timestamp] =
           ack.received_packet_times[i_src + 1];
       if (timestamp > prev_timestamp) {
@@ -114,16 +108,14 @@
       }
 
       should_open_new_range = prev_packet_number != (packet_number + 1);
-      timestamps_[i] = encoder.Encode(effective_previous_time - timestamp);
+      timestamps_[i] =
+          encoder.Encode(effective_previous_time - timestamp.raw_value());
       effective_previous_time =
           effective_previous_time - encoder.Decode(timestamps_[i]);
     } else {
-      QUICHE_DCHECK(!effective_previous_time.IsInitialized());
       should_open_new_range = true;
-      timestamps_[i] =
-          encoder.EncodeWhileRoundingUp(timestamp - receive_timestamp_basis);
-      effective_previous_time =
-          receive_timestamp_basis + encoder.Decode(timestamps_[i]);
+      timestamps_[i] = encoder.EncodeWhileRoundingUp(timestamp.raw_value());
+      effective_previous_time = encoder.Decode(timestamps_[i]);
     }
 
     if (should_open_new_range) {
diff --git a/quiche/quic/core/quic_ack_timestamp_list.h b/quiche/quic/core/quic_ack_timestamp_list.h
index 9499056..51133e9 100644
--- a/quiche/quic/core/quic_ack_timestamp_list.h
+++ b/quiche/quic/core/quic_ack_timestamp_list.h
@@ -25,8 +25,7 @@
   // Constructs the ACK timestamp list from the provided `ack` frame.  If an
   // error is encountered, `error()` will be set to a non-empty string.
   explicit QuicAckTimestampList(const QuicAckFrame& ack, uint32_t max_ack_count,
-                                uint32_t exponent,
-                                QuicTime receive_timestamp_basis);
+                                uint32_t exponent);
 
   // Avoid moving QuicAckTimestampList.  It should always be allocated on the
   // stack, since all of the data here is transient.
diff --git a/quiche/quic/core/quic_ack_timestamp_list_test.cc b/quiche/quic/core/quic_ack_timestamp_list_test.cc
index bc1ab42..e40052d 100644
--- a/quiche/quic/core/quic_ack_timestamp_list_test.cc
+++ b/quiche/quic/core/quic_ack_timestamp_list_test.cc
@@ -23,14 +23,14 @@
 
 class QuicAckTimestampListTest : public quiche::test::QuicheTest {
  protected:
-  QuicAckTimestampListTest()
-      : basis_(QuicTime::Zero() + QuicTime::Delta::FromMilliseconds(1000)) {
+  QuicAckTimestampListTest() {
     // Packets numbers higher than 1000 will trigger a QUIC_BUG.
     ack_.packets.AddRange(QuicPacketNumber(1), QuicPacketNumber(1001));
   }
 
-  QuicTime TimePlus(uint64_t offset_us) {
-    return basis_ + QuicTime::Delta::FromMicroseconds(offset_us);
+  QuicReceiveTime TimePlus(uint64_t offset_us) {
+    return *QuicReceiveTime::FromPeerTime(
+        QuicTime::Delta::FromMicroseconds(offset_us));
   }
 
   void ExpectBinaryEncoding(const QuicAckTimestampList& list,
@@ -48,13 +48,11 @@
   }
 
   QuicAckFrame ack_;
-  QuicTime basis_;
 };
 
 TEST_F(QuicAckTimestampListTest, EmptyList) {
   ack_.largest_acked = QuicPacketNumber(100);
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0,
-                            basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
   ExpectBinaryEncoding(list,
                        // Range count = 0
@@ -77,8 +75,7 @@
       {QuicPacketNumber(100), TimePlus(380)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0,
-                            basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
   ExpectBinaryEncoding(list,
                        "02"            // Range count = 2
@@ -113,8 +110,7 @@
       {QuicPacketNumber(95), TimePlus(395)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0,
-                            basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
   ExpectBinaryEncoding(list,
                        "03"          // Range count = 3
@@ -150,8 +146,7 @@
       {QuicPacketNumber(998), TimePlus(400)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0,
-                            basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/100, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
   ExpectBinaryEncoding(list,
                        "03"        // Range count = 3
@@ -183,7 +178,7 @@
   };
 
   // Only the 3 newest timestamps (packets 10, 9, 8) should be included.
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/3, /*exponent=*/0, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/3, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
   ExpectBinaryEncoding(list,
                        "01"        // Range count = 1
@@ -212,7 +207,7 @@
   // Third delta:
   //         (32 - 17) >> 3 = 1
   //         Effective time = 24.
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/3, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/3);
   EXPECT_EQ(list.error(), "");
   ExpectBinaryEncoding(list,
                        "01"      // Range count = 1
@@ -226,12 +221,12 @@
 TEST_F(QuicAckTimestampListTest, EncodeWhileRoundingUpZeroDelta) {
   ack_.largest_acked = QuicPacketNumber(3);
   ack_.received_packet_times = {
-      {QuicPacketNumber(1), basis_},
-      {QuicPacketNumber(2), basis_},
-      {QuicPacketNumber(3), basis_},
+      {QuicPacketNumber(1), TimePlus(0)},
+      {QuicPacketNumber(2), TimePlus(0)},
+      {QuicPacketNumber(3), TimePlus(0)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/3, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/3);
   EXPECT_EQ(list.error(), "");
   ExpectBinaryEncoding(list,
                        "01"      // Range count = 1
@@ -242,24 +237,13 @@
   );
 }
 
-TEST_F(QuicAckTimestampListTest, ErrorPacketEarlierThanBasis) {
-  ack_.largest_acked = QuicPacketNumber(10);
-  ack_.received_packet_times = {
-      {QuicPacketNumber(10), basis_ - QuicTime::Delta::FromMicroseconds(1)},
-  };
-
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0, basis_);
-  EXPECT_EQ(list.error(),
-            "Packet is received earlier than framer creation time");
-}
-
 TEST_F(QuicAckTimestampListTest, ErrorPacketNumberHigherThanLargestAcked) {
   ack_.largest_acked = QuicPacketNumber(50);
   ack_.received_packet_times = {
       {QuicPacketNumber(51), TimePlus(100)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0);
   EXPECT_EQ(list.error(), "Packet number listed higher than largest_acked");
 }
 
@@ -270,7 +254,7 @@
       {QuicPacketNumber(10), TimePlus(90)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0);
   EXPECT_EQ(list.error(),
             "Receive timestamps have to be in monotonically ascending order.");
 }
@@ -281,12 +265,11 @@
       {QuicPacketNumber(10), TimePlus(100)},
   };
 
-  QuicAckTimestampList valid_list(ack_, /*max_ack_count=*/10, /*exponent=*/20,
-                                  basis_);
+  QuicAckTimestampList valid_list(ack_, /*max_ack_count=*/10, /*exponent=*/20);
   EXPECT_EQ(valid_list.error(), "");
 
-  QuicAckTimestampList invalid_list(ack_, /*max_ack_count=*/10, /*exponent=*/21,
-                                    basis_);
+  QuicAckTimestampList invalid_list(ack_, /*max_ack_count=*/10,
+                                    /*exponent=*/21);
   EXPECT_EQ(
       invalid_list.error(),
       "The specified exponent exceeds the one allowed by the specification");
@@ -299,7 +282,7 @@
       {QuicPacketNumber(100), TimePlus(20)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
 
   // Any buffer of the size at least one byte should let serialization succeed.
@@ -318,7 +301,7 @@
       {QuicPacketNumber(100), TimePlus(20)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
   EXPECT_EQ(list.MaxEncodedSize(), 6u);
 
@@ -340,7 +323,7 @@
       {QuicPacketNumber(100), TimePlus(20)},
   };
 
-  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0, basis_);
+  QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0);
   EXPECT_EQ(list.error(), "");
   EXPECT_EQ(list.MaxEncodedSize(), 8u);
 
@@ -392,8 +375,7 @@
 
   EXPECT_QUIC_BUG(
       {
-        QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0,
-                                  basis_);
+        QuicAckTimestampList list(ack_, /*max_ack_count=*/10, /*exponent=*/0);
         EXPECT_EQ(list.error(), "");
         ExpectBinaryEncoding(list, "020002140a0a0105");
       },
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 8018c7d..4edbe61 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -1634,7 +1634,7 @@
 }
 
 bool QuicConnection::OnAckTimestamp(QuicPacketNumber packet_number,
-                                    QuicTime timestamp) {
+                                    QuicReceiveTime timestamp) {
   QUIC_BUG_IF(quic_bug_10511_7, !connected_)
       << "Processing ACK frame time stamp when connection is closed. Received "
          "packet info: "
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index e778916..a136cbb 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -86,6 +86,7 @@
 #include "quiche/quic/core/quic_packets.h"
 #include "quiche/quic/core/quic_path_validator.h"
 #include "quiche/quic/core/quic_ping_manager.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_sent_packet_manager.h"
 #include "quiche/quic/core/quic_tag.h"
 #include "quiche/quic/core/quic_time.h"
@@ -775,7 +776,7 @@
                        QuicTime::Delta ack_delay_time) override;
   bool OnAckRange(QuicPacketNumber start, QuicPacketNumber end) override;
   bool OnAckTimestamp(QuicPacketNumber packet_number,
-                      QuicTime timestamp) override;
+                      QuicReceiveTime timestamp) override;
   bool OnAckFrameEnd(QuicPacketNumber start,
                      const std::optional<QuicEcnCounts>& ecn_counts) override;
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override;
diff --git a/quiche/quic/core/quic_framer.cc b/quiche/quic/core/quic_framer.cc
index ed5a691..582efce 100644
--- a/quiche/quic/core/quic_framer.cc
+++ b/quiche/quic/core/quic_framer.cc
@@ -3855,7 +3855,7 @@
   }
 
   uint64_t total_timestamp_count = 0;
-  std::optional<QuicTime::Delta> last_timestamp;
+  std::optional<QuicTimeDelta> last_timestamp;
 
   // Iterate through all timestamp ranges, each of which represents a block of
   // contiguous packets for which receive timestamps are being reported. Each
@@ -3901,7 +3901,7 @@
       // The IETF draft does not allow exponents above 20; this ensures that the
       // bitshifts below are always valid.
       QUICHE_DCHECK_LE(local_receive_timestamps_exponent_, 20u);
-      // The first timestamp delta is relative to framer creation time; whereas
+      // The first timestamp delta is relative to peer's reference time; whereas
       // subsequent deltas are relative to the previous delta in decreasing
       // packet order.
       if (timestamp_delta >
@@ -3916,13 +3916,14 @@
       } else {
         last_timestamp = *last_timestamp -
                          QuicTime::Delta::FromMicroseconds(timestamp_delta);
-        if (*last_timestamp < QuicTime::Delta::Zero()) {
-          set_detailed_error("Receive timestamp delta too high.");
-          return false;
-        }
       }
-      if (!visitor_->OnAckTimestamp(packet_number,
-                                    creation_time_ + *last_timestamp)) {
+      std::optional<QuicReceiveTime> receive_time =
+          QuicReceiveTime::FromPeerTime(*last_timestamp);
+      if (!receive_time.has_value()) {
+        set_detailed_error("Receive timestamp delta too high.");
+        return false;
+      }
+      if (!visitor_->OnAckTimestamp(packet_number, *receive_time)) {
         set_detailed_error(
             "Visitor suppresses further processing of ACK frame.");
         return false;
@@ -4809,8 +4810,7 @@
 size_t QuicFramer::GetIetfAckFrameTimestampSize(const QuicAckFrame& ack) {
   QUICHE_DCHECK(!ack.received_packet_times.empty());
   QuicAckTimestampList timestamp_list(ack, peer_max_receive_timestamps_per_ack_,
-                                      peer_receive_timestamps_exponent_,
-                                      creation_time_);
+                                      peer_receive_timestamps_exponent_);
   if (!timestamp_list.error().empty()) {
     QUIC_BUG(quic_framer_ack_ts_list_error) << timestamp_list.error();
     return 0;
@@ -5467,9 +5467,9 @@
 bool QuicFramer::AppendIetfTimestampsToAckFrame(const QuicAckFrame& frame,
                                                 QuicDataWriter* writer) {
   QUICHE_DCHECK(!frame.received_packet_times.empty());
-  QuicAckTimestampList timestamp_list(
-      frame, peer_max_receive_timestamps_per_ack_,
-      peer_receive_timestamps_exponent_, creation_time_);
+  QuicAckTimestampList timestamp_list(frame,
+                                      peer_max_receive_timestamps_per_ack_,
+                                      peer_receive_timestamps_exponent_);
   if (!timestamp_list.error().empty()) {
     QUIC_BUG(quic_framer_ack_ts_list_error) << timestamp_list.error();
     set_detailed_error(timestamp_list.error());
diff --git a/quiche/quic/core/quic_framer.h b/quiche/quic/core/quic_framer.h
index 6ad31c9..433deb1 100644
--- a/quiche/quic/core/quic_framer.h
+++ b/quiche/quic/core/quic_framer.h
@@ -165,7 +165,7 @@
 
   // Called when a timestamp in the AckFrame has been parsed.
   virtual bool OnAckTimestamp(QuicPacketNumber packet_number,
-                              QuicTime timestamp) = 0;
+                              QuicReceiveTime timestamp) = 0;
 
   // Called after the last ack range in an AckFrame has been parsed.
   // |start| is the starting value of the last ack range. |ecn_counts| are
diff --git a/quiche/quic/core/quic_framer_test.cc b/quiche/quic/core/quic_framer_test.cc
index 8a4e557..65a9e3d 100644
--- a/quiche/quic/core/quic_framer_test.cc
+++ b/quiche/quic/core/quic_framer_test.cc
@@ -31,6 +31,7 @@
 #include "quiche/quic/core/quic_interval.h"
 #include "quiche/quic/core/quic_packet_number.h"
 #include "quiche/quic/core/quic_packets.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_utils.h"
@@ -383,7 +384,7 @@
   }
 
   bool OnAckTimestamp(QuicPacketNumber packet_number,
-                      QuicTime timestamp) override {
+                      QuicReceiveTime timestamp) override {
     ack_frames_[ack_frames_.size() - 1]->received_packet_times.push_back(
         std::make_pair(packet_number, timestamp));
     if (VersionIsIetfQuic(transport_version_)) {
@@ -934,9 +935,9 @@
            ((n - 1) * QuicUtils::StreamIdDelta(transport_version));
   }
 
-  QuicTime CreationTimePlus(uint64_t offset_us) {
-    return framer_.creation_time() +
-           QuicTime::Delta::FromMicroseconds(offset_us);
+  QuicReceiveTime CreationTimePlus(uint64_t offset_us) {
+    return *QuicReceiveTime::FromPeerTime(
+        QuicTime::Delta::FromMicroseconds(offset_us));
   }
 
   // Returns the size of the provided frame list when serialized, excluding the
diff --git a/quiche/quic/core/quic_receive_time.h b/quiche/quic/core/quic_receive_time.h
new file mode 100644
index 0000000..eb5e1dd
--- /dev/null
+++ b/quiche/quic/core/quic_receive_time.h
@@ -0,0 +1,84 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_QUIC_CORE_QUIC_RECEIVE_TIME_H_
+#define QUICHE_QUIC_CORE_QUIC_RECEIVE_TIME_H_
+
+#include <compare>
+#include <cstdint>
+#include <optional>
+#include <ostream>
+
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/common/platform/api/quiche_export.h"
+
+namespace quic {
+
+// QuicReceiveTime represents a QUIC receive timestamp, in microseconds. Both
+// timestamps generated locally and timestamps received from the peer are
+// represented using this class. If a timestamp was generated locally, it is
+// computed relative to the connection creation time using QuicClock associated
+// with the connection. If a timestamp is remote, it is assumed to be relative
+// to some fixed clock by the peer.
+//
+// Internally, QuicReceiveTime is a QuicTimeDelta; the class enforces an
+// invariant that the time delta has to be non-negative.
+class QUICHE_EXPORT QuicReceiveTime {
+ public:
+  // Creates a receive timestamp received from the peer.  Returns nullopt if the
+  // timestamp is negative.
+  static constexpr std::optional<QuicReceiveTime> FromPeerTime(
+      QuicTimeDelta timestamp) {
+    if (timestamp < QuicTimeDelta::Zero()) {
+      return std::nullopt;
+    }
+    return QuicReceiveTime(timestamp);
+  }
+
+  // Creates a receive timestamp from local clock time.  Returns nullopt if the
+  // timestamp is before the timestamp basis.
+  static constexpr std::optional<QuicReceiveTime> FromLocalTime(
+      QuicTime timestamp, QuicTime receive_timestamp_basis) {
+    if (timestamp < receive_timestamp_basis) {
+      return std::nullopt;
+    }
+    return QuicReceiveTime(timestamp - receive_timestamp_basis);
+  }
+
+  QuicReceiveTime(const QuicReceiveTime&) = default;
+  QuicReceiveTime(QuicReceiveTime&&) = default;
+  QuicReceiveTime& operator=(const QuicReceiveTime&) = default;
+  QuicReceiveTime& operator=(QuicReceiveTime&&) = default;
+
+  constexpr QuicTimeDelta raw_value() const {
+    return delta_from_reference_time_;
+  }
+
+  // API compatibility with QuicTime.
+  constexpr int64_t ToDebuggingValue() const {
+    return delta_from_reference_time_.ToMicroseconds();
+  }
+
+  constexpr std::strong_ordering operator<=>(
+      const QuicReceiveTime& other) const = default;
+  constexpr QuicTimeDelta operator-(const QuicReceiveTime& other) const {
+    return delta_from_reference_time_ - other.delta_from_reference_time_;
+  }
+
+  QUICHE_EXPORT friend std::ostream& operator<<(std::ostream& output,
+                                                QuicReceiveTime t) {
+    output << t.raw_value();
+    return output;
+  }
+
+ private:
+  explicit constexpr QuicReceiveTime(QuicTimeDelta value)
+      : delta_from_reference_time_(value) {}
+
+  QuicTimeDelta delta_from_reference_time_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_CORE_QUIC_RECEIVE_TIME_H_
diff --git a/quiche/quic/core/quic_received_packet_manager.cc b/quiche/quic/core/quic_received_packet_manager.cc
index 29104db..b2124ca 100644
--- a/quiche/quic/core/quic_received_packet_manager.cc
+++ b/quiche/quic/core/quic_received_packet_manager.cc
@@ -113,21 +113,23 @@
              "`receive_timestamp_basis_` is not set";
     }
 
-    if (receipt_time < receive_timestamp_basis_) {
+    std::optional<QuicReceiveTime> receive_time =
+        QuicReceiveTime::FromLocalTime(receipt_time, receive_timestamp_basis_);
+    if (!receive_time.has_value()) {
       QUIC_DLOG(WARNING) << "Receive time earlier than timestamp basis: "
                          << receipt_time.ToDebuggingValue() << " < "
                          << receive_timestamp_basis_.ToDebuggingValue();
     } else if (!ack_frame_.received_packet_times.empty() &&
-               ack_frame_.received_packet_times.back().second > receipt_time) {
+               ack_frame_.received_packet_times.back().second > *receive_time) {
       // The QUIC framer can only serialize timestamps if they are provided in
       // the receive time order.
       QUIC_LOG(WARNING)
           << "Receive time went backwards from: "
           << ack_frame_.received_packet_times.back().second.ToDebuggingValue()
-          << " to " << receipt_time.ToDebuggingValue();
+          << " to " << receive_time->ToDebuggingValue();
     } else {
       ack_frame_.received_packet_times.push_back(
-          std::make_pair(packet_number, receipt_time));
+          std::make_pair(packet_number, *receive_time));
     }
   }
 
@@ -210,10 +212,11 @@
 
   // Remove timestamps for packets that got removed from the packet ranges.
   if (!ack_frame_.received_packet_times.empty()) {
-    std::erase_if(ack_frame_.received_packet_times,
-                  [&](const std::pair<QuicPacketNumber, QuicTime>& entry) {
-                    return !ack_frame_.packets.Contains(entry.first);
-                  });
+    std::erase_if(
+        ack_frame_.received_packet_times,
+        [&](const std::pair<QuicPacketNumber, QuicReceiveTime>& entry) {
+          return !ack_frame_.packets.Contains(entry.first);
+        });
   }
 
 #if QUIC_FRAME_DEBUG
diff --git a/quiche/quic/core/quic_sent_packet_manager.cc b/quiche/quic/core/quic_sent_packet_manager.cc
index b842b31..9898f7b 100644
--- a/quiche/quic/core/quic_sent_packet_manager.cc
+++ b/quiche/quic/core/quic_sent_packet_manager.cc
@@ -28,6 +28,7 @@
 #include "quiche/quic/core/quic_constants.h"
 #include "quiche/quic/core/quic_packet_number.h"
 #include "quiche/quic/core/quic_packets.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_tag.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_transmission_info.h"
@@ -1342,7 +1343,7 @@
          --acked) {
       // Check if end is above the current range. If so add newly acked packets
       // in descending order.
-      packets_acked_.push_back(AckedPacket(acked, 0, QuicTime::Zero()));
+      packets_acked_.push_back(AckedPacket(acked, 0, std::nullopt));
       if (acked == FirstSendingPacketNumber()) {
         break;
       }
@@ -1358,7 +1359,7 @@
 }
 
 void QuicSentPacketManager::OnAckTimestamp(QuicPacketNumber packet_number,
-                                           QuicTime timestamp) {
+                                           QuicReceiveTime timestamp) {
   last_ack_frame_.received_packet_times.push_back({packet_number, timestamp});
   for (AckedPacket& packet : packets_acked_) {
     if (packet.packet_number == packet_number) {
@@ -1474,7 +1475,7 @@
     }
     if (packet_number_space == APPLICATION_DATA) {
       ++stats_->application_data_packets_acked;
-      if (acked_packet.receive_timestamp.IsInitialized()) {
+      if (acked_packet.receive_timestamp.has_value()) {
         ++stats_->application_data_packets_with_receive_timestamps_acked;
       }
     }
diff --git a/quiche/quic/core/quic_sent_packet_manager.h b/quiche/quic/core/quic_sent_packet_manager.h
index 5d6950a..9086619 100644
--- a/quiche/quic/core/quic_sent_packet_manager.h
+++ b/quiche/quic/core/quic_sent_packet_manager.h
@@ -24,6 +24,7 @@
 #include "quiche/quic/core/quic_constants.h"
 #include "quiche/quic/core/quic_packet_number.h"
 #include "quiche/quic/core/quic_packets.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_sustained_bandwidth_recorder.h"
 #include "quiche/quic/core/quic_tag.h"
 #include "quiche/quic/core/quic_time.h"
@@ -372,7 +373,8 @@
 
   // Called when a timestamp is processed.  If it's present in packets_acked_,
   // the timestamp field is set.  Otherwise, the timestamp is ignored.
-  void OnAckTimestamp(QuicPacketNumber packet_number, QuicTime timestamp);
+  void OnAckTimestamp(QuicPacketNumber packet_number,
+                      QuicReceiveTime timestamp);
 
   // Called when an ack frame is parsed completely.
   AckResult OnAckFrameEnd(QuicTime ack_receive_time,
diff --git a/quiche/quic/core/quic_sent_packet_manager_test.cc b/quiche/quic/core/quic_sent_packet_manager_test.cc
index 1d176c8..b0265c4 100644
--- a/quiche/quic/core/quic_sent_packet_manager_test.cc
+++ b/quiche/quic/core/quic_sent_packet_manager_test.cc
@@ -31,6 +31,7 @@
 #include "quiche/quic/core/quic_constants.h"
 #include "quiche/quic/core/quic_packet_number.h"
 #include "quiche/quic/core/quic_packets.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_tag.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
@@ -3777,7 +3778,9 @@
   manager_.OnAckFrameStart(QuicPacketNumber(4), QuicTime::Delta::Infinite(),
                            clock_.Now());
   manager_.OnAckRange(QuicPacketNumber(4), QuicPacketNumber(5));
-  manager_.OnAckTimestamp(QuicPacketNumber(4), clock_.Now());
+  manager_.OnAckTimestamp(
+      QuicPacketNumber(4),
+      *QuicReceiveTime::FromLocalTime(clock_.Now(), QuicTime::Zero()));
   EXPECT_EQ(PACKETS_NEWLY_ACKED,
             manager_.OnAckFrameEnd(clock_.Now(), QuicPacketNumber(4),
                                    ENCRYPTION_FORWARD_SECURE, kEmptyCounts));
diff --git a/quiche/quic/core/quic_time_test.cc b/quiche/quic/core/quic_time_test.cc
index 6877603..580421f 100644
--- a/quiche/quic/core/quic_time_test.cc
+++ b/quiche/quic/core/quic_time_test.cc
@@ -8,7 +8,9 @@
 #include <cstdint>
 #include <limits>
 #include <optional>
+#include <sstream>
 
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/quic/test_tools/mock_clock.h"
 
@@ -224,5 +226,59 @@
   EXPECT_FALSE(one <= zero);
 }
 
+class QuicReceiveTimeTest : public QuicTest {};
+
+TEST_F(QuicReceiveTimeTest, FromPeerTime) {
+  EXPECT_EQ(QuicReceiveTime::FromPeerTime(QuicTimeDelta::FromMicroseconds(-1)),
+            std::nullopt);
+  std::optional<QuicReceiveTime> zero =
+      QuicReceiveTime::FromPeerTime(QuicTimeDelta::Zero());
+  ASSERT_TRUE(zero.has_value());
+  EXPECT_EQ(zero->raw_value(), QuicTimeDelta::Zero());
+  EXPECT_EQ(zero->ToDebuggingValue(), 0);
+
+  std::optional<QuicReceiveTime> ts =
+      QuicReceiveTime::FromPeerTime(QuicTimeDelta::FromMicroseconds(12345));
+  ASSERT_TRUE(ts.has_value());
+  EXPECT_EQ(ts->raw_value(), QuicTimeDelta::FromMicroseconds(12345));
+  EXPECT_EQ(ts->ToDebuggingValue(), 12345);
+}
+
+TEST_F(QuicReceiveTimeTest, FromLocalTime) {
+  const QuicTime basis =
+      QuicTime::Zero() + QuicTimeDelta::FromMicroseconds(1000);
+  EXPECT_EQ(QuicReceiveTime::FromLocalTime(
+                basis - QuicTimeDelta::FromMicroseconds(1), basis),
+            std::nullopt);
+  std::optional<QuicReceiveTime> zero =
+      QuicReceiveTime::FromLocalTime(basis, basis);
+  ASSERT_TRUE(zero.has_value());
+  EXPECT_EQ(zero->raw_value(), QuicTimeDelta::Zero());
+
+  std::optional<QuicReceiveTime> ts = QuicReceiveTime::FromLocalTime(
+      basis + QuicTimeDelta::FromMicroseconds(500), basis);
+  ASSERT_TRUE(ts.has_value());
+  EXPECT_EQ(ts->raw_value(), QuicTimeDelta::FromMicroseconds(500));
+}
+
+TEST_F(QuicReceiveTimeTest, OperatorsAndFormatting) {
+  QuicReceiveTime t1 =
+      *QuicReceiveTime::FromPeerTime(QuicTimeDelta::FromMicroseconds(1000));
+  QuicReceiveTime t2 =
+      *QuicReceiveTime::FromPeerTime(QuicTimeDelta::FromMicroseconds(2500));
+  EXPECT_LT(t1, t2);
+  EXPECT_LE(t1, t2);
+  EXPECT_GT(t2, t1);
+  EXPECT_GE(t2, t1);
+  EXPECT_EQ(t1, t1);
+  EXPECT_NE(t1, t2);
+  EXPECT_EQ(t2 - t1, QuicTimeDelta::FromMicroseconds(1500));
+  EXPECT_EQ(t1 - t2, QuicTimeDelta::FromMicroseconds(-1500));
+
+  std::ostringstream oss;
+  oss << t2;
+  EXPECT_EQ(oss.str(), "2500us");
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_types.cc b/quiche/quic/core/quic_types.cc
index 34a4d92..f26b88c 100644
--- a/quiche/quic/core/quic_types.cc
+++ b/quiche/quic/core/quic_types.cc
@@ -80,7 +80,10 @@
 std::ostream& operator<<(std::ostream& os, const AckedPacket& acked_packet) {
   os << "{ packet_number: " << acked_packet.packet_number
      << ", bytes_acked: " << acked_packet.bytes_acked << ", receive_timestamp: "
-     << acked_packet.receive_timestamp.ToDebuggingValue() << "} ";
+     << (acked_packet.receive_timestamp.has_value()
+             ? absl::StrCat(acked_packet.receive_timestamp->ToDebuggingValue())
+             : "none")
+     << "} ";
   return os;
 }
 
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index 2a3c822..287940b 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -22,6 +22,7 @@
 #include "quiche/quic/core/quic_connection_id.h"  // IWYU pragma: export
 #include "quiche/quic/core/quic_error_codes.h"    // IWYU pragma: export
 #include "quiche/quic/core/quic_packet_number.h"
+#include "quiche/quic/core/quic_receive_time.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/platform/api/quic_flags.h"  // IWYU pragma: export
 #include "quiche/common/platform/api/quiche_export.h"
@@ -49,7 +50,8 @@
 using QuicPublicResetNonceProof = uint64_t;
 using QuicStreamOffset = uint64_t;
 using DiversificationNonce = std::array<char, 32>;
-using PacketTimeVector = std::vector<std::pair<QuicPacketNumber, QuicTime>>;
+using PacketTimeVector =
+    std::vector<std::pair<QuicPacketNumber, QuicReceiveTime>>;
 
 inline constexpr size_t kStatelessResetTokenLength = 16;
 using StatelessResetToken = std::array<char, kStatelessResetTokenLength>;
@@ -611,7 +613,7 @@
 struct QUICHE_EXPORT AckedPacket {
   constexpr AckedPacket(QuicPacketNumber packet_number,
                         QuicPacketLength bytes_acked,
-                        QuicTime receive_timestamp)
+                        std::optional<QuicReceiveTime> receive_timestamp)
       : packet_number(packet_number),
         bytes_acked(bytes_acked),
         receive_timestamp(receive_timestamp) {}
@@ -627,8 +629,8 @@
   bool spurious_loss = false;
   // The time |packet_number| was received by the peer, according to the
   // optional timestamp the peer included in the ACK frame which acknowledged
-  // |packet_number|. Zero if no timestamp was available for this packet.
-  QuicTime receive_timestamp;
+  // |packet_number|. Nullopt if no timestamp was available for this packet.
+  std::optional<QuicReceiveTime> receive_timestamp;
 };
 
 // A vector of acked packets.
diff --git a/quiche/quic/core/tls_chlo_extractor.h b/quiche/quic/core/tls_chlo_extractor.h
index bd988c2..c3faa45 100644
--- a/quiche/quic/core/tls_chlo_extractor.h
+++ b/quiche/quic/core/tls_chlo_extractor.h
@@ -129,7 +129,7 @@
     return true;
   }
   bool OnAckTimestamp(QuicPacketNumber /*packet_number*/,
-                      QuicTime /*timestamp*/) override {
+                      QuicReceiveTime /*timestamp*/) override {
     return true;
   }
   bool OnAckFrameEnd(
diff --git a/quiche/quic/core/uber_received_packet_manager_test.cc b/quiche/quic/core/uber_received_packet_manager_test.cc
index a30a16e..545d6d8 100644
--- a/quiche/quic/core/uber_received_packet_manager_test.cc
+++ b/quiche/quic/core/uber_received_packet_manager_test.cc
@@ -608,14 +608,11 @@
   manager_->EnableMultiplePacketNumberSpacesSupport(Perspective::IS_SERVER);
   manager_->set_save_timestamps(true);
 
-  const QuicTime initial_time =
-      clock_.ApproximateNow() + QuicTime::Delta::FromMilliseconds(1);
-  const QuicTime handshake_time =
-      clock_.ApproximateNow() + QuicTime::Delta::FromMilliseconds(2);
-  const QuicTime zero_rtt_time =
-      clock_.ApproximateNow() + QuicTime::Delta::FromMilliseconds(3);
-  const QuicTime one_rtt_time =
-      clock_.ApproximateNow() + QuicTime::Delta::FromMilliseconds(4);
+  const QuicTime basis = clock_.ApproximateNow();
+  const QuicTime initial_time = basis + QuicTime::Delta::FromMilliseconds(1);
+  const QuicTime handshake_time = basis + QuicTime::Delta::FromMilliseconds(2);
+  const QuicTime zero_rtt_time = basis + QuicTime::Delta::FromMilliseconds(3);
+  const QuicTime one_rtt_time = basis + QuicTime::Delta::FromMilliseconds(4);
   RecordPacketReceipt(ENCRYPTION_INITIAL, 1, initial_time);
   RecordPacketReceipt(ENCRYPTION_HANDSHAKE, 1, handshake_time);
   // 0-RTT packets are still recorded, since the ACKs for 0-RTT packets are sent
@@ -627,9 +624,12 @@
               IsEmpty());
   EXPECT_THAT(manager_->GetAckFrame(HANDSHAKE_DATA).received_packet_times,
               IsEmpty());
-  EXPECT_THAT(manager_->GetAckFrame(APPLICATION_DATA).received_packet_times,
-              ElementsAre(Pair(QuicPacketNumber(1), zero_rtt_time),
-                          Pair(QuicPacketNumber(2), one_rtt_time)));
+  EXPECT_THAT(
+      manager_->GetAckFrame(APPLICATION_DATA).received_packet_times,
+      ElementsAre(Pair(QuicPacketNumber(1),
+                       *QuicReceiveTime::FromLocalTime(zero_rtt_time, basis)),
+                  Pair(QuicPacketNumber(2),
+                       *QuicReceiveTime::FromLocalTime(one_rtt_time, basis))));
 }
 
 }  // namespace
diff --git a/quiche/quic/test_tools/quic_test_utils.cc b/quiche/quic/test_tools/quic_test_utils.cc
index 5b173b1..96733a3 100644
--- a/quiche/quic/test_tools/quic_test_utils.cc
+++ b/quiche/quic/test_tools/quic_test_utils.cc
@@ -390,7 +390,7 @@
 }
 
 bool NoOpFramerVisitor::OnAckTimestamp(QuicPacketNumber /*packet_number*/,
-                                       QuicTime /*timestamp*/) {
+                                       QuicReceiveTime /*timestamp*/) {
   return true;
 }
 
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h
index ba59ea3..6ba7cac 100644
--- a/quiche/quic/test_tools/quic_test_utils.h
+++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -327,7 +327,8 @@
               (override));
   MOCK_METHOD(bool, OnAckRange, (QuicPacketNumber, QuicPacketNumber),
               (override));
-  MOCK_METHOD(bool, OnAckTimestamp, (QuicPacketNumber, QuicTime), (override));
+  MOCK_METHOD(bool, OnAckTimestamp, (QuicPacketNumber, QuicReceiveTime),
+              (override));
   MOCK_METHOD(bool, OnAckFrameEnd,
               (QuicPacketNumber, const std::optional<QuicEcnCounts>&),
               (override));
@@ -415,7 +416,7 @@
                        QuicTime::Delta ack_delay_time) override;
   bool OnAckRange(QuicPacketNumber start, QuicPacketNumber end) override;
   bool OnAckTimestamp(QuicPacketNumber packet_number,
-                      QuicTime timestamp) override;
+                      QuicReceiveTime timestamp) override;
   bool OnAckFrameEnd(QuicPacketNumber start,
                      const std::optional<QuicEcnCounts>& ecn_counts) override;
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override;
diff --git a/quiche/quic/test_tools/simple_quic_framer.cc b/quiche/quic/test_tools/simple_quic_framer.cc
index 9590ed8..cf8df8b 100644
--- a/quiche/quic/test_tools/simple_quic_framer.cc
+++ b/quiche/quic/test_tools/simple_quic_framer.cc
@@ -117,7 +117,7 @@
   }
 
   bool OnAckTimestamp(QuicPacketNumber /*packet_number*/,
-                      QuicTime /*timestamp*/) override {
+                      QuicReceiveTime /*timestamp*/) override {
     return true;
   }
 
diff --git a/quiche/quic/tools/quic_packet_printer_bin.cc b/quiche/quic/tools/quic_packet_printer_bin.cc
index 561c374..6ba7b7a 100644
--- a/quiche/quic/tools/quic_packet_printer_bin.cc
+++ b/quiche/quic/tools/quic_packet_printer_bin.cc
@@ -123,7 +123,7 @@
     return true;
   }
   bool OnAckTimestamp(QuicPacketNumber packet_number,
-                      QuicTime timestamp) override {
+                      QuicReceiveTime timestamp) override {
     std::cerr << "OnAckTimestamp: [" << packet_number << ", "
               << timestamp.ToDebuggingValue() << ")";
     return true;