Merge "Scouting: add way to view data before submitting"
diff --git a/aos/BUILD b/aos/BUILD
index 0792e67..a5fb6c8 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -776,3 +776,17 @@
         "//third_party/cargo:cxx_cc",
     ],
 )
+
+cc_library(
+    name = "sha256",
+    srcs = [
+        "sha256.cc",
+    ],
+    hdrs = ["sha256.h"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "@boringssl//:crypto",
+        "@com_google_absl//absl/types:span",
+    ],
+)
diff --git a/aos/configuration.cc b/aos/configuration.cc
index ad0a489..4e1316d 100644
--- a/aos/configuration.cc
+++ b/aos/configuration.cc
@@ -338,6 +338,9 @@
       if (c->name()->string_view().back() == '/') {
         LOG(FATAL) << "Channel names can't end with '/'";
       }
+      if (c->name()->string_view().front() != '/') {
+        LOG(FATAL) << "Channel names must start with '/'";
+      }
       if (c->name()->string_view().find("//") != std::string_view::npos) {
         LOG(FATAL) << ": Invalid channel name " << c->name()->string_view()
                    << ", can't use //.";
@@ -984,8 +987,20 @@
         }
       }
 
-      CHECK(found_schema != nullptr)
-          << ": Failed to find schema for " << FlatbufferToJson(c);
+      if (found_schema == nullptr) {
+        std::stringstream ss;
+        for (const aos::FlatbufferVector<reflection::Schema> &schema :
+             schemas) {
+          if (schema.message().root_table() == nullptr) {
+            continue;
+          }
+          auto name = schema.message().root_table()->name()->string_view();
+          ss << "\n\tname: " << name;
+        }
+        LOG(FATAL) << ": Failed to find schema for " << FlatbufferToJson(c)
+                   << "\n\tThe following schemas were found:\n"
+                   << ss.str();
+      }
 
       // Now copy the message manually.
       auto cached_schema = schema_cache.find(c->type()->string_view());
diff --git a/aos/configuration_test.cc b/aos/configuration_test.cc
index 823f43c..cb44f1b 100644
--- a/aos/configuration_test.cc
+++ b/aos/configuration_test.cc
@@ -136,6 +136,13 @@
         LOG(FATAL) << "Foo";
       },
       "Invalid channel name");
+  EXPECT_DEATH(
+      {
+        FlatbufferDetachedBuffer<Configuration> config =
+            ReadConfig(ArtifactPath("aos/testdata/invalid_channel_name4.json"));
+        LOG(FATAL) << "Foo";
+      },
+      "Channel names must start with '/'");
 }
 
 // Tests that we can modify a config with a json snippet.
diff --git a/aos/events/event_loop.cc b/aos/events/event_loop.cc
index c29d820..2d932cc 100644
--- a/aos/events/event_loop.cc
+++ b/aos/events/event_loop.cc
@@ -34,6 +34,25 @@
 }
 }  // namespace
 
+std::pair<SharedSpan, absl::Span<uint8_t>> MakeSharedSpan(size_t size) {
+  AlignedOwningSpan *const span = reinterpret_cast<AlignedOwningSpan *>(
+      malloc(sizeof(AlignedOwningSpan) + size + kChannelDataAlignment - 1));
+
+  absl::Span<uint8_t> mutable_span(
+      reinterpret_cast<uint8_t *>(RoundChannelData(span->data(), size)), size);
+  // Use the placement new operator to construct an actual absl::Span in place.
+  new (span) AlignedOwningSpan(mutable_span);
+
+  return std::make_pair(
+      SharedSpan(std::shared_ptr<AlignedOwningSpan>(span,
+                                                    [](AlignedOwningSpan *s) {
+                                                      s->~AlignedOwningSpan();
+                                                      free(s);
+                                                    }),
+                 &span->span),
+      mutable_span);
+}
+
 std::ostream &operator<<(std::ostream &os, const RawSender::Error err) {
   os << ErrorToString(err);
   return os;
diff --git a/aos/events/event_loop.h b/aos/events/event_loop.h
index 23250e1..8825464 100644
--- a/aos/events/event_loop.h
+++ b/aos/events/event_loop.h
@@ -133,6 +133,25 @@
   Ftrace ftrace_;
 };
 
+using SharedSpan = std::shared_ptr<const absl::Span<const uint8_t>>;
+
+// Holds storage for a span object and the data referenced by that span for
+// compatibility with SharedSpan users. If constructed with MakeSharedSpan, span
+// points to only the aligned segment of the entire data.
+struct AlignedOwningSpan {
+  AlignedOwningSpan(absl::Span<const uint8_t> new_span) : span(new_span) {}
+
+  AlignedOwningSpan(const AlignedOwningSpan &) = delete;
+  AlignedOwningSpan &operator=(const AlignedOwningSpan &) = delete;
+  absl::Span<const uint8_t> span;
+  char *data() { return reinterpret_cast<char *>(this + 1); }
+};
+
+// Constructs a span which owns its data through a shared_ptr. The owning span
+// points to a const view of the data; also returns a temporary mutable span
+// which is only valid while the const shared span is kept alive.
+std::pair<SharedSpan, absl::Span<uint8_t>> MakeSharedSpan(size_t size);
+
 // Raw version of sender.  Sends a block of data.  This is used for reflection
 // and as a building block to implement typed senders.
 class RawSender {
diff --git a/aos/events/event_loop_param_test.cc b/aos/events/event_loop_param_test.cc
index 07bd6d4..2076467 100644
--- a/aos/events/event_loop_param_test.cc
+++ b/aos/events/event_loop_param_test.cc
@@ -2389,6 +2389,68 @@
   }
 }
 
+// Tests that the RawSender::Send(SharedSpan) overload works.
+TEST_P(AbstractEventLoopTest, SharedSenderTimingReport) {
+  gflags::FlagSaver flag_saver;
+  FLAGS_timing_report_ms = 1000;
+  auto loop1 = Make();
+  auto loop2 = MakePrimary();
+
+  const FlatbufferDetachedBuffer<TestMessage> kMessage =
+      JsonToFlatbuffer<TestMessage>("{}");
+
+  std::unique_ptr<aos::RawSender> sender =
+      loop2->MakeRawSender(configuration::GetChannel(
+          loop2->configuration(), "/test", "aos.TestMessage", "", nullptr));
+
+  Fetcher<timing::Report> report_fetcher =
+      loop1->MakeFetcher<timing::Report>("/aos");
+  EXPECT_FALSE(report_fetcher.Fetch());
+
+  loop2->OnRun([&]() {
+    for (int ii = 0; ii < TestChannelQueueSize(loop2.get()); ++ii) {
+      auto shared_span = MakeSharedSpan(kMessage.span().size());
+      memcpy(shared_span.second.data(), kMessage.span().data(),
+             kMessage.span().size());
+      EXPECT_EQ(sender->Send(std::move(shared_span.first)),
+                RawSender::Error::kOk);
+    }
+    auto shared_span = MakeSharedSpan(kMessage.span().size());
+    memcpy(shared_span.second.data(), kMessage.span().data(),
+           kMessage.span().size());
+    EXPECT_EQ(sender->Send(std::move(shared_span.first)),
+              RawSender::Error::kMessagesSentTooFast);
+  });
+  // Quit after 1 timing report, mid way through the next cycle.
+  EndEventLoop(loop2.get(), chrono::milliseconds(1500));
+
+  Run();
+
+  if (do_timing_reports() == DoTimingReports::kYes) {
+    // Check that the sent too fast actually got recorded by the timing report.
+    FlatbufferDetachedBuffer<timing::Report> primary_report =
+        FlatbufferDetachedBuffer<timing::Report>::Empty();
+    while (report_fetcher.FetchNext()) {
+      if (report_fetcher->name()->string_view() == "primary") {
+        primary_report = CopyFlatBuffer(report_fetcher.get());
+      }
+    }
+
+    EXPECT_EQ(primary_report.message().name()->string_view(), "primary");
+
+    ASSERT_NE(primary_report.message().senders(), nullptr);
+    EXPECT_EQ(primary_report.message().senders()->size(), 3);
+    EXPECT_EQ(
+        primary_report.message()
+            .senders()
+            ->Get(0)
+            ->error_counts()
+            ->Get(static_cast<size_t>(timing::SendError::MESSAGE_SENT_TOO_FAST))
+            ->count(),
+        1);
+  }
+}
+
 // Tests that senders count correctly in the timing report.
 TEST_P(AbstractEventLoopTest, WatcherTimingReport) {
   FLAGS_timing_report_ms = 1000;
@@ -2619,9 +2681,10 @@
           loop3->configuration(), "/test", "aos.TestMessage", "", nullptr));
 
   loop2->OnRun([&]() {
-    EXPECT_EQ(sender->Send(std::make_shared<absl::Span<const uint8_t>>(
-                  kMessage.span().data(), kMessage.span().size())),
-              RawSender::Error::kOk);
+    auto shared_span = MakeSharedSpan(kMessage.span().size());
+    memcpy(shared_span.second.data(), kMessage.span().data(),
+           kMessage.span().size());
+    sender->CheckOk(sender->Send(std::move(shared_span.first)));
   });
 
   bool happened = false;
@@ -3191,12 +3254,12 @@
 
   auto sender = event_loop->MakeSender<TestMessage>("/test");
 
-  // We are sending messages at 1 kHz, so we will be sending too fast after
-  // queue_size (1600) ms. After this, keep sending messages, and exactly a
-  // channel storage duration (2s) after we send the first message we should
-  // be able to successfully send a message.
+  // We are sending bunches of messages at 100 Hz, so we will be sending too
+  // fast after queue_size (800) ms. After this, keep sending messages, and
+  // exactly a channel storage duration (2s) after we send the first message we
+  // should be able to successfully send a message.
 
-  const monotonic_clock::duration kInterval = std::chrono::milliseconds(1);
+  const std::chrono::milliseconds kInterval = std::chrono::milliseconds(10);
   const monotonic_clock::duration channel_storage_duration =
       std::chrono::nanoseconds(
           event_loop->configuration()->channel_storage_duration());
@@ -3207,33 +3270,38 @@
   auto start = monotonic_clock::min_time;
 
   event_loop->AddPhasedLoop(
-      [&](int) {
-        const auto actual_err = SendTestMessage(sender);
-        const bool done_waiting = (start != monotonic_clock::min_time &&
-                                   sender.monotonic_sent_time() >=
-                                       (start + channel_storage_duration));
-        const auto expected_err =
-            (msgs_sent < queue_size || done_waiting
-                 ? RawSender::Error::kOk
-                 : RawSender::Error::kMessagesSentTooFast);
+      [&](int elapsed_cycles) {
+        // The queue is setup for 800 messages/sec.  We want to fill that up at
+        // a rate of 2000 messages/sec so we make sure we fill it up.
+        for (int i = 0; i < 2 * kInterval.count() * elapsed_cycles; ++i) {
+          const auto actual_err = SendTestMessage(sender);
+          const bool done_waiting = (start != monotonic_clock::min_time &&
+                                     sender.monotonic_sent_time() >=
+                                         (start + channel_storage_duration));
+          const auto expected_err =
+              (msgs_sent < queue_size || done_waiting
+                   ? RawSender::Error::kOk
+                   : RawSender::Error::kMessagesSentTooFast);
 
-        if (start == monotonic_clock::min_time) {
-          start = sender.monotonic_sent_time();
-        }
+          if (start == monotonic_clock::min_time) {
+            start = sender.monotonic_sent_time();
+          }
 
-        ASSERT_EQ(actual_err, expected_err);
-        counter.Count(actual_err);
-        msgs_sent++;
+          ASSERT_EQ(actual_err, expected_err);
+          counter.Count(actual_err);
+          msgs_sent++;
 
-        EXPECT_EQ(counter.failures(),
-                  msgs_sent <= queue_size
-                      ? 0
-                      : (msgs_sent - queue_size) -
-                            (actual_err == RawSender::Error::kOk ? 1 : 0));
-        EXPECT_EQ(counter.just_failed(), actual_err != RawSender::Error::kOk);
+          EXPECT_EQ(counter.failures(),
+                    msgs_sent <= queue_size
+                        ? 0
+                        : (msgs_sent - queue_size) -
+                              (actual_err == RawSender::Error::kOk ? 1 : 0));
+          EXPECT_EQ(counter.just_failed(), actual_err != RawSender::Error::kOk);
 
-        if (done_waiting) {
-          Exit();
+          if (done_waiting) {
+            Exit();
+            return;
+          }
         }
       },
       kInterval);
diff --git a/aos/events/logging/BUILD b/aos/events/logging/BUILD
index 6cb3740..bfa51fa 100644
--- a/aos/events/logging/BUILD
+++ b/aos/events/logging/BUILD
@@ -146,6 +146,7 @@
         ":buffer_encoder",
         ":logger_fbs",
         ":log_backend",
+        "//aos:sha256",
         "//aos:uuid",
         "//aos:configuration",
         "//aos:flatbuffer_merge",
@@ -158,7 +159,6 @@
         "@com_github_google_flatbuffers//:flatbuffers",
         "@com_github_google_glog//:glog",
         "@com_google_absl//absl/types:span",
-        "@boringssl//:crypto",
     ] + select({
         "//tools:cpu_k8": [
             ":s3_fetcher",
@@ -380,6 +380,7 @@
     deps = [
         ":log_namer",
         "//aos:configuration",
+        "//aos:sha256",
         "//aos/events:event_loop",
         "//aos/events:simulated_event_loop",
         "//aos/network:message_bridge_server_fbs",
@@ -435,6 +436,7 @@
         "//aos:configuration",
         "//aos:init",
         "//aos:json_to_flatbuffer",
+        "//aos:sha256",
         "//aos/events:simulated_event_loop",
         "@com_github_gflags_gflags//:gflags",
         "@com_github_google_glog//:glog",
@@ -606,6 +608,36 @@
 )
 
 aos_config(
+    name = "multinode_pingpong_split4_mixed1_config",
+    src = "multinode_pingpong_split4_mixed1.json",
+    flatbuffers = [
+        "//aos/events:ping_fbs",
+        "//aos/events:pong_fbs",
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:remote_message_fbs",
+        "//aos/network:timestamp_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//aos/events:aos_config"],
+)
+
+aos_config(
+    name = "multinode_pingpong_split4_mixed2_config",
+    src = "multinode_pingpong_split4_mixed2.json",
+    flatbuffers = [
+        "//aos/events:ping_fbs",
+        "//aos/events:pong_fbs",
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:remote_message_fbs",
+        "//aos/network:timestamp_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//aos/events:aos_config"],
+)
+
+aos_config(
     name = "multinode_pingpong_split4_reliable_config",
     src = "multinode_pingpong_split4_reliable.json",
     flatbuffers = [
@@ -696,6 +728,8 @@
         ":multinode_pingpong_combined_config",
         ":multinode_pingpong_split3_config",
         ":multinode_pingpong_split4_config",
+        ":multinode_pingpong_split4_mixed1_config",
+        ":multinode_pingpong_split4_mixed2_config",
         ":multinode_pingpong_split4_reliable_config",
         ":multinode_pingpong_split_config",
         ":multinode_pingpong_triangle_split_config",
diff --git a/aos/events/logging/log_backend.cc b/aos/events/logging/log_backend.cc
index a8b7a43..07bb780 100644
--- a/aos/events/logging/log_backend.cc
+++ b/aos/events/logging/log_backend.cc
@@ -16,9 +16,104 @@
     "If true, sync data to disk as we go so we don't get too far ahead.  Also "
     "fadvise that we are done with the memory once it hits disk.");
 
+DEFINE_uint32(queue_reserve, 32, "Pre-reserved size of write queue.");
+
 namespace aos::logger {
 namespace {
 constexpr const char *kTempExtension = ".tmp";
+
+// Assuming that kSector is power of 2, it aligns address to the left size.
+inline size_t AlignToLeft(size_t value) {
+  return value & (~(FileHandler::kSector - 1));
+}
+
+inline bool IsAligned(size_t value) {
+  return value % FileHandler::kSector == 0;
+}
+
+inline bool IsAlignedStart(const absl::Span<const uint8_t> span) {
+  return (reinterpret_cast<size_t>(span.data()) % FileHandler::kSector) == 0;
+}
+
+inline bool IsAlignedLength(const absl::Span<const uint8_t> span) {
+  return (span.size() % FileHandler::kSector) == 0;
+}
+
+}  // namespace
+
+logger::QueueAligner::QueueAligner() {
+  aligned_queue_.reserve(FLAGS_queue_reserve);
+}
+
+void logger::QueueAligner::FillAlignedQueue(
+    const absl::Span<const absl::Span<const uint8_t>> &queue) {
+  aligned_queue_.clear();
+
+  for (const auto &span : queue) {
+    // Generally, every span might have 3 optional parts (i.e. 2^3 cases):
+    // 1. unaligned prefix -  from start till first aligned block.
+    // 2. aligned main - block with aligned start and size
+    // 3. unaligned suffix - block with aligned start, and size less than one
+    // sector. If size of the span is less than 1 sector, let's call it prefix.
+
+    auto *data = span.data();
+    size_t size = span.size();
+    const auto start = reinterpret_cast<size_t>(data);
+    VLOG(2) << "Consider span starting at " << std::hex << start
+            << " with size " << size;
+
+    CHECK_GT(size, 0u) << ": Nobody should be sending empty messages.";
+
+    const auto next_aligned =
+        IsAligned(start) ? start : AlignToLeft(start) + FileHandler::kSector;
+    const auto prefix_size = next_aligned - start;
+    VLOG(2) << "Calculated prefix size " << std::hex << prefix_size;
+
+    if (prefix_size >= size) {
+      // size of prefix >= size of span - alignment is not possible, accept the
+      // whole span
+      VLOG(2) << "Only prefix found";
+      CHECK_GT(size, 0u);
+      aligned_queue_.emplace_back(data, size, false);
+      continue;
+    }
+    CHECK_LT(prefix_size, FileHandler::kSector)
+        << ": Wrong calculation of 'next' aligned position";
+    if (prefix_size > 0) {
+      // Cut the prefix and move to the main part.
+      VLOG(2) << "Cutting prefix at " << std::hex << start << " of size "
+              << prefix_size;
+      aligned_queue_.emplace_back(data, prefix_size, false);
+      data += prefix_size;
+      size -= prefix_size;
+      CHECK(data <= span.data() + span.size()) << " :Boundaries after prefix";
+    }
+
+    if (IsAligned(size)) {
+      // the rest is aligned.
+      VLOG(2) << "Returning aligned main part";
+      CHECK_GT(size, 0u);
+      aligned_queue_.emplace_back(data, size, true);
+      continue;
+    }
+
+    const auto aligned_size = AlignToLeft(size);
+    CHECK(aligned_size < size) << ": Wrong calculation of 'main' size";
+    if (aligned_size > 0) {
+      VLOG(2) << "Cutting main part starting " << std::hex
+              << reinterpret_cast<size_t>(data) << " of size " << aligned_size;
+      aligned_queue_.emplace_back(data, aligned_size, true);
+
+      data += aligned_size;
+      size -= aligned_size;
+      CHECK(data <= span.data() + span.size()) << " :Boundaries after main";
+    }
+
+    VLOG(2) << "Cutting suffix part starting " << std::hex
+            << reinterpret_cast<size_t>(data) << " of size " << size;
+    CHECK_GT(size, 0u);
+    aligned_queue_.emplace_back(data, size, false);
+  }
 }
 
 FileHandler::FileHandler(std::string filename)
@@ -40,7 +135,7 @@
     }
 
     flags_ = fcntl(fd_, F_GETFL, 0);
-    PCHECK(flags_ >= 0) << ": Failed to get flags for " << this->filename();
+    PCHECK(flags_ >= 0) << ": Failed to get flags for " << filename_;
 
     EnableDirect();
 
@@ -56,10 +151,10 @@
     // Track if we failed to set O_DIRECT.  Note: Austin hasn't seen this call
     // fail.  The write call tends to fail instead.
     if (fcntl(fd_, F_SETFL, new_flags) == -1) {
-      PLOG(WARNING) << "Failed to set O_DIRECT on " << filename();
+      PLOG(WARNING) << "Failed to set O_DIRECT on " << filename_;
       supports_odirect_ = false;
     } else {
-      VLOG(1) << "Enabled O_DIRECT on " << filename();
+      VLOG(1) << "Enabled O_DIRECT on " << filename_;
       flags_ = new_flags;
     }
   }
@@ -69,7 +164,7 @@
   if (supports_odirect_ && ODirectEnabled()) {
     flags_ = flags_ & (~O_DIRECT);
     PCHECK(fcntl(fd_, F_SETFL, flags_) != -1) << ": Failed to disable O_DIRECT";
-    VLOG(1) << "Disabled O_DIRECT on " << filename();
+    VLOG(1) << "Disabled O_DIRECT on " << filename_;
   }
 }
 
@@ -77,9 +172,9 @@
     const absl::Span<const absl::Span<const uint8_t>> &queue) {
   iovec_.clear();
   CHECK_LE(queue.size(), static_cast<size_t>(IOV_MAX));
-  iovec_.resize(queue.size());
-  // Size of the data currently in iovec_.
-  size_t counted_size = 0;
+
+  queue_aligner_.FillAlignedQueue(queue);
+  CHECK_LE(queue_aligner_.aligned_queue().size(), static_cast<size_t>(IOV_MAX));
 
   // Ok, we now need to figure out if we were aligned, and if we were, how much
   // of the data we are being asked to write is aligned.
@@ -89,119 +184,47 @@
   // kSector in memory, and the length being written is a multiple of kSector.
   // Some of the callers use an aligned ResizeableBuffer to generate 512 byte
   // aligned buffers for this code to find and use.
-  bool aligned = (total_write_bytes_ % kSector) == 0;
+  bool was_aligned = IsAligned(total_write_bytes_);
+  VLOG(1) << "Started " << (was_aligned ? "aligned" : "unaligned")
+          << " at offset " << total_write_bytes_ << " on " << filename();
 
-  // Index we are filling in next.  Keeps resetting back to 0 as we write
-  // intermediates.
-  size_t write_index = 0;
-  for (size_t i = 0; i < queue.size(); ++i) {
-    iovec_[write_index].iov_base = const_cast<uint8_t *>(queue[i].data());
-    iovec_[write_index].iov_len = queue[i].size();
-
-    // Make sure the address is aligned, or give up.  This should be uncommon,
-    // but is always possible.
-    if ((reinterpret_cast<size_t>(iovec_[write_index].iov_base) % kSector) !=
-        0) {
-      // Flush if we were aligned and have data.
-      if (aligned && write_index != 0) {
-        VLOG(1) << "Was aligned, now is not, writing previous data";
-        const auto code =
-            WriteV(iovec_.data(), write_index, true, counted_size);
+  // Walk through aligned queue and batch writes based on aligned flag
+  for (const auto &item : queue_aligner_.aligned_queue()) {
+    if (was_aligned != item.aligned) {
+      // Switching aligned context. Let's flush current batch.
+      if (!iovec_.empty()) {
+        // Flush current queue if we need.
+        const auto code = WriteV(iovec_, was_aligned);
         if (code == WriteCode::kOutOfSpace) {
+          // We cannot say anything about what number of messages was written
+          // for sure.
           return {
               .code = code,
-              .messages_written = i,
+              .messages_written = queue.size(),
           };
         }
-
-        // Now, everything before here has been written.  Make an iovec out of
-        // the rest and keep going.
-        write_index = 0;
-        counted_size = 0;
-
-        iovec_[write_index].iov_base = const_cast<uint8_t *>(queue[i].data());
-        iovec_[write_index].iov_len = queue[i].size();
+        iovec_.clear();
       }
-      aligned = false;
-    } else {
-      // We are now starting aligned again, and have data worth writing!  Flush
-      // what was there before.
-      if (!aligned && iovec_[write_index].iov_len >= kSector &&
-          write_index != 0) {
-        VLOG(1) << "Was not aligned, now is, writing previous data";
-
-        const auto code =
-            WriteV(iovec_.data(), write_index, false, counted_size);
-        if (code == WriteCode::kOutOfSpace) {
-          return {
-              .code = code,
-              .messages_written = i,
-          };
-        }
-
-        // Now, everything before here has been written.  Make an iovec out of
-        // the rest and keep going.
-        write_index = 0;
-        counted_size = 0;
-
-        iovec_[write_index].iov_base = const_cast<uint8_t *>(queue[i].data());
-        iovec_[write_index].iov_len = queue[i].size();
-        aligned = true;
-      }
+      // Write queue is flushed. WriteV updates the total_write_bytes_.
+      was_aligned = IsAligned(total_write_bytes_) && item.aligned;
     }
-
-    // Now, see if the length is a multiple of kSector.  The goal is to figure
-    // out if/how much memory we can write out with O_DIRECT so that only the
-    // last little bit is done with non-direct IO to keep it fast.
-    if ((iovec_[write_index].iov_len % kSector) != 0) {
-      VLOG(1) << "Unaligned length " << iovec_[write_index].iov_len << " on "
-              << filename();
-      // If we've got over a sector of data to write, write it out with O_DIRECT
-      // and then continue writing the rest unaligned.
-      if (aligned && iovec_[write_index].iov_len > kSector) {
-        const size_t aligned_size =
-            iovec_[write_index].iov_len & (~(kSector - 1));
-        VLOG(1) << "Was aligned, writing last chunk rounded from "
-                << queue[i].size() << " to " << aligned_size;
-        iovec_[write_index].iov_len = aligned_size;
-
-        const auto code =
-            WriteV(iovec_.data(), write_index + 1, true, counted_size + aligned_size);
-        if (code == WriteCode::kOutOfSpace) {
-          return {
-              .code = code,
-              .messages_written = i,
-          };
-        }
-
-        // Now, everything before here has been written.  Make an iovec out of
-        // the rest and keep going.
-        write_index = 0;
-        counted_size = 0;
-
-        iovec_[write_index].iov_base =
-            const_cast<uint8_t *>(queue[i].data() + aligned_size);
-        iovec_[write_index].iov_len = queue[i].size() - aligned_size;
-      }
-      aligned = false;
-    }
-    VLOG(1) << "Writing " << iovec_[write_index].iov_len << " to "
-            << filename();
-    counted_size += iovec_[write_index].iov_len;
-    ++write_index;
+    iovec_.push_back(
+        {.iov_base = const_cast<uint8_t *>(item.data), .iov_len = item.size});
   }
 
-  // Either write the aligned data if it is all aligned, or write the rest
-  // unaligned if we wrote aligned up above.
-  const auto code = WriteV(iovec_.data(), write_index, aligned, counted_size);
+  WriteCode result_code = WriteCode::kOk;
+  if (!iovec_.empty()) {
+    // Flush current queue if we need.
+    result_code = WriteV(iovec_, was_aligned);
+  }
   return {
-      .code = code,
+      .code = result_code,
       .messages_written = queue.size(),
   };
 }
 
-WriteCode FileHandler::WriteV(struct iovec *iovec_data, size_t iovec_size,
-                              bool aligned, size_t counted_size) {
+WriteCode FileHandler::WriteV(const std::vector<struct iovec> &iovec,
+                              bool aligned) {
   // Configure the file descriptor to match the mode we should be in.  This is
   // safe to over-call since it only does the syscall if needed.
   if (aligned) {
@@ -210,42 +233,41 @@
     DisableDirect();
   }
 
+  VLOG(2) << "Flushing queue of " << iovec.size() << " elements, "
+          << (aligned ? "aligned" : "unaligned");
+
+  CHECK_GT(iovec.size(), 0u);
   const auto start = aos::monotonic_clock::now();
 
-  if (VLOG_IS_ON(2)) {
-    size_t to_be_written = 0;
-    for (size_t i = 0; i < iovec_size; ++i) {
-      VLOG(2) << "  iov_base " << static_cast<void *>(iovec_data[i].iov_base)
-              << ", iov_len " << iovec_data[i].iov_len;
-      to_be_written += iovec_data[i].iov_len;
-    }
-    CHECK_GT(to_be_written, 0u);
-    VLOG(2) << "Going to write " << to_be_written;
-  }
+  // Validation of alignment assumptions.
+  if (aligned) {
+    CHECK(IsAligned(total_write_bytes_))
+        << ": Failed after writing " << total_write_bytes_
+        << " to the file, attempting aligned write with unaligned start.";
 
-  const ssize_t written = writev(fd_, iovec_data, iovec_size);
-  VLOG(2) << "Wrote " << written << ", for iovec size " << iovec_size;
-
-  if (FLAGS_sync && written > 0) {
-    // Flush asynchronously and force the data out of the cache.
-    sync_file_range(fd_, total_write_bytes_, written, SYNC_FILE_RANGE_WRITE);
-    if (last_synced_bytes_ != 0) {
-      // Per Linus' recommendation online on how to do fast file IO, do a
-      // blocking flush of the previous write chunk, and then tell the kernel to
-      // drop the pages from the cache.  This makes sure we can't get too far
-      // ahead.
-      sync_file_range(fd_, last_synced_bytes_,
-                      total_write_bytes_ - last_synced_bytes_,
-                      SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE |
-                          SYNC_FILE_RANGE_WAIT_AFTER);
-      posix_fadvise(fd_, last_synced_bytes_,
-                    total_write_bytes_ - last_synced_bytes_,
-                    POSIX_FADV_DONTNEED);
-
-      last_synced_bytes_ = total_write_bytes_;
+    for (const auto &iovec_item : iovec) {
+      absl::Span<const uint8_t> data(
+          reinterpret_cast<const uint8_t *>(iovec_item.iov_base),
+          iovec_item.iov_len);
+      VLOG(2) << "  iov_base " << static_cast<void *>(iovec_item.iov_base)
+              << ", iov_len " << iovec_item.iov_len;
+      CHECK(IsAlignedStart(data) && IsAlignedLength(data));
     }
   }
 
+  // Calculation of expected written size.
+  size_t counted_size = 0;
+  for (const auto &iovec_item : iovec) {
+    CHECK_GT(iovec_item.iov_len, 0u);
+    counted_size += iovec_item.iov_len;
+  }
+
+  VLOG(2) << "Going to write " << counted_size;
+  CHECK_GT(counted_size, 0u);
+
+  const ssize_t written = writev(fd_, iovec.data(), iovec.size());
+  VLOG(2) << "Wrote " << written << ", for iovec size " << iovec.size();
+
   const auto end = aos::monotonic_clock::now();
   if (written == -1 && errno == ENOSPC) {
     return WriteCode::kOutOfSpace;
@@ -259,8 +281,30 @@
     return WriteCode::kOutOfSpace;
   }
 
+  if (FLAGS_sync) {
+    // Flush asynchronously and force the data out of the cache.
+    sync_file_range(fd_, total_write_bytes_, written, SYNC_FILE_RANGE_WRITE);
+    if (last_synced_bytes_ != 0) {
+      // Per Linus' recommendation online on how to do fast file IO, do a
+      // blocking flush of the previous write chunk, and then tell the kernel to
+      // drop the pages from the cache.  This makes sure we can't get too far
+      // ahead.
+      sync_file_range(fd_, last_synced_bytes_,
+                      total_write_bytes_ - last_synced_bytes_,
+                      SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE |
+                          SYNC_FILE_RANGE_WAIT_AFTER);
+      posix_fadvise(fd_, last_synced_bytes_,
+                    total_write_bytes_ - last_synced_bytes_,
+                    POSIX_FADV_DONTNEED);
+    }
+    last_synced_bytes_ = total_write_bytes_;
+  }
+
   total_write_bytes_ += written;
-  write_stats_.UpdateStats(end - start, written, iovec_size);
+  if (aligned) {
+    written_aligned_ += written;
+  }
+  WriteStatistics()->UpdateStats(end - start, written, iovec.size());
   return WriteCode::kOk;
 }
 
@@ -284,7 +328,7 @@
 FileBackend::FileBackend(std::string_view base_name)
     : base_name_(base_name), separator_(base_name_.back() == '/' ? "" : "_") {}
 
-std::unique_ptr<FileHandler> FileBackend::RequestFile(std::string_view id) {
+std::unique_ptr<LogSink> FileBackend::RequestFile(std::string_view id) {
   const std::string filename = absl::StrCat(base_name_, separator_, id);
   return std::make_unique<FileHandler>(filename);
 }
@@ -292,7 +336,7 @@
 RenamableFileBackend::RenamableFileBackend(std::string_view base_name)
     : base_name_(base_name), separator_(base_name_.back() == '/' ? "" : "_") {}
 
-std::unique_ptr<FileHandler> RenamableFileBackend::RequestFile(
+std::unique_ptr<LogSink> RenamableFileBackend::RequestFile(
     std::string_view id) {
   const std::string filename =
       absl::StrCat(base_name_, separator_, id, temp_suffix_);
@@ -408,4 +452,5 @@
   }
   return WriteCode::kOk;
 }
+
 }  // namespace aos::logger
diff --git a/aos/events/logging/log_backend.h b/aos/events/logging/log_backend.h
index 20ade91..1b1417e 100644
--- a/aos/events/logging/log_backend.h
+++ b/aos/events/logging/log_backend.h
@@ -77,6 +77,71 @@
   size_t messages_written = 0;
 };
 
+// Interface that abstract writing to log from media.
+class LogSink {
+ public:
+  LogSink() = default;
+  virtual ~LogSink() = default;
+
+  LogSink(const LogSink &) = delete;
+  LogSink &operator=(const LogSink &) = delete;
+
+  // Try to open file. App will crash if there are other than out-of-space
+  // problems with backend media.
+  virtual WriteCode OpenForWrite() = 0;
+
+  // Close the file handler.
+  virtual WriteCode Close() = 0;
+
+  // Returns true if sink is open and need to be closed.
+  virtual bool is_open() const = 0;
+
+  // Peeks messages from queue and writes it to file. Returns code when
+  // out-of-space problem occurred along with number of messages from queue that
+  // was written.
+  virtual WriteResult Write(
+      const absl::Span<const absl::Span<const uint8_t>> &queue) = 0;
+
+  // Get access to statistics related to the write operations.
+  WriteStats *WriteStatistics() { return &write_stats_; }
+
+  // Name of the log sink.
+  virtual std::string_view name() const = 0;
+
+ private:
+  WriteStats write_stats_;
+};
+
+// Source for iovec with an additional flag that pointer and size of data is
+// aligned and be ready for O_DIRECT operation.
+struct AlignedIovec {
+  const uint8_t *data;
+  size_t size;
+  bool aligned;
+
+  AlignedIovec(const uint8_t *data, size_t size, bool aligned)
+      : data(data), size(size), aligned(aligned) {}
+};
+
+// Converts queue of pieces to write to the disk to the queue where every
+// element is either aligned for O_DIRECT operation or marked as not aligned.
+class QueueAligner {
+ public:
+  QueueAligner();
+
+  // Reads input queue and fills with aligned and unaligned pieces. It is easy
+  // to deal with smaller pieces and batch it during the write operation.
+  void FillAlignedQueue(
+      const absl::Span<const absl::Span<const uint8_t>> &queue);
+
+  const std::vector<AlignedIovec> &aligned_queue() const {
+    return aligned_queue_;
+  }
+
+ private:
+  std::vector<AlignedIovec> aligned_queue_;
+};
+
 // FileHandler is a replacement for bare filename in log writing and reading
 // operations.
 //
@@ -94,30 +159,28 @@
 //  4) Not all filesystems support O_DIRECT, and different sizes may be optimal
 //     for different machines.  The defaults should work decently anywhere and
 //     be tunable for faster systems.
-// TODO (Alexei): need 2 variations, to support systems with and without
-// O_DIRECT
-class FileHandler {
+class FileHandler : public LogSink {
  public:
   // Size of an aligned sector used to detect when the data is aligned enough to
   // use O_DIRECT instead.
   static constexpr size_t kSector = 512u;
 
   explicit FileHandler(std::string filename);
-  virtual ~FileHandler();
+  ~FileHandler() override;
 
   FileHandler(const FileHandler &) = delete;
   FileHandler &operator=(const FileHandler &) = delete;
 
   // Try to open file. App will crash if there are other than out-of-space
   // problems with backend media.
-  virtual WriteCode OpenForWrite();
+  WriteCode OpenForWrite() override;
 
   // Close the file handler.
-  virtual WriteCode Close();
+  WriteCode Close() override;
 
   // This will be true until Close() is called, unless the file couldn't be
   // created due to running out of space.
-  bool is_open() const { return fd_ != -1; }
+  bool is_open() const override { return fd_ != -1; }
 
   // Peeks messages from queue and writes it to file. Returns code when
   // out-of-space problem occurred along with number of messages from queue that
@@ -127,18 +190,19 @@
   // write faster if the spans passed in start at aligned addresses, and are
   // multiples of kSector long (and the data written so far is also a multiple
   // of kSector length).
-  virtual WriteResult Write(
-      const absl::Span<const absl::Span<const uint8_t>> &queue);
+  WriteResult Write(
+      const absl::Span<const absl::Span<const uint8_t>> &queue) override;
 
-  // TODO (Alexei): it is rather leaked abstraction.
-  // Path to the concrete log file.
+  // Name of the log sink mostly for informational purposes.
+  std::string_view name() const override { return filename_; }
+
+  // Number of bytes written in aligned mode. It is mostly for testing.
+  size_t written_aligned() const { return written_aligned_; }
+
+ protected:
+  // This is used by subclasses who need to access filename.
   std::string_view filename() const { return filename_; }
 
-  int fd() const { return fd_; }
-
-  // Get access to statistics related to the write operations.
-  WriteStats *WriteStatistics() { return &write_stats_; }
-
  private:
   // Enables O_DIRECT on the open file if it is supported.  Cheap to call if it
   // is already enabled.
@@ -149,11 +213,9 @@
 
   bool ODirectEnabled() const { return !!(flags_ & O_DIRECT); }
 
-  // Writes a chunk of iovecs.  aligned is true if all the data is kSector byte
-  // aligned and multiples of it in length, and counted_size is the sum of the
-  // sizes of all the chunks of data.
-  WriteCode WriteV(struct iovec *iovec_data, size_t iovec_size, bool aligned,
-                   size_t counted_size);
+  // Writes a chunk of iovecs. aligned is true if all the data is kSector byte
+  // aligned and multiples of it in length.
+  WriteCode WriteV(const std::vector<struct iovec> &iovec, bool aligned);
 
   const std::string filename_;
 
@@ -163,13 +225,15 @@
   // churn.
   std::vector<struct iovec> iovec_;
 
+  QueueAligner queue_aligner_;
+
   int total_write_bytes_ = 0;
   int last_synced_bytes_ = 0;
 
+  size_t written_aligned_ = 0;
+
   bool supports_odirect_ = true;
   int flags_ = 0;
-
-  WriteStats write_stats_;
 };
 
 // Class that decouples log writing and media (file system or memory). It is
@@ -181,7 +245,7 @@
   // Request file-like object from the log backend. It maybe a file on a disk or
   // in memory. id is usually generated by log namer and looks like name of the
   // file within a log folder.
-  virtual std::unique_ptr<FileHandler> RequestFile(std::string_view id) = 0;
+  virtual std::unique_ptr<LogSink> RequestFile(std::string_view id) = 0;
 };
 
 // Implements requests log files from file system.
@@ -192,7 +256,7 @@
   ~FileBackend() override = default;
 
   // Request file from a file system. It is not open yet.
-  std::unique_ptr<FileHandler> RequestFile(std::string_view id) override;
+  std::unique_ptr<LogSink> RequestFile(std::string_view id) override;
 
  private:
   const std::string base_name_;
@@ -210,7 +274,7 @@
         : FileHandler(std::move(filename)), owner_(owner) {}
     ~RenamableFileHandler() final = default;
 
-    // Returns false if not enough memory, true otherwise.
+    // Closes and if needed renames file.
     WriteCode Close() final;
 
    private:
@@ -221,11 +285,11 @@
   ~RenamableFileBackend() = default;
 
   // Request file from a file system. It is not open yet.
-  std::unique_ptr<FileHandler> RequestFile(std::string_view id) override;
+  std::unique_ptr<LogSink> RequestFile(std::string_view id) override;
 
   // TODO (Alexei): it is called by Logger, and left here for compatibility.
   // Logger should not call it.
-  std::string_view base_name() { return base_name_; }
+  std::string_view base_name() const { return base_name_; }
 
   // If temp files are enabled, then this will write files with the .tmp
   // suffix, and then rename them to the desired name after they are fully
diff --git a/aos/events/logging/log_backend_test.cc b/aos/events/logging/log_backend_test.cc
index e940948..928bb24 100644
--- a/aos/events/logging/log_backend_test.cc
+++ b/aos/events/logging/log_backend_test.cc
@@ -14,13 +14,24 @@
 #include "gtest/gtest.h"
 
 namespace aos::logger::testing {
+namespace {
+// Helper to write simple string to the log sink
+WriteResult Write(LogSink *log_sink, std::string_view content) {
+  auto span = absl::Span<const uint8_t>(
+      reinterpret_cast<const unsigned char *>(content.data()), content.size());
+  auto queue = absl::Span<const absl::Span<const uint8_t>>(&span, 1);
+  return log_sink->Write(queue);
+}
+}  // namespace
+
 TEST(LogBackendTest, CreateSimpleFile) {
   const std::string logevent = aos::testing::TestTmpDir() + "/logevent/";
   FileBackend backend(logevent);
   auto file = backend.RequestFile("test.log");
   ASSERT_EQ(file->OpenForWrite(), WriteCode::kOk);
-  auto result = write(file->fd(), "test", 4);
-  EXPECT_GT(result, 0);
+  auto result = Write(file.get(), "test");
+  EXPECT_EQ(result.code, WriteCode::kOk);
+  EXPECT_EQ(result.messages_written, 1);
   EXPECT_EQ(file->Close(), WriteCode::kOk);
   EXPECT_TRUE(std::filesystem::exists(logevent + "test.log"));
 }
@@ -30,8 +41,9 @@
   RenamableFileBackend backend(logevent);
   auto file = backend.RequestFile("test.log");
   ASSERT_EQ(file->OpenForWrite(), WriteCode::kOk);
-  auto result = write(file->fd(), "testtest", 8);
-  EXPECT_GT(result, 0);
+  auto result = Write(file.get(), "test");
+  EXPECT_EQ(result.code, WriteCode::kOk);
+  EXPECT_EQ(result.messages_written, 1);
   EXPECT_EQ(file->Close(), WriteCode::kOk);
   EXPECT_TRUE(std::filesystem::exists(logevent + "test.log"));
 }
@@ -42,8 +54,9 @@
   backend.EnableTempFiles();
   auto file = backend.RequestFile("test.log");
   ASSERT_EQ(file->OpenForWrite(), WriteCode::kOk);
-  auto result = write(file->fd(), "testtest", 8);
-  EXPECT_GT(result, 0);
+  auto result = Write(file.get(), "test");
+  EXPECT_EQ(result.code, WriteCode::kOk);
+  EXPECT_EQ(result.messages_written, 1);
   EXPECT_TRUE(std::filesystem::exists(logevent + "test.log.tmp"));
 
   EXPECT_EQ(file->Close(), WriteCode::kOk);
@@ -56,8 +69,9 @@
   RenamableFileBackend backend(logevent);
   auto file = backend.RequestFile("test.log");
   ASSERT_EQ(file->OpenForWrite(), WriteCode::kOk);
-  auto result = write(file->fd(), "testtest", 8);
-  EXPECT_GT(result, 0);
+  auto result = Write(file.get(), "test");
+  EXPECT_EQ(result.code, WriteCode::kOk);
+  EXPECT_EQ(result.messages_written, 1);
   EXPECT_TRUE(std::filesystem::exists(logevent + "test.log"));
 
   std::string renamed = aos::testing::TestTmpDir() + "/renamed/";
@@ -77,8 +91,9 @@
   backend.EnableTempFiles();
   auto file = backend.RequestFile("test.log");
   ASSERT_EQ(file->OpenForWrite(), WriteCode::kOk);
-  auto result = write(file->fd(), "testtest", 8);
-  EXPECT_GT(result, 0);
+  auto result = Write(file.get(), "test");
+  EXPECT_EQ(result.code, WriteCode::kOk);
+  EXPECT_EQ(result.messages_written, 1);
   EXPECT_TRUE(std::filesystem::exists(logevent + "test.log.tmp"));
 
   std::string renamed = aos::testing::TestTmpDir() + "/renamed/";
@@ -92,6 +107,109 @@
   EXPECT_TRUE(std::filesystem::exists(renamed + "test.log"));
 }
 
+TEST(QueueAlignmentTest, Cases) {
+  QueueAligner aligner;
+  uint8_t *start = nullptr;
+  {
+    // Only prefix
+    std::vector<absl::Span<const uint8_t>> queue;
+    const uint8_t *current = start + 1;
+    queue.emplace_back(current, FileHandler::kSector - 2);
+    aligner.FillAlignedQueue(queue);
+    ASSERT_EQ(aligner.aligned_queue().size(), 1);
+    const auto &prefix = aligner.aligned_queue()[0];
+    EXPECT_FALSE(prefix.aligned);
+    EXPECT_EQ(prefix.size, FileHandler::kSector - 2);
+  }
+  {
+    // Only main
+    std::vector<absl::Span<const uint8_t>> queue;
+    const uint8_t *current = start;
+    queue.emplace_back(current, FileHandler::kSector);
+    aligner.FillAlignedQueue(queue);
+    ASSERT_EQ(aligner.aligned_queue().size(), 1);
+    const auto &main = aligner.aligned_queue()[0];
+    EXPECT_TRUE(main.aligned);
+    EXPECT_EQ(main.size, FileHandler::kSector);
+  }
+  {
+    // Empty
+    std::vector<absl::Span<const uint8_t>> queue;
+    const uint8_t *current = start;
+    queue.emplace_back(current, 0);
+    EXPECT_DEATH(aligner.FillAlignedQueue(queue),
+                 "Nobody should be sending empty messages");
+  }
+  {
+    // Main and suffix
+    std::vector<absl::Span<const uint8_t>> queue;
+    const uint8_t *current = start;
+    queue.emplace_back(current, FileHandler::kSector + 1);
+    aligner.FillAlignedQueue(queue);
+    ASSERT_EQ(aligner.aligned_queue().size(), 2);
+
+    const auto &main = aligner.aligned_queue()[0];
+    EXPECT_TRUE(main.aligned);
+    EXPECT_EQ(main.size, FileHandler::kSector);
+
+    const auto &suffix = aligner.aligned_queue()[1];
+    EXPECT_FALSE(suffix.aligned);
+    EXPECT_EQ(suffix.size, 1);
+  }
+  {
+    // Prefix, main
+    std::vector<absl::Span<const uint8_t>> queue;
+    const uint8_t *current = start + 1;
+    queue.emplace_back(current, 2 * FileHandler::kSector - 1);
+    aligner.FillAlignedQueue(queue);
+    ASSERT_EQ(aligner.aligned_queue().size(), 2);
+
+    const auto &prefix = aligner.aligned_queue()[0];
+    EXPECT_FALSE(prefix.aligned);
+    EXPECT_EQ(prefix.size, FileHandler::kSector - 1);
+
+    const auto &main = aligner.aligned_queue()[1];
+    EXPECT_TRUE(main.aligned);
+    EXPECT_EQ(main.size, FileHandler::kSector);
+  }
+  {
+    // Prefix and suffix
+    std::vector<absl::Span<const uint8_t>> queue;
+    const uint8_t *current = start + 1;
+    queue.emplace_back(current, 2 * FileHandler::kSector - 2);
+    aligner.FillAlignedQueue(queue);
+    ASSERT_EQ(aligner.aligned_queue().size(), 2);
+
+    const auto &prefix = aligner.aligned_queue()[0];
+    EXPECT_FALSE(prefix.aligned);
+    EXPECT_EQ(prefix.size, FileHandler::kSector - 1);
+
+    const auto &suffix = aligner.aligned_queue()[1];
+    EXPECT_FALSE(suffix.aligned);
+    EXPECT_EQ(suffix.size, FileHandler::kSector - 1);
+  }
+  {
+    // Prefix, main and suffix
+    std::vector<absl::Span<const uint8_t>> queue;
+    const uint8_t *current = start + 1;
+    queue.emplace_back(current, 3 * FileHandler::kSector - 2);
+    aligner.FillAlignedQueue(queue);
+    ASSERT_EQ(aligner.aligned_queue().size(), 3);
+
+    const auto &prefix = aligner.aligned_queue()[0];
+    EXPECT_FALSE(prefix.aligned);
+    EXPECT_EQ(prefix.size, FileHandler::kSector - 1);
+
+    const auto &main = aligner.aligned_queue()[1];
+    EXPECT_TRUE(main.aligned);
+    EXPECT_EQ(main.size, FileHandler::kSector);
+
+    const auto &suffix = aligner.aligned_queue()[2];
+    EXPECT_FALSE(suffix.aligned);
+    EXPECT_EQ(suffix.size, FileHandler::kSector - 1);
+  }
+}
+
 // It represents calls to Write function (batching of calls and messages) where
 // int values are sizes of each message in the queue.
 using WriteRecipe = std::vector<std::vector<int>>;
@@ -209,7 +327,7 @@
   std::uniform_int_distribution<int> lengths_distribution{
       0, static_cast<int>(lengths.size() - 1)};
 
-  for (int i = 0; i < 100000; ++i) {
+  for (int i = 0; i < 1000; ++i) {
     WriteRecipe recipe;
     int number_of_writes = count_distribution(engine2);
     for (int j = 0; j < number_of_writes; ++j) {
@@ -261,6 +379,8 @@
   auto result = handler->Write(queue);
   EXPECT_EQ(result.code, WriteCode::kOk);
   EXPECT_EQ(result.messages_written, queue.size());
+  FileHandler *file_handler = reinterpret_cast<FileHandler *>(handler.get());
+  EXPECT_GT(file_handler->written_aligned(), 0);
 
   ASSERT_EQ(handler->Close(), WriteCode::kOk);
   EXPECT_TRUE(std::filesystem::exists(file));
diff --git a/aos/events/logging/log_cat.cc b/aos/events/logging/log_cat.cc
index b9e940c..b06cf20 100644
--- a/aos/events/logging/log_cat.cc
+++ b/aos/events/logging/log_cat.cc
@@ -13,6 +13,7 @@
 #include "aos/events/simulated_event_loop.h"
 #include "aos/init.h"
 #include "aos/json_to_flatbuffer.h"
+#include "aos/sha256.h"
 #include "gflags/gflags.h"
 
 DEFINE_string(
@@ -131,9 +132,8 @@
                                         .max_vector_size = static_cast<size_t>(
                                             FLAGS_max_vector_size)})
               << std::endl;
-    CHECK_EQ(
-        full_header->configuration_sha256()->string_view(),
-        aos::logger::Sha256(raw_header_reader->raw_log_file_header().span()));
+    CHECK_EQ(full_header->configuration_sha256()->string_view(),
+             aos::Sha256(raw_header_reader->raw_log_file_header().span()));
     full_header = raw_header_reader->log_file_header();
   }
 
diff --git a/aos/events/logging/log_edit.cc b/aos/events/logging/log_edit.cc
index 55b7666..7e822ca 100644
--- a/aos/events/logging/log_edit.cc
+++ b/aos/events/logging/log_edit.cc
@@ -48,12 +48,13 @@
     aos::logger::SpanReader span_reader(orig_path);
     CHECK(!span_reader.ReadMessage().empty()) << ": Empty header, aborting";
 
-    aos::logger::DetachedBufferFileWriter buffer_writer(
-        FLAGS_logfile,
+    aos::logger::FileBackend file_backend("/");
+    aos::logger::DetachedBufferWriter buffer_writer(
+        file_backend.RequestFile(FLAGS_logfile),
         std::make_unique<aos::logger::DummyEncoder>(FLAGS_max_message_size));
     {
-      aos::logger::DataEncoder::SpanCopier coppier(header.span());
-      buffer_writer.CopyMessage(&coppier, aos::monotonic_clock::min_time);
+      aos::logger::DataEncoder::SpanCopier copier(header.span());
+      buffer_writer.CopyMessage(&copier, aos::monotonic_clock::min_time);
     }
 
     while (true) {
@@ -63,8 +64,8 @@
       }
 
       {
-        aos::logger::DataEncoder::SpanCopier coppier(msg_data);
-        buffer_writer.CopyMessage(&coppier, aos::monotonic_clock::min_time);
+        aos::logger::DataEncoder::SpanCopier copier(msg_data);
+        buffer_writer.CopyMessage(&copier, aos::monotonic_clock::min_time);
       }
     }
   } else {
diff --git a/aos/events/logging/log_namer.cc b/aos/events/logging/log_namer.cc
index 8ec1e70..4b23268 100644
--- a/aos/events/logging/log_namer.cc
+++ b/aos/events/logging/log_namer.cc
@@ -45,7 +45,7 @@
 void NewDataWriter::Rotate() {
   // No need to rotate if nothing has been written.
   if (header_written_) {
-    VLOG(1) << "Rotated " << filename();
+    VLOG(1) << "Rotated " << name();
     ++parts_index_;
     reopen_(this);
     header_written_ = false;
@@ -77,7 +77,7 @@
 
   state_[node_index_].boot_uuid = source_node_boot_uuid;
 
-  VLOG(1) << "Rebooted " << filename();
+  VLOG(1) << "Rebooted " << name();
 }
 
 void NewDataWriter::UpdateBoot(const UUID &source_node_boot_uuid) {
@@ -101,7 +101,7 @@
 
   // Did the remote boot UUID change?
   if (state.boot_uuid != remote_node_boot_uuid) {
-    VLOG(1) << filename() << " Remote " << remote_node_index << " updated to "
+    VLOG(1) << name() << " Remote " << remote_node_index << " updated to "
             << remote_node_boot_uuid << " from " << state.boot_uuid;
     state.boot_uuid = remote_node_boot_uuid;
     state.oldest_remote_monotonic_timestamp = monotonic_clock::max_time;
@@ -124,7 +124,7 @@
   if (!reliable) {
     if (state.oldest_remote_unreliable_monotonic_timestamp >
         monotonic_remote_time) {
-      VLOG(1) << filename() << " Remote " << remote_node_index
+      VLOG(1) << name() << " Remote " << remote_node_index
               << " oldest_remote_unreliable_monotonic_timestamp updated from "
               << state.oldest_remote_unreliable_monotonic_timestamp << " to "
               << monotonic_remote_time;
@@ -136,7 +136,7 @@
   } else {
     if (state.oldest_remote_reliable_monotonic_timestamp >
         monotonic_remote_time) {
-      VLOG(1) << filename() << " Remote " << remote_node_index
+      VLOG(1) << name() << " Remote " << remote_node_index
               << " oldest_remote_reliable_monotonic_timestamp updated from "
               << state.oldest_remote_reliable_monotonic_timestamp << " to "
               << monotonic_remote_time;
@@ -153,7 +153,7 @@
     if (monotonic_event_time <
         logger_state.oldest_logger_remote_unreliable_monotonic_timestamp) {
       VLOG(1)
-          << filename() << " Remote " << node_index_
+          << name() << " Remote " << node_index_
           << " oldest_logger_remote_unreliable_monotonic_timestamp updated "
              "from "
           << logger_state.oldest_logger_remote_unreliable_monotonic_timestamp
@@ -169,7 +169,7 @@
 
   // Did any of the timestamps change?
   if (state.oldest_remote_monotonic_timestamp > monotonic_remote_time) {
-    VLOG(1) << filename() << " Remote " << remote_node_index
+    VLOG(1) << name() << " Remote " << remote_node_index
             << " oldest_remote_monotonic_timestamp updated from "
             << state.oldest_remote_monotonic_timestamp << " to "
             << monotonic_remote_time;
@@ -205,7 +205,7 @@
   CHECK_EQ(state_[node_index_].boot_uuid, source_node_boot_uuid);
   CHECK(writer);
   CHECK(header_written_) << ": Attempting to write message before header to "
-                         << writer->filename();
+                         << writer->name();
   writer->CopyMessage(coppier, now);
 }
 
@@ -214,7 +214,7 @@
   const size_t logger_node_index = log_namer_->logger_node_index();
   const UUID &logger_node_boot_uuid = log_namer_->logger_node_boot_uuid();
   if (state_[logger_node_index].boot_uuid == UUID::Zero()) {
-    VLOG(1) << filename() << " Logger node is " << logger_node_index
+    VLOG(1) << name() << " Logger node is " << logger_node_index
             << " and uuid is " << logger_node_boot_uuid;
     state_[logger_node_index].boot_uuid = logger_node_boot_uuid;
   } else {
@@ -227,7 +227,7 @@
 void NewDataWriter::QueueHeader(
     aos::SizePrefixedFlatbufferDetachedBuffer<LogFileHeader> &&header) {
   CHECK(!header_written_) << ": Attempting to write duplicate header to "
-                          << writer->filename();
+                          << writer->name();
   CHECK(header.message().has_source_node_boot_uuid());
   CHECK_EQ(state_[node_index_].boot_uuid,
            UUID::FromString(header.message().source_node_boot_uuid()));
@@ -245,7 +245,7 @@
     reopen_(this);
   }
 
-  VLOG(1) << "Writing to " << filename() << " "
+  VLOG(1) << "Writing to " << name() << " "
           << aos::FlatbufferToJson(
                  header, {.multi_line = false, .max_vector_size = 100});
 
@@ -560,14 +560,14 @@
   return result;
 }
 
-MultiNodeLogNamer::MultiNodeLogNamer(
-    std::unique_ptr<RenamableFileBackend> log_backend, EventLoop *event_loop)
+MultiNodeLogNamer::MultiNodeLogNamer(std::unique_ptr<LogBackend> log_backend,
+                                     EventLoop *event_loop)
     : MultiNodeLogNamer(std::move(log_backend), event_loop->configuration(),
                         event_loop, event_loop->node()) {}
 
-MultiNodeLogNamer::MultiNodeLogNamer(
-    std::unique_ptr<RenamableFileBackend> log_backend,
-    const Configuration *configuration, EventLoop *event_loop, const Node *node)
+MultiNodeLogNamer::MultiNodeLogNamer(std::unique_ptr<LogBackend> log_backend,
+                                     const Configuration *configuration,
+                                     EventLoop *event_loop, const Node *node)
     : LogNamer(configuration, event_loop, node),
       log_backend_(std::move(log_backend)),
       encoder_factory_([](size_t max_message_size) {
@@ -714,9 +714,13 @@
   return data_writer_.get();
 }
 
-void MultiNodeLogNamer::Close() {
+WriteCode MultiNodeLogNamer::Close() {
   data_writers_.clear();
   data_writer_.reset();
+  if (ran_out_of_space_) {
+    return WriteCode::kOutOfSpace;
+  }
+  return WriteCode::kOk;
 }
 
 void MultiNodeLogNamer::ResetStatistics() {
@@ -822,7 +826,6 @@
     return;
   }
   DetachedBufferWriter *const writer = writer_pointer->get();
-  const bool was_open = writer->is_open();
   writer->Close();
 
   const auto *stats = writer->WriteStatistics();
@@ -840,11 +843,6 @@
     ran_out_of_space_ = true;
     writer->acknowledge_out_of_space();
   }
-
-  if (!was_open) {
-    CHECK(access(std::string(writer->filename()).c_str(), F_OK) == -1)
-        << ": File should not exist: " << writer->filename();
-  }
 }
 
 }  // namespace logger
diff --git a/aos/events/logging/log_namer.h b/aos/events/logging/log_namer.h
index 3b54025..8842f4e 100644
--- a/aos/events/logging/log_namer.h
+++ b/aos/events/logging/log_namer.h
@@ -43,7 +43,8 @@
 
   void UpdateMaxMessageSize(size_t new_size) {
     if (new_size > max_message_size_) {
-      CHECK(!header_written_);
+      CHECK(!header_written_) << ": Tried to update to " << new_size << ", was "
+                              << max_message_size_ << " for " << name();
       max_message_size_ = new_size;
     }
   }
@@ -77,10 +78,8 @@
   // update the remote timestamps.
   void UpdateBoot(const UUID &source_node_boot_uuid);
 
-  // Returns the filename of the writer.
-  std::string_view filename() const {
-    return writer ? writer->filename() : "(closed)";
-  }
+  // Returns the name of the writer. It may be a filename, but assume it is not.
+  std::string_view name() const { return writer ? writer->name() : "(closed)"; }
 
   void Close();
 
@@ -176,17 +175,6 @@
   }
   virtual ~LogNamer() = default;
 
-  virtual std::string_view base_name() const = 0;
-
-  // Rotate should be called at least once in between calls to set_base_name.
-  // Otherwise temporary files will not be recoverable.
-  // Rotate is called by Logger::RenameLogBase, which is currently the only user
-  // of this method.
-  // Only renaming the folder is supported, not the file base name.
-  // TODO (Alexei): it should not be in interface, since it is not applied to
-  // files.
-  virtual void set_base_name(std::string_view base_name) = 0;
-
   // Returns a writer for writing data from messages on this channel (on the
   // primary node).
   //
@@ -216,6 +204,10 @@
   // Returns all the nodes that data is being written for.
   const std::vector<const Node *> &nodes() const { return nodes_; }
 
+  // Closes all existing log data writers. No more data may be written after
+  // this.
+  virtual WriteCode Close() = 0;
+
   // Returns the node the logger is running on.
   const Node *node() const { return node_; }
   const UUID &logger_node_boot_uuid() const { return logger_node_boot_uuid_; }
@@ -302,28 +294,13 @@
 // Log namer which uses a config to name a bunch of files.
 class MultiNodeLogNamer : public LogNamer {
  public:
-  MultiNodeLogNamer(std::unique_ptr<RenamableFileBackend> log_backend,
+  MultiNodeLogNamer(std::unique_ptr<LogBackend> log_backend,
                     EventLoop *event_loop);
-  MultiNodeLogNamer(std::unique_ptr<RenamableFileBackend> log_backend,
+  MultiNodeLogNamer(std::unique_ptr<LogBackend> log_backend,
                     const Configuration *configuration, EventLoop *event_loop,
                     const Node *node);
   ~MultiNodeLogNamer() override;
 
-  std::string_view base_name() const final { return log_backend_->base_name(); }
-
-  void set_base_name(std::string_view base_name) final {
-    log_backend_->RenameLogBase(base_name);
-  }
-
-  // When enabled, this will write files under names beginning
-  // with the .tmp suffix, and then rename them to the desired name after
-  // they are fully written.
-  //
-  // This is useful to enable incremental copying of the log files.
-  //
-  // Defaults to writing directly to the final filename.
-  void EnableTempFiles() { log_backend_->EnableTempFiles(); }
-
   // Sets the function for creating encoders.  The argument is the max message
   // size (including headers) that will be written into this encoder.
   //
@@ -387,7 +364,7 @@
   // Closes all existing log files. No more data may be written after this.
   //
   // This may set ran_out_of_space().
-  void Close();
+  WriteCode Close() override;
 
   // Accessors for various statistics. See the identically-named methods in
   // DetachedBufferWriter for documentation. These are aggregated across all
@@ -458,6 +435,12 @@
 
   void ResetStatistics();
 
+ protected:
+  // TODO (Alexei): consider to move ownership of log_namer to concrete sub
+  // class and make log_backend_ raw pointer.
+  LogBackend *log_backend() { return log_backend_.get(); }
+  const LogBackend *log_backend() const { return log_backend_.get(); }
+
  private:
   // Opens up a writer for timestamps forwarded back.
   void OpenForwardedTimestampWriter(const Channel *channel,
@@ -488,7 +471,7 @@
     return t;
   }
 
-  std::unique_ptr<RenamableFileBackend> log_backend_;
+  std::unique_ptr<LogBackend> log_backend_;
 
   bool ran_out_of_space_ = false;
   std::vector<std::string> all_filenames_;
@@ -524,6 +507,36 @@
       : MultiNodeLogNamer(std::make_unique<RenamableFileBackend>(base_name),
                           configuration, event_loop, node) {}
   ~MultiNodeFilesLogNamer() override = default;
+
+  std::string_view base_name() const {
+    return renamable_file_backend()->base_name();
+  }
+
+  // Rotate should be called at least once in between calls to set_base_name.
+  // Otherwise, temporary files will not be recoverable.
+  // Rotate is called by Logger::RenameLogBase, which is currently the only user
+  // of this method.
+  // Only renaming the folder is supported, not the file base name.
+  void set_base_name(std::string_view base_name) {
+    renamable_file_backend()->RenameLogBase(base_name);
+  }
+
+  // When enabled, this will write files under names beginning
+  // with the .tmp suffix, and then rename them to the desired name after
+  // they are fully written.
+  //
+  // This is useful to enable incremental copying of the log files.
+  //
+  // Defaults to writing directly to the final filename.
+  void EnableTempFiles() { renamable_file_backend()->EnableTempFiles(); }
+
+ private:
+  RenamableFileBackend *renamable_file_backend() {
+    return reinterpret_cast<RenamableFileBackend *>(log_backend());
+  }
+  const RenamableFileBackend *renamable_file_backend() const {
+    return reinterpret_cast<const RenamableFileBackend *>(log_backend());
+  }
 };
 
 }  // namespace logger
diff --git a/aos/events/logging/log_reader.cc b/aos/events/logging/log_reader.cc
index 0af69c7..a299c61 100644
--- a/aos/events/logging/log_reader.cc
+++ b/aos/events/logging/log_reader.cc
@@ -1,5 +1,6 @@
 #include "aos/events/logging/log_reader.h"
 
+#include <dirent.h>
 #include <fcntl.h>
 #include <sys/stat.h>
 #include <sys/types.h>
@@ -729,6 +730,7 @@
   // running until the last node.
 
   for (std::unique_ptr<State> &state : states_) {
+    CHECK(state);
     VLOG(1) << "Start time is " << state->monotonic_start_time(0)
             << " for node " << MaybeNodeName(state->node()) << "now "
             << state->monotonic_now();
@@ -1977,9 +1979,8 @@
 
   // Send!  Use the replayed queue index here instead of the logged queue index
   // for the remote queue index.  This makes re-logging work.
-  const auto err = sender->Send(
-      RawSender::SharedSpan(timestamped_message.data,
-                            &timestamped_message.data->span),
+  const RawSender::Error err = sender->Send(
+      SharedSpan(timestamped_message.data, &timestamped_message.data->span),
       timestamped_message.monotonic_remote_time.time,
       timestamped_message.realtime_remote_time, remote_queue_index,
       (channel_source_state_[timestamped_message.channel_index] != nullptr
diff --git a/aos/events/logging/log_reader.h b/aos/events/logging/log_reader.h
index 6a0fcea..e536c46 100644
--- a/aos/events/logging/log_reader.h
+++ b/aos/events/logging/log_reader.h
@@ -289,6 +289,9 @@
   }
 
   std::string_view name() const { return log_files_[0].name; }
+  std::string_view log_event_uuid() const {
+    return log_files_[0].log_event_uuid;
+  }
 
   // Set whether to exit the SimulatedEventLoopFactory when we finish reading
   // the logfile.
@@ -543,6 +546,7 @@
     }
 
     monotonic_clock::time_point monotonic_now() const {
+      CHECK_NOTNULL(event_loop_);
       return event_loop_->monotonic_now();
     }
 
@@ -729,8 +733,8 @@
     std::vector<message_bridge::NoncausalOffsetEstimator *> filters_;
     message_bridge::MultiNodeNoncausalOffsetEstimator *multinode_filters_;
 
-    // List of NodeEventLoopFactorys (or nullptr if it isn't a forwarded
-    // channel) which correspond to the originating node.
+    // List of States (or nullptr if it isn't a forwarded channel) which
+    // correspond to the originating node.
     std::vector<State *> channel_source_state_;
 
     // This is a cache for channel, connection mapping to the corresponding
diff --git a/aos/events/logging/log_writer.cc b/aos/events/logging/log_writer.cc
index 1a333e7..17c8da2 100644
--- a/aos/events/logging/log_writer.cc
+++ b/aos/events/logging/log_writer.cc
@@ -11,6 +11,7 @@
 #include "aos/network/message_bridge_server_generated.h"
 #include "aos/network/team_number.h"
 #include "aos/network/timestamp_channel.h"
+#include "aos/sha256.h"
 
 namespace aos {
 namespace logger {
@@ -230,17 +231,6 @@
   }
 }
 
-bool Logger::RenameLogBase(std::string new_base_name) {
-  // TODO(Naman): Got a crash in RenameLogBase. Putting in a CHECK_NOTNULL to
-  // catch the bug if it happens again
-  if (new_base_name == CHECK_NOTNULL(log_namer_)->base_name()) {
-    return true;
-  }
-  log_namer_->set_base_name(new_base_name);
-  Rotate();
-  return true;
-}
-
 std::string Logger::WriteConfiguration(LogNamer *log_namer) {
   std::string config_sha256;
 
@@ -348,11 +338,44 @@
 
   VLOG(1) << "Restarting logger for " << FlatbufferToJson(node_);
 
-  // Force out every currently pending message, pointing all fetchers at the
-  // last (currently available) records.  Note that LogUntil() updates
-  // last_synchronized_time_ to the time value that it receives.
-  while (LogUntil(last_synchronized_time_ + polling_period_))
-    ;
+  // Make sure not to write past now so we don't risk out of order problems.  We
+  // don't want to get into a situation where we write out up to now + 0.1 sec,
+  // and that operation takes ~0.1 seconds, so we end up writing a different
+  // amount of the early and late channels.  That would then result in the next
+  // go around finding more than 0.1 sec of data on the early channels.
+  //
+  // Make sure we read up until "now" and log it.  This sets us up so that we
+  // are unlikely to fetch a message far in the future and have a ton of data
+  // before the offical start time.
+  monotonic_clock::time_point newest_record = monotonic_clock::min_time;
+  while (true) {
+    aos::monotonic_clock::time_point next_time =
+        last_synchronized_time_ + polling_period_;
+    const aos::monotonic_clock::time_point monotonic_now =
+        event_loop_->monotonic_now();
+    if (next_time > monotonic_now) {
+      next_time = monotonic_now;
+    }
+
+    bool wrote_messages = false;
+    std::tie(wrote_messages, newest_record) = LogUntil(next_time);
+
+    if (next_time == monotonic_now &&
+        (!wrote_messages || newest_record < monotonic_now + polling_period_)) {
+      // If we stopped writing messages, then we probably have stopped making
+      // progress. If the newest record (unwritten or written) on a channel is
+      // very close to the current time, then there won't be much data
+      // officially after the end of the last log but before the start of the
+      // current one.  We need to pick the start of the current log to be after
+      // the last message on record so we don't have holes in the log.
+      break;
+    }
+  }
+
+  // We are now synchronized up to last_synchronized_time_.  Our start time can
+  // safely be "newest_record".  But, we need to guarentee that the start time
+  // is after the newest message we have a record of, and that we don't skip any
+  // messages as we rotate.  This means we can't call Fetch anywhere.
 
   std::unique_ptr<LogNamer> old_log_namer = std::move(log_namer_);
   log_namer_ = std::move(log_namer);
@@ -371,13 +394,11 @@
                  << "ns to swap log_namer";
   }
 
-  // Since we are going to log all in 1 big go, we need our log start time to
-  // be after the previous LogUntil call finished, but before 1 period after
-  // it. The best way to guarentee that is to pick a start time that is the
-  // earliest of the two.  That covers the case where the OS puts us to sleep
-  // between when we finish LogUntil and capture beginning_time.
-  const aos::monotonic_clock::time_point monotonic_start_time =
-      std::min(last_synchronized_time_, beginning_time);
+  // Our start time is now the newest message we have a record of.  We will
+  // declare the old log "done", and start in on the new one, double-logging
+  // anything we have a record of so we have all the messages from before the
+  // start.
+  const aos::monotonic_clock::time_point monotonic_start_time = newest_record;
   const aos::realtime_clock::time_point realtime_start_time =
       (beginning_time_rt + (monotonic_start_time.time_since_epoch() -
                             ((beginning_time.time_since_epoch() +
@@ -402,66 +423,32 @@
   const aos::monotonic_clock::time_point header_time =
       event_loop_->monotonic_now();
 
-  // Write the transition record(s) for each channel ...
+  // Close out the old writers to free up memory to be used by the new writers.
+  old_log_namer->Close();
+
   for (FetcherStruct &f : fetchers_) {
     // Create writers from the new namer
-    NewDataWriter *next_writer = nullptr;
-    NewDataWriter *next_timestamp_writer = nullptr;
-    NewDataWriter *next_contents_writer = nullptr;
 
     if (f.wants_writer) {
-      next_writer = log_namer_->MakeWriter(f.channel);
+      f.writer = log_namer_->MakeWriter(f.channel);
     }
     if (f.wants_timestamp_writer) {
-      next_timestamp_writer = log_namer_->MakeTimestampWriter(f.channel);
+      f.timestamp_writer = log_namer_->MakeTimestampWriter(f.channel);
     }
     if (f.wants_contents_writer) {
-      next_contents_writer = log_namer_->MakeForwardedTimestampWriter(
+      f.contents_writer = log_namer_->MakeForwardedTimestampWriter(
           f.channel, CHECK_NOTNULL(f.timestamp_node));
     }
 
-    if (f.fetcher->context().data != nullptr) {
-      // Write the last message fetched as the first of the new log of this
-      // type. The timestamps on these will all be before the new start time.
-      WriteData(next_writer, f);
-      WriteTimestamps(next_timestamp_writer, f);
-      WriteContent(next_contents_writer, f);
-
-      // It is possible that a few more snuck in. Write them all out also,
-      // including any that should also be in the old log.
-      while (true) {
-        // Get the next message ...
-        const auto start = event_loop_->monotonic_now();
-        const bool got_new = f.fetcher->FetchNext();
-        const auto end = event_loop_->monotonic_now();
-        RecordFetchResult(start, end, got_new, &f);
-
-        if (got_new) {
-          if (f.fetcher->context().monotonic_event_time <=
-              last_synchronized_time_) {
-            WriteFetchedRecord(f);
-            WriteData(next_writer, f);
-            WriteTimestamps(next_timestamp_writer, f);
-            WriteContent(next_contents_writer, f);
-
-          } else {
-            f.written = false;
-            break;
-          }
-
-        } else {
-          f.written = true;
-          break;
-        }
-      }
-    }
-
-    // Switch fully over to the new writers.
-    f.writer = next_writer;
-    f.timestamp_writer = next_timestamp_writer;
-    f.contents_writer = next_contents_writer;
+    // Mark each channel with data as not written.  That triggers each channel
+    // to be re-logged.
+    f.written = f.fetcher->context().data == nullptr;
   }
 
+  // And now make sure to log everything up to the start time in 1 big go so we
+  // make sure we have it before we let the world start logging normally again.
+  LogUntil(monotonic_start_time);
+
   const aos::monotonic_clock::time_point channel_time =
       event_loop_->monotonic_now();
 
@@ -498,6 +485,8 @@
   log_event_uuid_ = UUID::Zero();
   log_start_uuid_ = std::nullopt;
 
+  log_namer_->Close();
+
   return std::move(log_namer_);
 }
 
@@ -746,7 +735,7 @@
     VLOG(2) << "Wrote data as node " << FlatbufferToJson(node_)
             << " for channel "
             << configuration::CleanedChannelToString(f.fetcher->channel())
-            << " to " << writer->filename();
+            << " to " << writer->name();
   }
 }
 
@@ -771,7 +760,7 @@
     VLOG(2) << "Wrote timestamps as node " << FlatbufferToJson(node_)
             << " for channel "
             << configuration::CleanedChannelToString(f.fetcher->channel())
-            << " to " << timestamp_writer->filename() << " timestamp";
+            << " to " << timestamp_writer->name() << " timestamp";
   }
 }
 
@@ -828,8 +817,10 @@
   WriteContent(f.contents_writer, f);
 }
 
-bool Logger::LogUntil(monotonic_clock::time_point t) {
-  bool has_pending_messages = false;
+std::pair<bool, monotonic_clock::time_point> Logger::LogUntil(
+    monotonic_clock::time_point t) {
+  bool wrote_messages = false;
+  monotonic_clock::time_point newest_record = monotonic_clock::min_time;
 
   // Grab the latest ServerStatistics message.  This will always have the
   // oppertunity to be >= to the current time, so it will always represent any
@@ -838,6 +829,11 @@
 
   // Write each channel to disk, one at a time.
   for (FetcherStruct &f : fetchers_) {
+    if (f.fetcher->context().data != nullptr) {
+      newest_record =
+          std::max(newest_record, f.fetcher->context().monotonic_event_time);
+    }
+
     while (true) {
       if (f.written) {
         const auto start = event_loop_->monotonic_now();
@@ -850,23 +846,25 @@
                          f.fetcher->channel());
           break;
         }
+        newest_record =
+            std::max(newest_record, f.fetcher->context().monotonic_event_time);
         f.written = false;
       }
 
       // TODO(james): Write tests to exercise this logic.
       if (f.fetcher->context().monotonic_event_time >= t) {
-        has_pending_messages = true;
         break;
       }
 
       WriteFetchedRecord(f);
+      wrote_messages = true;
 
       f.written = true;
     }
   }
   last_synchronized_time_ = t;
 
-  return has_pending_messages;
+  return std::make_pair(wrote_messages, newest_record);
 }
 
 void Logger::DoLogData(const monotonic_clock::time_point end_time,
diff --git a/aos/events/logging/log_writer.h b/aos/events/logging/log_writer.h
index 0063c9b..65c68f4 100644
--- a/aos/events/logging/log_writer.h
+++ b/aos/events/logging/log_writer.h
@@ -139,11 +139,6 @@
       std::unique_ptr<LogNamer> log_namer,
       std::optional<UUID> log_start_uuid = std::nullopt);
 
-  // Moves the current log location to the new name. Returns true if a change
-  // was made, false otherwise.
-  // Only renaming the folder is supported, not the file base name.
-  bool RenameLogBase(std::string new_base_name);
-
   // Stops logging. Ensures any messages through end_time make it into the log.
   //
   // If you want to stop ASAP, pass min_time to avoid reading any more messages.
@@ -250,9 +245,12 @@
   // Fetches from each channel until all the data is logged.  This is dangerous
   // because it lets you log for more than 1 period.  All calls need to verify
   // that t isn't greater than 1 period in the future.
-  // Returns true if there is at least one message that has been fetched but
-  // not yet written.
-  bool LogUntil(monotonic_clock::time_point t);
+  //
+  // Returns true if there is at least one message written, and also returns the
+  // timestamp of the newest record that any fetcher is pointing to, or min_time
+  // if there are no messages published on any logged channels.
+  std::pair<bool, monotonic_clock::time_point> LogUntil(
+      monotonic_clock::time_point t);
 
   void RecordFetchResult(aos::monotonic_clock::time_point start,
                          aos::monotonic_clock::time_point end, bool got_new,
diff --git a/aos/events/logging/logfile_sorting.cc b/aos/events/logging/logfile_sorting.cc
index 89801b7..f0161c9 100644
--- a/aos/events/logging/logfile_sorting.cc
+++ b/aos/events/logging/logfile_sorting.cc
@@ -11,9 +11,9 @@
 #include "aos/events/logging/logfile_utils.h"
 #include "aos/flatbuffer_merge.h"
 #include "aos/flatbuffers.h"
+#include "aos/sha256.h"
 #include "aos/time/time.h"
 #include "dirent.h"
-#include "openssl/sha.h"
 #include "sys/stat.h"
 
 #if ENABLE_S3
@@ -1276,6 +1276,21 @@
         //       .oldest_remote_unreliable_monotonic_timestamp=9223372036.854775807sec,
         //       .oldest_local_unreliable_monotonic_timestamp=9223372036.854775807sec
         //      }
+        //  4) One reliable, one unreliable, local times don't match. 1 < 2
+        //     same message got sent, and with reliable timestamps, we don't
+        //     know how long it took to cross the network.
+        //      {
+        //       .oldest_remote_reliable_monotonic_timestamp=9223372036.854775807sec,
+        //       .oldest_local_reliable_monotonic_timestamp=9223372036.854775807sec
+        //       .oldest_remote_unreliable_monotonic_timestamp=10.122999611sec,
+        //       .oldest_local_unreliable_monotonic_timestamp=9.400951024sec,
+        //      }
+        //      {
+        //       .oldest_remote_reliable_monotonic_timestamp=11.798054208sec,
+        //       .oldest_local_reliable_monotonic_timestamp=23457.772660691sec,
+        //       .oldest_remote_unreliable_monotonic_timestamp=9223372036.854775807sec,
+        //       .oldest_local_unreliable_monotonic_timestamp=9223372036.854775807sec
+        //      }
         //
         //  Writing all this out for which timestamps we have out of all 32
         //  combinations, and which cases each of the correspond to:
@@ -1286,10 +1301,10 @@
         //  {  }, {ru} no match -> fail, won't be in the list
         //  { u}, {  } no match -> fail, won't be in the list
         //  { u}, { u} no match -> 1
-        //  { u}, {r } no match -> fail
+        //  { u}, {r } no match -> 4
         //  { u}, {ru} no match -> 1
         //  {r }, {  } no match -> fail, won't be in the list
-        //  {r }, { u} no match -> fail
+        //  {r }, { u} no match -> 4
         //  {r }, {r } no match -> 2
         //  {r }, {ru} no match -> 2
         //  {ru}, {  } no match -> fail, won't be in the list
@@ -1353,11 +1368,13 @@
                       aos::monotonic_clock::max_time;
 
               if (both_unreliable) {
+                VLOG(1) << "Both Unreliable";
                 return std::get<1>(a)
                            .oldest_local_unreliable_monotonic_timestamp <
                        std::get<1>(b)
                            .oldest_local_unreliable_monotonic_timestamp;
               } else if (both_reliable) {
+                VLOG(1) << "Both Reliable";
                 CHECK_NE(
                     std::get<1>(a).oldest_local_reliable_monotonic_timestamp,
                     std::get<1>(b).oldest_local_reliable_monotonic_timestamp)
@@ -1367,6 +1384,42 @@
                 return std::get<1>(a)
                            .oldest_local_reliable_monotonic_timestamp <
                        std::get<1>(b).oldest_local_reliable_monotonic_timestamp;
+
+              } else if (std::get<1>(a)
+                                 .oldest_local_reliable_monotonic_timestamp !=
+                             aos::monotonic_clock::max_time &&
+                         std::get<1>(b)
+                                 .oldest_local_unreliable_monotonic_timestamp !=
+                             aos::monotonic_clock::max_time) {
+                VLOG(1)
+                    << " Comparing Reliable  "
+                    << std::get<1>(a).oldest_local_reliable_monotonic_timestamp;
+                VLOG(1) << "   Versus Uneliable  "
+                        << std::get<1>(b)
+                               .oldest_local_unreliable_monotonic_timestamp;
+
+                return std::get<1>(a)
+                           .oldest_local_reliable_monotonic_timestamp <
+                       std::get<1>(b)
+                           .oldest_local_unreliable_monotonic_timestamp;
+
+              } else if (std::get<1>(a)
+                                 .oldest_local_unreliable_monotonic_timestamp !=
+                             aos::monotonic_clock::max_time &&
+                         std::get<1>(b)
+                                 .oldest_local_reliable_monotonic_timestamp !=
+                             aos::monotonic_clock::max_time) {
+                VLOG(1) << " Comparing Unreliable  "
+                        << std::get<1>(a)
+                               .oldest_local_unreliable_monotonic_timestamp;
+                VLOG(1)
+                    << "   Versus Reliable     "
+                    << std::get<1>(b).oldest_local_reliable_monotonic_timestamp;
+
+                return std::get<1>(a)
+                           .oldest_local_unreliable_monotonic_timestamp <
+                       std::get<1>(b).oldest_local_reliable_monotonic_timestamp;
+
               } else {
                 LOG(FATAL) << "Broken logic, unable to compare timestamps "
                            << std::get<1>(a) << ", " << std::get<1>(b);
@@ -2143,19 +2196,5 @@
   return stream;
 }
 
-std::string Sha256(const absl::Span<const uint8_t> str) {
-  unsigned char hash[SHA256_DIGEST_LENGTH];
-  SHA256_CTX sha256;
-  SHA256_Init(&sha256);
-  SHA256_Update(&sha256, str.data(), str.size());
-  SHA256_Final(hash, &sha256);
-  std::stringstream ss;
-  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
-    ss << std::hex << std::setw(2) << std::setfill('0')
-       << static_cast<int>(hash[i]);
-  }
-  return ss.str();
-}
-
 }  // namespace logger
 }  // namespace aos
diff --git a/aos/events/logging/logfile_sorting.h b/aos/events/logging/logfile_sorting.h
index 9e99bb2..6f895f1 100644
--- a/aos/events/logging/logfile_sorting.h
+++ b/aos/events/logging/logfile_sorting.h
@@ -153,9 +153,6 @@
 // Recursively searches for logfiles in argv[1] and onward.
 std::vector<std::string> FindLogs(int argc, char **argv);
 
-// Returns the sha256 of a span.
-std::string Sha256(const absl::Span<const uint8_t> str);
-
 }  // namespace logger
 }  // namespace aos
 
diff --git a/aos/events/logging/logfile_utils.cc b/aos/events/logging/logfile_utils.cc
index 990e742..490393d 100644
--- a/aos/events/logging/logfile_utils.cc
+++ b/aos/events/logging/logfile_utils.cc
@@ -80,12 +80,11 @@
 }
 }  // namespace
 
-DetachedBufferWriter::DetachedBufferWriter(
-    std::unique_ptr<FileHandler> file_handler,
-    std::unique_ptr<DataEncoder> encoder)
-    : file_handler_(std::move(file_handler)), encoder_(std::move(encoder)) {
-  CHECK(file_handler_);
-  ran_out_of_space_ = file_handler_->OpenForWrite() == WriteCode::kOutOfSpace;
+DetachedBufferWriter::DetachedBufferWriter(std::unique_ptr<LogSink> log_sink,
+                                           std::unique_ptr<DataEncoder> encoder)
+    : log_sink_(std::move(log_sink)), encoder_(std::move(encoder)) {
+  CHECK(log_sink_);
+  ran_out_of_space_ = log_sink_->OpenForWrite() == WriteCode::kOutOfSpace;
   if (ran_out_of_space_) {
     LOG(WARNING) << "And we are out of space";
   }
@@ -108,7 +107,7 @@
 // (because that data will then be its data).
 DetachedBufferWriter &DetachedBufferWriter::operator=(
     DetachedBufferWriter &&other) {
-  std::swap(file_handler_, other.file_handler_);
+  std::swap(log_sink_, other.log_sink_);
   std::swap(encoder_, other.encoder_);
   std::swap(ran_out_of_space_, other.ran_out_of_space_);
   std::swap(acknowledge_ran_out_of_space_, other.acknowledge_ran_out_of_space_);
@@ -147,14 +146,15 @@
 }
 
 void DetachedBufferWriter::Close() {
-  if (!file_handler_->is_open()) {
+  if (!log_sink_->is_open()) {
     return;
   }
   encoder_->Finish();
   while (encoder_->queue_size() > 0) {
     Flush(monotonic_clock::max_time);
   }
-  ran_out_of_space_ = file_handler_->Close() == WriteCode::kOutOfSpace;
+  encoder_.reset();
+  ran_out_of_space_ = log_sink_->Close() == WriteCode::kOutOfSpace;
 }
 
 void DetachedBufferWriter::Flush(aos::monotonic_clock::time_point now) {
@@ -177,7 +177,7 @@
     return;
   }
 
-  const WriteResult result = file_handler_->Write(queue);
+  const WriteResult result = log_sink_->Write(queue);
   encoder_->Clear(result.messages_written);
   ran_out_of_space_ = result.code == WriteCode::kOutOfSpace;
 }
diff --git a/aos/events/logging/logfile_utils.h b/aos/events/logging/logfile_utils.h
index c75c8fb..0bf60a0 100644
--- a/aos/events/logging/logfile_utils.h
+++ b/aos/events/logging/logfile_utils.h
@@ -51,7 +51,7 @@
   // Marker struct for one of our constructor overloads.
   struct already_out_of_space_t {};
 
-  DetachedBufferWriter(std::unique_ptr<FileHandler> file_handler,
+  DetachedBufferWriter(std::unique_ptr<LogSink> log_sink,
                        std::unique_ptr<DataEncoder> encoder);
   // Creates a dummy instance which won't even open a file. It will act as if
   // opening the file ran out of space immediately.
@@ -64,11 +64,11 @@
   DetachedBufferWriter &operator=(DetachedBufferWriter &&other);
   DetachedBufferWriter &operator=(const DetachedBufferWriter &) = delete;
 
-  std::string_view filename() const { return file_handler_->filename(); }
+  std::string_view name() const { return log_sink_->name(); }
 
   // This will be true until Close() is called, unless the file couldn't be
   // created due to running out of space.
-  bool is_open() const { return file_handler_->is_open(); }
+  bool is_open() const { return log_sink_->is_open(); }
 
   // Queues up a finished FlatBufferBuilder to be encoded and written.
   //
@@ -106,7 +106,7 @@
     return encoder_->total_bytes();
   }
 
-  WriteStats* WriteStatistics() const { return file_handler_->WriteStatistics(); }
+  WriteStats *WriteStatistics() const { return log_sink_->WriteStatistics(); }
 
  private:
   // Performs a single writev call with as much of the data we have queued up as
@@ -124,7 +124,7 @@
   // the current time.  It just needs to be close.
   void FlushAtThreshold(aos::monotonic_clock::time_point now);
 
-  std::unique_ptr<FileHandler> file_handler_;
+  std::unique_ptr<LogSink> log_sink_;
   std::unique_ptr<DataEncoder> encoder_;
 
   bool ran_out_of_space_ = false;
@@ -134,17 +134,6 @@
       aos::monotonic_clock::min_time;
 };
 
-// Specialized writer to single file
-class DetachedBufferFileWriter : public FileBackend,
-                                 public DetachedBufferWriter {
- public:
-  DetachedBufferFileWriter(std::string_view filename,
-                           std::unique_ptr<DataEncoder> encoder)
-      : FileBackend("/"),
-        DetachedBufferWriter(FileBackend::RequestFile(filename),
-                             std::move(encoder)) {}
-};
-
 // Repacks the provided RemoteMessage into fbb.
 flatbuffers::Offset<MessageHeader> PackRemoteMessage(
     flatbuffers::FlatBufferBuilder *fbb,
diff --git a/aos/events/logging/logfile_utils_out_of_space_test_runner.cc b/aos/events/logging/logfile_utils_out_of_space_test_runner.cc
index 03bfd0a..71c537f 100644
--- a/aos/events/logging/logfile_utils_out_of_space_test_runner.cc
+++ b/aos/events/logging/logfile_utils_out_of_space_test_runner.cc
@@ -18,8 +18,9 @@
   std::array<uint8_t, 10240> data;
   data.fill(0);
 
-  aos::logger::DetachedBufferFileWriter writer(
-      FLAGS_tmpfs + "/file",
+  aos::logger::FileBackend file_backend("/");
+  aos::logger::DetachedBufferWriter writer(
+      file_backend.RequestFile(FLAGS_tmpfs + "/file"),
       std::make_unique<aos::logger::DummyEncoder>(data.size()));
   for (int i = 0; i < 8; ++i) {
     aos::logger::DataEncoder::SpanCopier coppier(data);
diff --git a/aos/events/logging/logfile_utils_test.cc b/aos/events/logging/logfile_utils_test.cc
index e2dc8aa..8d5b0fb 100644
--- a/aos/events/logging/logfile_utils_test.cc
+++ b/aos/events/logging/logfile_utils_test.cc
@@ -30,13 +30,15 @@
 
 // Adapter class to make it easy to test DetachedBufferWriter without adding
 // test only boilerplate to DetachedBufferWriter.
-class TestDetachedBufferWriter : public DetachedBufferFileWriter {
+class TestDetachedBufferWriter : public FileBackend,
+                                 public DetachedBufferWriter {
  public:
   // Pick a max size that is rather conservative.
   static constexpr size_t kMaxMessageSize = 128 * 1024;
   TestDetachedBufferWriter(std::string_view filename)
-      : DetachedBufferFileWriter(
-            filename, std::make_unique<DummyEncoder>(kMaxMessageSize)) {}
+      : FileBackend("/"),
+        DetachedBufferWriter(FileBackend::RequestFile(filename),
+                             std::make_unique<DummyEncoder>(kMaxMessageSize)) {}
   void WriteSizedFlatbuffer(flatbuffers::DetachedBuffer &&buffer) {
     QueueSpan(absl::Span<const uint8_t>(buffer.data(), buffer.size()));
   }
diff --git a/aos/events/logging/multinode_logger_test.cc b/aos/events/logging/multinode_logger_test.cc
index 18337be..9e2d3a3 100644
--- a/aos/events/logging/multinode_logger_test.cc
+++ b/aos/events/logging/multinode_logger_test.cc
@@ -1,3 +1,5 @@
+#include <algorithm>
+
 #include "aos/events/logging/log_reader.h"
 #include "aos/events/logging/multinode_logger_test_lib.h"
 #include "aos/events/message_counter.h"
@@ -1814,8 +1816,16 @@
   logfile_base1_ = tmp_dir_ + "/new-good/multi_logfile1";
   logfile_base2_ = tmp_dir_ + "/new-good/multi_logfile2";
   logfiles_ = MakeLogFiles(logfile_base1_, logfile_base2_);
-  ASSERT_TRUE(pi1_logger.logger->RenameLogBase(logfile_base1_));
-  ASSERT_TRUE(pi2_logger.logger->RenameLogBase(logfile_base2_));
+
+  // Sequence of set_base_name and Rotate simulates rename operation. Since
+  // rename is not supported by all namers, RenameLogBase moved from logger to
+  // the higher level abstraction, yet log_namers support rename, and it is
+  // legal to test it here.
+  pi1_logger.log_namer->set_base_name(logfile_base1_);
+  pi1_logger.logger->Rotate();
+  pi2_logger.log_namer->set_base_name(logfile_base2_);
+  pi2_logger.logger->Rotate();
+
   for (auto &file : logfiles_) {
     struct stat s;
     EXPECT_EQ(0, stat(file.c_str(), &s));
@@ -1834,7 +1844,7 @@
   StartLogger(&pi1_logger);
   event_loop_factory_.RunFor(chrono::milliseconds(10000));
   logfile_base1_ = tmp_dir_ + "/new-renamefile/new_multi_logfile1";
-  EXPECT_DEATH({ pi1_logger.logger->RenameLogBase(logfile_base1_); },
+  EXPECT_DEATH({ pi1_logger.log_namer->set_base_name(logfile_base1_); },
                "Rename of file base from");
 }
 
@@ -3593,6 +3603,146 @@
   ConfirmReadable(filenames);
 }
 
+// Tests that we properly handle only one direction ever existing after a reboot
+// with mixed unreliable vs reliable, where reliable has an earlier timestamp
+// than unreliable.
+TEST(MissingDirectionTest, OneDirectionAfterRebootMixedCase1) {
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(ArtifactPath(
+          "aos/events/logging/multinode_pingpong_split4_mixed1_config.json"));
+  message_bridge::TestingTimeConverter time_converter(
+      configuration::NodesCount(&config.message()));
+  SimulatedEventLoopFactory event_loop_factory(&config.message());
+  event_loop_factory.SetTimeConverter(&time_converter);
+
+  NodeEventLoopFactory *const pi1 =
+      event_loop_factory.GetNodeEventLoopFactory("pi1");
+  const size_t pi1_index = configuration::GetNodeIndex(
+      event_loop_factory.configuration(), pi1->node());
+  NodeEventLoopFactory *const pi2 =
+      event_loop_factory.GetNodeEventLoopFactory("pi2");
+  const size_t pi2_index = configuration::GetNodeIndex(
+      event_loop_factory.configuration(), pi2->node());
+  std::vector<std::string> filenames;
+
+  {
+    CHECK_EQ(pi1_index, 0u);
+    CHECK_EQ(pi2_index, 1u);
+
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch(),
+        {BootTimestamp::epoch(), BootTimestamp::epoch()});
+
+    const chrono::nanoseconds reboot_time = chrono::milliseconds(5000);
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch() + reboot_time,
+        {BootTimestamp{.boot = 1, .time = monotonic_clock::epoch()},
+         BootTimestamp::epoch() + reboot_time});
+  }
+
+  const std::string kLogfile2_1 =
+      aos::testing::TestTmpDir() + "/multi_logfile2.1/";
+  util::UnlinkRecursive(kLogfile2_1);
+
+  // The following sequence using the above reference config creates
+  // a reliable message timestamp < unreliable message timestamp.
+  {
+    pi1->DisableStatistics();
+    pi2->DisableStatistics();
+
+    event_loop_factory.RunFor(chrono::milliseconds(95));
+
+    pi1->AlwaysStart<Ping>("ping");
+
+    event_loop_factory.RunFor(chrono::milliseconds(5250));
+
+    pi1->EnableStatistics();
+
+    event_loop_factory.RunFor(chrono::milliseconds(1000));
+
+    LoggerState pi2_logger = MakeLoggerState(
+        pi2, &event_loop_factory, SupportedCompressionAlgorithms()[0]);
+
+    pi2_logger.StartLogger(kLogfile2_1);
+
+    event_loop_factory.RunFor(chrono::milliseconds(5000));
+    pi2_logger.AppendAllFilenames(&filenames);
+  }
+
+  const std::vector<LogFile> sorted_parts = SortParts(filenames);
+  ConfirmReadable(filenames);
+}
+
+// Tests that we properly handle only one direction ever existing after a reboot
+// with mixed unreliable vs reliable, where unreliable has an earlier timestamp
+// than reliable.
+TEST(MissingDirectionTest, OneDirectionAfterRebootMixedCase2) {
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(ArtifactPath(
+          "aos/events/logging/multinode_pingpong_split4_mixed2_config.json"));
+  message_bridge::TestingTimeConverter time_converter(
+      configuration::NodesCount(&config.message()));
+  SimulatedEventLoopFactory event_loop_factory(&config.message());
+  event_loop_factory.SetTimeConverter(&time_converter);
+
+  NodeEventLoopFactory *const pi1 =
+      event_loop_factory.GetNodeEventLoopFactory("pi1");
+  const size_t pi1_index = configuration::GetNodeIndex(
+      event_loop_factory.configuration(), pi1->node());
+  NodeEventLoopFactory *const pi2 =
+      event_loop_factory.GetNodeEventLoopFactory("pi2");
+  const size_t pi2_index = configuration::GetNodeIndex(
+      event_loop_factory.configuration(), pi2->node());
+  std::vector<std::string> filenames;
+
+  {
+    CHECK_EQ(pi1_index, 0u);
+    CHECK_EQ(pi2_index, 1u);
+
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch(),
+        {BootTimestamp::epoch(), BootTimestamp::epoch()});
+
+    const chrono::nanoseconds reboot_time = chrono::milliseconds(5000);
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch() + reboot_time,
+        {BootTimestamp{.boot = 1, .time = monotonic_clock::epoch()},
+         BootTimestamp::epoch() + reboot_time});
+  }
+
+  const std::string kLogfile2_1 =
+      aos::testing::TestTmpDir() + "/multi_logfile2.1/";
+  util::UnlinkRecursive(kLogfile2_1);
+
+  // The following sequence using the above reference config creates
+  // an unreliable message timestamp < reliable message timestamp.
+  {
+    pi1->DisableStatistics();
+    pi2->DisableStatistics();
+
+    event_loop_factory.RunFor(chrono::milliseconds(95));
+
+    pi1->AlwaysStart<Ping>("ping");
+
+    event_loop_factory.RunFor(chrono::milliseconds(5250));
+
+    pi1->EnableStatistics();
+
+    event_loop_factory.RunFor(chrono::milliseconds(1000));
+
+    LoggerState pi2_logger = MakeLoggerState(
+        pi2, &event_loop_factory, SupportedCompressionAlgorithms()[0]);
+
+    pi2_logger.StartLogger(kLogfile2_1);
+
+    event_loop_factory.RunFor(chrono::milliseconds(5000));
+    pi2_logger.AppendAllFilenames(&filenames);
+  }
+
+  const std::vector<LogFile> sorted_parts = SortParts(filenames);
+  ConfirmReadable(filenames);
+}
+
 // Tests that we properly handle what used to be a time violation in one
 // direction.  This can occur when one direction goes down after sending some
 // data, but the other keeps working.  The down direction ends up resolving to a
@@ -3802,6 +3952,52 @@
   auto result = ConfirmReadable(filenames);
 }
 
+// Tests that RestartLogging works in the simple case.  Unfortunately, the
+// failure cases involve simulating time elapsing in callbacks, which is really
+// hard.  The best we can reasonably do is make sure 2 back to back logs are
+// parseable together.
+TEST_P(MultinodeLoggerTest, RestartLogging) {
+  time_converter_.AddMonotonic(
+      {BootTimestamp::epoch(), BootTimestamp::epoch() + chrono::seconds(1000)});
+  std::vector<std::string> filenames;
+  {
+    LoggerState pi1_logger = MakeLogger(pi1_);
+
+    event_loop_factory_.RunFor(chrono::milliseconds(95));
+
+    StartLogger(&pi1_logger, logfile_base1_);
+    aos::monotonic_clock::time_point last_rotation_time =
+        pi1_logger.event_loop->monotonic_now();
+    pi1_logger.logger->set_on_logged_period([&] {
+      const auto now = pi1_logger.event_loop->monotonic_now();
+      if (now > last_rotation_time + std::chrono::seconds(5)) {
+        pi1_logger.AppendAllFilenames(&filenames);
+        std::unique_ptr<MultiNodeFilesLogNamer> namer =
+            pi1_logger.MakeLogNamer(logfile_base2_);
+        pi1_logger.log_namer = namer.get();
+
+        pi1_logger.logger->RestartLogging(std::move(namer));
+        last_rotation_time = now;
+      }
+    });
+
+    event_loop_factory_.RunFor(chrono::milliseconds(7000));
+
+    pi1_logger.AppendAllFilenames(&filenames);
+  }
+
+  for (const auto &x : filenames) {
+    LOG(INFO) << x;
+  }
+
+  EXPECT_GE(filenames.size(), 2u);
+
+  ConfirmReadable(filenames);
+
+  // TODO(austin): It would be good to confirm that any one time messages end up
+  // in both logs correctly.
+}
+
 }  // namespace testing
 }  // namespace logger
 }  // namespace aos
diff --git a/aos/events/logging/multinode_logger_test_lib.cc b/aos/events/logging/multinode_logger_test_lib.cc
index 22822e7..fdee4d8 100644
--- a/aos/events/logging/multinode_logger_test_lib.cc
+++ b/aos/events/logging/multinode_logger_test_lib.cc
@@ -29,6 +29,16 @@
           params};
 }
 
+std::unique_ptr<MultiNodeFilesLogNamer> LoggerState::MakeLogNamer(
+    std::string logfile_base) {
+  std::unique_ptr<MultiNodeFilesLogNamer> namer =
+      std::make_unique<MultiNodeFilesLogNamer>(logfile_base, configuration,
+                                               event_loop.get(), node);
+  namer->set_extension(params.extension);
+  namer->set_encoder_factory(params.encoder_factory);
+  return namer;
+}
+
 void LoggerState::StartLogger(std::string logfile_base) {
   CHECK(!logfile_base.empty());
 
@@ -41,11 +51,7 @@
   logger->set_logger_version(
       absl::StrCat("logger_version_", event_loop->node()->name()->str()));
   event_loop->OnRun([this, logfile_base]() {
-    std::unique_ptr<MultiNodeFilesLogNamer> namer =
-        std::make_unique<MultiNodeFilesLogNamer>(logfile_base, configuration,
-                                            event_loop.get(), node);
-    namer->set_extension(params.extension);
-    namer->set_encoder_factory(params.encoder_factory);
+    std::unique_ptr<MultiNodeFilesLogNamer> namer = MakeLogNamer(logfile_base);
     log_namer = namer.get();
 
     logger->StartLogging(std::move(namer));
diff --git a/aos/events/logging/multinode_logger_test_lib.h b/aos/events/logging/multinode_logger_test_lib.h
index 40b5933..7502e4d 100644
--- a/aos/events/logging/multinode_logger_test_lib.h
+++ b/aos/events/logging/multinode_logger_test_lib.h
@@ -43,11 +43,14 @@
 struct LoggerState {
   void StartLogger(std::string logfile_base);
 
+  std::unique_ptr<MultiNodeFilesLogNamer> MakeLogNamer(
+      std::string logfile_base);
+
   std::unique_ptr<EventLoop> event_loop;
   std::unique_ptr<Logger> logger;
   const Configuration *configuration;
   const Node *node;
-  MultiNodeLogNamer *log_namer;
+  MultiNodeFilesLogNamer *log_namer;
   CompressionParams params;
 
   void AppendAllFilenames(std::vector<std::string> *filenames);
@@ -56,13 +59,13 @@
 };
 
 constexpr std::string_view kCombinedConfigSha1() {
-  return "c8cd3762e42a4e19b2155f63ccec97d1627a2fbd34d3da3ea6541128ca22b899";
+  return "e630fdd5533159ddad89075f93d9df90ae93a5a5841d6af7e1ec86875792bf27";
 }
 constexpr std::string_view kSplitConfigSha1() {
-  return "0ee6360b3e82a46f3f8b241661934abac53957d494a81ed1938899c220334954";
+  return "7ed547b800f84e5b56825d11d39d3686fb770c2021658c3a9031f2cbf94e82a4";
 }
 constexpr std::string_view kReloggedSplitConfigSha1() {
-  return "cc31e1a644dd7bf65d72247aea3e09b3474753e01921f3b6272f8233f288a16b";
+  return "7b17a3349852133aa56790fce93650b82455bad36ac669a4adebf33419c8ece9";
 }
 
 LoggerState MakeLoggerState(NodeEventLoopFactory *node,
diff --git a/aos/events/logging/multinode_pingpong_split4_mixed1.json b/aos/events/logging/multinode_pingpong_split4_mixed1.json
new file mode 100644
index 0000000..d376b00
--- /dev/null
+++ b/aos/events/logging/multinode_pingpong_split4_mixed1.json
@@ -0,0 +1,176 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    /* Logged on pi1 locally */
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi2"],
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi1"],
+      "source_node": "pi2",
+      "destination_nodes": [
+        {
+          "name": "pi1"
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1",
+      "frequency": 150
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/test/aos-examples-Pong",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi2",
+      "frequency": 150
+    },
+    /* Forwarded to pi2 */
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi2"],
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1
+        }
+      ],
+      "frequency": 150
+    },
+    /* Forwarded back to pi1.
+     * The message is logged both on the sending node and the receiving node
+     * (to make it easier to look at the results for now).
+     *
+     * The timestamps are logged on the receiving node.
+     */
+    {
+      "name": "/test",
+      "type": "aos.examples.Pong",
+      "source_node": "pi2",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi1"],
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1
+        }
+      ],
+      "frequency": 150
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1",
+      "hostname": "raspberrypi",
+      "port": 9971
+    },
+    {
+      "name": "pi2",
+      "hostname": "raspberrypi2",
+      "port": 9971
+    }
+  ]
+}
diff --git a/aos/events/logging/multinode_pingpong_split4_mixed2.json b/aos/events/logging/multinode_pingpong_split4_mixed2.json
new file mode 100644
index 0000000..57235e0
--- /dev/null
+++ b/aos/events/logging/multinode_pingpong_split4_mixed2.json
@@ -0,0 +1,176 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    /* Logged on pi1 locally */
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "logger": "LOCAL_LOGGER",
+      "source_node": "pi2"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi2"],
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2"
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi1"],
+      "source_node": "pi2",
+      "destination_nodes": [
+        {
+          "name": "pi1"
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi1",
+      "frequency": 150
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/test/aos-examples-Pong",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "num_senders": 2,
+      "source_node": "pi2",
+      "frequency": 150
+    },
+    /* Forwarded to pi2 */
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi2"],
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ],
+      "frequency": 150
+    },
+    /* Forwarded back to pi1.
+     * The message is logged both on the sending node and the receiving node
+     * (to make it easier to look at the results for now).
+     *
+     * The timestamps are logged on the receiving node.
+     */
+    {
+      "name": "/test",
+      "type": "aos.examples.Pong",
+      "source_node": "pi2",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi1"],
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1
+        }
+      ],
+      "frequency": 150
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1",
+      "hostname": "raspberrypi",
+      "port": 9971
+    },
+    {
+      "name": "pi2",
+      "hostname": "raspberrypi2",
+      "port": 9971
+    }
+  ]
+}
diff --git a/aos/events/simulated_event_loop.cc b/aos/events/simulated_event_loop.cc
index c679b21..c021a84 100644
--- a/aos/events/simulated_event_loop.cc
+++ b/aos/events/simulated_event_loop.cc
@@ -49,41 +49,6 @@
   const bool prior_;
 };
 
-// Holds storage for a span object and the data referenced by that span for
-// compatibility with RawSender::SharedSpan users. If constructed with
-// MakeSharedSpan, span points to only the aligned segment of the entire data.
-struct AlignedOwningSpan {
-  AlignedOwningSpan(const AlignedOwningSpan &) = delete;
-  AlignedOwningSpan &operator=(const AlignedOwningSpan &) = delete;
-  absl::Span<const uint8_t> span;
-  char data[];
-};
-
-// Constructs a span which owns its data through a shared_ptr. The owning span
-// points to a const view of the data; also returns a temporary mutable span
-// which is only valid while the const shared span is kept alive.
-std::pair<RawSender::SharedSpan, absl::Span<uint8_t>> MakeSharedSpan(
-    size_t size) {
-  AlignedOwningSpan *const span = reinterpret_cast<AlignedOwningSpan *>(
-      malloc(sizeof(AlignedOwningSpan) + size + kChannelDataAlignment - 1));
-
-  absl::Span<uint8_t> mutable_span(
-      reinterpret_cast<uint8_t *>(RoundChannelData(&span->data[0], size)),
-      size);
-  // Use the placement new operator to construct an actual absl::Span in place.
-  new (&span->span) absl::Span(mutable_span);
-
-  return std::make_pair(
-      RawSender::SharedSpan(
-          std::shared_ptr<AlignedOwningSpan>(span,
-                                             [](AlignedOwningSpan *s) {
-                                               s->~AlignedOwningSpan();
-                                               free(s);
-                                             }),
-          &span->span),
-      mutable_span);
-}
-
 // Container for both a message, and the context for it for simulation.  This
 // makes tracking the timestamps associated with the data easy.
 struct SimulatedMessage final {
@@ -93,8 +58,8 @@
 
   // Creates a SimulatedMessage with size bytes of storage.
   // This is a shared_ptr so we don't have to implement refcounting or copying.
-  static std::shared_ptr<SimulatedMessage> Make(
-      SimulatedChannel *channel, const RawSender::SharedSpan data);
+  static std::shared_ptr<SimulatedMessage> Make(SimulatedChannel *channel,
+                                                const SharedSpan data);
 
   // Context for the data.
   Context context;
@@ -103,7 +68,7 @@
 
   // Owning span to this message's data. Depending on the sender may either
   // represent the data of just the flatbuffer, or max channel size.
-  RawSender::SharedSpan data;
+  SharedSpan data;
 
   // Mutable view of above data. If empty, this message is not mutable.
   absl::Span<uint8_t> mutable_data;
@@ -336,7 +301,7 @@
 namespace {
 
 std::shared_ptr<SimulatedMessage> SimulatedMessage::Make(
-    SimulatedChannel *channel, RawSender::SharedSpan data) {
+    SimulatedChannel *channel, SharedSpan data) {
   // The allocations in here are due to infrastructure and don't count in the no
   // mallocs in RT code.
   ScopedNotRealtime nrt;
@@ -1165,8 +1130,7 @@
 }
 
 RawSender::Error SimulatedSender::DoSend(
-    const RawSender::SharedSpan data,
-    monotonic_clock::time_point monotonic_remote_time,
+    const SharedSpan data, monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid) {
   CHECK_LE(data->size(), this->size())
diff --git a/aos/ipc_lib/lockless_queue.cc b/aos/ipc_lib/lockless_queue.cc
index f380848..8e0e3d8 100644
--- a/aos/ipc_lib/lockless_queue.cc
+++ b/aos/ipc_lib/lockless_queue.cc
@@ -1063,11 +1063,12 @@
                    "Retrying.";
         continue;
       } else {
-        VLOG(3) << "Messages sent too fast. Returning. Attempted index: "
+        VLOG(1) << "Messages sent too fast. Returning. Attempted index: "
                 << decremented_queue_index.index()
                 << " message sent time: " << message->header.monotonic_sent_time
                 << "  message to replace sent time: "
                 << to_replace_monotonic_sent_time;
+
         // Since we are not using the message obtained from scratch_index
         // and we are not retrying, we need to invalidate its queue_index.
         message->header.queue_index.Invalidate();
diff --git a/aos/libc/aos_strsignal.cc b/aos/libc/aos_strsignal.cc
index 0f9b065..cf1aad1 100644
--- a/aos/libc/aos_strsignal.cc
+++ b/aos/libc/aos_strsignal.cc
@@ -15,9 +15,23 @@
     return buffer;
   }
 
+// sys_strsignal depricated in glibc2.32
+#ifdef __GLIBC__
+  #if __GLIBC_PREREQ(2, 32)
+  if (signal > 0 && signal < NSIG && sigdescr_np(signal) != nullptr) {
+    return sigdescr_np(signal);
+  }
+  #else
   if (signal > 0 && signal < NSIG && sys_siglist[signal] != nullptr) {
     return sys_siglist[signal];
   }
+  #endif
+// If not using GLIBC assume we can use sys_siglist
+#else
+  if (signal > 0 && signal < NSIG && sys_siglist[signal] != nullptr) {
+    return sys_siglist[signal];
+  }
+#endif
 
   CHECK_GT(snprintf(buffer, sizeof(buffer), "Unknown signal %d", signal), 0);
   return buffer;
diff --git a/aos/network/BUILD b/aos/network/BUILD
index 88df785..527fd46 100644
--- a/aos/network/BUILD
+++ b/aos/network/BUILD
@@ -274,6 +274,7 @@
         ":message_bridge_server_lib",
         "//aos:init",
         "//aos:json_to_flatbuffer",
+        "//aos:sha256",
         "//aos/events:shm_event_loop",
         "//aos/logging:dynamic_logging",
     ],
@@ -356,6 +357,7 @@
         ":message_bridge_client_lib",
         "//aos:init",
         "//aos:json_to_flatbuffer",
+        "//aos:sha256",
         "//aos/events:shm_event_loop",
         "//aos/logging:dynamic_logging",
     ],
@@ -422,6 +424,7 @@
         ":message_bridge_client_lib",
         ":message_bridge_server_lib",
         "//aos:json_to_flatbuffer",
+        "//aos:sha256",
         "//aos/events:ping_fbs",
         "//aos/events:pong_fbs",
         "//aos/events:shm_event_loop",
diff --git a/aos/network/connect.fbs b/aos/network/connect.fbs
index 7ff2fbe..5c7fb21 100644
--- a/aos/network/connect.fbs
+++ b/aos/network/connect.fbs
@@ -13,6 +13,9 @@
 
   // The UUID that this node booted with.
   boot_uuid: string (id: 2);
+
+  // Sha256 of the AOS config that this node is running with.
+  config_sha256: string (id: 3);
 }
 
 root_type Connect;
diff --git a/aos/network/message_bridge_client.cc b/aos/network/message_bridge_client.cc
index 8aad867..c3f55ba 100644
--- a/aos/network/message_bridge_client.cc
+++ b/aos/network/message_bridge_client.cc
@@ -2,6 +2,7 @@
 #include "aos/init.h"
 #include "aos/logging/dynamic_logging.h"
 #include "aos/network/message_bridge_client_lib.h"
+#include "aos/sha256.h"
 
 DEFINE_string(config, "aos_config.json", "Path to the config.");
 DEFINE_int32(rt_priority, -1, "If > 0, run as this RT priority");
@@ -18,7 +19,7 @@
     event_loop.SetRuntimeRealtimePriority(FLAGS_rt_priority);
   }
 
-  MessageBridgeClient app(&event_loop);
+  MessageBridgeClient app(&event_loop, Sha256(config.span()));
 
   logging::DynamicLogging dynamic_logging(&event_loop);
   // TODO(austin): Save messages into a vector to be logged.  One file per
diff --git a/aos/network/message_bridge_client_lib.cc b/aos/network/message_bridge_client_lib.cc
index b7ee3cc..0aefb5c 100644
--- a/aos/network/message_bridge_client_lib.cc
+++ b/aos/network/message_bridge_client_lib.cc
@@ -99,11 +99,11 @@
     aos::ShmEventLoop *const event_loop, std::string_view remote_name,
     const Node *my_node, std::string_view local_host,
     std::vector<SctpClientChannelState> *channels, int client_index,
-    MessageBridgeClientStatus *client_status)
+    MessageBridgeClientStatus *client_status, std::string_view config_sha256)
     : event_loop_(event_loop),
       connect_message_(MakeConnectMessage(event_loop->configuration(), my_node,
-                                          remote_name,
-                                          event_loop->boot_uuid())),
+                                          remote_name, event_loop->boot_uuid(),
+                                          config_sha256)),
       message_reception_reply_(MakeMessageHeaderReply()),
       remote_node_(CHECK_NOTNULL(
           configuration::GetNode(event_loop->configuration(), remote_name))),
@@ -128,22 +128,35 @@
   event_loop_->OnRun(
       [this]() { connect_timer_->Setup(event_loop_->monotonic_now()); });
 
-  int max_size = connect_message_.span().size();
+  size_t max_write_size =
+      std::max(kHeaderSizeOverhead(), connect_message_.span().size());
+  size_t max_read_size = 0u;
 
   for (const Channel *channel : *event_loop_->configuration()->channels()) {
     CHECK(channel->has_source_node());
 
     if (configuration::ChannelIsSendableOnNode(channel, remote_node_) &&
         configuration::ChannelIsReadableOnNode(channel, event_loop_->node())) {
-      LOG(INFO) << "Receiving channel "
-                << configuration::CleanedChannelToString(channel);
-      max_size = std::max(channel->max_size(), max_size);
+      VLOG(1) << "Receiving channel "
+              << configuration::CleanedChannelToString(channel);
+      max_read_size = std::max(
+          static_cast<size_t>(channel->max_size() + kHeaderSizeOverhead()),
+          max_read_size);
     }
   }
 
   // Buffer up the max size a bit so everything fits nicely.
-  LOG(INFO) << "Max message size for all servers is " << max_size;
-  client_.SetMaxSize(max_size + 100);
+  LOG(INFO) << "Max read message size for all servers is " << max_read_size;
+  LOG(INFO) << "Max write message size for all servers is " << max_write_size;
+  // RemoteMessage header appears to be between 100 and 204 bytes of overhead
+  // from the vector of data.  No need to get super tight to that bound.
+  client_.SetMaxReadSize(max_read_size);
+  client_.SetMaxWriteSize(max_write_size);
+
+  // 1 client talks to 1 server.  With interleaving support 1 turned on, we'll
+  // at most see 1 partial message, and 1 incoming part, for a total of 2
+  // messages in flight.
+  client_.SetPoolSize(2u);
 
   event_loop_->epoll()->OnReadable(client_.fd(),
                                    [this]() { MessageReceived(); });
@@ -193,6 +206,7 @@
   } else if (message->message_type == Message::kMessage) {
     HandleData(message.get());
   }
+  client_.FreeMessage(std::move(message));
 }
 
 void SctpClientConnection::SendConnect() {
@@ -262,7 +276,7 @@
       monotonic_clock::time_point(
           chrono::nanoseconds(remote_data->monotonic_sent_time())) ==
           channel_state->last_timestamp) {
-    LOG(INFO) << "Duplicate message from " << message->PeerAddress();
+    VLOG(1) << "Duplicate message from " << message->PeerAddress();
     connection_->mutate_duplicate_packets(connection_->duplicate_packets() + 1);
     // Duplicate message, ignore.
   } else {
@@ -342,8 +356,11 @@
           << " cumtsn=" << message->header.rcvinfo.rcv_cumtsn << ")";
 }
 
-MessageBridgeClient::MessageBridgeClient(aos::ShmEventLoop *event_loop)
-    : event_loop_(event_loop), client_status_(event_loop_) {
+MessageBridgeClient::MessageBridgeClient(aos::ShmEventLoop *event_loop,
+                                         std::string config_sha256)
+    : event_loop_(event_loop),
+      client_status_(event_loop_),
+      config_sha256_(std::move(config_sha256)) {
   std::string_view node_name = event_loop->node()->name()->string_view();
 
   // Find all the channels which are supposed to be delivered to us.
@@ -390,7 +407,8 @@
     // Open an unspecified connection (:: in ipv6 terminology)
     connections_.emplace_back(new SctpClientConnection(
         event_loop, source_node, event_loop->node(), "", &channels_,
-        client_status_.FindClientIndex(source_node), &client_status_));
+        client_status_.FindClientIndex(source_node), &client_status_,
+        config_sha256_));
   }
 }
 
diff --git a/aos/network/message_bridge_client_lib.h b/aos/network/message_bridge_client_lib.h
index 23c982d..6e108df 100644
--- a/aos/network/message_bridge_client_lib.h
+++ b/aos/network/message_bridge_client_lib.h
@@ -37,7 +37,8 @@
                        std::string_view local_host,
                        std::vector<SctpClientChannelState> *channels,
                        int client_index,
-                       MessageBridgeClientStatus *client_status);
+                       MessageBridgeClientStatus *client_status,
+                       std::string_view config_sha256);
 
   ~SctpClientConnection() { event_loop_->epoll()->DeleteFd(client_.fd()); }
 
@@ -101,7 +102,7 @@
 // node.
 class MessageBridgeClient {
  public:
-  MessageBridgeClient(aos::ShmEventLoop *event_loop);
+  MessageBridgeClient(aos::ShmEventLoop *event_loop, std::string config_sha256);
 
   ~MessageBridgeClient() {}
 
@@ -116,6 +117,8 @@
 
   // List of connections.  These correspond to the nodes in source_node_names_
   std::vector<std::unique_ptr<SctpClientConnection>> connections_;
+
+  std::string config_sha256_;
 };
 
 }  // namespace message_bridge
diff --git a/aos/network/message_bridge_protocol.cc b/aos/network/message_bridge_protocol.cc
index 87114ed..38e16b2 100644
--- a/aos/network/message_bridge_protocol.cc
+++ b/aos/network/message_bridge_protocol.cc
@@ -13,7 +13,8 @@
 
 aos::FlatbufferDetachedBuffer<aos::message_bridge::Connect> MakeConnectMessage(
     const Configuration *config, const Node *my_node,
-    std::string_view remote_name, const UUID &boot_uuid) {
+    std::string_view remote_name, const UUID &boot_uuid,
+    std::string_view config_sha256) {
   CHECK(config->has_nodes()) << ": Config must have nodes to transfer.";
 
   flatbuffers::FlatBufferBuilder fbb;
@@ -33,11 +34,16 @@
         if (connection->name()->string_view() == node_name &&
             channel->source_node()->string_view() == remote_name) {
           // Remove the schema to save some space on the wire.
-          aos::FlatbufferDetachedBuffer<Channel> cleaned_channel =
-              RecursiveCopyFlatBuffer<Channel>(channel);
-          cleaned_channel.mutable_message()->clear_schema();
-          channel_offsets.emplace_back(
-              CopyFlatBuffer<Channel>(&cleaned_channel.message(), &fbb));
+          flatbuffers::Offset<flatbuffers::String> name_offset =
+              fbb.CreateSharedString(channel->name()->string_view());
+          flatbuffers::Offset<flatbuffers::String> type_offset =
+              fbb.CreateSharedString(channel->type()->string_view());
+
+          // We only really care about name, type, and max size.
+          Channel::Builder channel_builder(fbb);
+          channel_builder.add_name(name_offset);
+          channel_builder.add_type(type_offset);
+          channel_offsets.emplace_back(channel_builder.Finish());
         }
       }
     }
@@ -46,10 +52,14 @@
   flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Channel>>>
       channels_offset = fbb.CreateVector(channel_offsets);
 
+  flatbuffers::Offset<flatbuffers::String> config_sha256_offset =
+      fbb.CreateString(config_sha256);
+
   Connect::Builder connect_builder(fbb);
   connect_builder.add_channels_to_transfer(channels_offset);
   connect_builder.add_node(node_offset);
   connect_builder.add_boot_uuid(boot_uuid_offset);
+  connect_builder.add_config_sha256(config_sha256_offset);
   fbb.Finish(connect_builder.Finish());
 
   return fbb.Release();
diff --git a/aos/network/message_bridge_protocol.h b/aos/network/message_bridge_protocol.h
index d0a28a1..23a6e0f 100644
--- a/aos/network/message_bridge_protocol.h
+++ b/aos/network/message_bridge_protocol.h
@@ -31,10 +31,15 @@
 // The stream on which timestamp replies are sent.
 constexpr size_t kTimestampStream() { return 1; }
 
+// Overhead constant for headers.  Both remote timestamps and the extra context
+// inside RemoteData need to fit inside this.
+constexpr size_t kHeaderSizeOverhead() { return 208u; }
+
 // Builds up a subscription request for my_node to remote_name.
 aos::FlatbufferDetachedBuffer<aos::message_bridge::Connect> MakeConnectMessage(
     const Configuration *config, const Node *my_node,
-    std::string_view remote_name, const UUID &boot_uuid);
+    std::string_view remote_name, const UUID &boot_uuid,
+    std::string_view config_sha256);
 
 }  // namespace message_bridge
 }  // namespace aos
diff --git a/aos/network/message_bridge_server.cc b/aos/network/message_bridge_server.cc
index 3b5d30b..ec3cdc4 100644
--- a/aos/network/message_bridge_server.cc
+++ b/aos/network/message_bridge_server.cc
@@ -2,6 +2,7 @@
 #include "aos/init.h"
 #include "aos/logging/dynamic_logging.h"
 #include "aos/network/message_bridge_server_lib.h"
+#include "aos/sha256.h"
 #include "gflags/gflags.h"
 #include "glog/logging.h"
 
@@ -20,7 +21,7 @@
     event_loop.SetRuntimeRealtimePriority(FLAGS_rt_priority);
   }
 
-  MessageBridgeServer app(&event_loop);
+  MessageBridgeServer app(&event_loop, Sha256(config.span()));
 
   logging::DynamicLogging dynamic_logging(&event_loop);
 
diff --git a/aos/network/message_bridge_server.fbs b/aos/network/message_bridge_server.fbs
index 031f801..30017c4 100644
--- a/aos/network/message_bridge_server.fbs
+++ b/aos/network/message_bridge_server.fbs
@@ -41,6 +41,10 @@
 
   // Number of times we've established a connection to the server.
   connection_count:uint (id: 8);
+
+  // Number of times we've had an invalid connection with something wrong in
+  // the connection message, but we were able to match which node it was.
+  invalid_connection_count:uint (id: 9);
 }
 
 // Statistics for all connections to all the clients.
@@ -49,6 +53,11 @@
 
   // Count of timestamp send failures
   timestamp_send_failures:uint64 (id: 1);
+
+  // Number of times we've had an invalid connection with something wrong in
+  // the connection message.  The most likely cause is that the config sha256
+  // doesn't match between nodes.
+  invalid_connection_count:uint (id: 2);
 }
 
 root_type ServerStatistics;
diff --git a/aos/network/message_bridge_server_lib.cc b/aos/network/message_bridge_server_lib.cc
index b0973ac..df6d4b1 100644
--- a/aos/network/message_bridge_server_lib.cc
+++ b/aos/network/message_bridge_server_lib.cc
@@ -13,23 +13,23 @@
 #include "aos/network/sctp_server.h"
 #include "aos/network/timestamp_channel.h"
 #include "glog/logging.h"
+#include "glog/raw_logging.h"
 
 namespace aos {
 namespace message_bridge {
 namespace chrono = std::chrono;
 
 bool ChannelState::Matches(const Channel *other_channel) {
-  // Confirm the normal tuple, plus make sure that the other side isn't going to
-  // send more data over than we expect with a mismatching size.
-  return (
-      channel_->name()->string_view() == other_channel->name()->string_view() &&
-      channel_->type()->string_view() == other_channel->type()->string_view() &&
-      channel_->max_size() == other_channel->max_size());
+  return channel_->name()->string_view() ==
+             other_channel->name()->string_view() &&
+         channel_->type()->string_view() ==
+             other_channel->type()->string_view();
 }
 
 flatbuffers::FlatBufferBuilder ChannelState::PackContext(
-    const Context &context) {
-  flatbuffers::FlatBufferBuilder fbb(channel_->max_size() + 100);
+    FixedAllocator *allocator, const Context &context) {
+  flatbuffers::FlatBufferBuilder fbb(
+      channel_->max_size() + kHeaderSizeOverhead(), allocator);
   fbb.ForceDefaults(true);
   VLOG(2) << "Found " << peers_.size() << " peers on channel "
           << channel_->name()->string_view() << " "
@@ -59,10 +59,11 @@
   return fbb;
 }
 
-void ChannelState::SendData(SctpServer *server, const Context &context) {
+void ChannelState::SendData(SctpServer *server, FixedAllocator *allocator,
+                            const Context &context) {
   // TODO(austin): I don't like allocating this buffer when we are just freeing
   // it at the end of the function.
-  flatbuffers::FlatBufferBuilder fbb = PackContext(context);
+  flatbuffers::FlatBufferBuilder fbb = PackContext(allocator, context);
 
   // TODO(austin): Track which connections need to be reliable and handle
   // resending properly.
@@ -194,6 +195,7 @@
 
 int ChannelState::NodeConnected(const Node *node, sctp_assoc_t assoc_id,
                                 int stream, SctpServer *server,
+                                FixedAllocator *allocator,
                                 aos::monotonic_clock::time_point monotonic_now,
                                 std::vector<sctp_assoc_t> *reconnected) {
   VLOG(1) << "Channel " << channel_->name()->string_view() << " "
@@ -211,14 +213,18 @@
                      peer.sac_assoc_id) == reconnected->end())) {
         reconnected->push_back(peer.sac_assoc_id);
         if (peer.sac_assoc_id == assoc_id) {
-          LOG_EVERY_T(WARNING, 0.025)
-              << "Node " << node->name()->string_view() << " reconnecting on "
-              << assoc_id << " with the same ID, something got lost";
+          if (VLOG_IS_ON(1)) {
+            LOG_EVERY_T(WARNING, 0.025)
+                << "Node " << node->name()->string_view() << " reconnecting on "
+                << assoc_id << " with the same ID, something got lost";
+          }
         } else {
-          LOG_EVERY_T(WARNING, 0.025)
-              << "Node " << node->name()->string_view() << " "
-              << " already connected on " << peer.sac_assoc_id
-              << " aborting old connection and switching to " << assoc_id;
+          if (VLOG_IS_ON(1)) {
+            LOG_EVERY_T(WARNING, 0.025)
+                << "Node " << node->name()->string_view() << " "
+                << " already connected on " << peer.sac_assoc_id
+                << " aborting old connection and switching to " << assoc_id;
+          }
           server->Abort(peer.sac_assoc_id);
         }
       }
@@ -237,10 +243,8 @@
                 << (last_message_fetcher_->context().data != nullptr);
         if (last_message_fetcher_->context().data != nullptr) {
           // SendData sends to all...  Only send to the new one.
-          // TODO(austin): I don't like allocating this buffer when we are just
-          // freeing it at the end of the function.
           flatbuffers::FlatBufferBuilder fbb =
-              PackContext(last_message_fetcher_->context());
+              PackContext(allocator, last_message_fetcher_->context());
 
           if (server->Send(std::string_view(reinterpret_cast<const char *>(
                                                 fbb.GetBufferPointer()),
@@ -261,18 +265,26 @@
   return -1;
 }
 
-MessageBridgeServer::MessageBridgeServer(aos::ShmEventLoop *event_loop)
+MessageBridgeServer::MessageBridgeServer(aos::ShmEventLoop *event_loop,
+                                         std::string config_sha256)
     : event_loop_(event_loop),
       timestamp_loggers_(event_loop_),
       server_(max_channels() + kControlStreams(), "",
               event_loop->node()->port()),
-      server_status_(event_loop, [this](const Context &context) {
-        timestamp_state_->SendData(&server_, context);
-      }) {
+      server_status_(event_loop,
+                     [this](const Context &context) {
+                       timestamp_state_->SendData(&server_, &allocator_,
+                                                  context);
+                     }),
+      config_sha256_(std::move(config_sha256)),
+      allocator_(0) {
+  CHECK_EQ(config_sha256_.size(), 64u) << ": Wrong length sha256sum";
   CHECK(event_loop_->node() != nullptr) << ": No nodes configured.";
 
-  size_t max_size = 0;
+  // Start out with a decent size big enough to hold timestamps.
+  size_t max_size = 204;
 
+  size_t destination_nodes = 0u;
   // Seed up all the per-node connection state.
   // We are making the assumption here that every connection is bidirectional
   // (data is being sent both ways).  This is pretty safe because we are
@@ -280,6 +292,7 @@
   for (std::string_view destination_node_name :
        configuration::DestinationNodeNames(event_loop->configuration(),
                                            event_loop->node())) {
+    ++destination_nodes;
     // Find the largest connection message so we can size our buffers big enough
     // to receive a connection message.  The connect message comes from the
     // client to the server, so swap the node arguments.
@@ -288,7 +301,7 @@
                            configuration::GetNode(event_loop->configuration(),
                                                   destination_node_name),
                            event_loop->node()->name()->string_view(),
-                           UUID::Zero())
+                           UUID::Zero(), config_sha256_)
             .span()
             .size();
     VLOG(1) << "Connection to " << destination_node_name << " has size "
@@ -303,6 +316,7 @@
   LOG(INFO) << "Hostname: " << event_loop_->node()->hostname()->string_view();
 
   int channel_index = 0;
+  size_t max_channel_size = 0u;
   const Channel *const timestamp_channel = configuration::GetChannel(
       event_loop_->configuration(), "/aos", Timestamp::GetFullyQualifiedName(),
       event_loop_->name(), event_loop_->node());
@@ -324,10 +338,10 @@
           any_reliable = true;
         }
       }
-      max_size =
-          std::max(static_cast<size_t>(channel->max_size() *
-                                       channel->destination_nodes()->size()),
-                   max_size);
+
+      max_channel_size =
+          std::max(static_cast<size_t>(channel->max_size()), max_channel_size);
+
       std::unique_ptr<ChannelState> state(new ChannelState{
           channel, channel_index,
           any_reliable ? event_loop_->MakeRawFetcher(channel) : nullptr});
@@ -361,7 +375,7 @@
         event_loop_->MakeRawWatcher(
             channel, [this, state_ptr](const Context &context,
                                        const void * /*message*/) {
-              state_ptr->SendData(&server_, context);
+              state_ptr->SendData(&server_, &allocator_, context);
             });
       } else {
         for (const Connection *connection : *channel->destination_nodes()) {
@@ -387,8 +401,24 @@
   CHECK(timestamp_state_ != nullptr);
 
   // Buffer up the max size a bit so everything fits nicely.
-  LOG(INFO) << "Max message size for all clients is " << max_size;
-  server_.SetMaxSize(max_size + 100u);
+  LOG(INFO) << "Max message read size for all clients is " << max_size;
+  LOG(INFO) << "Max message write size for all clients is "
+            << max_channel_size + kHeaderSizeOverhead();
+  server_.SetMaxReadSize(max_size);
+  server_.SetMaxWriteSize(max_channel_size + kHeaderSizeOverhead());
+
+  // Since we are doing interleaving mode 1, we will see at most 1 message being
+  // delivered at a time for an association.  That means, if a message is
+  // started to be delivered, all the following parts will be from the same
+  // message in the same stream.  The server can have at most 1 association per
+  // client active, and can then (reasonably) have 1 new client connecting
+  // trying to talk.  And 2 messages per association (one partially filled one,
+  // and 1 new one with more of the data).
+  server_.SetPoolSize((destination_nodes + 1) * 2);
+
+  allocator_ = FixedAllocator(max_channel_size + kHeaderSizeOverhead());
+
+  reconnected_.reserve(max_channels());
 }
 
 void MessageBridgeServer::NodeConnected(sctp_assoc_t assoc_id) {
@@ -465,6 +495,35 @@
   } else if (message->message_type == Message::kMessage) {
     HandleData(message.get());
   }
+  server_.FreeMessage(std::move(message));
+}
+
+void MessageBridgeServer::MaybeIncrementInvalidConnectionCount(
+    const Node *node) {
+  server_status_.increment_invalid_connection_count();
+
+  if (node == nullptr) {
+    return;
+  }
+
+  if (!node->has_name()) {
+    return;
+  }
+
+  const aos::Node *client_node = configuration::GetNode(
+      event_loop_->configuration(), node->name()->string_view());
+
+  if (client_node == nullptr) {
+    return;
+  }
+
+  const int node_index =
+      configuration::GetNodeIndex(event_loop_->configuration(), client_node);
+
+  ServerConnection *connection = server_status_.server_connection()[node_index];
+
+  connection->mutate_invalid_connection_count(
+      connection->invalid_connection_count() + 1);
 }
 
 void MessageBridgeServer::HandleData(const Message *message) {
@@ -475,13 +534,54 @@
     const Connect *connect = flatbuffers::GetRoot<Connect>(message->data());
     {
       flatbuffers::Verifier verifier(message->data(), message->size);
-      CHECK(connect->Verify(verifier));
+      if (!connect->Verify(verifier)) {
+        if (VLOG_IS_ON(1)) {
+          LOG_EVERY_T(WARNING, 1.0)
+              << "Failed to verify message, disconnecting client";
+        }
+        server_.Abort(message->header.rcvinfo.rcv_assoc_id);
+
+        MaybeIncrementInvalidConnectionCount(nullptr);
+        return;
+      }
     }
     VLOG(1) << FlatbufferToJson(connect);
 
-    CHECK_LE(connect->channels_to_transfer()->size(),
-             static_cast<size_t>(max_channels()))
-        << ": Client has more channels than we do";
+    if (!connect->has_config_sha256()) {
+      if (VLOG_IS_ON(1)) {
+        LOG(WARNING) << "Client missing config_sha256, disconnecting client";
+      }
+      server_.Abort(message->header.rcvinfo.rcv_assoc_id);
+
+      MaybeIncrementInvalidConnectionCount(connect->node());
+      return;
+    }
+
+    if (connect->config_sha256()->string_view() != config_sha256_) {
+      if (VLOG_IS_ON(1)) {
+        LOG(WARNING) << "Client config sha256 of "
+                     << connect->config_sha256()->string_view()
+                     << " doesn't match our config sha256 of " << config_sha256_
+                     << ", disconnecting client";
+      }
+      server_.Abort(message->header.rcvinfo.rcv_assoc_id);
+
+      MaybeIncrementInvalidConnectionCount(connect->node());
+      return;
+    }
+
+    if (connect->channels_to_transfer()->size() >
+        static_cast<size_t>(max_channels())) {
+      if (VLOG_IS_ON(1)) {
+        LOG(WARNING)
+            << "Client has more channels than we do, disconnecting client";
+      }
+      server_.Abort(message->header.rcvinfo.rcv_assoc_id);
+
+      MaybeIncrementInvalidConnectionCount(connect->node());
+      return;
+    }
+
     monotonic_clock::time_point monotonic_now = event_loop_->monotonic_now();
 
     // Account for the control channel and delivery times channel.
@@ -493,8 +593,7 @@
     // number of messages is overwhelming right now at first boot. This also
     // should mean that we only send a single abort per association change,
     // which is more correct behavior.
-    std::vector<sctp_assoc_t> reconnected;
-    reconnected.reserve(connect->channels_to_transfer()->size());
+    reconnected_.clear();
     for (const Channel *channel : *connect->channels_to_transfer()) {
       bool matched = false;
       for (std::unique_ptr<ChannelState> &channel_state : channels_) {
@@ -504,15 +603,25 @@
         if (channel_state->Matches(channel)) {
           node_index = channel_state->NodeConnected(
               connect->node(), message->header.rcvinfo.rcv_assoc_id,
-              channel_index, &server_, monotonic_now, &reconnected);
-          CHECK_NE(node_index, -1);
+              channel_index, &server_, &allocator_, monotonic_now,
+              &reconnected_);
+          CHECK_NE(node_index, -1)
+              << ": Failed to find node "
+              << aos::FlatbufferToJson(connect->node()) << " for connection "
+              << aos::FlatbufferToJson(connect);
           matched = true;
           break;
         }
       }
       if (!matched) {
-        LOG(ERROR) << "Remote tried registering for unknown channel "
-                   << FlatbufferToJson(channel);
+        if (VLOG_IS_ON(1)) {
+          LOG(ERROR) << "Remote tried registering for unknown channel "
+                     << FlatbufferToJson(channel);
+        }
+        server_.Abort(message->header.rcvinfo.rcv_assoc_id);
+
+        MaybeIncrementInvalidConnectionCount(connect->node());
+        return;
       }
       ++channel_index;
     }
@@ -551,11 +660,13 @@
       message->LogRcvInfo();
     }
   } else {
-    message->LogRcvInfo();
-    // TODO(sarah.newman): add some versioning concept such that if this was a
-    // fatal error, we would never get here.
-    LOG_FIRST_N(ERROR, 20) << "Unexpected stream id "
-                           << message->header.rcvinfo.rcv_sid;
+    // We should never see the client sending us something on the wrong stream.
+    // Just explode...  In theory, this could let a client DOS us, but we trust
+    // the client.
+    if (VLOG_IS_ON(2)) {
+      message->LogRcvInfo();
+    }
+    LOG(FATAL) << "Unexpected stream id " << message->header.rcvinfo.rcv_sid;
   }
 }
 
diff --git a/aos/network/message_bridge_server_lib.h b/aos/network/message_bridge_server_lib.h
index 6f2be08..98e1dd0 100644
--- a/aos/network/message_bridge_server_lib.h
+++ b/aos/network/message_bridge_server_lib.h
@@ -68,7 +68,7 @@
   // This will potentially grow to the number of associations as we find reconnects.
   int NodeDisconnected(sctp_assoc_t assoc_id);
   int NodeConnected(const Node *node, sctp_assoc_t assoc_id, int stream,
-                    SctpServer *server,
+                    SctpServer *server, FixedAllocator *allocator,
                     aos::monotonic_clock::time_point monotonic_now,
                     std::vector<sctp_assoc_t> *reconnected);
 
@@ -83,10 +83,12 @@
   bool Matches(const Channel *other_channel);
 
   // Sends the data in context using the provided server.
-  void SendData(SctpServer *server, const Context &context);
+  void SendData(SctpServer *server, FixedAllocator *allocator,
+                const Context &context);
 
   // Packs a context into a size prefixed message header for transmission.
-  flatbuffers::FlatBufferBuilder PackContext(const Context &context);
+  flatbuffers::FlatBufferBuilder PackContext(FixedAllocator *allocator,
+                                             const Context &context);
 
   // Handles reception of delivery times.
   void HandleDelivery(sctp_assoc_t rcv_assoc_id, uint16_t ssn,
@@ -109,7 +111,7 @@
 // node.  It handles the session and dispatches data to the ChannelState.
 class MessageBridgeServer {
  public:
-  MessageBridgeServer(aos::ShmEventLoop *event_loop);
+  MessageBridgeServer(aos::ShmEventLoop *event_loop, std::string config_sha256);
 
   ~MessageBridgeServer() { event_loop_->epoll()->DeleteFd(server_.fd()); }
 
@@ -126,6 +128,10 @@
   // received.
   void HandleData(const Message *message);
 
+  // Increments the invalid connection count overall, and per node if we know
+  // which node (ie, node is not nullptr).
+  void MaybeIncrementInvalidConnectionCount(const Node *node);
+
   // The maximum number of channels we support on a single connection. We need
   // to configure the SCTP socket with this before any clients connect, so we
   // need an upper bound on the number of channels any of them will use.
@@ -147,6 +153,14 @@
   // List of channels.  The entries that aren't sent from this node are left
   // null.
   std::vector<std::unique_ptr<ChannelState>> channels_;
+
+  const std::string config_sha256_;
+
+  // List of assoc_id's that have been found already when connecting.  This is a
+  // member variable so the memory is allocated in the constructor.
+  std::vector<sctp_assoc_t> reconnected_;
+
+  FixedAllocator allocator_;
 };
 
 }  // namespace message_bridge
diff --git a/aos/network/message_bridge_server_status.cc b/aos/network/message_bridge_server_status.cc
index 4f6abff..df87134 100644
--- a/aos/network/message_bridge_server_status.cc
+++ b/aos/network/message_bridge_server_status.cc
@@ -40,6 +40,7 @@
     connection_builder.add_connected_since_time(
         monotonic_clock::min_time.time_since_epoch().count());
     connection_builder.add_connection_count(0);
+    connection_builder.add_invalid_connection_count(0);
     connection_offsets.emplace_back(connection_builder.Finish());
   }
   flatbuffers::Offset<
@@ -87,6 +88,7 @@
       send_data_(send_data) {
   server_connection_offsets_.reserve(
       statistics_.message().connections()->size());
+  client_offsets_.reserve(statistics_.message().connections()->size());
 
   filters_.resize(event_loop->configuration()->nodes()->size());
   partial_deliveries_.resize(event_loop->configuration()->nodes()->size());
@@ -231,6 +233,11 @@
           connection->connection_count());
     }
 
+    if (connection->invalid_connection_count() != 0) {
+      server_connection_builder.add_invalid_connection_count(
+          connection->invalid_connection_count());
+    }
+
     // TODO(austin): If it gets stale, drop it too.
     if (!filters_[node_index].MissingSamples()) {
       server_connection_builder.add_monotonic_offset(
@@ -254,6 +261,8 @@
   server_statistics_builder.add_connections(server_connections_offset);
   server_statistics_builder.add_timestamp_send_failures(
       timestamp_failure_counter_.failures());
+  server_statistics_builder.add_invalid_connection_count(
+      invalid_connection_count_);
 
   builder.CheckOk(builder.Send(server_statistics_builder.Finish()));
 }
@@ -282,7 +291,7 @@
 
   if (client_statistics_fetcher_.get()) {
     // Build up the list of client offsets.
-    std::vector<flatbuffers::Offset<ClientOffset>> client_offsets;
+    client_offsets_.clear();
 
     // Iterate through the connections this node has made.
     for (const ClientConnection *connection :
@@ -369,10 +378,10 @@
       client_offset_builder.add_node(node_offset);
       client_offset_builder.add_monotonic_offset(
           connection->monotonic_offset());
-      client_offsets.emplace_back(client_offset_builder.Finish());
+      client_offsets_.emplace_back(client_offset_builder.Finish());
     }
     flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<ClientOffset>>>
-        offsets_offset = fbb->CreateVector(client_offsets);
+        offsets_offset = fbb->CreateVector(client_offsets_);
 
     Timestamp::Builder builder(*fbb);
     builder.add_offsets(offsets_offset);
diff --git a/aos/network/message_bridge_server_status.h b/aos/network/message_bridge_server_status.h
index 327930c..624b376 100644
--- a/aos/network/message_bridge_server_status.h
+++ b/aos/network/message_bridge_server_status.h
@@ -68,7 +68,7 @@
   ServerConnection *FindServerConnection(std::string_view node_name);
   ServerConnection *FindServerConnection(const Node *node);
 
-  std::vector<ServerConnection *> server_connection() {
+  const std::vector<ServerConnection *> &server_connection() {
     return server_connection_;
   }
 
@@ -77,6 +77,10 @@
   // Enables sending out any statistics messages.
   void EnableStatistics();
 
+  // Increments invalid_connection_count_, marking that we had another bad
+  // connection that got rejected.
+  void increment_invalid_connection_count() { ++invalid_connection_count_; }
+
  private:
   static constexpr std::chrono::nanoseconds kStatisticsPeriod =
       std::chrono::seconds(1);
@@ -126,6 +130,10 @@
   bool send_ = true;
 
   std::vector<uint32_t> partial_deliveries_;
+
+  size_t invalid_connection_count_ = 0u;
+
+  std::vector<flatbuffers::Offset<ClientOffset>> client_offsets_;
 };
 
 }  // namespace message_bridge
diff --git a/aos/network/message_bridge_test.cc b/aos/network/message_bridge_test.cc
index 226daf7..681001e 100644
--- a/aos/network/message_bridge_test.cc
+++ b/aos/network/message_bridge_test.cc
@@ -8,6 +8,7 @@
 #include "aos/network/message_bridge_client_lib.h"
 #include "aos/network/message_bridge_server_lib.h"
 #include "aos/network/team_number.h"
+#include "aos/sha256.h"
 #include "aos/testing/path.h"
 #include "aos/util/file.h"
 #include "gtest/gtest.h"
@@ -53,6 +54,7 @@
   MessageBridgeParameterizedTest()
       : config(aos::configuration::ReadConfig(
             ArtifactPath(absl::StrCat("aos/network/", GetParam().config)))),
+        config_sha256(Sha256(config.span())),
         pi1_boot_uuid_(UUID::Random()),
         pi2_boot_uuid_(UUID::Random()) {
     util::UnlinkRecursive(ShmBase("pi1"));
@@ -73,14 +75,16 @@
     FLAGS_boot_uuid = pi2_boot_uuid_.ToString();
   }
 
-  void MakePi1Server() {
+  void MakePi1Server(std::string server_config_sha256 = "") {
     OnPi1();
     FLAGS_application_name = "pi1_message_bridge_server";
     pi1_server_event_loop =
         std::make_unique<aos::ShmEventLoop>(&config.message());
     pi1_server_event_loop->SetRuntimeRealtimePriority(1);
-    pi1_message_bridge_server =
-        std::make_unique<MessageBridgeServer>(pi1_server_event_loop.get());
+    pi1_message_bridge_server = std::make_unique<MessageBridgeServer>(
+        pi1_server_event_loop.get(), server_config_sha256.size() == 0
+                                         ? config_sha256
+                                         : server_config_sha256);
   }
 
   void RunPi1Server(chrono::nanoseconds duration) {
@@ -118,8 +122,8 @@
     pi1_client_event_loop =
         std::make_unique<aos::ShmEventLoop>(&config.message());
     pi1_client_event_loop->SetRuntimeRealtimePriority(1);
-    pi1_message_bridge_client =
-        std::make_unique<MessageBridgeClient>(pi1_client_event_loop.get());
+    pi1_message_bridge_client = std::make_unique<MessageBridgeClient>(
+        pi1_client_event_loop.get(), config_sha256);
   }
 
   void StartPi1Client() {
@@ -183,8 +187,8 @@
     pi2_server_event_loop =
         std::make_unique<aos::ShmEventLoop>(&config.message());
     pi2_server_event_loop->SetRuntimeRealtimePriority(1);
-    pi2_message_bridge_server =
-        std::make_unique<MessageBridgeServer>(pi2_server_event_loop.get());
+    pi2_message_bridge_server = std::make_unique<MessageBridgeServer>(
+        pi2_server_event_loop.get(), config_sha256);
   }
 
   void RunPi2Server(chrono::nanoseconds duration) {
@@ -222,8 +226,8 @@
     pi2_client_event_loop =
         std::make_unique<aos::ShmEventLoop>(&config.message());
     pi2_client_event_loop->SetRuntimeRealtimePriority(1);
-    pi2_message_bridge_client =
-        std::make_unique<MessageBridgeClient>(pi2_client_event_loop.get());
+    pi2_message_bridge_client = std::make_unique<MessageBridgeClient>(
+        pi2_client_event_loop.get(), config_sha256);
   }
 
   void RunPi2Client(chrono::nanoseconds duration) {
@@ -297,6 +301,8 @@
   }
 
   aos::FlatbufferDetachedBuffer<aos::Configuration> config;
+  std::string config_sha256;
+
   const UUID pi1_boot_uuid_;
   const UUID pi2_boot_uuid_;
 
@@ -351,6 +357,8 @@
   MakePi1Server();
   MakePi1Client();
 
+  const std::string long_data = std::string(10000, 'a');
+
   // And build the app which sends the pings.
   FLAGS_application_name = "ping";
   aos::ShmEventLoop ping_event_loop(&config.message());
@@ -416,7 +424,8 @@
   int ping_count = 0;
   int pi1_server_statistics_count = 0;
   ping_event_loop.MakeWatcher("/pi1/aos", [this, &ping_count, &ping_sender,
-                                           &pi1_server_statistics_count](
+                                           &pi1_server_statistics_count,
+                                           &long_data](
                                               const ServerStatistics &stats) {
     VLOG(1) << "/pi1/aos ServerStatistics " << FlatbufferToJson(&stats);
 
@@ -454,6 +463,7 @@
     if (connected) {
       VLOG(1) << "Connected!  Sent ping.";
       auto builder = ping_sender.MakeBuilder();
+      builder.fbb()->CreateString(long_data);
       examples::Ping::Builder ping_builder =
           builder.MakeBuilder<examples::Ping>();
       ping_builder.add_value(ping_count + 971);
@@ -1342,6 +1352,109 @@
   pi1_remote_timestamp_thread.join();
 }
 
+// Test that differing config sha256's result in no connection.
+TEST_P(MessageBridgeParameterizedTest, MismatchedSha256) {
+  // This is rather annoying to set up.  We need to start up a client and
+  // server, on the same node, but get them to think that they are on different
+  // nodes.
+  //
+  // We need the client to not post directly to "/test" like it would in a
+  // real system, otherwise we will re-send the ping message... So, use an
+  // application specific map to have the client post somewhere else.
+  //
+  // To top this all off, each of these needs to be done with a ShmEventLoop,
+  // which needs to run in a separate thread...  And it is really hard to get
+  // everything started up reliably.  So just be super generous on timeouts and
+  // hope for the best.  We can be more generous in the future if we need to.
+  //
+  // We are faking the application names by passing in --application_name=foo
+  OnPi1();
+
+  MakePi1Server(
+      "dummy sha256                                                    ");
+  MakePi1Client();
+
+  // And build the app for testing.
+  MakePi1Test();
+  aos::Fetcher<ServerStatistics> pi1_server_statistics_fetcher =
+      pi1_test_event_loop->MakeFetcher<ServerStatistics>("/pi1/aos");
+  aos::Fetcher<ClientStatistics> pi1_client_statistics_fetcher =
+      pi1_test_event_loop->MakeFetcher<ClientStatistics>("/pi1/aos");
+
+  // Now do it for "raspberrypi2", the client.
+  OnPi2();
+  MakePi2Server();
+
+  // And build the app for testing.
+  MakePi2Test();
+  aos::Fetcher<ServerStatistics> pi2_server_statistics_fetcher =
+      pi2_test_event_loop->MakeFetcher<ServerStatistics>("/pi2/aos");
+  aos::Fetcher<ClientStatistics> pi2_client_statistics_fetcher =
+      pi2_test_event_loop->MakeFetcher<ClientStatistics>("/pi2/aos");
+
+  // Wait until we are connected, then send.
+
+  StartPi1Test();
+  StartPi2Test();
+  StartPi1Server();
+  StartPi1Client();
+  StartPi2Server();
+
+  {
+    MakePi2Client();
+
+    RunPi2Client(chrono::milliseconds(3050));
+
+    // Now confirm we are synchronized.
+    EXPECT_TRUE(pi1_server_statistics_fetcher.Fetch());
+    EXPECT_TRUE(pi1_client_statistics_fetcher.Fetch());
+    EXPECT_TRUE(pi2_server_statistics_fetcher.Fetch());
+    EXPECT_TRUE(pi2_client_statistics_fetcher.Fetch());
+
+    const ServerConnection *const pi1_connection =
+        pi1_server_statistics_fetcher->connections()->Get(0);
+    const ClientConnection *const pi1_client_connection =
+        pi1_client_statistics_fetcher->connections()->Get(0);
+    const ServerConnection *const pi2_connection =
+        pi2_server_statistics_fetcher->connections()->Get(0);
+    const ClientConnection *const pi2_client_connection =
+        pi2_client_statistics_fetcher->connections()->Get(0);
+
+    // Make sure one direction is disconnected with a bunch of connection
+    // attempts and failures.
+    EXPECT_EQ(pi1_connection->state(), State::DISCONNECTED);
+    EXPECT_EQ(pi1_connection->connection_count(), 0u);
+    EXPECT_GT(pi1_connection->invalid_connection_count(), 10u);
+
+    EXPECT_EQ(pi2_client_connection->state(), State::DISCONNECTED);
+    EXPECT_GT(pi2_client_connection->connection_count(), 10u);
+
+    // And the other direction is happy.
+    EXPECT_EQ(pi2_connection->state(), State::CONNECTED);
+    EXPECT_EQ(pi2_connection->connection_count(), 1u);
+    EXPECT_TRUE(pi2_connection->has_connected_since_time());
+    EXPECT_FALSE(pi2_connection->has_monotonic_offset());
+    EXPECT_TRUE(pi2_connection->has_boot_uuid());
+
+    EXPECT_EQ(pi1_client_connection->state(), State::CONNECTED);
+    EXPECT_EQ(pi1_client_connection->connection_count(), 1u);
+
+    VLOG(1) << aos::FlatbufferToJson(pi2_server_statistics_fetcher.get());
+    VLOG(1) << aos::FlatbufferToJson(pi1_server_statistics_fetcher.get());
+    VLOG(1) << aos::FlatbufferToJson(pi2_client_statistics_fetcher.get());
+    VLOG(1) << aos::FlatbufferToJson(pi1_client_statistics_fetcher.get());
+
+    StopPi2Client();
+  }
+
+  // Shut everyone else down
+  StopPi1Server();
+  StopPi1Client();
+  StopPi2Server();
+  StopPi1Test();
+  StopPi2Test();
+}
+
 INSTANTIATE_TEST_SUITE_P(
     MessageBridgeTests, MessageBridgeParameterizedTest,
     ::testing::Values(
diff --git a/aos/network/message_bridge_test_combined_timestamps_common.json b/aos/network/message_bridge_test_combined_timestamps_common.json
index 5d82965..13a0514 100644
--- a/aos/network/message_bridge_test_combined_timestamps_common.json
+++ b/aos/network/message_bridge_test_combined_timestamps_common.json
@@ -110,7 +110,8 @@
           "timestamp_logger": "REMOTE_LOGGER",
           "timestamp_logger_nodes": ["pi1"]
         }
-      ]
+      ],
+      "max_size": 20480
     },
     {
       "name": "/test",
@@ -125,7 +126,8 @@
           "timestamp_logger": "REMOTE_LOGGER",
           "timestamp_logger_nodes": ["pi1"]
         }
-      ]
+      ],
+      "max_size": 20480
     },
     {
       "name": "/unreliable",
@@ -139,7 +141,8 @@
           "timestamp_logger_nodes": ["pi1"],
           "time_to_live": 5000000
         }
-      ]
+      ],
+      "max_size": 20480
     }
   ],
   "maps": [
diff --git a/aos/network/message_bridge_test_common.json b/aos/network/message_bridge_test_common.json
index a30734d..9bb0863 100644
--- a/aos/network/message_bridge_test_common.json
+++ b/aos/network/message_bridge_test_common.json
@@ -75,28 +75,33 @@
     {
       "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
       "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
       "source_node": "pi1",
       "frequency": 15
     },
     {
       "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
       "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
       "source_node": "pi2",
       "frequency": 15
     },
     {
       "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
       "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
       "source_node": "pi1"
     },
     {
       "name": "/pi2/aos/remote_timestamps/pi1/test/aos-examples-Pong",
       "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
       "source_node": "pi2"
     },
     {
       "name": "/pi1/aos/remote_timestamps/pi2/unreliable/aos-examples-Ping",
       "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
       "source_node": "pi1"
     },
     {
@@ -126,7 +131,8 @@
           "timestamp_logger": "REMOTE_LOGGER",
           "timestamp_logger_nodes": ["pi1"]
         }
-      ]
+      ],
+      "max_size": 20480
     },
     {
       "name": "/test",
@@ -141,7 +147,8 @@
           "timestamp_logger": "REMOTE_LOGGER",
           "timestamp_logger_nodes": ["pi2"]
         }
-      ]
+      ],
+      "max_size": 20480
     },
     {
       "name": "/unreliable",
@@ -155,7 +162,8 @@
           "timestamp_logger_nodes": ["pi1"],
           "time_to_live": 5000000
         }
-      ]
+      ],
+      "max_size": 20480
     }
   ],
   "maps": [
diff --git a/aos/network/sctp_client.cc b/aos/network/sctp_client.cc
index e3da03a..ab70c20 100644
--- a/aos/network/sctp_client.cc
+++ b/aos/network/sctp_client.cc
@@ -51,7 +51,9 @@
   message_bridge::LogSctpStatus(fd(), assoc_id);
 }
 
-void SctpClient::SetPriorityScheduler(sctp_assoc_t assoc_id) {
+void SctpClient::SetPriorityScheduler([[maybe_unused]] sctp_assoc_t assoc_id) {
+// Kernel 4.9 does not have SCTP_SS_PRIO
+#ifdef SCTP_SS_PRIO
   struct sctp_assoc_value scheduler;
   memset(&scheduler, 0, sizeof(scheduler));
   scheduler.assoc_id = assoc_id;
@@ -61,6 +63,7 @@
     LOG_FIRST_N(WARNING, 1) << "Failed to set scheduler: " << strerror(errno)
                             << " [" << errno << "]";
   }
+#endif
 }
 
 }  // namespace message_bridge
diff --git a/aos/network/sctp_client.h b/aos/network/sctp_client.h
index 1ba32ff..d7a2b43 100644
--- a/aos/network/sctp_client.h
+++ b/aos/network/sctp_client.h
@@ -47,12 +47,18 @@
 
   void LogSctpStatus(sctp_assoc_t assoc_id);
 
-  void SetMaxSize(size_t max_size) { sctp_.SetMaxSize(max_size); }
+  void SetMaxReadSize(size_t max_size) { sctp_.SetMaxReadSize(max_size); }
+  void SetMaxWriteSize(size_t max_size) { sctp_.SetMaxWriteSize(max_size); }
+  void SetPoolSize(size_t pool_size) { sctp_.SetPoolSize(pool_size); }
 
   void SetAssociationId(sctp_assoc_t sac_assoc_id) {
     sac_assoc_id_ = sac_assoc_id;
   }
 
+  void FreeMessage(aos::unique_c_ptr<Message> &&message) {
+    sctp_.FreeMessage(std::move(message));
+  }
+
  private:
   struct sockaddr_storage sockaddr_remote_;
   struct sockaddr_storage sockaddr_local_;
diff --git a/aos/network/sctp_lib.cc b/aos/network/sctp_lib.cc
index 9632d3c..3f482c3 100644
--- a/aos/network/sctp_lib.cc
+++ b/aos/network/sctp_lib.cc
@@ -334,25 +334,52 @@
   return true;
 }
 
+void SctpReadWrite::FreeMessage(aos::unique_c_ptr<Message> &&message) {
+  if (use_pool_) {
+    free_messages_.emplace_back(std::move(message));
+  }
+}
+
+void SctpReadWrite::SetPoolSize(size_t pool_size) {
+  CHECK(!use_pool_);
+  free_messages_.reserve(pool_size);
+  for (size_t i = 0; i < pool_size; ++i) {
+    free_messages_.emplace_back(AcquireMessage());
+  }
+  use_pool_ = true;
+}
+
+aos::unique_c_ptr<Message> SctpReadWrite::AcquireMessage() {
+  if (!use_pool_) {
+    constexpr size_t kMessageAlign = alignof(Message);
+    const size_t max_message_size =
+        ((sizeof(Message) + max_read_size_ + 1 + (kMessageAlign - 1)) /
+         kMessageAlign) *
+        kMessageAlign;
+    aos::unique_c_ptr<Message> result(reinterpret_cast<Message *>(
+        aligned_alloc(kMessageAlign, max_message_size)));
+    return result;
+  } else {
+    CHECK_GT(free_messages_.size(), 0u);
+    aos::unique_c_ptr<Message> result = std::move(free_messages_.back());
+    free_messages_.pop_back();
+    return result;
+  }
+}
+
 // We read each fragment into a fresh Message, because most of them won't be
 // fragmented. If we do end up with a fragment, then we copy the data out of it.
 aos::unique_c_ptr<Message> SctpReadWrite::ReadMessage() {
   CHECK(fd_ != -1);
 
   while (true) {
-    constexpr size_t kMessageAlign = alignof(Message);
-    const size_t max_message_size =
-        ((sizeof(Message) + max_size_ + 1 + (kMessageAlign - 1)) /
-         kMessageAlign) *
-        kMessageAlign;
-    aos::unique_c_ptr<Message> result(reinterpret_cast<Message *>(
-        aligned_alloc(kMessageAlign, max_message_size)));
+    aos::unique_c_ptr<Message> result = AcquireMessage();
 
     struct msghdr inmessage;
     memset(&inmessage, 0, sizeof(struct msghdr));
 
     struct iovec iov;
-    iov.iov_len = max_size_ + 1;
+    iov.iov_len = max_read_size_ + 1;
     iov.iov_base = result->mutable_data();
 
     inmessage.msg_iov = &iov;
@@ -377,7 +404,7 @@
     CHECK(!(inmessage.msg_flags & MSG_CTRUNC))
         << ": Control message truncated.";
 
-    CHECK_LE(size, static_cast<ssize_t>(max_size_))
+    CHECK_LE(size, static_cast<ssize_t>(max_read_size_))
         << ": Message overflowed buffer on stream "
         << result->header.rcvinfo.rcv_sid << ".";
 
@@ -445,7 +472,7 @@
           << result->header.rcvinfo.rcv_assoc_id;
 
       // Now copy the data over and update the size.
-      CHECK_LE(partial_message->size + result->size, max_size_)
+      CHECK_LE(partial_message->size + result->size, max_read_size_)
           << ": Assembled fragments overflowed buffer on stream "
           << result->header.rcvinfo.rcv_sid << ".";
       memcpy(partial_message->mutable_data() + partial_message->size,
@@ -541,10 +568,10 @@
 }
 
 void SctpReadWrite::DoSetMaxSize() {
-  size_t max_size = max_size_;
+  size_t max_size = max_write_size_;
 
   // This sets the max packet size that we can send.
-  CHECK_GE(ReadWMemMax(), max_size)
+  CHECK_GE(ReadWMemMax(), max_write_size_)
       << "wmem_max is too low. To increase wmem_max temporarily, do sysctl "
          "-w net.core.wmem_max="
       << max_size;
diff --git a/aos/network/sctp_lib.h b/aos/network/sctp_lib.h
index 6cd11a3..a852365 100644
--- a/aos/network/sctp_lib.h
+++ b/aos/network/sctp_lib.h
@@ -105,17 +105,35 @@
 
   int fd() const { return fd_; }
 
-  void SetMaxSize(size_t max_size) {
+  void SetMaxReadSize(size_t max_size) {
     CHECK(partial_messages_.empty())
         << ": May not update size with queued fragments because we do not "
            "track individual message sizes";
-    max_size_ = max_size;
+    max_read_size_ = max_size;
     if (fd_ != -1) {
       DoSetMaxSize();
     }
   }
 
+  void SetMaxWriteSize(size_t max_size) {
+    CHECK(partial_messages_.empty())
+        << ": May not update size with queued fragments because we do not "
+           "track individual message sizes";
+    max_write_size_ = max_size;
+    if (fd_ != -1) {
+      DoSetMaxSize();
+    }
+  }
+
+  // Returns a message returned from ReadMessage back to the pool.
+  void FreeMessage(aos::unique_c_ptr<Message> &&message);
+
+  // Allocates messages for the pool.  SetMaxSize must be set first.
+  void SetPoolSize(size_t pool_size);
+
  private:
+  aos::unique_c_ptr<Message> AcquireMessage();
+
   void CloseSocket();
   void DoSetMaxSize();
 
@@ -128,9 +146,13 @@
   // We use this as a unique identifier that just increments for each message.
   uint32_t send_ppid_ = 0;
 
-  size_t max_size_ = 1000;
+  size_t max_read_size_ = 1000;
+  size_t max_write_size_ = 1000;
 
   std::vector<aos::unique_c_ptr<Message>> partial_messages_;
+
+  bool use_pool_ = false;
+  std::vector<aos::unique_c_ptr<Message>> free_messages_;
 };
 
 // Returns the max network buffer available for reading for a socket.
diff --git a/aos/network/sctp_server.cc b/aos/network/sctp_server.cc
index 2f6a041..a78aa34 100644
--- a/aos/network/sctp_server.cc
+++ b/aos/network/sctp_server.cc
@@ -64,12 +64,15 @@
 
     PCHECK(listen(fd(), 100) == 0);
 
-    SetMaxSize(1000);
+    SetMaxReadSize(1000);
+    SetMaxWriteSize(1000);
     break;
   }
 }
 
-void SctpServer::SetPriorityScheduler(sctp_assoc_t assoc_id) {
+void SctpServer::SetPriorityScheduler([[maybe_unused]] sctp_assoc_t assoc_id) {
+// Kernel 4.9 does not have SCTP_SS_PRIO
+#ifdef SCTP_SS_PRIO
   struct sctp_assoc_value scheduler;
   memset(&scheduler, 0, sizeof(scheduler));
   scheduler.assoc_id = assoc_id;
@@ -79,10 +82,14 @@
     LOG_FIRST_N(WARNING, 1) << "Failed to set scheduler: " << strerror(errno)
                             << " [" << errno << "]";
   }
+#endif
 }
 
-void SctpServer::SetStreamPriority(sctp_assoc_t assoc_id, int stream_id,
-                                   uint16_t priority) {
+void SctpServer::SetStreamPriority([[maybe_unused]] sctp_assoc_t assoc_id,
+                                   [[maybe_unused]] int stream_id,
+                                   [[maybe_unused]] uint16_t priority) {
+// Kernel 4.9 does not have SCTP_STREAM_SCHEDULER_VALUE
+#ifdef SCTP_STREAM_SCHEDULER_VALUE
   struct sctp_stream_value sctp_priority;
   memset(&sctp_priority, 0, sizeof(sctp_priority));
   sctp_priority.assoc_id = assoc_id;
@@ -93,6 +100,7 @@
     LOG_FIRST_N(WARNING, 1) << "Failed to set scheduler: " << strerror(errno)
                             << " [" << errno << "]";
   }
+#endif
 }
 
 }  // namespace message_bridge
diff --git a/aos/network/sctp_server.h b/aos/network/sctp_server.h
index dbfd1ac..800c2c1 100644
--- a/aos/network/sctp_server.h
+++ b/aos/network/sctp_server.h
@@ -30,6 +30,11 @@
   // Receives the next packet from the remote.
   aos::unique_c_ptr<Message> Read() { return sctp_.ReadMessage(); }
 
+  // Frees the message returned by Read();
+  void FreeMessage(aos::unique_c_ptr<Message> &&message) {
+    sctp_.FreeMessage(std::move(message));
+  }
+
   // Sends a block of data to a client on a stream with a TTL.  Returns true on
   // success.
   bool Send(std::string_view data, sctp_assoc_t snd_assoc_id, int stream,
@@ -52,7 +57,10 @@
   void SetStreamPriority(sctp_assoc_t assoc_id, int stream_id,
                          uint16_t priority);
 
-  void SetMaxSize(size_t max_size) { sctp_.SetMaxSize(max_size); }
+  void SetMaxReadSize(size_t max_size) { sctp_.SetMaxReadSize(max_size); }
+  void SetMaxWriteSize(size_t max_size) { sctp_.SetMaxWriteSize(max_size); }
+
+  void SetPoolSize(size_t pool_size) { sctp_.SetPoolSize(pool_size); }
 
  private:
   struct sockaddr_storage sockaddr_local_;
diff --git a/aos/realtime.cc b/aos/realtime.cc
index 2f299e6..2f5e1d9 100644
--- a/aos/realtime.cc
+++ b/aos/realtime.cc
@@ -20,7 +20,7 @@
 #include "glog/raw_logging.h"
 
 DEFINE_bool(
-    die_on_malloc, false,
+    die_on_malloc, true,
     "If true, die when the application allocates memory in a RT section.");
 DEFINE_bool(skip_realtime_scheduler, false,
             "If true, skip changing the scheduler.  Pretend that we changed "
@@ -337,7 +337,9 @@
         has_malloc_hook = false;
       }
     } else {
-      RAW_LOG(INFO, "Replacing glibc malloc");
+      if (VLOG_IS_ON(1)) {
+        RAW_LOG(INFO, "Replacing glibc malloc");
+      }
       if (&malloc != &aos_malloc_hook) {
         has_malloc_hook = false;
       }
diff --git a/aos/sha256.cc b/aos/sha256.cc
new file mode 100644
index 0000000..ae83792
--- /dev/null
+++ b/aos/sha256.cc
@@ -0,0 +1,26 @@
+#include "aos/sha256.h"
+
+#include <iomanip>
+#include <sstream>
+#include <string>
+
+#include "absl/types/span.h"
+#include "openssl/sha.h"
+
+namespace aos {
+
+std::string Sha256(const absl::Span<const uint8_t> str) {
+  unsigned char hash[SHA256_DIGEST_LENGTH];
+  SHA256_CTX sha256;
+  SHA256_Init(&sha256);
+  SHA256_Update(&sha256, str.data(), str.size());
+  SHA256_Final(hash, &sha256);
+  std::stringstream ss;
+  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+    ss << std::hex << std::setw(2) << std::setfill('0')
+       << static_cast<int>(hash[i]);
+  }
+  return ss.str();
+}
+
+}  // namespace aos
diff --git a/aos/sha256.h b/aos/sha256.h
new file mode 100644
index 0000000..7fb9b2f
--- /dev/null
+++ b/aos/sha256.h
@@ -0,0 +1,15 @@
+#ifndef AOS_SHA256_H_
+#define AOS_SHA256_H_
+
+#include <string>
+
+#include "absl/types/span.h"
+
+namespace aos {
+
+// Returns the sha256 of a span.
+std::string Sha256(const absl::Span<const uint8_t> str);
+
+}  // namespace aos
+
+#endif  // AOS_SHA256_H_
diff --git a/aos/starter/BUILD b/aos/starter/BUILD
index 9068caa..7ef3777 100644
--- a/aos/starter/BUILD
+++ b/aos/starter/BUILD
@@ -118,6 +118,7 @@
         "//aos/events:ping_fbs",
         "//aos/events:pong_fbs",
         "//aos/events:simulated_event_loop",
+        "//aos/ipc_lib:event",
         "//aos/testing:googletest",
         "//aos/testing:path",
         "//aos/testing:tmpdir",
diff --git a/aos/starter/starter_rpc_lib.cc b/aos/starter/starter_rpc_lib.cc
index 15132ec..5f14e21 100644
--- a/aos/starter/starter_rpc_lib.cc
+++ b/aos/starter/starter_rpc_lib.cc
@@ -225,10 +225,12 @@
   // Clear commands prior to calling handlers to allow the handler to call
   // SendCommands() again if desired.
   current_commands_.clear();
+  // Clear the timer before calling success handler, in case the success
+  // handler needs to modify timeout handler.
+  timeout_timer_->Disable();
   if (success_handler_) {
     success_handler_();
   }
-  timeout_timer_->Disable();
 }
 
 bool SendCommandBlocking(aos::starter::Command command, std::string_view name,
diff --git a/aos/starter/starter_test.cc b/aos/starter/starter_test.cc
index 87cb544..94566ae 100644
--- a/aos/starter/starter_test.cc
+++ b/aos/starter/starter_test.cc
@@ -1,9 +1,11 @@
+#include <chrono>
 #include <csignal>
 #include <future>
 #include <thread>
 
 #include "aos/events/ping_generated.h"
 #include "aos/events/pong_generated.h"
+#include "aos/ipc_lib/event.h"
 #include "aos/network/team_number.h"
 #include "aos/testing/path.h"
 #include "aos/testing/tmpdir.h"
@@ -19,11 +21,9 @@
 
 class StarterdTest : public ::testing::Test {
  public:
-  StarterdTest() : shm_dir_(aos::testing::TestTmpDir() + "/aos") {
-    FLAGS_shm_base = shm_dir_;
-
+  StarterdTest() {
     // Nuke the shm dir:
-    aos::util::UnlinkRecursive(shm_dir_);
+    aos::util::UnlinkRecursive(FLAGS_shm_base);
   }
 
  protected:
@@ -35,11 +35,10 @@
           }
         })
         ->Setup(starter->event_loop()->monotonic_now(),
-                std::chrono::seconds(1));
+                std::chrono::milliseconds(100));
   }
 
   gflags::FlagSaver flag_saver_;
-  std::string shm_dir_;
   // Used to track when the test completes so that we can clean up the starter
   // in its thread.
   std::atomic<bool> test_done_{false};
@@ -79,8 +78,8 @@
                                     "args": ["--shm_base", "%s", "--config", "%s", "--override_hostname", "%s"]
                                   }
                                 ]})",
-          ArtifactPath("aos/events/ping"), shm_dir_, config_file,
-          GetParam().hostname, ArtifactPath("aos/events/pong"), shm_dir_,
+          ArtifactPath("aos/events/ping"), FLAGS_shm_base, config_file,
+          GetParam().hostname, ArtifactPath("aos/events/pong"), FLAGS_shm_base,
           config_file, GetParam().hostname));
 
   const aos::Configuration *config_msg = &new_config.message();
@@ -161,12 +160,25 @@
 
   SetupStarterCleanup(&starter);
 
-  std::thread starterd_thread([&starter] { starter.Run(); });
-  std::thread client_thread([&client_loop] { client_loop.Run(); });
-  watcher_loop.Run();
+  Event starter_started;
+  std::thread starterd_thread([&starter, &starter_started] {
+    starter.event_loop()->OnRun(
+        [&starter_started]() { starter_started.Set(); });
+    starter.Run();
+  });
+  starter_started.Wait();
 
+  Event client_started;
+  std::thread client_thread([&client_loop, &client_started] {
+    client_loop.OnRun([&client_started]() { client_started.Set(); });
+    client_loop.Run();
+  });
+  client_started.Wait();
+
+  watcher_loop.Run();
   test_done_ = true;
   client_thread.join();
+  ASSERT_TRUE(success);
   starterd_thread.join();
 }
 
@@ -197,8 +209,8 @@
                                     "args": ["--shm_base", "%s"]
                                   }
                                 ]})",
-                             ArtifactPath("aos/events/ping"), shm_dir_,
-                             ArtifactPath("aos/events/pong"), shm_dir_));
+                             ArtifactPath("aos/events/ping"), FLAGS_shm_base,
+                             ArtifactPath("aos/events/pong"), FLAGS_shm_base));
 
   const aos::Configuration *config_msg = &new_config.message();
 
@@ -257,7 +269,13 @@
 
   SetupStarterCleanup(&starter);
 
-  std::thread starterd_thread([&starter] { starter.Run(); });
+  Event starter_started;
+  std::thread starterd_thread([&starter, &starter_started] {
+    starter.event_loop()->OnRun(
+        [&starter_started]() { starter_started.Set(); });
+    starter.Run();
+  });
+  starter_started.Wait();
   watcher_loop.Run();
 
   test_done_ = true;
@@ -287,8 +305,8 @@
                                     "args": ["--shm_base", "%s"]
                                   }
                                 ]})",
-                             ArtifactPath("aos/events/ping"), shm_dir_,
-                             ArtifactPath("aos/events/pong"), shm_dir_));
+                             ArtifactPath("aos/events/ping"), FLAGS_shm_base,
+                             ArtifactPath("aos/events/pong"), FLAGS_shm_base));
 
   const aos::Configuration *config_msg = &new_config.message();
 
@@ -346,7 +364,13 @@
 
   SetupStarterCleanup(&starter);
 
-  std::thread starterd_thread([&starter] { starter.Run(); });
+  Event starter_started;
+  std::thread starterd_thread([&starter, &starter_started] {
+    starter.event_loop()->OnRun(
+        [&starter_started]() { starter_started.Set(); });
+    starter.Run();
+  });
+  starter_started.Wait();
   watcher_loop.Run();
 
   test_done_ = true;
@@ -362,25 +386,23 @@
   aos::FlatbufferDetachedBuffer<aos::Configuration> config =
       aos::configuration::ReadConfig(config_file);
 
-  const std::string test_dir = aos::testing::TestTmpDir();
-
   auto new_config = aos::configuration::MergeWithConfig(
       &config.message(), absl::StrFormat(
                              R"({"applications": [
                                   {
                                     "name": "ping",
                                     "executable_name": "%s",
-                                    "args": ["--shm_base", "%s/aos"],
+                                    "args": ["--shm_base", "%s"],
                                     "autorestart": false
                                   },
                                   {
                                     "name": "pong",
                                     "executable_name": "%s",
-                                    "args": ["--shm_base", "%s/aos"]
+                                    "args": ["--shm_base", "%s"]
                                   }
                                 ]})",
-                             ArtifactPath("aos/events/ping"), test_dir,
-                             ArtifactPath("aos/events/pong"), test_dir));
+                             ArtifactPath("aos/events/ping"), FLAGS_shm_base,
+                             ArtifactPath("aos/events/pong"), FLAGS_shm_base));
 
   const aos::Configuration *config_msg = &new_config.message();
 
@@ -439,7 +461,13 @@
 
   SetupStarterCleanup(&starter);
 
-  std::thread starterd_thread([&starter] { starter.Run(); });
+  Event starter_started;
+  std::thread starterd_thread([&starter, &starter_started] {
+    starter.event_loop()->OnRun(
+        [&starter_started]() { starter_started.Set(); });
+    starter.Run();
+  });
+  starter_started.Wait();
   watcher_loop.Run();
 
   test_done_ = true;
@@ -447,5 +475,121 @@
   starterd_thread.join();
 }
 
+TEST_F(StarterdTest, StarterChainTest) {
+  // This test was written in response to a bug that was found
+  // in StarterClient::Succeed. The bug caused the timeout handler
+  // to be reset after the success handler was called.
+  // the bug has been fixed, and this test will ensure it does
+  // not regress.
+  const std::string config_file =
+      ArtifactPath("aos/events/pingpong_config.json");
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(config_file);
+  auto new_config = aos::configuration::MergeWithConfig(
+      &config.message(), absl::StrFormat(
+                             R"({"applications": [
+                                {
+                                  "name": "ping",
+                                  "executable_name": "%s",
+                                  "args": ["--shm_base", "%s"],
+                                  "autorestart": false
+                                },
+                                {
+                                  "name": "pong",
+                                  "executable_name": "%s",
+                                  "args": ["--shm_base", "%s"]
+                                }
+                              ]})",
+                             ArtifactPath("aos/events/ping"), FLAGS_shm_base,
+                             ArtifactPath("aos/events/pong"), FLAGS_shm_base));
+
+  const aos::Configuration *config_msg = &new_config.message();
+  // Set up starter with config file
+  aos::starter::Starter starter(config_msg);
+  aos::ShmEventLoop client_loop(config_msg);
+  client_loop.SkipAosLog();
+  StarterClient client(&client_loop);
+  bool success = false;
+  auto client_node = client_loop.node();
+
+  // limit the amount of time we will wait for the test to finish.
+  client_loop
+      .AddTimer([&client_loop] {
+        client_loop.Exit();
+        FAIL() << "ERROR: The test has failed, the watcher has timed out. "
+                  "The chain of stages defined below did not complete "
+                  "within the time limit.";
+      })
+      ->Setup(client_loop.monotonic_now() + std::chrono::seconds(20));
+
+  // variables have been defined, here we define the body of the test.
+  // We want stage1 to succeed, triggering stage2.
+  // We want stage2 to timeout, triggering stage3.
+
+  auto stage3 = [&client_loop, &success]() {
+    LOG(INFO) << "Begin stage3.";
+    SUCCEED();
+    success = true;
+    client_loop.Exit();
+    LOG(INFO) << "End stage3.";
+  };
+  auto stage2 = [this, &starter, &client, &client_node, &stage3] {
+    LOG(INFO) << "Begin stage2";
+    test_done_ = true;  // trigger `starter` to exit.
+
+    // wait for the starter event loop to close, so we can
+    // intentionally trigger a timeout.
+    int attempts = 0;
+    while (starter.event_loop()->is_running()) {
+      ++attempts;
+      if (attempts > 5) {
+        LOG(INFO) << "Timeout while waiting for starter to exit";
+        return;
+      }
+      LOG(INFO) << "Waiting for starter to close.";
+      std::this_thread::sleep_for(std::chrono::seconds(1));
+    }
+    client.SetTimeoutHandler(stage3);
+    client.SetSuccessHandler([]() {
+      LOG(INFO) << "stage3 success handler called.";
+      FAIL() << ": Command should not have succeeded here.";
+    });
+    // we want this command to timeout
+    client.SendCommands({{Command::START, "ping", {client_node}}},
+                        std::chrono::seconds(5));
+    LOG(INFO) << "End stage2";
+  };
+  auto stage1 = [&client, &client_node, &stage2] {
+    LOG(INFO) << "Begin stage1";
+    client.SetTimeoutHandler(
+        []() { FAIL() << ": Command should not have timed out."; });
+    client.SetSuccessHandler(stage2);
+    client.SendCommands({{Command::STOP, "ping", {client_node}}},
+                        std::chrono::seconds(5));
+    LOG(INFO) << "End stage1";
+  };
+  // start the test body
+  client_loop.AddTimer(stage1)->Setup(client_loop.monotonic_now() +
+                                      std::chrono::milliseconds(1));
+
+  // prepare the cleanup for starter. This will finish when we call
+  // `test_done_ = true;`.
+  SetupStarterCleanup(&starter);
+
+  // run `starter.Run()` in a thread to simulate it running on
+  // another process.
+  Event started;
+  std::thread starterd_thread([&starter, &started] {
+    starter.event_loop()->OnRun([&started]() { started.Set(); });
+    starter.Run();
+  });
+
+  started.Wait();
+  client_loop.Run();
+  EXPECT_TRUE(success);
+  ASSERT_FALSE(starter.event_loop()->is_running());
+  starterd_thread.join();
+}
+
 }  // namespace starter
 }  // namespace aos
diff --git a/aos/starter/starterd_lib.cc b/aos/starter/starterd_lib.cc
index b8b7343..30e0887 100644
--- a/aos/starter/starterd_lib.cc
+++ b/aos/starter/starterd_lib.cc
@@ -35,7 +35,10 @@
         SendStatus();
         status_count_ = 0;
       })),
-      cleanup_timer_(event_loop_.AddTimer([this] { event_loop_.Exit(); })),
+      cleanup_timer_(event_loop_.AddTimer([this] {
+        event_loop_.Exit();
+        LOG(INFO) << "Starter event loop exit finished.";
+      })),
       max_status_count_(
           event_loop_.GetChannel<aos::starter::Status>("/aos")->frequency() -
           1),
diff --git a/aos/testdata/BUILD b/aos/testdata/BUILD
index 82e67f9..8b87682 100644
--- a/aos/testdata/BUILD
+++ b/aos/testdata/BUILD
@@ -20,6 +20,7 @@
         "invalid_channel_name1.json",
         "invalid_channel_name2.json",
         "invalid_channel_name3.json",
+        "invalid_channel_name4.json",
         "invalid_destination_node.json",
         "invalid_logging_configuration.json",
         "invalid_nodes.json",
diff --git a/aos/testdata/invalid_channel_name4.json b/aos/testdata/invalid_channel_name4.json
new file mode 100644
index 0000000..44ed8db
--- /dev/null
+++ b/aos/testdata/invalid_channel_name4.json
@@ -0,0 +1,9 @@
+{
+  "channels": [
+    {
+      "name": "foo",
+      "type": ".aos.bar",
+      "max_size": 5
+    }
+  ]
+}
diff --git a/aos/util/BUILD b/aos/util/BUILD
index 2f10a70..8ce96f0 100644
--- a/aos/util/BUILD
+++ b/aos/util/BUILD
@@ -1,10 +1,10 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
 load("//aos:flatbuffers.bzl", "cc_static_flatbuffer")
-load("config_validator_macro.bzl", "config_validator_rule")
+load("config_validator_macro.bzl", "config_validator_test")
 
 package(default_visibility = ["//visibility:public"])
 
-config_validator_rule(
+config_validator_test(
     name = "config_validator_test",
     config = "//aos/events:pingpong_config",
 )
@@ -499,17 +499,14 @@
     ],
 )
 
-cc_binary(
+cc_library(
     name = "config_validator",
     testonly = True,
     srcs = ["config_validator.cc"],
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
-        "//aos:init",
+        ":config_validator_lib",
         "//aos:json_to_flatbuffer",
-        "//aos/events:simulated_event_loop",
-        "//aos/events/logging:log_reader",
-        "//aos/events/logging:log_writer",
         "//aos/testing:googletest",
         "@com_github_gflags_gflags//:gflags",
         "@com_github_google_glog//:glog",
@@ -528,3 +525,59 @@
         "@com_github_gflags_gflags//:gflags",
     ],
 )
+
+cc_library(
+    name = "simulation_logger",
+    srcs = ["simulation_logger.cc"],
+    hdrs = ["simulation_logger.h"],
+    deps = [
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_writer",
+    ],
+)
+
+flatbuffer_cc_library(
+    name = "config_validator_config_fbs",
+    srcs = ["config_validator_config.fbs"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+)
+
+cc_library(
+    name = "config_validator_lib",
+    testonly = True,
+    srcs = ["config_validator_lib.cc"],
+    hdrs = ["config_validator_lib.h"],
+    deps = [
+        ":config_validator_config_fbs",
+        ":simulation_logger",
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_reader",
+        "//aos/events/logging:log_writer",
+        "//aos/network:timestamp_channel",
+        "//aos/testing:tmpdir",
+        "@com_github_google_glog//:glog",
+        "@com_google_googletest//:gtest",
+    ],
+)
+
+cc_test(
+    name = "config_validator_lib_test",
+    srcs = ["config_validator_lib_test.cc"],
+    data = [
+        "//aos/util/test_data:multinode_common_logger",
+        "//aos/util/test_data:multinode_extraneous_timestamp",
+        "//aos/util/test_data:multinode_invalid_timestamp_logger_list",
+        "//aos/util/test_data:multinode_no_logged_timestamps",
+        "//aos/util/test_data:multinode_no_statistics",
+        "//aos/util/test_data:multinode_timestamp_typo",
+        "//aos/util/test_data:valid_multinode_config",
+        "//aos/util/test_data:valid_singlenode_config",
+    ],
+    deps = [
+        ":config_validator_lib",
+        "//aos:json_to_flatbuffer",
+        "//aos/testing:googletest",
+        "//aos/testing:path",
+    ],
+)
diff --git a/aos/util/config_validator.cc b/aos/util/config_validator.cc
index d5bd6ba..df21ba0 100644
--- a/aos/util/config_validator.cc
+++ b/aos/util/config_validator.cc
@@ -1,16 +1,9 @@
-#include <chrono>
-
-#include "aos/configuration.h"
-#include "aos/events/logging/log_reader.h"
-#include "aos/events/logging/log_writer.h"
-#include "aos/events/simulated_event_loop.h"
-#include "aos/init.h"
 #include "aos/json_to_flatbuffer.h"
-#include "aos/network/team_number.h"
-#include "gflags/gflags.h"
-#include "gtest/gtest.h"
+#include "aos/util/config_validator_lib.h"
 
 DEFINE_string(config, "", "Name of the config file to replay using.");
+DEFINE_string(validation_config, "{}",
+              "JSON config to use to validate the config.");
 /* This binary is used to validate that all of the
    needed remote timestamps channels are in the config
    to log the timestamps.
@@ -26,9 +19,11 @@
   const aos::FlatbufferDetachedBuffer<aos::Configuration> config =
       aos::configuration::ReadConfig(FLAGS_config);
 
-  aos::SimulatedEventLoopFactory factory(&config.message());
-
-  factory.RunFor(std::chrono::seconds(1));
+  const aos::FlatbufferDetachedBuffer<aos::util::ConfigValidatorConfig>
+      validator_config =
+          aos::JsonToFlatbuffer<aos::util::ConfigValidatorConfig>(
+              FLAGS_validation_config);
+  aos::util::ConfigIsValid(&config.message(), &validator_config.message());
 }
 
 // TODO(milind): add more tests, the above one doesn't
diff --git a/aos/util/config_validator_config.fbs b/aos/util/config_validator_config.fbs
new file mode 100644
index 0000000..cda84b2
--- /dev/null
+++ b/aos/util/config_validator_config.fbs
@@ -0,0 +1,55 @@
+namespace aos.util;
+
+// This file defines a schema for what to validate when we run the
+// config_validator against an AOS config.
+// The primary purpose of this config is to allow the user to specify what
+// sets of nodes they expect to be able to log on so that we can validate the
+// logging configurations. In the future this may also include flags to indicate
+// how aggressively to do certain checks.
+//
+// This flatbuffer should not exist in serialized form anywhere, and so is
+// safe to modify in non-backwards-compatible ways.
+
+// Species a set of nodes that you should be able to combine the logs from and
+// subsequently replay. E.g., this allows you to write a check that says
+// "If you combine logs from pi2 & pi4, you should be able to replay data from
+// nodes pi2, pi4, and pi6"; or
+// "When logs from all nodes are combined, you should be able to replay data
+// for all nodes;" or
+// "Each node should log all the data needed to replay its own data"
+// (this would require muliple LoggerNodeSetValidation's).
+//
+// Each LoggerNodeSetValidation table represents a single set of logging nodes
+// that should be able to replay data on some number of other nodes. An empty
+// list of loggers or replay_nodes indicates "all nodes." The above examples
+// could then be represented by, e.g.:
+// "pi2 & pi4 -> pi2, pi4, & pi6":
+//   {"loggers": ["pi2", "pi4"], "replay_nodes": ["pi2", "pi4", "pi6"]}
+// "all -> all": {"logger": [], "replay_nodes": []}
+// "each node -> itself": [
+//   {"logger": ["pi1"], "replay_nodes": ["pi1"]},
+//   {"logger": ["pi2"], "replay_nodes": ["pi2"]},
+//   {"logger": ["pi3"], "replay_nodes": ["pi3"]},
+//   {"logger": ["pi4"], "replay_nodes": ["pi4"]}]
+table LoggerNodeSetValidation {
+  loggers:[string] (id: 0);
+  replay_nodes:[string] (id: 1);
+}
+
+// This table specifies which
+table LoggingConfigValidation {
+  // If true, all channels should be logged by some valid set of loggers.
+  // Essentially, this is checking that no channels are configured to be
+  // NOT_LOGGED except for remote timestamp channels.
+  all_channels_logged:bool = true (id: 0);
+  // A list of all the sets of logger nodes that we care about. Typically this
+  // should at least include an entry that says that "logs from all nodes should
+  // combine to allow you to replay all nodes."
+  logger_sets:[LoggerNodeSetValidation] (id: 1);
+}
+
+table ConfigValidatorConfig {
+  logging:LoggingConfigValidation (id: 0);
+}
+
+root_type ConfigValidatorConfig;
diff --git a/aos/util/config_validator_lib.cc b/aos/util/config_validator_lib.cc
new file mode 100644
index 0000000..0f90ed6
--- /dev/null
+++ b/aos/util/config_validator_lib.cc
@@ -0,0 +1,292 @@
+#include "aos/util/config_validator_lib.h"
+
+#include <chrono>
+
+#include "aos/events/logging/log_reader.h"
+#include "aos/events/logging/log_writer.h"
+#include "aos/events/simulated_event_loop.h"
+#include "aos/network/remote_message_generated.h"
+#include "aos/network/timestamp_channel.h"
+#include "aos/testing/tmpdir.h"
+#include "aos/util/simulation_logger.h"
+
+DECLARE_bool(validate_timestamp_logger_nodes);
+
+namespace aos::util {
+
+namespace {
+void RunSimulationAndExit(const aos::Configuration *config) {
+  aos::SimulatedEventLoopFactory factory(config);
+
+  factory.RunFor(std::chrono::seconds(1));
+
+  std::exit(EXIT_SUCCESS);
+}
+
+// Checks if either the node is in the specified list of node names or if the
+// list is empty (in which case it is treated as matching all nodes).
+bool NodeInList(
+    const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *list,
+    const aos::Node *node) {
+  if (list == nullptr || list->size() == 0) {
+    return true;
+  }
+  for (const flatbuffers::String *name : *list) {
+    if (name->string_view() == node->name()->string_view()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+}  // namespace
+
+void ConfigIsValid(const aos::Configuration *config,
+                   const ConfigValidatorConfig *validation_config) {
+  ASSERT_TRUE(config->has_channels())
+      << "An AOS config must have channels. If you have a valid use-case for "
+         "channels with no channels, please write a design proposal.";
+
+  // First, we do some sanity checks--these are likely to indicate a malformed
+  // config, and so catching them early with a clear error message is likely to
+  // help.
+
+  // The set of all channels that are required by the channels that are
+  // configured--these are the remote timestamp channels that *must* be present,
+  // and ideally there are no other channels present.
+  std::set<const Channel *> required_timestamp_channels;
+  // The set of all channels that *look* like remote timestamp channels. This
+  // may include channels that are improperly configured and thus have typos &
+  // aren't actually going to do anything at runtime.
+  std::set<const Channel *> configured_timestamp_channels;
+  bool validation_failed = false;
+  for (size_t channel_index = 0; channel_index < config->channels()->size();
+       ++channel_index) {
+    const aos::Channel *channel = config->channels()->Get(channel_index);
+    ASSERT_TRUE(channel->has_name()) << "All AOS channels must have a name.";
+    ASSERT_TRUE(channel->has_type()) << "All AOS channels must have a type.";
+
+    const bool channel_looks_like_remote_message_channel =
+        channel->type()->string_view() ==
+        message_bridge::RemoteMessage::GetFullyQualifiedName();
+
+    const bool check_for_not_logged_channels =
+        !validation_config->has_logging() ||
+        validation_config->logging()->all_channels_logged();
+    const bool channel_is_not_logged =
+        channel->logger() == aos::LoggerConfig::NOT_LOGGED;
+    if (check_for_not_logged_channels) {
+      if (channel_looks_like_remote_message_channel != channel_is_not_logged) {
+        LOG(WARNING)
+            << "Channel " << configuration::StrippedChannelToString(channel)
+            << " is " << EnumNameLoggerConfig(channel->logger()) << " but "
+            << (channel_looks_like_remote_message_channel ? "is" : "is not")
+            << " a remote timestamp channel. This is almost certainly wrong.";
+        validation_failed = true;
+      }
+    }
+
+    if (channel_looks_like_remote_message_channel) {
+      configured_timestamp_channels.insert(channel);
+    } else {
+      if (channel->has_destination_nodes()) {
+        // TODO(james): Technically the timestamp finder should receive a
+        // non-empty application name. However, there are no known users that
+        // care at this moment.
+        message_bridge::ChannelTimestampFinder timestamp_finder(
+            config, "",
+            configuration::GetNode(config,
+                                   channel->source_node()->string_view()));
+        for (const Connection *connection : *channel->destination_nodes()) {
+          switch (connection->timestamp_logger()) {
+            case LoggerConfig::NOT_LOGGED:
+            case LoggerConfig::LOCAL_LOGGER:
+              if (connection->has_timestamp_logger_nodes()) {
+                LOG(WARNING)
+                    << "Connections that are "
+                    << EnumNameLoggerConfig(connection->timestamp_logger())
+                    << " should not have remote timestamp logger nodes "
+                       "populated. This is for the connection to "
+                    << connection->name()->string_view() << " on "
+                    << configuration::StrippedChannelToString(channel);
+                validation_failed = true;
+              }
+              break;
+            case LoggerConfig::REMOTE_LOGGER:
+            case LoggerConfig::LOCAL_AND_REMOTE_LOGGER:
+              if (!connection->has_timestamp_logger_nodes() ||
+                  connection->timestamp_logger_nodes()->size() != 1 ||
+                  connection->timestamp_logger_nodes()->Get(0)->string_view() !=
+                      channel->source_node()->string_view()) {
+                LOG(WARNING)
+                    << "Connections that are "
+                    << EnumNameLoggerConfig(connection->timestamp_logger())
+                    << " should have exactly 1 remote timestamp logger node "
+                       "populated, and that node should be the source_node ("
+                    << channel->source_node()->string_view()
+                    << "). This is for the connection to "
+                    << connection->name()->string_view() << " on "
+                    << configuration::StrippedChannelToString(channel);
+                validation_failed = true;
+              }
+              // TODO(james): This will be overly noisy, as it ends up
+              // CHECK-failing.
+              required_timestamp_channels.insert(CHECK_NOTNULL(
+                  timestamp_finder.ForChannel(channel, connection)));
+              break;
+          }
+        }
+      }
+    }
+  }
+
+  // Check that all of the things that look like timestamp channels are indeed
+  // required.
+  // Note: Because ForChannel() will die if a required channel is not present,
+  // we do not do a separate check that all the required channels exist.
+  for (const auto &channel : configured_timestamp_channels) {
+    if (required_timestamp_channels.count(channel) == 0) {
+      LOG(WARNING) << "Timestamp channel "
+                   << configuration::StrippedChannelToString(channel)
+                   << " was specified in the config but is not used.";
+      validation_failed = true;
+    }
+  }
+
+  if (validation_failed) {
+    FAIL() << "Remote timestamp linting failed.";
+    return;
+  }
+
+  // Because the most common way for simulation to fail involves it dying, force
+  // it to fail in a slightly more controlled manner.
+  ASSERT_EXIT(RunSimulationAndExit(config),
+              ::testing::ExitedWithCode(EXIT_SUCCESS), "");
+
+  if (!validation_config->has_logging() || !configuration::MultiNode(config)) {
+    return;
+  }
+
+  // We will run all the logger configs in two modes:
+  // 1) We don't send any data on any non-infrastructure channels; this confirms
+  //    that the logs are readable in the absence of any user applications being
+  //    present.
+  // 2) We confirm that we can generate a good logfile that actually has data
+  //    on every channel (some checks in the LogReader may not get hit if there
+  //    is no data on a given channel).
+  const std::string log_path = aos::testing::TestTmpDir() + "/logs/";
+  for (const bool send_data_on_channels : {false, true}) {
+    SCOPED_TRACE(send_data_on_channels);
+    for (const LoggerNodeSetValidation *logger_set :
+         *validation_config->logging()->logger_sets()) {
+      SCOPED_TRACE(aos::FlatbufferToJson(logger_set));
+      aos::SimulatedEventLoopFactory factory(config);
+      std::vector<std::unique_ptr<LoggerState>> loggers;
+      if (logger_set->has_loggers() && logger_set->loggers()->size() > 0) {
+        std::vector<std::string> logger_nodes;
+        for (const auto &node : *logger_set->loggers()) {
+          logger_nodes.push_back(node->str());
+        }
+        loggers = MakeLoggersForNodes(&factory, logger_nodes, log_path);
+      } else {
+        loggers = MakeLoggersForAllNodes(&factory, log_path);
+      }
+
+      std::vector<std::unique_ptr<EventLoop>> test_loops;
+      std::map<std::string, std::vector<std::unique_ptr<RawSender>>>
+          test_senders;
+
+      if (send_data_on_channels) {
+        // Make a sender on every non-infrastructure channel on every node
+        // (including channels that may not be observable by the current logger
+        // set).
+        for (const aos::Node *node : configuration::GetNodes(config)) {
+          test_loops.emplace_back(factory.MakeEventLoop("", node));
+          for (const aos::Channel *channel : *config->channels()) {
+            // TODO(james): Make a more sophisticated check for "infrastructure"
+            // channels than just looking for a "/aos" in the channel--we don't
+            // accidentally want to spam nonsense data onto any timestamp
+            // channels, though.
+            if (configuration::ChannelIsSendableOnNode(channel, node) &&
+                channel->name()->str().find("/aos") == std::string::npos &&
+                channel->logger() != LoggerConfig::NOT_LOGGED) {
+              test_senders[node->name()->str()].emplace_back(
+                  test_loops.back()->MakeRawSender(channel));
+              RawSender *sender =
+                  test_senders[node->name()->str()].back().get();
+              test_loops.back()->OnRun([sender, channel]() {
+                flatbuffers::DetachedBuffer buffer =
+                    JsonToFlatbuffer("{}", channel->schema());
+                sender->CheckOk(sender->Send(buffer.data(), buffer.size()));
+              });
+            }
+          }
+        }
+      }
+
+      factory.RunFor(std::chrono::seconds(2));
+
+      // Get all of the loggers to close before trying to read the logfiles.
+      loggers.clear();
+
+      // Confirm that we can read the log, and that if we put data in it that we
+      // can find data on all the nodes that the user cares about.
+      logger::LogReader reader(logger::SortParts(logger::FindLogs(log_path)));
+      SimulatedEventLoopFactory replay_factory(reader.configuration());
+      reader.RegisterWithoutStarting(&replay_factory);
+
+      // Find every channel we deliberately sent data on, and if it is for a
+      // node that we care about, confirm that we get it during replay.
+      std::vector<std::unique_ptr<EventLoop>> replay_loops;
+      std::vector<std::unique_ptr<RawFetcher>> fetchers;
+      for (const aos::Node *node :
+           configuration::GetNodes(replay_factory.configuration())) {
+        // If the user doesn't care about this node, don't check it.
+        if (!NodeInList(logger_set->replay_nodes(), node)) {
+          continue;
+        }
+        replay_loops.emplace_back(replay_factory.MakeEventLoop("", node));
+        for (const auto &sender : test_senders[node->name()->str()]) {
+          const aos::Channel *channel = configuration::GetChannel(
+              replay_factory.configuration(), sender->channel(), "", node);
+          fetchers.emplace_back(replay_loops.back()->MakeRawFetcher(channel));
+        }
+      }
+
+      std::vector<std::pair<const aos::Node *, std::unique_ptr<RawFetcher>>>
+          remote_fetchers;
+      for (const auto &fetcher : fetchers) {
+        for (auto &loop : replay_loops) {
+          const Connection *connection =
+              configuration::ConnectionToNode(fetcher->channel(), loop->node());
+          if (connection != nullptr) {
+            remote_fetchers.push_back(std::make_pair(
+                loop->node(), loop->MakeRawFetcher(fetcher->channel())));
+          }
+        }
+      }
+
+      replay_factory.Run();
+
+      for (auto &fetcher : fetchers) {
+        EXPECT_TRUE(fetcher->Fetch())
+            << "Failed to log or replay any data on "
+            << configuration::StrippedChannelToString(fetcher->channel());
+      }
+
+      for (auto &pair : remote_fetchers) {
+        EXPECT_TRUE(pair.second->Fetch())
+            << "Failed to log or replay any data on "
+            << configuration::StrippedChannelToString(pair.second->channel())
+            << " from remote node " << logger::MaybeNodeName(pair.first) << ".";
+      }
+
+      reader.Deregister();
+
+      // Clean up the logs.
+      UnlinkRecursive(log_path);
+    }
+  }
+}
+
+}  // namespace aos::util
diff --git a/aos/util/config_validator_lib.h b/aos/util/config_validator_lib.h
new file mode 100644
index 0000000..61658e5
--- /dev/null
+++ b/aos/util/config_validator_lib.h
@@ -0,0 +1,12 @@
+#ifndef AOS_UTIL_CONFIG_VALIDATOR_H_
+#define AOS_UTIL_CONFIG_VALIDATOR_H_
+
+#include "aos/configuration.h"
+#include "aos/util/config_validator_config_generated.h"
+#include "gtest/gtest.h"
+namespace aos::util {
+
+void ConfigIsValid(const aos::Configuration *config,
+                   const ConfigValidatorConfig *validation_config);
+}  // namespace aos::util
+#endif  // AOS_UTIL_CONFIG_VALIDATOR_H_
diff --git a/aos/util/config_validator_lib_test.cc b/aos/util/config_validator_lib_test.cc
new file mode 100644
index 0000000..c68695c
--- /dev/null
+++ b/aos/util/config_validator_lib_test.cc
@@ -0,0 +1,189 @@
+#include "aos/util/config_validator_lib.h"
+
+#include "aos/json_to_flatbuffer.h"
+#include "aos/testing/path.h"
+#include "gtest/gtest-spi.h"
+
+using aos::testing::ArtifactPath;
+namespace aos::util::testing {
+
+// Check that a reasonably normal config passes the config validator with a
+// reasonable set of checks turned on.
+TEST(ConfigValidatorTest, NoErrorOnValidConfigs) {
+  const FlatbufferDetachedBuffer<Configuration> config =
+      configuration::ReadConfig(
+          ArtifactPath("aos/util/test_data/valid_multinode_config.json"));
+  const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+      JsonToFlatbuffer<ConfigValidatorConfig>(R"json({"logging": {
+      "all_channels_logged": true,
+      "logger_sets": [
+        {
+          "loggers": [],
+          "replay_nodes": []
+        },
+        {
+          "loggers": ["pi1"],
+          "replay_nodes": ["pi1"]
+        },
+        {
+          "loggers": ["pi2"],
+          "replay_nodes": ["pi2"]
+        }
+      ]}})json");
+  ConfigIsValid(&config.message(), &validator_config.message());
+}
+
+// Check that a reasonably normal single-node config passes the config validator
+// with a reasonable set of checks turned on.
+TEST(ConfigValidatorTest, NoErrorOnValidSingleNodeConfig) {
+  const FlatbufferDetachedBuffer<Configuration> config =
+      configuration::ReadConfig(
+          ArtifactPath("aos/util/test_data/valid_singlenode_config.json"));
+  const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+      JsonToFlatbuffer<ConfigValidatorConfig>(R"json({"logging": {
+      "all_channels_logged": true,
+      "logger_sets": [
+        {
+          "loggers": [],
+          "replay_nodes": []
+        }
+      ]}})json");
+  ConfigIsValid(&config.message(), &validator_config.message());
+}
+
+// Checks that the validator fails if the message bridge statistics channels are
+// missing.
+TEST(ConfigValidatorTest, FailOnMissingStatisticsChannels) {
+  EXPECT_FATAL_FAILURE(
+      {
+        const FlatbufferDetachedBuffer<Configuration> config =
+            configuration::ReadConfig(ArtifactPath(
+                "aos/util/test_data/multinode_no_statistics.json"));
+        const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+            JsonToFlatbuffer<ConfigValidatorConfig>("{}");
+        ConfigIsValid(&config.message(), &validator_config.message());
+      },
+      "Statistics");
+}
+
+// Checks that the validator fails if a timestamp channel has a typo and so
+// doesn't exist.
+TEST(ConfigValidatorTest, FailOnTimestampTypo) {
+  EXPECT_DEATH(
+      {
+        const FlatbufferDetachedBuffer<Configuration> config =
+            configuration::ReadConfig(ArtifactPath(
+                "aos/util/test_data/multinode_timestamp_typo.json"));
+        const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+            JsonToFlatbuffer<ConfigValidatorConfig>("{}");
+        ConfigIsValid(&config.message(), &validator_config.message());
+      },
+      "not found in config");
+}
+
+// Checks that the validator fails if there is a RemoteMessage channel that is
+// *not* a timestamp channel (Since this is almost always a typo).
+TEST(ConfigValidatorTest, FailOnExtraneousTimestampChannel) {
+  EXPECT_FATAL_FAILURE(
+      {
+        const FlatbufferDetachedBuffer<Configuration> config =
+            configuration::ReadConfig(ArtifactPath(
+                "aos/util/test_data/multinode_extraneous_timestamp.json"));
+        const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+            JsonToFlatbuffer<ConfigValidatorConfig>("{}");
+        ConfigIsValid(&config.message(), &validator_config.message());
+      },
+      "linting failed");
+}
+
+// Checks that the validator fails on timestamp logger nodes that won't really
+// log the timestamps.
+TEST(ConfigValidatorTest, FailOnInvalidRemoteTimestampLogger) {
+  EXPECT_FATAL_FAILURE(
+      {
+        const FlatbufferDetachedBuffer<Configuration> config =
+            configuration::ReadConfig(
+                ArtifactPath("aos/util/test_data/"
+                             "multinode_invalid_timestamp_logger_list.json"));
+        const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+            JsonToFlatbuffer<ConfigValidatorConfig>(R"json({"logging": {
+      "all_channels_logged": true,
+      "logger_sets": [
+        {
+          "loggers": [],
+          "replay_nodes": []
+        }
+      ]}})json");
+        ConfigIsValid(&config.message(), &validator_config.message());
+      },
+      "linting failed");
+}
+
+// Checks that if you attempt to log on pi2 but expect it to have data for pi1
+// then the test fails (at least, for a config which does not forward all the
+// channels between the nodes).
+TEST(ConfigValidatorTest, FailOnNormalInsufficientLogging) {
+  EXPECT_NONFATAL_FAILURE(
+      {
+        const FlatbufferDetachedBuffer<Configuration> config =
+            configuration::ReadConfig(
+                ArtifactPath("aos/util/test_data/valid_multinode_config.json"));
+        const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+            JsonToFlatbuffer<ConfigValidatorConfig>(R"json({"logging": {
+      "all_channels_logged": true,
+      "logger_sets": [
+        {
+          "loggers": ["pi2"],
+          "replay_nodes": ["pi1"]
+        }
+      ]}})json");
+        ConfigIsValid(&config.message(), &validator_config.message());
+      },
+      "Failed to log");
+}
+
+// Checks that if we have a node that is configured to log all the data from all
+// the nodes that the test passes.
+TEST(ConfigValidatorTest, PassCommonLoggerNode) {
+  const FlatbufferDetachedBuffer<Configuration> config =
+      configuration::ReadConfig(
+          ArtifactPath("aos/util/test_data/multinode_common_logger.json"));
+  const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+      JsonToFlatbuffer<ConfigValidatorConfig>(R"json({"logging": {
+      "all_channels_logged": true,
+      "logger_sets": [
+        {
+          "loggers": ["pi2"],
+          "replay_nodes": ["pi1"]
+        },
+        {
+          "loggers": [],
+          "replay_nodes": []
+        }
+      ]}})json");
+  ConfigIsValid(&config.message(), &validator_config.message());
+}
+
+// Sets up a config that will not actually log sufficient timestamp data to
+// support full replay, and ensures that we identify that.
+TEST(ConfigValidatorTest, FailOnInsufficientConfiguredTimestampData) {
+  EXPECT_NONFATAL_FAILURE(
+      {
+        const FlatbufferDetachedBuffer<Configuration> config =
+            configuration::ReadConfig(ArtifactPath(
+                "aos/util/test_data/multinode_no_logged_timestamps.json"));
+        const FlatbufferDetachedBuffer<ConfigValidatorConfig> validator_config =
+            JsonToFlatbuffer<ConfigValidatorConfig>(R"json({"logging": {
+      "all_channels_logged": true,
+      "logger_sets": [
+        {
+          "loggers": [],
+          "replay_nodes": []
+        }
+      ]}})json");
+        ConfigIsValid(&config.message(), &validator_config.message());
+      },
+      R"json(Failed to log or replay any data on { "name": "/test", "type": "aos.examples.Ping" } from remote node pi2)json");
+}
+
+}  // namespace aos::util::testing
diff --git a/aos/util/config_validator_macro.bzl b/aos/util/config_validator_macro.bzl
index 453c5c2..cd05bab 100644
--- a/aos/util/config_validator_macro.bzl
+++ b/aos/util/config_validator_macro.bzl
@@ -1,20 +1,17 @@
-def config_validator_rule(name, config, extension = ".bfbs", visibility = None):
+def config_validator_test(name, config, logger_sets = [{}], check_for_not_logged_channels = False, extension = ".bfbs", visibility = None):
     '''
     Macro to take a config and pass it to the config validator to validate that it will work on a real system.
 
-    Currently just checks that the system can startup, but will check that timestamp channels are properly logged in the future.
-
     Args:
         name: name that the config validator uses, e.g. "test_config",
         config: config rule that needs to be validated, e.g. "//aos/events:pingpong_config",
     '''
     config_file = config + extension
-    native.genrule(
+    config_json = json.encode({"logging": {"all_channels_logged": check_for_not_logged_channels, "logger_sets": logger_sets}})
+    native.cc_test(
         name = name,
-        outs = [name + ".txt"],
-        cmd = "$(location //aos/util:config_validator) --config $(location %s) > $@" % config_file,
-        srcs = [config_file],
-        tools = ["//aos/util:config_validator"],
-        testonly = True,
+        deps = ["//aos/util:config_validator"],
+        args = ["--config=$(location %s)" % config_file, "--validation_config='%s'" % config_json],
+        data = [config_file],
         visibility = visibility,
     )
diff --git a/aos/util/file_test.cc b/aos/util/file_test.cc
index d4382c4..ba03ea5 100644
--- a/aos/util/file_test.cc
+++ b/aos/util/file_test.cc
@@ -7,8 +7,6 @@
 #include "aos/testing/tmpdir.h"
 #include "gtest/gtest.h"
 
-DECLARE_bool(die_on_malloc);
-
 namespace aos {
 namespace util {
 namespace testing {
@@ -52,9 +50,6 @@
 
   FileReader reader(test_file);
 
-  gflags::FlagSaver flag_saver;
-  FLAGS_die_on_malloc = true;
-  RegisterMallocHook();
   aos::ScopedRealtime realtime;
   {
     std::array<char, 20> contents;
@@ -79,9 +74,6 @@
 
   FileWriter writer(test_file);
 
-  gflags::FlagSaver flag_saver;
-  FLAGS_die_on_malloc = true;
-  RegisterMallocHook();
   FileWriter::WriteResult result;
   {
     aos::ScopedRealtime realtime;
@@ -104,9 +96,6 @@
   // Mess up the file management by closing the file descriptor.
   PCHECK(0 == close(writer.fd()));
 
-  gflags::FlagSaver flag_saver;
-  FLAGS_die_on_malloc = true;
-  RegisterMallocHook();
   FileWriter::WriteResult result;
   {
     aos::ScopedRealtime realtime;
diff --git a/aos/util/log_to_mcap.cc b/aos/util/log_to_mcap.cc
index 38a3be7..d4e240f 100644
--- a/aos/util/log_to_mcap.cc
+++ b/aos/util/log_to_mcap.cc
@@ -74,33 +74,31 @@
   std::unique_ptr<aos::EventLoop> clock_event_loop;
   std::unique_ptr<aos::ClockPublisher> clock_publisher;
   if (FLAGS_include_clocks) {
-    reader.OnStart(node, [&clock_event_loop, &reader, &clock_publisher,
-                          &factory, node]() {
-      clock_event_loop =
-          reader.event_loop_factory()->MakeEventLoop("clock", node);
-      clock_publisher = std::make_unique<aos::ClockPublisher>(
-          &factory, clock_event_loop.get());
-    });
+    reader.OnStart(
+        node, [&clock_event_loop, &reader, &clock_publisher, &factory, node]() {
+          clock_event_loop =
+              reader.event_loop_factory()->MakeEventLoop("clock", node);
+          clock_publisher = std::make_unique<aos::ClockPublisher>(
+              &factory, clock_event_loop.get());
+        });
   }
 
   std::unique_ptr<aos::EventLoop> mcap_event_loop;
   CHECK(!FLAGS_output_path.empty());
   std::unique_ptr<aos::McapLogger> relogger;
-  factory.GetNodeEventLoopFactory(node)
-      ->OnStartup([&relogger, &mcap_event_loop, &reader, node]() {
-        mcap_event_loop =
-            reader.event_loop_factory()->MakeEventLoop("mcap", node);
-        relogger = std::make_unique<aos::McapLogger>(
-            mcap_event_loop.get(), FLAGS_output_path,
-            FLAGS_mode == "flatbuffer"
-                ? aos::McapLogger::Serialization::kFlatbuffer
-                : aos::McapLogger::Serialization::kJson,
-            FLAGS_canonical_channel_names
-                ? aos::McapLogger::CanonicalChannelNames::kCanonical
-                : aos::McapLogger::CanonicalChannelNames::kShortened,
-            FLAGS_compress ? aos::McapLogger::Compression::kLz4
-                           : aos::McapLogger::Compression::kNone);
-      });
+  factory.GetNodeEventLoopFactory(node)->OnStartup([&relogger, &mcap_event_loop,
+                                                    &reader, node]() {
+    mcap_event_loop = reader.event_loop_factory()->MakeEventLoop("mcap", node);
+    relogger = std::make_unique<aos::McapLogger>(
+        mcap_event_loop.get(), FLAGS_output_path,
+        FLAGS_mode == "flatbuffer" ? aos::McapLogger::Serialization::kFlatbuffer
+                                   : aos::McapLogger::Serialization::kJson,
+        FLAGS_canonical_channel_names
+            ? aos::McapLogger::CanonicalChannelNames::kCanonical
+            : aos::McapLogger::CanonicalChannelNames::kShortened,
+        FLAGS_compress ? aos::McapLogger::Compression::kLz4
+                       : aos::McapLogger::Compression::kNone);
+  });
   reader.event_loop_factory()->Run();
   reader.Deregister();
 }
diff --git a/aos/util/simulation_logger.cc b/aos/util/simulation_logger.cc
new file mode 100644
index 0000000..1f55ece
--- /dev/null
+++ b/aos/util/simulation_logger.cc
@@ -0,0 +1,40 @@
+#include "aos/util/simulation_logger.h"
+#include "aos/events/logging/logfile_utils.h"
+
+namespace aos::util {
+LoggerState::LoggerState(aos::SimulatedEventLoopFactory *factory,
+                         const aos::Node *node, std::string_view output_folder)
+    : event_loop_(factory->MakeEventLoop("logger", node)),
+      namer_(std::make_unique<aos::logger::MultiNodeFilesLogNamer>(
+          absl::StrCat(output_folder, "/", logger::MaybeNodeName(node), "/"),
+          event_loop_.get())),
+      logger_(std::make_unique<aos::logger::Logger>(event_loop_.get())) {
+  event_loop_->SkipTimingReport();
+  event_loop_->SkipAosLog();
+  event_loop_->OnRun([this]() { logger_->StartLogging(std::move(namer_)); });
+}
+
+std::vector<std::unique_ptr<LoggerState>> MakeLoggersForNodes(
+    aos::SimulatedEventLoopFactory *factory,
+    const std::vector<std::string> &nodes_to_log,
+    std::string_view output_folder) {
+  std::vector<std::unique_ptr<LoggerState>> loggers;
+  for (const std::string &node : nodes_to_log) {
+    loggers.emplace_back(std::make_unique<LoggerState>(
+        factory, aos::configuration::GetNode(factory->configuration(), node),
+        output_folder));
+  }
+  return loggers;
+}
+
+std::vector<std::unique_ptr<LoggerState>> MakeLoggersForAllNodes(
+    aos::SimulatedEventLoopFactory *factory, std::string_view output_folder) {
+  std::vector<std::unique_ptr<LoggerState>> loggers;
+  for (const aos::Node *node : configuration::GetNodes(factory->configuration())) {
+    loggers.emplace_back(
+        std::make_unique<LoggerState>(factory, node, output_folder));
+  }
+  return loggers;
+}
+
+}  // namespace aos::util
diff --git a/aos/util/simulation_logger.h b/aos/util/simulation_logger.h
new file mode 100644
index 0000000..f431b4c
--- /dev/null
+++ b/aos/util/simulation_logger.h
@@ -0,0 +1,33 @@
+#ifndef AOS_UTIL_SIMULATION_LOGGER_H_
+#define AOS_UTIL_SIMULATION_LOGGER_H_
+#include <string_view>
+
+#include "aos/events/logging/log_writer.h"
+#include "aos/events/simulated_event_loop.h"
+namespace aos::util {
+
+class LoggerState {
+ public:
+  LoggerState(aos::SimulatedEventLoopFactory *factory, const aos::Node *node,
+              std::string_view output_folder);
+
+ private:
+  std::unique_ptr<aos::EventLoop> event_loop_;
+  std::unique_ptr<aos::logger::LogNamer> namer_;
+  std::unique_ptr<aos::logger::Logger> logger_;
+};
+
+// Creates a logger for each of the specified nodes. This makes it so that you
+// can easily setup some number of loggers in simulation or log replay without
+// needing to redo all the boilerplate every time.
+std::vector<std::unique_ptr<LoggerState>> MakeLoggersForNodes(
+    aos::SimulatedEventLoopFactory *factory,
+    const std::vector<std::string> &nodes_to_log,
+    std::string_view output_folder);
+
+// Creates loggers for all of the nodes.
+std::vector<std::unique_ptr<LoggerState>> MakeLoggersForAllNodes(
+    aos::SimulatedEventLoopFactory *factory, std::string_view output_folder);
+
+}  // namespace aos::util
+#endif  // AOS_UTIL_SIMULATION_LOGGER_H_
diff --git a/aos/util/test_data/BUILD b/aos/util/test_data/BUILD
new file mode 100644
index 0000000..016dd07
--- /dev/null
+++ b/aos/util/test_data/BUILD
@@ -0,0 +1,28 @@
+load("//aos:config.bzl", "aos_config")
+
+[
+    aos_config(
+        name = name,
+        src = name + "_source.json",
+        flatbuffers = [
+            "//aos/network:remote_message_fbs",
+            "//aos/events:ping_fbs",
+            "//aos/network:message_bridge_client_fbs",
+            "//aos/network:message_bridge_server_fbs",
+            "//aos/network:timestamp_fbs",
+        ],
+        target_compatible_with = ["@platforms//os:linux"],
+        visibility = ["//visibility:public"],
+        deps = ["//aos/events:aos_config"],
+    )
+    for name in [
+        "valid_multinode_config",
+        "valid_singlenode_config",
+        "multinode_no_statistics",
+        "multinode_timestamp_typo",
+        "multinode_extraneous_timestamp",
+        "multinode_invalid_timestamp_logger_list",
+        "multinode_common_logger",
+        "multinode_no_logged_timestamps",
+    ]
+]
diff --git a/aos/util/test_data/multinode_common_logger_source.json b/aos/util/test_data/multinode_common_logger_source.json
new file mode 100644
index 0000000..81aa065
--- /dev/null
+++ b/aos/util/test_data/multinode_common_logger_source.json
@@ -0,0 +1,156 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi2",
+      "frequency": 2
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": ["pi2"],
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "max_size": 20480
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    }
+  ]
+}
diff --git a/aos/util/test_data/multinode_extraneous_timestamp_source.json b/aos/util/test_data/multinode_extraneous_timestamp_source.json
new file mode 100644
index 0000000..2e889d0
--- /dev/null
+++ b/aos/util/test_data/multinode_extraneous_timestamp_source.json
@@ -0,0 +1,161 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi2",
+      "frequency": 2
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/os-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "max_size": 20480
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    }
+  ]
+}
diff --git a/aos/util/test_data/multinode_invalid_timestamp_logger_list_source.json b/aos/util/test_data/multinode_invalid_timestamp_logger_list_source.json
new file mode 100644
index 0000000..e501437
--- /dev/null
+++ b/aos/util/test_data/multinode_invalid_timestamp_logger_list_source.json
@@ -0,0 +1,154 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi2",
+      "frequency": 2
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "max_size": 20480
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    }
+  ]
+}
diff --git a/aos/util/test_data/multinode_no_logged_timestamps_source.json b/aos/util/test_data/multinode_no_logged_timestamps_source.json
new file mode 100644
index 0000000..a956f73
--- /dev/null
+++ b/aos/util/test_data/multinode_no_logged_timestamps_source.json
@@ -0,0 +1,147 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi2",
+      "frequency": 2
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "NOT_LOGGED"
+        }
+      ],
+      "max_size": 20480
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    }
+  ]
+}
diff --git a/aos/util/test_data/multinode_no_statistics_source.json b/aos/util/test_data/multinode_no_statistics_source.json
new file mode 100644
index 0000000..44fa5d8
--- /dev/null
+++ b/aos/util/test_data/multinode_no_statistics_source.json
@@ -0,0 +1,130 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "max_size": 20480
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    }
+  ]
+}
diff --git a/aos/util/test_data/multinode_timestamp_typo_source.json b/aos/util/test_data/multinode_timestamp_typo_source.json
new file mode 100644
index 0000000..afb4275
--- /dev/null
+++ b/aos/util/test_data/multinode_timestamp_typo_source.json
@@ -0,0 +1,154 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi2",
+      "frequency": 2
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestam",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "max_size": 20480
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    }
+  ]
+}
diff --git a/aos/util/test_data/valid_multinode_config_source.json b/aos/util/test_data/valid_multinode_config_source.json
new file mode 100644
index 0000000..0c35251
--- /dev/null
+++ b/aos/util/test_data/valid_multinode_config_source.json
@@ -0,0 +1,154 @@
+{
+  "channels": [
+    {
+      "name": "/pi1/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 15,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi2"],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi2",
+      "frequency": 2
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1",
+      "frequency": 15
+    },
+    {
+      "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi2",
+      "frequency": 15
+    },
+    {
+      "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
+      "type": "aos.message_bridge.RemoteMessage",
+      "logger": "NOT_LOGGED",
+      "source_node": "pi1"
+    },
+    {
+      "name": "/pi1/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/pi2/aos",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "source_node": "pi1",
+      "destination_nodes": [
+        {
+          "name": "pi2",
+          "priority": 1,
+          "timestamp_logger": "REMOTE_LOGGER",
+          "timestamp_logger_nodes": ["pi1"]
+        }
+      ],
+      "max_size": 20480
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/aos"
+      }
+    },
+    {
+      "match": {
+        "name": "/aos*",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/aos"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    }
+  ]
+}
diff --git a/aos/util/test_data/valid_singlenode_config_source.json b/aos/util/test_data/valid_singlenode_config_source.json
new file mode 100644
index 0000000..736eace
--- /dev/null
+++ b/aos/util/test_data/valid_singlenode_config_source.json
@@ -0,0 +1,23 @@
+{
+  "channels": [
+    {
+      "name": "/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "frequency": 200,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/aos",
+      "type": "aos.timing.Report",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/test",
+      "type": "aos.examples.Ping",
+      "max_size": 20480
+    }
+  ]
+}
diff --git a/frc971/control_loops/drivetrain/localization/puppet_localizer_test.cc b/frc971/control_loops/drivetrain/localization/puppet_localizer_test.cc
index d64c419..e45f880 100644
--- a/frc971/control_loops/drivetrain/localization/puppet_localizer_test.cc
+++ b/frc971/control_loops/drivetrain/localization/puppet_localizer_test.cc
@@ -16,7 +16,6 @@
 
 DEFINE_string(output_folder, "",
               "If set, logs all channels to the provided logfile.");
-DECLARE_bool(die_on_malloc);
 
 namespace frc971 {
 namespace control_loops {
@@ -78,7 +77,6 @@
         drivetrain_plant_(drivetrain_plant_event_loop_.get(),
                           drivetrain_plant_imu_event_loop_.get(), dt_config_,
                           std::chrono::microseconds(500)) {
-    FLAGS_die_on_malloc = true;
     set_team_id(frc971::control_loops::testing::kTeamNumber);
     set_battery_voltage(12.0);
 
diff --git a/frc971/solvers/BUILD b/frc971/solvers/BUILD
new file mode 100644
index 0000000..fc89616
--- /dev/null
+++ b/frc971/solvers/BUILD
@@ -0,0 +1,22 @@
+cc_library(
+    name = "convex",
+    hdrs = ["convex.h"],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_google_glog//:glog",
+        "@org_tuxfamily_eigen//:eigen",
+    ],
+)
+
+cc_test(
+    name = "convex_test",
+    srcs = [
+        "convex_test.cc",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":convex",
+        "//aos/testing:googletest",
+    ],
+)
diff --git a/frc971/solvers/convex.h b/frc971/solvers/convex.h
new file mode 100644
index 0000000..aa14c1a
--- /dev/null
+++ b/frc971/solvers/convex.h
@@ -0,0 +1,381 @@
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <Eigen/Dense>
+#include <iomanip>
+
+#include "absl/strings/str_join.h"
+#include "glog/logging.h"
+
+namespace frc971 {
+namespace solvers {
+
+// TODO(austin): Steal JET from Ceres to generate the derivatives easily and
+// quickly?
+//
+// States is the number of inputs to the optimization problem.
+// M is the number of inequality constraints.
+// N is the number of equality constraints.
+template <size_t States, size_t M, size_t N>
+class ConvexProblem {
+ public:
+  // Returns the function to minimize and it's derivatives.
+  virtual double f0(
+      Eigen::Ref<const Eigen::Matrix<double, States, 1>> X) const = 0;
+  virtual Eigen::Matrix<double, States, 1> df0(
+      Eigen::Ref<const Eigen::Matrix<double, States, 1>> X) const = 0;
+  virtual Eigen::Matrix<double, States, States> ddf0(
+      Eigen::Ref<const Eigen::Matrix<double, States, 1>> X) const = 0;
+
+  // Returns the constraints f(X) < 0, and their derivative.
+  virtual Eigen::Matrix<double, M, 1> f(
+      Eigen::Ref<const Eigen::Matrix<double, States, 1>> X) const = 0;
+  virtual Eigen::Matrix<double, M, States> df(
+      Eigen::Ref<const Eigen::Matrix<double, States, 1>> X) const = 0;
+
+  // Returns the equality constraints of the form A x = b
+  virtual Eigen::Matrix<double, N, States> A() const = 0;
+  virtual Eigen::Matrix<double, N, 1> b() const = 0;
+};
+
+// Implements a Primal-Dual Interior point method convex solver.
+// See 11.7 of https://web.stanford.edu/~boyd/cvxbook/bv_cvxbook.pdf
+//
+// States is the number of inputs to the optimization problem.
+// M is the number of inequality constraints.
+// N is the number of equality constraints.
+template <size_t States, size_t M, size_t N>
+class Solver {
+ public:
+  // Ratio to require the cost to decrease when line searching.
+  static constexpr double kAlpha = 0.05;
+  // Line search step parameter.
+  static constexpr double kBeta = 0.5;
+  static constexpr double kMu = 2.0;
+  // Terminal condition for the primal problem (equality constraints) and dual
+  // (gradient + inequality constraints).
+  static constexpr double kEpsilonF = 1e-6;
+  // Terminal condition for nu, the surrogate duality gap.
+  static constexpr double kEpsilon = 1e-6;
+
+  // Solves the problem given a feasible initial solution.
+  Eigen::Matrix<double, States, 1> Solve(
+      const ConvexProblem<States, M, N> &problem,
+      Eigen::Ref<const Eigen::Matrix<double, States, 1>> X_initial);
+
+ private:
+  // Class to hold all the derivataves and function evaluations.
+  struct Derivatives {
+    Eigen::Matrix<double, States, 1> gradient;
+    Eigen::Matrix<double, States, States> hessian;
+
+    // Inequality function f
+    Eigen::Matrix<double, M, 1> f;
+    // df
+    Eigen::Matrix<double, M, States> df;
+
+    // ddf is assumed to be 0 because for the linear constraint distance
+    // function we are using, it is actually 0, and by assuming it is zero
+    // rather than passing it through as 0 to the solver, we can save enough CPU
+    // to make it worth it.
+
+    // A
+    Eigen::Matrix<double, N, States> A;
+    // Ax - b
+    Eigen::Matrix<double, N, 1> Axmb;
+  };
+
+  // Computes all the values for the given problem at the given state.
+  Derivatives ComputeDerivative(
+      const ConvexProblem<States, M, N> &problem,
+      const Eigen::Ref<const Eigen::Matrix<double, States + M + N, 1>> y);
+
+  // Computes Rt at the given state and with the given t_inverse.  See 11.53 of
+  // cvxbook.pdf.
+  Eigen::Matrix<double, States + M + N, 1> Rt(
+      const Derivatives &derivatives,
+      Eigen::Matrix<double, States + M + N, 1> y, double t_inverse);
+
+  // Prints out all the derivatives with VLOG at the provided verbosity.
+  void PrintDerivatives(
+      const Derivatives &derivatives,
+      const Eigen::Ref<const Eigen::Matrix<double, States + M + N, 1>> y,
+      std::string_view prefix, int verbosity);
+};
+
+template <size_t States, size_t M, size_t N>
+Eigen::Matrix<double, States + M + N, 1> Solver<States, M, N>::Rt(
+    const Derivatives &derivatives, Eigen::Matrix<double, States + M + N, 1> y,
+    double t_inverse) {
+  Eigen::Matrix<double, States + M + N, 1> result;
+
+  Eigen::Ref<Eigen::Matrix<double, States, 1>> r_dual =
+      result.template block<States, 1>(0, 0);
+  Eigen::Ref<Eigen::Matrix<double, M, 1>> r_cent =
+      result.template block<M, 1>(States, 0);
+  Eigen::Ref<Eigen::Matrix<double, N, 1>> r_pri =
+      result.template block<N, 1>(States + M, 0);
+
+  Eigen::Ref<const Eigen::Matrix<double, M, 1>> lambda =
+      y.template block<M, 1>(States, 0);
+  Eigen::Ref<const Eigen::Matrix<double, N, 1>> v =
+      y.template block<N, 1>(States + M, 0);
+
+  r_dual = derivatives.gradient + derivatives.df.transpose() * lambda +
+           derivatives.A.transpose() * v;
+  r_cent = -(Eigen::DiagonalMatrix<double, M>(lambda) * derivatives.f +
+             t_inverse * Eigen::Matrix<double, M, 1>::Ones());
+  r_pri = derivatives.Axmb;
+
+  return result;
+}
+
+template <size_t States, size_t M, size_t N>
+Eigen::Matrix<double, States, 1> Solver<States, M, N>::Solve(
+    const ConvexProblem<States, M, N> &problem,
+    Eigen::Ref<const Eigen::Matrix<double, States, 1>> X_initial) {
+  const Eigen::IOFormat kHeavyFormat(Eigen::StreamPrecision, 0, ", ",
+                                     ",\n                        "
+                                     "                                     ",
+                                     "[", "]", "[", "]");
+
+  Eigen::Matrix<double, States + M + N, 1> y =
+      Eigen::Matrix<double, States + M + N, 1>::Constant(1.0);
+  y.template block<States, 1>(0, 0) = X_initial;
+
+  Derivatives derivatives = ComputeDerivative(problem, y);
+
+  for (size_t i = 0; i < M; ++i) {
+    CHECK_LE(derivatives.f(i, 0), 0.0)
+        << ": Initial state " << X_initial.transpose().format(kHeavyFormat)
+        << " not feasible";
+  }
+
+  PrintDerivatives(derivatives, y, "", 1);
+
+  size_t iteration = 0;
+  while (true) {
+    // Solve for the primal-dual search direction by solving the newton step.
+    Eigen::Ref<const Eigen::Matrix<double, M, 1>> lambda =
+        y.template block<M, 1>(States, 0);
+
+    const double nu = -(derivatives.f.transpose() * lambda)(0, 0);
+    const double t_inverse = nu / (kMu * lambda.rows());
+    Eigen::Matrix<double, States + M + N, 1> rt_orig =
+        Rt(derivatives, y, t_inverse);
+
+    Eigen::Matrix<double, States + M + N, States + M + N> m1;
+    m1.setZero();
+    m1.template block<States, States>(0, 0) = derivatives.hessian;
+    m1.template block<States, M>(0, States) = derivatives.df.transpose();
+    m1.template block<States, N>(0, States + M) = derivatives.A.transpose();
+    m1.template block<M, States>(States, 0) =
+        -(Eigen::DiagonalMatrix<double, M>(lambda) * derivatives.df);
+    m1.template block<M, M>(States, States) -=
+        Eigen::DiagonalMatrix<double, M>(derivatives.f);
+    m1.template block<N, States>(States + M, 0) = derivatives.A;
+
+    Eigen::Matrix<double, States + M + N, 1> dy =
+        m1.colPivHouseholderQr().solve(-rt_orig);
+
+    Eigen::Ref<Eigen::Matrix<double, M, 1>> dlambda =
+        dy.template block<M, 1>(States, 0);
+
+    double s = 1.0;
+
+    // Now, time to do line search.
+    //
+    // Start by keeping lambda positive.  Make sure our step doesn't let
+    // lambda cross 0.
+    for (int i = 0; i < dlambda.rows(); ++i) {
+      if (lambda(i) + s * dlambda(i) < 0.0) {
+        // Ignore tiny steps in lambda.  They cause issues when we get really
+        // close to having our constraints met but haven't converged the rest
+        // of the problem and start to run into rounding issues in the matrix
+        // solve portion.
+        if (dlambda(i) < 0.0 && dlambda(i) > -1e-12) {
+          VLOG(1) << "  lambda(" << i << ") " << lambda(i) << " + " << s
+                  << " * " << dlambda(i) << " -> s would be now "
+                  << -lambda(i) / dlambda(i);
+          dlambda(i) = 0.0;
+          VLOG(1) << "  dy -> " << std::setprecision(12) << std::fixed
+                  << std::setfill(' ') << dy.transpose().format(kHeavyFormat);
+          continue;
+        }
+        VLOG(1) << "  lambda(" << i << ") " << lambda(i) << " + " << s << " * "
+                << dlambda(i) << " -> s now " << -lambda(i) / dlambda(i);
+        s = -lambda(i) / dlambda(i);
+      }
+    }
+
+    VLOG(1) << "  After lambda line search, s is " << s;
+
+    VLOG(3) << "  Initial step " << iteration << " -> " << std::setprecision(12)
+            << std::fixed << std::setfill(' ')
+            << dy.transpose().format(kHeavyFormat);
+    VLOG(3) << "   rt ->                                        "
+            << std::setprecision(12) << std::fixed << std::setfill(' ')
+            << rt_orig.transpose().format(kHeavyFormat);
+
+    const double rt_orig_squared_norm = rt_orig.squaredNorm();
+
+    Eigen::Matrix<double, States + M + N, 1> next_y;
+    Eigen::Matrix<double, States + M + N, 1> rt;
+    Derivatives next_derivatives;
+    while (true) {
+      next_y = y + s * dy;
+      next_derivatives = ComputeDerivative(problem, next_y);
+      rt = Rt(next_derivatives, next_y, t_inverse);
+
+      const Eigen::Ref<const Eigen::VectorXd> next_x =
+          next_y.block(0, 0, next_derivatives.hessian.rows(), 1);
+      const Eigen::Ref<const Eigen::VectorXd> next_lambda =
+          next_y.block(next_x.rows(), 0, next_derivatives.f.rows(), 1);
+
+      const Eigen::Ref<const Eigen::VectorXd> next_v = next_y.block(
+          next_x.rows() + next_lambda.rows(), 0, next_derivatives.A.rows(), 1);
+
+      VLOG(1) << "    next_rt(" << iteration << ") is " << rt.norm() << " -> "
+              << std::setprecision(12) << std::fixed << std::setfill(' ')
+              << rt.transpose().format(kHeavyFormat);
+
+      PrintDerivatives(next_derivatives, next_y, "next_", 3);
+
+      if (next_derivatives.f.maxCoeff() > 0.0) {
+        VLOG(1) << "   f_next > 0.0  -> " << next_derivatives.f.maxCoeff()
+                << ", continuing line search.";
+        s *= kBeta;
+      } else if (next_derivatives.Axmb.squaredNorm() < 0.1 &&
+                 rt.squaredNorm() >
+                     std::pow(1.0 - kAlpha * s, 2.0) * rt_orig_squared_norm) {
+        VLOG(1) << "   |Rt| > |Rt+1| " << rt.norm() << " >  " << rt_orig.norm()
+                << ", drt -> " << std::setprecision(12) << std::fixed
+                << std::setfill(' ')
+                << (rt_orig - rt).transpose().format(kHeavyFormat);
+        s *= kBeta;
+      } else {
+        break;
+      }
+    }
+
+    VLOG(1) << "  Terminated line search with s " << s << ", " << rt.norm()
+            << "(|Rt+1|) < " << rt_orig.norm() << "(|Rt|)";
+    y = next_y;
+
+    const Eigen::Ref<const Eigen::VectorXd> next_lambda =
+        y.template block<M, 1>(States, 0);
+
+    // See if we hit our convergence criteria.
+    const double r_primal_squared_norm =
+        rt.template block<N, 1>(States + M, 0).squaredNorm();
+    VLOG(1) << "  rt_next(" << iteration << ") is " << rt.norm() << " -> "
+            << std::setprecision(12) << std::fixed << std::setfill(' ')
+            << rt.transpose().format(kHeavyFormat);
+    if (r_primal_squared_norm < kEpsilonF * kEpsilonF) {
+      const double r_dual_squared_norm =
+          rt.template block<States, 1>(0, 0).squaredNorm();
+      if (r_dual_squared_norm < kEpsilonF * kEpsilonF) {
+        const double next_nu =
+            -(next_derivatives.f.transpose() * next_lambda)(0, 0);
+        if (next_nu < kEpsilon) {
+          VLOG(1) << "  r_primal(" << iteration << ") -> "
+                  << std::sqrt(r_primal_squared_norm) << " < " << kEpsilonF
+                  << ", r_dual(" << iteration << ") -> "
+                  << std::sqrt(r_dual_squared_norm) << " < " << kEpsilonF
+                  << ", nu(" << iteration << ") -> " << next_nu << " < "
+                  << kEpsilon;
+          break;
+        } else {
+          VLOG(1) << "  nu(" << iteration << ") -> " << next_nu << " < "
+                  << kEpsilon << ", not done yet";
+        }
+
+      } else {
+        VLOG(1) << "  r_dual(" << iteration << ") -> "
+                << std::sqrt(r_dual_squared_norm) << " < " << kEpsilonF
+                << ", not done yet";
+      }
+    } else {
+      VLOG(1) << "  r_primal(" << iteration << ") -> "
+              << std::sqrt(r_primal_squared_norm) << " < " << kEpsilonF
+              << ", not done yet";
+    }
+    VLOG(1) << "  step(" << iteration << ") " << std::setprecision(12)
+            << (s * dy).transpose().format(kHeavyFormat);
+    VLOG(1) << " y(" << iteration << ") is now " << std::setprecision(12)
+            << y.transpose().format(kHeavyFormat);
+
+    // Very import, use the last set of derivatives we picked for our new y
+    // for the next iteration.  This avoids re-computing it.
+    derivatives = std::move(next_derivatives);
+
+    ++iteration;
+    if (iteration > 100) {
+      LOG(FATAL) << "Too many iterations";
+    }
+  }
+
+  return y.template block<States, 1>(0, 0);
+}
+
+template <size_t States, size_t M, size_t N>
+typename Solver<States, M, N>::Derivatives
+Solver<States, M, N>::ComputeDerivative(
+    const ConvexProblem<States, M, N> &problem,
+    const Eigen::Ref<const Eigen::Matrix<double, States + M + N, 1>> y) {
+  const Eigen::Ref<const Eigen::Matrix<double, States, 1>> x =
+      y.template block<States, 1>(0, 0);
+
+  Derivatives derivatives;
+  derivatives.gradient = problem.df0(x);
+  derivatives.hessian = problem.ddf0(x);
+  derivatives.f = problem.f(x);
+  derivatives.df = problem.df(x);
+  derivatives.A = problem.A();
+  derivatives.Axmb =
+      derivatives.A * y.template block<States, 1>(0, 0) - problem.b();
+  return derivatives;
+}
+
+template <size_t States, size_t M, size_t N>
+void Solver<States, M, N>::PrintDerivatives(
+    const Derivatives &derivatives,
+    const Eigen::Ref<const Eigen::Matrix<double, States + M + N, 1>> y,
+    std::string_view prefix, int verbosity) {
+  const Eigen::Ref<const Eigen::VectorXd> x =
+      y.block(0, 0, derivatives.hessian.rows(), 1);
+  const Eigen::Ref<const Eigen::VectorXd> lambda =
+      y.block(x.rows(), 0, derivatives.f.rows(), 1);
+
+  if (VLOG_IS_ON(verbosity)) {
+    Eigen::IOFormat heavy(Eigen::StreamPrecision, 0, ", ",
+                          ",\n                        "
+                          "                                     ",
+                          "[", "]", "[", "]");
+    heavy.rowSeparator =
+        heavy.rowSeparator +
+        std::string(absl::StrCat(getpid()).size() + prefix.size(), ' ');
+
+    const Eigen::Ref<const Eigen::VectorXd> v =
+        y.block(x.rows() + lambda.rows(), 0, derivatives.A.rows(), 1);
+    VLOG(verbosity) << "   " << prefix << "x: " << x.transpose().format(heavy);
+    VLOG(verbosity) << "   " << prefix
+                    << "lambda: " << lambda.transpose().format(heavy);
+    VLOG(verbosity) << "   " << prefix << "v: " << v.transpose().format(heavy);
+    VLOG(verbosity) << "  " << prefix
+                    << "hessian:     " << derivatives.hessian.format(heavy);
+    VLOG(verbosity) << "  " << prefix
+                    << "gradient:    " << derivatives.gradient.format(heavy);
+    VLOG(verbosity) << "  " << prefix
+                    << "A:           " << derivatives.A.format(heavy);
+    VLOG(verbosity) << "  " << prefix
+                    << "Ax-b:        " << derivatives.Axmb.format(heavy);
+    VLOG(verbosity) << "  " << prefix
+                    << "f:           " << derivatives.f.format(heavy);
+    VLOG(verbosity) << "  " << prefix
+                    << "df:          " << derivatives.df.format(heavy);
+  }
+}
+
+};  // namespace solvers
+};  // namespace frc971
diff --git a/frc971/solvers/convex_test.cc b/frc971/solvers/convex_test.cc
new file mode 100644
index 0000000..213e70b
--- /dev/null
+++ b/frc971/solvers/convex_test.cc
@@ -0,0 +1,106 @@
+#include "frc971/solvers/convex.h"
+
+#include "gtest/gtest.h"
+
+namespace frc971 {
+namespace solvers {
+namespace testing {
+
+const Eigen::IOFormat kHeavyFormat(Eigen::StreamPrecision, 0, ", ",
+                                   ",\n                        "
+                                   "                                     ",
+                                   "[", "]", "[", "]");
+
+class SimpleQP : public ConvexProblem<2, 4, 1> {
+ public:
+  // QP of the for 0.5 * X^t Q_ X + p.T * X
+  SimpleQP(Eigen::Matrix<double, 2, 2> Q, Eigen::Matrix<double, 2, 1> p,
+           double x0_max, double x0_min, double x1_max, double x1_min)
+      : Q_(Q), p_(p) {
+    C_ << 1, 0, -1, 0, 0, 1, 0, -1;
+    c_ << x0_max, -x0_min, x1_max, -x1_min;
+  }
+
+  double f0(Eigen::Ref<const Eigen::Matrix<double, 2, 1>> X) const override {
+    return 0.5 * (X.transpose() * Q_ * X)(0, 0);
+  }
+
+  Eigen::Matrix<double, 2, 1> df0(
+      Eigen::Ref<const Eigen::Matrix<double, 2, 1>> X) const override {
+    return Q_ * X + p_;
+  }
+
+  Eigen::Matrix<double, 2, 2> ddf0(
+      Eigen::Ref<const Eigen::Matrix<double, 2, 1>> /*X*/) const override {
+    return Q_;
+  }
+
+  // Returns the constraints f(X) < 0, and their derivitive.
+  Eigen::Matrix<double, 4, 1> f(
+      Eigen::Ref<const Eigen::Matrix<double, 2, 1>> X) const override {
+    return C_ * X - c_;
+  }
+  Eigen::Matrix<double, 4, 2> df(
+      Eigen::Ref<const Eigen::Matrix<double, 2, 1>> /*X*/) const override {
+    return C_;
+  }
+
+  // Returns the equality constraints of the form A x = b
+  Eigen::Matrix<double, 1, 2> A() const override {
+    return Eigen::Matrix<double, 1, 2>(1, -1);
+  }
+  Eigen::Matrix<double, 1, 1> b() const override {
+    return Eigen::Matrix<double, 1, 1>(0);
+  }
+
+ private:
+  Eigen::Matrix<double, 2, 2> Q_;
+  Eigen::Matrix<double, 2, 1> p_;
+
+  Eigen::Matrix<double, 4, 2> C_;
+  Eigen::Matrix<double, 4, 1> c_;
+};
+
+// Test a constrained quadratic problem where the constraints aren't active.
+TEST(SolverTest, SimpleQP) {
+  Eigen::Matrix<double, 2, 2> Q = Eigen::DiagonalMatrix<double, 2>(1.0, 1.0);
+  Eigen::Matrix<double, 2, 1> p(-4, -6);
+
+  SimpleQP qp(Q, p, 6, -1, 6, -1);
+  Solver<2, 4, 1> s;
+  Eigen::Vector2d result = s.Solve(qp, Eigen::Matrix<double, 2, 1>(0, 0));
+  LOG(INFO) << "Result is " << std::setprecision(12)
+            << result.transpose().format(kHeavyFormat);
+  EXPECT_NEAR((result - Eigen::Vector2d(5.0, 5.0)).norm(), 0.0, 1e-6);
+}
+
+// Test a constrained quadratic problem where the constraints are active.
+TEST(SolverTest, Constrained) {
+  Eigen::Matrix<double, 2, 2> Q = Eigen::DiagonalMatrix<double, 2>(1.0, 2.0);
+  Eigen::Matrix<double, 2, 1> p(-5, -10);
+
+  SimpleQP qp(Q, p, 4, -1, 5, -1);
+  Solver<2, 4, 1> s;
+  Eigen::Vector2d result = s.Solve(qp, Eigen::Matrix<double, 2, 1>(3, 4));
+  LOG(INFO) << "Result is " << std::setprecision(12)
+            << result.transpose().format(kHeavyFormat);
+  EXPECT_NEAR((result - Eigen::Vector2d(4.0, 4.0)).norm(), 0.0, 1e-6);
+}
+
+// Test a constrained quadratic problem where the constraints are active and the
+// initial value is the solution.
+TEST(SolverTest, ConstrainedFromSolution) {
+  Eigen::Matrix<double, 2, 2> Q = Eigen::DiagonalMatrix<double, 2>(1.0, 2.0);
+  Eigen::Matrix<double, 2, 1> p(-5, -10);
+
+  SimpleQP qp(Q, p, 4, -1, 5, -1);
+  Solver<2, 4, 1> s;
+  Eigen::Vector2d result = s.Solve(qp, Eigen::Matrix<double, 2, 1>(4, 4));
+  LOG(INFO) << "Result is " << std::setprecision(12)
+            << result.transpose().format(kHeavyFormat);
+  EXPECT_NEAR((result - Eigen::Vector2d(4.0, 4.0)).norm(), 0.0, 1e-6);
+}
+
+}  // namespace testing
+}  // namespace solvers
+}  // namespace frc971
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 18d54a4..ac9a6e8 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -62,9 +62,8 @@
 	CompLevel   string `gorm:"primaryKey"`
 	// This contains a serialized scouting.webserver.requests.ActionType flatbuffer.
 	CompletedAction []byte
-	// TODO(phil): Get all the spellings of "timestamp" to be the same.
-	TimeStamp   int64 `gorm:"primaryKey"`
-	CollectedBy string
+	Timestamp       int64 `gorm:"primaryKey"`
+	CollectedBy     string
 }
 
 type NotesData struct {
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 9d1184a..8a4c0bc 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -813,27 +813,27 @@
 	correct := []Action{
 		Action{
 			TeamNumber: "1235", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0000, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0000, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1236", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0321, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0321, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1237", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0222, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0222, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1238", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0110, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0110, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1239", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0004, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0004, CollectedBy: "",
 		},
 		Action{
 			TeamNumber: "1233", MatchNumber: 94, SetNumber: 1, CompLevel: "quals",
-			CompletedAction: []byte(""), TimeStamp: 0005, CollectedBy: "",
+			CompletedAction: []byte(""), Timestamp: 0005, CollectedBy: "",
 		},
 	}
 
diff --git a/scouting/webserver/requests/messages/request_shift_schedule_response.fbs b/scouting/webserver/requests/messages/request_shift_schedule_response.fbs
index 611db49..fe44ef0 100644
--- a/scouting/webserver/requests/messages/request_shift_schedule_response.fbs
+++ b/scouting/webserver/requests/messages/request_shift_schedule_response.fbs
@@ -2,16 +2,16 @@
 
 table MatchAssignment {
     match_number:int (id:0);
-    R1scouter:string (id:1);
-    R2scouter:string (id:2);
-    R3scouter:string (id:3);
-    B1scouter:string (id:4);
-    B2scouter:string (id:5);
-    B3scouter:string (id:6);
+    r1_scouter:string (id:1);
+    r2_scouter:string (id:2);
+    r3_scouter:string (id:3);
+    b1_scouter:string (id:4);
+    b2_scouter:string (id:5);
+    b3_scouter:string (id:6);
 }
 
 table RequestShiftScheduleResponse {
     shift_schedule:[MatchAssignment] (id:0);
 }
 
-root_type RequestShiftScheduleResponse;
\ No newline at end of file
+root_type RequestShiftScheduleResponse;
diff --git a/scouting/webserver/requests/messages/submit_shift_schedule.fbs b/scouting/webserver/requests/messages/submit_shift_schedule.fbs
index 1f1833e..5292544 100644
--- a/scouting/webserver/requests/messages/submit_shift_schedule.fbs
+++ b/scouting/webserver/requests/messages/submit_shift_schedule.fbs
@@ -2,16 +2,16 @@
 
 table MatchAssignment {
     match_number:int (id:0);
-    R1scouter:string (id:1);
-    R2scouter:string (id:2);
-    R3scouter:string (id:3);
-    B1scouter:string (id:4);
-    B2scouter:string (id:5);
-    B3scouter:string (id:6);
+    r1_scouter:string (id:1);
+    r2_scouter:string (id:2);
+    r3_scouter:string (id:3);
+    b1_scouter:string (id:4);
+    b2_scouter:string (id:5);
+    b3_scouter:string (id:6);
 }
 
 table SubmitShiftSchedule {
     shift_schedule:[MatchAssignment] (id:0);
 }
 
-root_type SubmitShiftSchedule;
\ No newline at end of file
+root_type SubmitShiftSchedule;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 86a09d1..37a6ff2 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -628,12 +628,12 @@
 	for _, shifts := range shiftData {
 		response.ShiftSchedule = append(response.ShiftSchedule, &request_shift_schedule_response.MatchAssignmentT{
 			MatchNumber: shifts.MatchNumber,
-			R1scouter:   shifts.R1scouter,
-			R2scouter:   shifts.R2scouter,
-			R3scouter:   shifts.R3scouter,
-			B1scouter:   shifts.B1scouter,
-			B2scouter:   shifts.B2scouter,
-			B3scouter:   shifts.B3scouter,
+			R1Scouter:   shifts.R1scouter,
+			R2Scouter:   shifts.R2scouter,
+			R3Scouter:   shifts.R3scouter,
+			B1Scouter:   shifts.B1scouter,
+			B2Scouter:   shifts.B2scouter,
+			B3Scouter:   shifts.B3scouter,
 		})
 	}
 
@@ -668,12 +668,12 @@
 		request.ShiftSchedule(&match_assignment, i)
 		current_shift := db.Shift{
 			MatchNumber: match_assignment.MatchNumber(),
-			R1scouter:   string(match_assignment.R1scouter()),
-			R2scouter:   string(match_assignment.R2scouter()),
-			R3scouter:   string(match_assignment.R3scouter()),
-			B1scouter:   string(match_assignment.B1scouter()),
-			B2scouter:   string(match_assignment.B2scouter()),
-			B3scouter:   string(match_assignment.B3scouter()),
+			R1scouter:   string(match_assignment.R1Scouter()),
+			R2scouter:   string(match_assignment.R2Scouter()),
+			R3scouter:   string(match_assignment.R3Scouter()),
+			B1scouter:   string(match_assignment.B1Scouter()),
+			B2scouter:   string(match_assignment.B2Scouter()),
+			B3scouter:   string(match_assignment.B3Scouter()),
 		}
 		err = handler.db.AddToShift(current_shift)
 		if err != nil {
@@ -834,7 +834,7 @@
 			CompLevel:   string(request.CompLevel()),
 			//TODO: Serialize CompletedAction
 			CompletedAction: []byte{},
-			TimeStamp:       action.Timestamp(),
+			Timestamp:       action.Timestamp(),
 			CollectedBy:     username,
 		}
 
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index ae017d2..c1fe860 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -553,21 +553,21 @@
 		ShiftSchedule: []*request_shift_schedule_response.MatchAssignmentT{
 			{
 				MatchNumber: 1,
-				R1scouter:   "Bob",
-				R2scouter:   "James",
-				R3scouter:   "Robert",
-				B1scouter:   "Alice",
-				B2scouter:   "Mary",
-				B3scouter:   "Patricia",
+				R1Scouter:   "Bob",
+				R2Scouter:   "James",
+				R3Scouter:   "Robert",
+				B1Scouter:   "Alice",
+				B2Scouter:   "Mary",
+				B3Scouter:   "Patricia",
 			},
 			{
 				MatchNumber: 2,
-				R1scouter:   "Liam",
-				R2scouter:   "Noah",
-				R3scouter:   "Oliver",
-				B1scouter:   "Emma",
-				B2scouter:   "Charlotte",
-				B3scouter:   "Amelia",
+				R1Scouter:   "Liam",
+				R2Scouter:   "Noah",
+				R3Scouter:   "Oliver",
+				B1Scouter:   "Emma",
+				B2Scouter:   "Charlotte",
+				B3Scouter:   "Amelia",
 			},
 		},
 	}
@@ -592,12 +592,12 @@
 	builder.Finish((&submit_shift_schedule.SubmitShiftScheduleT{
 		ShiftSchedule: []*submit_shift_schedule.MatchAssignmentT{
 			{MatchNumber: 1,
-				R1scouter: "Bob",
-				R2scouter: "James",
-				R3scouter: "Robert",
-				B1scouter: "Alice",
-				B2scouter: "Mary",
-				B3scouter: "Patricia"},
+				R1Scouter: "Bob",
+				R2Scouter: "James",
+				R3Scouter: "Robert",
+				B1Scouter: "Alice",
+				B2Scouter: "Mary",
+				B3Scouter: "Patricia"},
 		},
 	}).Pack(builder))
 
@@ -869,7 +869,7 @@
 			CompLevel:       "qual",
 			CollectedBy:     "debug_cli",
 			CompletedAction: []byte{},
-			TimeStamp:       2400,
+			Timestamp:       2400,
 		},
 		{
 			PreScouting:     true,
@@ -879,7 +879,7 @@
 			CompLevel:       "qual",
 			CollectedBy:     "debug_cli",
 			CompletedAction: []byte{},
-			TimeStamp:       1009,
+			Timestamp:       1009,
 		},
 	}
 
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index d6d6799..ada96ff 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -151,15 +151,33 @@
       <!-- 'Balancing' during auto. -->
       <div *ngIf="autoPhase" class="d-grid gap-2">
         <label>
-          <input #docked type="checkbox" />
+          <input
+            #docked
+            type="radio"
+            id="option1"
+            name="docked_engaged"
+            value="docked"
+          />
           Docked (on the charging station)
         </label>
         <label>
-          <input #engaged type="checkbox" />
-          Engaged (level &amp; station lights on)
+          <input
+            #engaged
+            type="radio"
+            id="option2"
+            name="docked_engaged"
+            value="dockedengaged"
+          />
+          Docked &amp; Engaged (level &amp; station lights on)
         </label>
         <label>
-          <input #attempted type="checkbox" />
+          <input
+            #attempted
+            type="radio"
+            id="option3"
+            name="docked_engaged"
+            value="failed"
+          />
           Attempted to dock and engage but failed
         </label>
         <button
@@ -233,15 +251,33 @@
       <!-- 'Balancing' during auto. -->
       <div *ngIf="autoPhase" class="d-grid gap-1">
         <label>
-          <input #docked type="checkbox" />
+          <input
+            #docked
+            type="radio"
+            id="option1"
+            name="docked_engaged"
+            value="docked"
+          />
           Docked (on the charging station)
         </label>
         <label>
-          <input #engaged type="checkbox" />
-          Engaged (level &amp; station lights on)
+          <input
+            #engaged
+            type="radio"
+            id="option2"
+            name="docked_engaged"
+            value="dockedengaged"
+          />
+          Docked &amp; Engaged (level &amp; station lights on)
         </label>
         <label>
-          <input #attempted type="checkbox" />
+          <input
+            #attempted
+            type="radio"
+            id="option3"
+            name="docked_engaged"
+            value="failed"
+          />
           Attempted to dock and engage but failed
         </label>
         <button
@@ -273,15 +309,33 @@
         DEAD
       </button>
       <label>
-        <input #docked type="checkbox" />
+        <input
+          #docked
+          type="radio"
+          id="option1"
+          name="docked_engaged"
+          value="docked"
+        />
         Docked (on the charging station)
       </label>
       <label>
-        <input #engaged type="checkbox" />
-        Engaged (level &amp; station lights on)
+        <input
+          #engaged
+          type="radio"
+          id="option2"
+          name="docked_engaged"
+          value="dockedengaged"
+        />
+        Docked &amp; Engaged (level &amp; station lights on)
       </label>
       <label>
-        <input #attempted type="checkbox" />
+        <input
+          #attempted
+          type="radio"
+          id="option3"
+          name="docked_engaged"
+          value="failed"
+        />
         Attempted to dock and engage but failed
       </label>
       <button
diff --git a/scouting/www/index.html b/scouting/www/index.html
index afc589e..46779cc 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.html
@@ -12,7 +12,8 @@
     />
     <link
       rel="stylesheet"
-      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.c"
+      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
+      integrity="d8824f7067cdfea38afec7e9ffaf072125266824206d69ef1f112d72153a505e"
     />
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
   </head>
diff --git a/y2019/control_loops/drivetrain/localizer.h b/y2019/control_loops/drivetrain/localizer.h
index c55ccc9..818e1b9 100644
--- a/y2019/control_loops/drivetrain/localizer.h
+++ b/y2019/control_loops/drivetrain/localizer.h
@@ -8,6 +8,15 @@
 #include "frc971/control_loops/drivetrain/hybrid_ekf.h"
 #include "frc971/control_loops/pose.h"
 
+#if !defined(__clang__) && defined(__GNUC__)
+// GCC miss-detects that when zero is set to true, the member variables could be
+// uninitialized.  Rather than spend the CPU to initialize them in addition to
+// the memory for no good reason, tell GCC to stop doing that.  Clang appears to
+// get it.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#endif
+
 namespace y2019 {
 namespace control_loops {
 
@@ -80,37 +89,71 @@
     // those mappings for all the remaining corrections.
     // As such, we need to store the EKF functions that the remaining targets
     // will need in arrays:
-    ::aos::SizedArray<::std::function<Output(const State &, const Input &)>,
+    ::aos::SizedArray<HFunction, max_targets_per_frame> h_functions;
+    ::aos::SizedArray<Eigen::Matrix<Scalar, kNOutputs, kNStates>,
                       max_targets_per_frame>
-        h_functions;
-    ::aos::SizedArray<::std::function<Eigen::Matrix<Scalar, kNOutputs,
-                                                    kNStates>(const State &)>,
-                      max_targets_per_frame>
-        dhdx_functions;
+        dhdx;
     make_h_queue_.CorrectKnownHBuilder(
         z, nullptr,
         ExpectedObservationBuilder(this, camera, targets, &h_functions,
-                                   &dhdx_functions),
+                                   &dhdx),
         R, t);
     // Fetch cache:
     for (size_t ii = 1; ii < targets.size(); ++ii) {
       TargetViewToMatrices(targets[ii], &z, &R);
       h_queue_.CorrectKnownH(
           z, nullptr,
-          ExpectedObservationFunctor(h_functions[ii], dhdx_functions[ii]), R,
+          ExpectedObservationFunctor(h_functions[ii], dhdx[ii]), R,
           t);
     }
   }
 
  private:
+  class HFunction {
+   public:
+    HFunction() : zero_(true) {}
+    HFunction(const Camera *camera, const TargetView &best_view,
+              const TargetView &target_view, TypedLocalizer *localizer)
+        : zero_(false),
+          camera_(camera),
+          best_view_(best_view),
+          target_view_(target_view),
+          localizer_(localizer) {}
+    Output operator()(const State &X, const Input &) {
+      if (zero_) {
+        return Output::Zero();
+      }
+
+      // This function actually handles determining what the Output should
+      // be at a given state, now that we have chosen the target that
+      // we want to match to.
+      *localizer_->robot_pose_->mutable_pos() << X(0, 0), X(1, 0), 0.0;
+      localizer_->robot_pose_->set_theta(X(2, 0));
+      const Pose relative_pose =
+          best_view_.target->pose().Rebase(&camera_->pose());
+      const Scalar heading = relative_pose.heading();
+      const Scalar distance = relative_pose.xy_norm();
+      const Scalar skew =
+          ::aos::math::NormalizeAngle(relative_pose.rel_theta() - heading);
+      return Output(heading, distance, skew);
+    }
+
+   private:
+    bool zero_ = false;
+
+    const Camera *camera_;
+    TargetView best_view_;
+    TargetView target_view_;
+    TypedLocalizer *localizer_;
+  };
+
+  friend class HFunction;
+
   class ExpectedObservationFunctor
       : public HybridEkf::ExpectedObservationFunctor {
    public:
-    ExpectedObservationFunctor(
-        ::std::function<Output(const State &, const Input &)> h,
-        ::std::function<
-            Eigen::Matrix<Scalar, kNOutputs, kNStates>(const State &)>
-            dhdx)
+    ExpectedObservationFunctor(const HFunction &h,
+                               Eigen::Matrix<Scalar, kNOutputs, kNStates> dhdx)
         : h_(h), dhdx_(dhdx) {}
 
     Output H(const State &state, const Input &input) final {
@@ -118,14 +161,13 @@
     }
 
     virtual Eigen::Matrix<Scalar, kNOutputs, kNStates> DHDX(
-        const State &state) final {
-      return dhdx_(state);
+        const State &) final {
+      return dhdx_;
     }
 
    private:
-    ::std::function<Output(const State &, const Input &)> h_;
-    ::std::function<Eigen::Matrix<Scalar, kNOutputs, kNStates>(const State &)>
-        dhdx_;
+    HFunction h_;
+    Eigen::Matrix<Scalar, kNOutputs, kNStates> dhdx_;
   };
   class ExpectedObservationBuilder
       : public HybridEkf::ExpectedObservationBuilder {
@@ -134,23 +176,20 @@
         TypedLocalizer *localizer, const Camera &camera,
         const ::aos::SizedArray<TargetView, max_targets_per_frame>
             &target_views,
-        ::aos::SizedArray<::std::function<Output(const State &, const Input &)>,
-                          max_targets_per_frame> *h_functions,
-        ::aos::SizedArray<::std::function<Eigen::Matrix<
-                              Scalar, kNOutputs, kNStates>(const State &)>,
-                          max_targets_per_frame> *dhdx_functions)
+        ::aos::SizedArray<HFunction, max_targets_per_frame> *h_functions,
+        ::aos::SizedArray<Eigen::Matrix<Scalar, kNOutputs, kNStates>,
+                          max_targets_per_frame> *dhdx)
         : localizer_(localizer),
           camera_(camera),
           target_views_(target_views),
           h_functions_(h_functions),
-          dhdx_functions_(dhdx_functions) {}
+          dhdx_(dhdx) {}
 
     virtual ExpectedObservationFunctor *MakeExpectedObservations(
         const State &state, const StateSquare &P) {
-      ::std::function<Output(const State &, const Input &)> h;
-      ::std::function<Eigen::Matrix<Scalar, kNOutputs, kNStates>(const State &)>
-          dhdx;
-      localizer_->MakeH(camera_, target_views_, h_functions_, dhdx_functions_,
+      HFunction h;
+      Eigen::Matrix<Scalar, kNOutputs, kNStates> dhdx;
+      localizer_->MakeH(camera_, target_views_, h_functions_, dhdx_,
                         state, P, &h, &dhdx);
       functor_.emplace(h, dhdx);
       return &functor_.value();
@@ -160,13 +199,12 @@
     TypedLocalizer *localizer_;
     const Camera &camera_;
     const ::aos::SizedArray<TargetView, max_targets_per_frame> &target_views_;
-    ::aos::SizedArray<::std::function<Output(const State &, const Input &)>,
-                      max_targets_per_frame> *h_functions_;
-    ::aos::SizedArray<::std::function<Eigen::Matrix<Scalar, kNOutputs,
-                                                    kNStates>(const State &)>,
-                      max_targets_per_frame> *dhdx_functions_;
+    ::aos::SizedArray<HFunction, max_targets_per_frame> *h_functions_;
+    ::aos::SizedArray<Eigen::Matrix<Scalar, kNOutputs, kNStates>,
+                      max_targets_per_frame> *dhdx_;
     std::optional<ExpectedObservationFunctor> functor_;
   };
+
   // The threshold to use for completely rejecting potentially bad target
   // matches.
   // TODO(james): Tune
@@ -220,15 +258,11 @@
   void MakeH(
       const Camera &camera,
       const ::aos::SizedArray<TargetView, max_targets_per_frame> &target_views,
-      ::aos::SizedArray<::std::function<Output(const State &, const Input &)>,
-                        max_targets_per_frame> *h_functions,
-      ::aos::SizedArray<::std::function<Eigen::Matrix<Scalar, kNOutputs,
-                                                      kNStates>(const State &)>,
-                        max_targets_per_frame> *dhdx_functions,
-      const State &X_hat, const StateSquare &P,
-      ::std::function<Output(const State &, const Input &)> *h,
-      ::std::function<Eigen::Matrix<Scalar, kNOutputs, kNStates>(const State &)>
-          *dhdx) {
+      ::aos::SizedArray<HFunction, max_targets_per_frame> *h_functions,
+      ::aos::SizedArray<Eigen::Matrix<Scalar, kNOutputs, kNStates>,
+                        max_targets_per_frame> *dhdx,
+      const State &X_hat, const StateSquare &P, HFunction *h,
+      Eigen::Matrix<Scalar, kNOutputs, kNStates> *current_dhdx) {
     // Because we need to match camera targets ("views") to actual field
     // targets, and because we want to take advantage of the correlations
     // between the targets (i.e., if we see two targets in the image, they
@@ -358,13 +392,11 @@
       AOS_LOG(DEBUG, "Unable to identify potential target matches.\n");
       // If we can't get a match, provide H = zero, which will make this
       // correction step a nop.
-      *h = [](const State &, const Input &) { return Output::Zero(); };
-      *dhdx = [](const State &) {
-        return Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero();
-      };
+      *h = HFunction();
+      *current_dhdx = Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero();
       for (size_t ii = 0; ii < target_views.size(); ++ii) {
         h_functions->push_back(*h);
-        dhdx_functions->push_back(*dhdx);
+        dhdx->push_back(*current_dhdx);
       }
     } else {
       // Go through and brute force the issue of what the best combination of
@@ -377,11 +409,9 @@
         size_t view_idx = best_frames[ii];
         if (view_idx >= camera_views.size()) {
           AOS_LOG(ERROR, "Somehow, the view scorer failed.\n");
-          h_functions->push_back(
-              [](const State &, const Input &) { return Output::Zero(); });
-          dhdx_functions->push_back([](const State &) {
-            return Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero();
-          });
+          h_functions->emplace_back();
+          dhdx->push_back(
+              Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero());
           continue;
         }
         const Eigen::Matrix<Scalar, kNOutputs, kNStates> best_H =
@@ -394,38 +424,18 @@
                   "Rejecting target at (%f, %f, %f, %f) due to high score.\n",
                   target_view.reading.heading, target_view.reading.distance,
                   target_view.reading.skew, target_view.reading.height);
-          h_functions->push_back(
-              [](const State &, const Input &) { return Output::Zero(); });
-          dhdx_functions->push_back([](const State &) {
-            return Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero();
-          });
+          h_functions->emplace_back();
+          dhdx->push_back(Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero());
         } else {
-          h_functions->push_back([this, &camera, best_view, target_view](
-                                     const State &X, const Input &) {
-            // This function actually handles determining what the Output should
-            // be at a given state, now that we have chosen the target that
-            // we want to match to.
-            *robot_pose_->mutable_pos() << X(0, 0), X(1, 0), 0.0;
-            robot_pose_->set_theta(X(2, 0));
-            const Pose relative_pose =
-                best_view.target->pose().Rebase(&camera.pose());
-            const Scalar heading = relative_pose.heading();
-            const Scalar distance = relative_pose.xy_norm();
-            const Scalar skew = ::aos::math::NormalizeAngle(
-                relative_pose.rel_theta() - heading);
-            return Output(heading, distance, skew);
-          });
+          h_functions->emplace_back(&camera, best_view, target_view, this);
 
           // TODO(james): Experiment to better understand whether we want to
           // recalculate H or not.
-          dhdx_functions->push_back(
-              [best_H](const Eigen::Matrix<Scalar, kNStates, 1> &) {
-                return best_H;
-              });
+          dhdx->push_back(best_H);
         }
       }
       *h = h_functions->at(0);
-      *dhdx = dhdx_functions->at(0);
+      *current_dhdx = dhdx->at(0);
     }
   }
 
@@ -563,7 +573,11 @@
       make_h_queue_;
 
   friend class ExpectedObservationBuilder;
-};  // class TypedLocalizer
+};
+
+#if !defined(__clang__) && defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
 
 }  // namespace control_loops
 }  // namespace y2019
diff --git a/y2020/BUILD b/y2020/BUILD
index 2f6f963..32d392a 100644
--- a/y2020/BUILD
+++ b/y2020/BUILD
@@ -2,6 +2,12 @@
 load("//aos:config.bzl", "aos_config")
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
 load("//tools/build_rules:template.bzl", "jinja2_template")
+load("//aos/util:config_validator_macro.bzl", "config_validator_test")
+
+config_validator_test(
+    name = "config_validator_test",
+    config = "//y2020:aos_config",
+)
 
 robot_downloader(
     binaries = [
diff --git a/y2020/control_loops/drivetrain/BUILD b/y2020/control_loops/drivetrain/BUILD
index fa153f0..8191404 100644
--- a/y2020/control_loops/drivetrain/BUILD
+++ b/y2020/control_loops/drivetrain/BUILD
@@ -207,6 +207,7 @@
         "//aos/events:simulated_event_loop",
         "//aos/events/logging:log_reader",
         "//aos/events/logging:log_writer",
+        "//aos/util:simulation_logger",
         "//frc971/control_loops/drivetrain:drivetrain_lib",
         "//frc971/control_loops/drivetrain:trajectory_generator",
         "//y2020:constants",
diff --git a/y2020/control_loops/drivetrain/drivetrain_replay.cc b/y2020/control_loops/drivetrain/drivetrain_replay.cc
index 57feabb..8373258 100644
--- a/y2020/control_loops/drivetrain/drivetrain_replay.cc
+++ b/y2020/control_loops/drivetrain/drivetrain_replay.cc
@@ -11,6 +11,7 @@
 #include "aos/init.h"
 #include "aos/json_to_flatbuffer.h"
 #include "aos/network/team_number.h"
+#include "aos/util/simulation_logger.h"
 #include "frc971/control_loops/drivetrain/drivetrain.h"
 #include "frc971/control_loops/drivetrain/trajectory_generator.h"
 #include "gflags/gflags.h"
@@ -26,27 +27,6 @@
 DEFINE_int32(team, 971, "Team number to use for logfile replay.");
 DEFINE_bool(log_all_nodes, false, "Whether to rerun the logger on every node.");
 
-class LoggerState {
- public:
-  LoggerState(aos::logger::LogReader *reader, const aos::Node *node)
-      : event_loop_(
-            reader->event_loop_factory()->MakeEventLoop("logger", node)),
-        namer_(std::make_unique<aos::logger::MultiNodeFilesLogNamer>(
-            absl::StrCat(FLAGS_output_folder, "/", node->name()->string_view(),
-                         "/"),
-            event_loop_.get())),
-        logger_(std::make_unique<aos::logger::Logger>(event_loop_.get())) {
-    event_loop_->SkipTimingReport();
-    event_loop_->SkipAosLog();
-    event_loop_->OnRun([this]() { logger_->StartLogging(std::move(namer_)); });
-  }
-
- private:
-  std::unique_ptr<aos::EventLoop> event_loop_;
-  std::unique_ptr<aos::logger::LogNamer> namer_;
-  std::unique_ptr<aos::logger::Logger> logger_;
-};
-
 // TODO(james): Currently, this replay produces logfiles that can't be read due
 // to time estimation issues. Pending the active refactorings of the
 // timestamp-related code, fix this.
@@ -82,21 +62,17 @@
                             "y2020.control_loops.superstructure.Output");
   reader.Register();
 
-  std::vector<std::unique_ptr<LoggerState>> loggers;
+  std::vector<std::unique_ptr<aos::util::LoggerState>> loggers;
   if (FLAGS_log_all_nodes) {
-    for (const aos::Node *node :
-         aos::configuration::GetNodes(reader.configuration())) {
-      loggers.emplace_back(std::make_unique<LoggerState>(&reader, node));
-    }
+    loggers = aos::util::MakeLoggersForAllNodes(reader.event_loop_factory(),
+                                                FLAGS_output_folder);
   } else {
     // List of nodes to create loggers for (note: currently just roborio; this
     // code was refactored to allow easily adding new loggers to accommodate
     // debugging and potential future changes).
     const std::vector<std::string> nodes_to_log = {"roborio"};
-    for (const std::string &node : nodes_to_log) {
-      loggers.emplace_back(std::make_unique<LoggerState>(
-          &reader, aos::configuration::GetNode(reader.configuration(), node)));
-    }
+    loggers = aos::util::MakeLoggersForNodes(reader.event_loop_factory(),
+                                             nodes_to_log, FLAGS_output_folder);
   }
 
   const aos::Node *node = nullptr;
diff --git a/y2020/control_loops/drivetrain/localizer_test.cc b/y2020/control_loops/drivetrain/localizer_test.cc
index d280523..9e9e7dd 100644
--- a/y2020/control_loops/drivetrain/localizer_test.cc
+++ b/y2020/control_loops/drivetrain/localizer_test.cc
@@ -15,7 +15,6 @@
 
 DEFINE_string(output_file, "",
               "If set, logs all channels to the provided logfile.");
-DECLARE_bool(die_on_malloc);
 
 // This file tests that the full 2020 localizer behaves sanely.
 
@@ -147,7 +146,6 @@
     CHECK_EQ(aos::configuration::GetNodeIndex(configuration(), pi1_), 1);
     set_team_id(frc971::control_loops::testing::kTeamNumber);
     set_battery_voltage(12.0);
-    FLAGS_die_on_malloc = true;
 
     if (!FLAGS_output_file.empty()) {
       logger_event_loop_ = MakeEventLoop("logger", roborio_);
diff --git a/y2020/y2020_pi_template.json b/y2020/y2020_pi_template.json
index cf54c23..c609632 100644
--- a/y2020/y2020_pi_template.json
+++ b/y2020/y2020_pi_template.json
@@ -92,12 +92,19 @@
           "time_to_live": 5000000,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "roborio"
+            "pi{{ NUM }}"
           ]
         }
       ]
     },
     {
+      "name": "/pi{{ NUM }}/aos/remote_timestamps/roborio/pi{{ NUM }}/aos/aos-message_bridge-Timestamp",
+      "type": "aos.message_bridge.RemoteMessage",
+      "frequency": 20,
+      "source_node": "pi{{ NUM }}",
+      "max_size": 208
+    },
+    {
       "name": "/pi{{ NUM }}/camera",
       "type": "frc971.vision.CameraImage",
       "source_node": "pi{{ NUM }}",
diff --git a/y2022/BUILD b/y2022/BUILD
index 1a04c2e..f8bf59f 100644
--- a/y2022/BUILD
+++ b/y2022/BUILD
@@ -2,6 +2,12 @@
 load("//aos:config.bzl", "aos_config")
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
 load("//tools/build_rules:template.bzl", "jinja2_template")
+load("//aos/util:config_validator_macro.bzl", "config_validator_test")
+
+config_validator_test(
+    name = "config_validator_test",
+    config = "//y2022:aos_config",
+)
 
 robot_downloader(
     binaries = [
diff --git a/y2022/localizer/BUILD b/y2022/localizer/BUILD
index dd0fb67..7f076e8 100644
--- a/y2022/localizer/BUILD
+++ b/y2022/localizer/BUILD
@@ -150,6 +150,7 @@
         "//aos/events:simulated_event_loop",
         "//aos/events/logging:log_reader",
         "//aos/events/logging:log_writer",
+        "//aos/util:simulation_logger",
         "//y2022/control_loops/drivetrain:drivetrain_base",
     ],
 )
diff --git a/y2022/localizer/localizer_replay.cc b/y2022/localizer/localizer_replay.cc
index 0c09535..6dcbb1e 100644
--- a/y2022/localizer/localizer_replay.cc
+++ b/y2022/localizer/localizer_replay.cc
@@ -1,6 +1,7 @@
 #include "aos/configuration.h"
 #include "aos/events/logging/log_reader.h"
 #include "aos/events/logging/log_writer.h"
+#include "aos/util/simulation_logger.h"
 #include "aos/events/simulated_event_loop.h"
 #include "aos/init.h"
 #include "aos/json_to_flatbuffer.h"
@@ -16,27 +17,6 @@
 DEFINE_string(output_folder, "/tmp/replayed",
               "Name of the folder to write replayed logs to.");
 
-class LoggerState {
- public:
-  LoggerState(aos::logger::LogReader *reader, const aos::Node *node)
-      : event_loop_(
-            reader->event_loop_factory()->MakeEventLoop("logger", node)),
-        namer_(std::make_unique<aos::logger::MultiNodeFilesLogNamer>(
-            absl::StrCat(FLAGS_output_folder, "/", node->name()->string_view(),
-                         "/"),
-            event_loop_.get())),
-        logger_(std::make_unique<aos::logger::Logger>(event_loop_.get())) {
-    event_loop_->SkipTimingReport();
-    event_loop_->SkipAosLog();
-    event_loop_->OnRun([this]() { logger_->StartLogging(std::move(namer_)); });
-  }
-
- private:
-  std::unique_ptr<aos::EventLoop> event_loop_;
-  std::unique_ptr<aos::logger::LogNamer> namer_;
-  std::unique_ptr<aos::logger::Logger> logger_;
-};
-
 // TODO(james): Currently, this replay produces logfiles that can't be read due
 // to time estimation issues. Pending the active refactorings of the
 // timestamp-related code, fix this.
@@ -71,15 +51,13 @@
 
   reader.Register(factory.get());
 
-  std::vector<std::unique_ptr<LoggerState>> loggers;
   // List of nodes to create loggers for (note: currently just roborio; this
   // code was refactored to allow easily adding new loggers to accommodate
   // debugging and potential future changes).
   const std::vector<std::string> nodes_to_log = {"imu"};
-  for (const std::string &node : nodes_to_log) {
-    loggers.emplace_back(std::make_unique<LoggerState>(
-        &reader, aos::configuration::GetNode(reader.configuration(), node)));
-  }
+  std::vector<std::unique_ptr<aos::util::LoggerState>> loggers =
+      aos::util::MakeLoggersForNodes(reader.event_loop_factory(), nodes_to_log,
+                                     FLAGS_output_folder);
 
   const aos::Node *node = nullptr;
   if (aos::configuration::MultiNode(reader.configuration())) {
diff --git a/y2022/y2022_logger.json b/y2022/y2022_logger.json
index 01800bf..65a6e98 100644
--- a/y2022/y2022_logger.json
+++ b/y2022/y2022_logger.json
@@ -35,7 +35,7 @@
           "priority": 2,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "roborio"
+            "logger"
           ],
           "time_to_live": 5000000
         }
@@ -46,7 +46,7 @@
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "logger",
       "logger": "NOT_LOGGED",
-      "frequency": 20,
+      "frequency": 200,
       "num_senders": 2,
       "max_size": 200
     },
diff --git a/y2022/y2022_pi_template.json b/y2022/y2022_pi_template.json
index 4d3c427..cb90fee 100644
--- a/y2022/y2022_pi_template.json
+++ b/y2022/y2022_pi_template.json
@@ -104,7 +104,7 @@
           "time_to_live": 5000000,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "roborio"
+            "pi{{ NUM }}"
           ]
         },
         {
@@ -113,7 +113,7 @@
           "time_to_live": 5000000,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "imu"
+            "pi{{ NUM }}"
           ]
         }
       ]
diff --git a/y2022/y2022_roborio.json b/y2022/y2022_roborio.json
index 068c543..d046568 100644
--- a/y2022/y2022_roborio.json
+++ b/y2022/y2022_roborio.json
@@ -459,7 +459,7 @@
           "priority": 5,
           "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
           "timestamp_logger_nodes": [
-            "imu"
+            "roborio"
           ],
           "time_to_live": 0
         }
diff --git a/y2023/BUILD b/y2023/BUILD
index 6be5cac..667b59d 100644
--- a/y2023/BUILD
+++ b/y2023/BUILD
@@ -2,9 +2,9 @@
 load("//aos:config.bzl", "aos_config")
 load("//tools/build_rules:template.bzl", "jinja2_template")
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
-load("//aos/util:config_validator_macro.bzl", "config_validator_rule")
+load("//aos/util:config_validator_macro.bzl", "config_validator_test")
 
-config_validator_rule(
+config_validator_test(
     name = "config_validator_test",
     config = "//y2023:aos_config",
 )
diff --git a/y2023/autonomous/splines/spline.1.json b/y2023/autonomous/splines/spline.1.json
index a98ac6e..5240ad8 100644
--- a/y2023/autonomous/splines/spline.1.json
+++ b/y2023/autonomous/splines/spline.1.json
@@ -1 +1 @@
-{"spline_count": 1, "spline_x": [1.609310857796625, 2.5819120488556946, 3.506443404506549, 5.556354709601551, 5.990235974918718, 6.419541332061575], "spline_y": [0.6043502546533336, 0.69141924611354, 1.0213742193777775, 0.22424951902207307, 0.24557526406496255, 0.24557526406496255], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.5}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2}, {"constraint_type": "VOLTAGE", "value": 12.0}]}
\ No newline at end of file
+{"spline_count": 1, "spline_x": [1.609310857796625, 2.5819120488556946, 3.506443404506549, 5.555694235956169, 5.989575501273337, 6.418880858416194], "spline_y": [0.6043502546533336, 0.69141924611354, 1.0213742193777775, 0.38712949808092717, 0.40845524312381665, 0.40845524312381665], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.5}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2}, {"constraint_type": "VOLTAGE", "value": 12.0}]}
\ No newline at end of file
diff --git a/y2023/autonomous/splines/spline.2.json b/y2023/autonomous/splines/spline.2.json
index 641b973..297966c 100644
--- a/y2023/autonomous/splines/spline.2.json
+++ b/y2023/autonomous/splines/spline.2.json
@@ -1 +1 @@
-{"spline_count": 1, "spline_x": [6.419541332061575, 6.028411344095072, 5.2762663069267655, 2.8053451665928835, 2.37026593061867, 1.5260719060059573], "spline_y": [0.24557526406496255, 0.24557526406496255, 0.3018576793840364, 1.3287637699876067, -0.026954142728124464, -0.5547144640218522], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.5}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2.5}, {"constraint_type": "VOLTAGE", "value": 12.0}]}
\ No newline at end of file
+{"spline_count": 1, "spline_x": [6.418880858416194, 6.02775087044969, 5.275605833281384, 2.8053451665928835, 2.37026593061867, 1.5260719060059573], "spline_y": [0.40845524312381665, 0.40845524312381665, 0.4647376584428905, 1.3287637699876067, -0.026954142728124464, -0.5547144640218522], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.5}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2.5}, {"constraint_type": "VOLTAGE", "value": 12.0}]}
\ No newline at end of file
diff --git a/y2023/autonomous/splines/spline.3.json b/y2023/autonomous/splines/spline.3.json
index f5651e7..6ac5412 100644
--- a/y2023/autonomous/splines/spline.3.json
+++ b/y2023/autonomous/splines/spline.3.json
@@ -1 +1 @@
-{"spline_count": 1, "spline_x": [1.5260719060059573, 3.487962685989344, 3.140736946266447, 5.361661826393136, 4.912898328525625, 6.376744631564399], "spline_y": [-0.5547144640218522, 0.6717904367469538, 0.6138773092943139, 0.6864204635819386, 0.27660573272799804, 0.2788534917250134], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.5}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2.6}, {"constraint_type": "VOLTAGE", "value": 12.0}, {"constraint_type": "VELOCITY", "value": 1.65, "start_distance": 4.1, "end_distance": 10.0}]}
\ No newline at end of file
+{"spline_count": 1, "spline_x": [1.5260719060059573, 3.487962685989344, 3.140736946266447, 5.366541573420288, 4.917778075552777, 6.381624378591551], "spline_y": [-0.5547144640218522, 0.6717904367469538, 0.6138773092943139, 0.7392105412041843, 0.3293958103502437, 0.331643569347259], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.5}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2.6}, {"constraint_type": "VOLTAGE", "value": 12.0}, {"constraint_type": "VELOCITY", "value": 1.65, "start_distance": 4.1, "end_distance": 10.0}]}
\ No newline at end of file
diff --git a/y2023/constants.cc b/y2023/constants.cc
index a6c0396..4ef5670 100644
--- a/y2023/constants.cc
+++ b/y2023/constants.cc
@@ -101,7 +101,7 @@
       arm_distal->zeroing.one_revolution_distance =
           M_PI * 2.0 * constants::Values::kDistalEncoderRatio();
 
-      roll_joint->zeroing.measured_absolute_position = 0.419144048980465;
+      roll_joint->zeroing.measured_absolute_position = 0.424187348328397;
       roll_joint->potentiometer_offset =
           -(3.87038557084874 - 0.0241774522172967 + 0.0711345168020632 -
             0.866186131631967 - 0.0256788357596952 + 0.18101759154572017 -
@@ -109,10 +109,11 @@
             0.5935210745062 + 0.166256655718334 - 0.12591438680483 +
             0.11972765117321 - 0.318724743041507) +
           0.0201047336425017 - 1.0173426655158 - 0.186085272847293 -
-          0.0317706563397807 - 2.6357823523782 + 0.871932806570122;
+          0.0317706563397807 - 2.6357823523782 + 0.871932806570122 +
+          1.09682107821155;
 
       wrist->subsystem_params.zeroing_constants.measured_absolute_position =
-          0.868820879549023;
+          0.886183343417664;
 
       break;
 
@@ -131,7 +132,7 @@
 
       arm_distal->zeroing.one_revolution_distance =
           M_PI * 2.0 * constants::Values::kDistalEncoderRatio() *
-          //3.11964893168338 / 3.148;
+          // 3.11964893168338 / 3.148;
           (3.12725165289659 + 0.002) / 3.1485739705977704;
 
       roll_joint->zeroing.measured_absolute_position = 1.79390317510529;
diff --git a/y2023/control_loops/python/graph_paths.py b/y2023/control_loops/python/graph_paths.py
index 4b8de27..eb92ec8 100644
--- a/y2023/control_loops/python/graph_paths.py
+++ b/y2023/control_loops/python/graph_paths.py
@@ -333,7 +333,7 @@
     ))
 
 points['HPPickupBackConeUp'] = to_theta_with_circular_index_and_roll(
-    -1.1200539, 1.325, np.pi / 2.0, circular_index=0)
+    -1.1200539, 1.335, np.pi / 2.0, circular_index=0)
 
 named_segments.append(
     ThetaSplineSegment(
diff --git a/y2023/joystick_reader.cc b/y2023/joystick_reader.cc
index a8f540f..874e691 100644
--- a/y2023/joystick_reader.cc
+++ b/y2023/joystick_reader.cc
@@ -71,7 +71,7 @@
 const ButtonLocation kSuck(2, 3);
 const ButtonLocation kBack(2, 4);
 
-const ButtonLocation kStayIn(2, 12);
+const ButtonLocation kStayIn(3, 2);
 
 const ButtonLocation kConeDownTip(1, 9);
 const ButtonLocation kConeDownBase(1, 10);
diff --git a/y2023/localizer/BUILD b/y2023/localizer/BUILD
index 3ef024c..f9d0d28 100644
--- a/y2023/localizer/BUILD
+++ b/y2023/localizer/BUILD
@@ -220,6 +220,7 @@
         "//aos/events:simulated_event_loop",
         "//aos/events/logging:log_reader",
         "//aos/events/logging:log_writer",
+        "//aos/util:simulation_logger",
         "//y2023/control_loops/drivetrain:drivetrain_base",
     ],
 )
diff --git a/y2023/localizer/localizer_replay.cc b/y2023/localizer/localizer_replay.cc
index c74b708..27f0d18 100644
--- a/y2023/localizer/localizer_replay.cc
+++ b/y2023/localizer/localizer_replay.cc
@@ -5,9 +5,10 @@
 #include "aos/init.h"
 #include "aos/json_to_flatbuffer.h"
 #include "aos/network/team_number.h"
-#include "y2023/localizer/localizer.h"
+#include "aos/util/simulation_logger.h"
 #include "gflags/gflags.h"
 #include "y2023/control_loops/drivetrain/drivetrain_base.h"
+#include "y2023/localizer/localizer.h"
 
 DEFINE_string(config, "y2023/aos_config.json",
               "Name of the config file to replay using.");
@@ -15,27 +16,6 @@
 DEFINE_string(output_folder, "/tmp/replayed",
               "Name of the folder to write replayed logs to.");
 
-class LoggerState {
- public:
-  LoggerState(aos::logger::LogReader *reader, const aos::Node *node)
-      : event_loop_(
-            reader->event_loop_factory()->MakeEventLoop("logger", node)),
-        namer_(std::make_unique<aos::logger::MultiNodeFilesLogNamer>(
-            absl::StrCat(FLAGS_output_folder, "/", node->name()->string_view(),
-                         "/"),
-            event_loop_.get())),
-        logger_(std::make_unique<aos::logger::Logger>(event_loop_.get())) {
-    event_loop_->SkipTimingReport();
-    event_loop_->SkipAosLog();
-    event_loop_->OnRun([this]() { logger_->StartLogging(std::move(namer_)); });
-  }
-
- private:
-  std::unique_ptr<aos::EventLoop> event_loop_;
-  std::unique_ptr<aos::logger::LogNamer> namer_;
-  std::unique_ptr<aos::logger::Logger> logger_;
-};
-
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
 
@@ -69,15 +49,13 @@
 
   reader.Register(factory.get());
 
-  std::vector<std::unique_ptr<LoggerState>> loggers;
   // List of nodes to create loggers for (note: currently just roborio; this
   // code was refactored to allow easily adding new loggers to accommodate
   // debugging and potential future changes).
   const std::vector<std::string> nodes_to_log = {"imu"};
-  for (const std::string &node : nodes_to_log) {
-    loggers.emplace_back(std::make_unique<LoggerState>(
-        &reader, aos::configuration::GetNode(reader.configuration(), node)));
-  }
+  std::vector<std::unique_ptr<aos::util::LoggerState>> loggers =
+      aos::util::MakeLoggersForNodes(reader.event_loop_factory(), nodes_to_log,
+                                     FLAGS_output_folder);
 
   const aos::Node *node = nullptr;
   if (aos::configuration::MultiNode(reader.configuration())) {
diff --git a/y2023/localizer/localizer_test.cc b/y2023/localizer/localizer_test.cc
index e8b7d56..2cb5756 100644
--- a/y2023/localizer/localizer_test.cc
+++ b/y2023/localizer/localizer_test.cc
@@ -14,7 +14,6 @@
 
 DEFINE_string(output_folder, "",
               "If set, logs all channels to the provided logfile.");
-DECLARE_bool(die_on_malloc);
 DECLARE_double(max_distance_to_target);
 
 namespace y2023::localizer::testing {
@@ -75,7 +74,6 @@
         status_fetcher_(
             imu_test_event_loop_->MakeFetcher<Status>("/localizer")) {
     FLAGS_max_distance_to_target = 100.0;
-    FLAGS_die_on_malloc = true;
     {
       aos::TimerHandler *timer = roborio_test_event_loop_->AddTimer([this]() {
         {
diff --git a/y2023/vision/camera_reader.cc b/y2023/vision/camera_reader.cc
index b526086..3001c04 100644
--- a/y2023/vision/camera_reader.cc
+++ b/y2023/vision/camera_reader.cc
@@ -12,7 +12,7 @@
 
 DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
 DEFINE_bool(lowlight_camera, true, "Switch to use imx462 image sensor.");
-DEFINE_int32(gain, 200, "analogue_gain");
+DEFINE_int32(gain, 150, "analogue_gain");
 
 DEFINE_double(red, 1.252, "Red gain");
 DEFINE_double(green, 1, "Green gain");
diff --git a/y2023/vision/maps/johnson.json b/y2023/vision/maps/johnson.json
new file mode 100644
index 0000000..b0aef36
--- /dev/null
+++ b/y2023/vision/maps/johnson.json
@@ -0,0 +1,152 @@
+{
+ "target_poses": [
+{
+   "id": 1,
+   "position": {
+    "x": 7.243997790506,
+    "y": -2.933106182702,
+    "z": 0.462948446708
+   },
+   "orientation": {
+    "w": 0.50135307601,
+    "x": -0.503852469447,
+    "y": 0.496681622743,
+    "z": -0.498081467069
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  },
+  {
+   "id": 2,
+   "position": {
+    "x": 7.233087584818,
+    "y": -1.266935526895,
+    "z": 0.458422750798
+   },
+   "orientation": {
+    "w": 0.503123586992,
+    "x": -0.498312711799,
+    "y": 0.4917162543,
+    "z": -0.506721050214
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  },
+  {
+   "id": 3,
+   "position": {
+    "x": 7.226895621731,
+    "y": 0.399357924781,
+    "z": 0.451036794292
+   },
+   "orientation": {
+    "w": 0.509600379834,
+    "x": -0.495432434727,
+    "y": 0.487971656326,
+    "z": -0.506693021577
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  },
+  {
+   "id": 4,
+   "position": {
+    "x": 7.909,
+    "y": 2.74,
+    "z": 0.695
+   },
+   "orientation": {
+    "w": -0.5,
+    "x": 0.5,
+    "y": -0.5,
+    "z": 0.5
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  },
+  {
+   "id": 5,
+   "position": {
+    "x": -7.908,
+    "y": 2.74,
+    "z": 0.695
+   },
+   "orientation": {
+    "w": 0.5,
+    "x": -0.5,
+    "y": -0.5,
+    "z": 0.5
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  },
+  {
+   "id": 6,
+   "position": {
+    "x": -7.239001740201,
+    "y": 0.422111705663,
+    "z": 0.464955016203
+   },
+   "orientation": {
+    "w": 0.498420780853,
+    "x": -0.499263204799,
+    "y": -0.500023026083,
+    "z": 0.502284730939
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  },
+  {
+   "id": 7,
+   "position": {
+    "x": -7.241093280921,
+    "y": -1.270115952319,
+    "z": 0.463955457197
+   },
+   "orientation": {
+    "w": -0.499112246574,
+    "x": 0.499432412584,
+    "y": 0.501866879868,
+    "z": -0.49958369216
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  },
+  {
+   "id": 8,
+   "position": {
+    "x": -7.240827110532,
+    "y": -2.95806661736,
+    "z": 0.458620705581
+   },
+   "orientation": {
+    "w": -0.499747319421,
+    "x": 0.499461045816,
+    "y": 0.501623538198,
+    "z": -0.499164333279
+   },
+   "confidence": 0.0,
+   "pose_error": 0.0,
+   "distortion_factor": 0.0,
+   "pose_error_ratio": 0.0
+  }
+ ],
+ "field_name": "johnson",
+ "monotonic_timestamp_ns": 0,
+ "rejections": 0
+}
+
diff --git a/y2023/y2023_roborio.json b/y2023/y2023_roborio.json
index 13e48dc..172d11c 100644
--- a/y2023/y2023_roborio.json
+++ b/y2023/y2023_roborio.json
@@ -18,15 +18,6 @@
       ]
     },
     {
-      "name": "/roborio/aos/remote_timestamps/imu/roborio/aos/aos-JoystickState",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "roborio",
-      "logger": "NOT_LOGGED",
-      "frequency": 300,
-      "num_senders": 2,
-      "max_size": 200
-    },
-    {
       "name": "/roborio/aos",
       "type": "aos.RobotState",
       "source_node": "roborio",
@@ -234,15 +225,6 @@
       ]
     },
     {
-      "name": "/roborio/aos/remote_timestamps/imu/drivetrain/frc971-control_loops-drivetrain-Output",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "roborio",
-      "logger": "NOT_LOGGED",
-      "frequency": 400,
-      "num_senders": 2,
-      "max_size": 200
-    },
-    {
       "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Status",
       "source_node": "roborio",
@@ -339,9 +321,6 @@
     {
       "name": "drivetrain",
       "executable_name": "drivetrain",
-      "args": [
-        "--die_on_malloc"
-      ],
       "nodes": [
         "roborio"
       ]
@@ -349,9 +328,6 @@
     {
       "name": "trajectory_generator",
       "executable_name": "trajectory_generator",
-      "args": [
-        "--die_on_malloc"
-      ],
       "nodes": [
         "roborio"
       ]
@@ -359,9 +335,6 @@
     {
       "name": "superstructure",
       "executable_name": "superstructure",
-      "args": [
-        "--die_on_malloc"
-      ],
       "nodes": [
         "roborio"
       ]
@@ -389,6 +362,9 @@
     {
       "name": "wpilib_interface",
       "executable_name": "wpilib_interface",
+      "args": [
+        "--nodie_on_malloc"
+      ],
       "nodes": [
         "roborio"
       ]