Map single-host port 10166 (instance 2) to host0 in BootTimeServiceImpl.

In `GrpcBlobMain`, three gRPC instances (`numInstances = 3`) are spawned
with offsets from `portNumber` (`10166`):
- `instance = 0`: port `10167` (`offset = 1`)
- `instance = 1`: port `10168` (`offset = 2`)
- `instance = 2`: port `10166` (`offset = 0`, single-CN default)

Because `gbmc-br` nftables rules (`50-gbmc-br-cn-redirect.rules` and
`50-gbmc-br.rules`) drop direct traffic to ports `10167-10168`
(`mark 0xff`) unless explicitly redirected for multi-host platforms,
single-host platforms (such as Velorum) communicate over port `10166`
(`instance = 2`).

While `BaremetalServiceImpl` and `BiosSettingsServiceImpl` check
`(instance + 1) == numInstances` to map `instance = 2` back to `0`
(`host0`), `BootTimeServiceImpl` omitted this check and mapped port
`10166` to `host2` (`/run/Boottime/host2` and
`/var/google/boottime/host2`), isolating loader boot times from the rest
of the BMC boot times stored under `host0`.

Update `BootTimeServiceImpl` to normalize `instance_number_` to `0`
when `(instance + 1) == numInstances`, matching `BaremetalServiceImpl`
and `BiosSettingsServiceImpl`. Also move the H1 header to line 1 of
`README.md` to satisfy `markdownlint` (`MD041`) in Kokoro presubmit and
export `boottime_test.cc` in `copy.bara.sky`.

PiperOrigin-RevId: 984494451
Change-Id: I4bff1bb94c7b7ad55e1dfd2ee815287fbe4e7fbb
diff --git a/README.md b/README.md
index 5f68988..42f3b99 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-TODO(kkachana): Add developer workflow guide
-
 # gRPC Blob Handler
 
+TODO(kkachana): Add developer workflow guide
+
 This is a general gRPC blob handler, receives blobs over gRPC and handles
 accordingly.
 
diff --git a/src/boottime_callback.cc b/src/boottime_callback.cc
index aa2f411..f5c24ba 100644
--- a/src/boottime_callback.cc
+++ b/src/boottime_callback.cc
@@ -14,6 +14,7 @@
 #include "boottime_api/boottime_api_common.h"
 #include "boottime_api/node_config.h"
 #include "boottime_api/resource.h"
+#include "grpcblob_defs.h"
 #include "grpcpp/server_context.h"
 #include "grpcpp/support/server_callback.h"
 #include "grpcpp/support/status.h"
@@ -50,9 +51,9 @@
 
 BootTimeServiceImpl::BootTimeServiceImpl(int instance)
     : phosphor::boottime::grpc_gen::BootTimeService::CallbackService(),
-      instance_number_(instance),
+      instance_number_((instance + 1 == numInstances) ? 0 : instance),
       base_path_(),
-      node_config_("host" + std::to_string(instance),
+      node_config_("host" + std::to_string(instance_number_),
                    std::string(btm::kBootTimeTagHost)) {
   RegisterSelfToApi().IgnoreError();
 }
@@ -60,9 +61,9 @@
 BootTimeServiceImpl::BootTimeServiceImpl(int instance,
                                          const std::string& basePath)
     : phosphor::boottime::grpc_gen::BootTimeService::CallbackService(),
-      instance_number_(instance),
+      instance_number_((instance + 1 == numInstances) ? 0 : instance),
       base_path_(basePath),
-      node_config_("host" + std::to_string(instance),
+      node_config_("host" + std::to_string(instance_number_),
                    std::string(btm::kBootTimeTagHost)),
       boottime_api_(basePath + std::string(btm::def::kLockFile),
                     absl::Seconds(5)) {
diff --git a/src/test/boottime_test.cc b/src/test/boottime_test.cc
new file mode 100644
index 0000000..098055b
--- /dev/null
+++ b/src/test/boottime_test.cc
@@ -0,0 +1,349 @@
+
+#include <chrono>  // NOLINT(build/c++11)
+#include <cstdint>
+#include <filesystem>  // NOLINT(build/c++17)
+#include <fstream>
+#include <memory>
+#include <string>
+#include <string_view>
+
+#include "boottime.grpc.pb.h"
+#include "boottime.pb.h"
+#include <gtest/gtest.h>
+#include "absl/time/clock.h"
+#include "absl/time/time.h"
+#include "grpcblob_server.h"
+#include "grpcpp/client_context.h"
+#include "grpcpp/create_channel.h"
+#include "grpcpp/security/credentials.h"
+#include "grpcpp/server_context.h"
+#include "grpcpp/support/status.h"
+
+#ifndef EXPECT_OK
+#define EXPECT_OK(x) EXPECT_TRUE(x.ok())
+#endif
+
+#ifndef TEMP_CLIENT_CONTEXT
+#define TEMP_CLIENT_CONTEXT(x, y) \
+  {                               \
+    ClientContext x;              \
+    y;                            \
+  }
+#endif
+
+namespace blobs {
+namespace {
+
+namespace fs = std::filesystem;
+using ::grpc::Channel;
+using ::grpc::ClientContext;
+using ::phosphor::boottime::GetBootTimeRequest;
+using ::phosphor::boottime::GetBootTimeResponse;
+using ::phosphor::boottime::RebootCompleteRequest;
+using ::phosphor::boottime::RebootCompleteResponse;
+using ::phosphor::boottime::SetCheckpointRequest;
+using ::phosphor::boottime::SetCheckpointResponse;
+using ::phosphor::boottime::SetDurationRequest;
+using ::phosphor::boottime::SetDurationResponse;
+using ::phosphor::boottime::grpc_gen::BootTimeService;
+
+class BootTimeTest : public ::testing::Test {
+ protected:
+  BootTimeTest() {
+    if (fs::exists(test_dir_)) {
+      fs::remove_all(test_dir_);
+    }
+    fs::create_directories(test_dir_);
+    LockFilePermissions(fs::perm_options::add);
+    if (server_ == nullptr) {
+      SetUpServer();
+    }
+  }
+
+  ~BootTimeTest() {
+    if (fs::exists(test_dir_)) {
+      fs::remove_all(test_dir_);
+    }
+  }
+
+  static int64_t GetMonoTimeMs() {
+    double monotime_tmp;
+    std::ifstream fin("/proc/uptime");
+    fin >> monotime_tmp;
+    return static_cast<int64_t>(monotime_tmp * 1000);
+  }
+
+  static void LockFilePermissions(fs::perm_options op) {
+    if (!fs::exists(test_lock_file_)) {
+      fs::create_directories(fs::path(test_lock_file_).remove_filename());
+      std::ofstream{std::string(test_lock_file_)};
+    }
+    fs::permissions(test_lock_file_,
+                    fs::perms::owner_write | fs::perms::group_write |
+                        fs::perms::others_write,
+                    op);
+  }
+
+  static void SetUpServer() {
+    server_ = std::make_unique<GrpcBlobServer>(std::string(test_dir_));
+    server_->start();
+    std::shared_ptr<Channel> channel = grpc::CreateChannel(
+        "localhost:10166", grpc::InsecureChannelCredentials());
+    boottime_stub_ = BootTimeService::NewStub(channel);
+  }
+
+  static void CloseServer() {
+    absl::SleepFor(absl::Seconds(1));
+    server_->stop();
+    server_.reset();
+    boottime_stub_.reset();
+  }
+
+  static void SetUpTestSuite() { SetUpServer(); }
+
+  static void TearDownTestSuite() { CloseServer(); }
+
+  static constexpr std::string_view test_dir_ = "/tmp/boottime_grpc_test/";
+  static constexpr std::string_view test_lock_file_ =
+      "/tmp/boottime_grpc_test/var/lock/boottime_api.lock";
+  static std::unique_ptr<GrpcBlobServer> server_;
+  static std::unique_ptr<BootTimeService::Stub> boottime_stub_;
+};
+
+std::unique_ptr<GrpcBlobServer> BootTimeTest::server_;
+std::unique_ptr<BootTimeService::Stub> BootTimeTest::boottime_stub_;
+
+TEST_F(BootTimeTest, BootTimeSetInvalidNameFormat) {
+  SetCheckpointRequest req_c;
+  SetCheckpointResponse resp_c;
+  SetDurationRequest req_d;
+  SetDurationResponse resp_d;
+
+  req_c.set_name("_**#-aa");
+  req_c.set_timestamp_ms(20000);
+  req_d.set_name("??_@@0002");
+  req_d.set_duration_ms(1500);
+  TEMP_CLIENT_CONTEXT(
+      c,
+      EXPECT_EQ(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c).error_code(),
+                grpc::StatusCode::INVALID_ARGUMENT));
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_EQ(boottime_stub_->SetDuration(&c, req_d, &resp_d).error_code(),
+                   grpc::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(BootTimeTest, BootTimeSetInvalidNameLength) {
+  SetCheckpointRequest req_c;
+  SetCheckpointResponse resp_c;
+  SetDurationRequest req_d;
+  SetDurationResponse resp_d;
+
+  req_c.mutable_name()->clear();
+  req_d.mutable_name()->clear();
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_EQ(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c),
+                   grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                                "Invalid name length")));
+  TEMP_CLIENT_CONTEXT(c,
+                      EXPECT_EQ(boottime_stub_->SetDuration(&c, req_d, &resp_d),
+                                grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                                             "Invalid name length")));
+
+  req_c.mutable_name()->reserve(1024 + 32);
+  req_d.mutable_name()->reserve(1024 + 32);
+  for (int i = 0; i <= (1024 / 16); ++i) {
+    req_c.mutable_name()->append("aaaaaaaaaaaaaaaa");
+    req_d.mutable_name()->append("aaaaaaaaaaaaaaaa");
+  }
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_EQ(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c),
+                   grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                                "Invalid name length")));
+  TEMP_CLIENT_CONTEXT(c,
+                      EXPECT_EQ(boottime_stub_->SetDuration(&c, req_d, &resp_d),
+                                grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                                             "Invalid name length")));
+}
+
+TEST_F(BootTimeTest, BootTimeSetAndGetData) {
+  SetCheckpointRequest req_c;
+  SetCheckpointResponse resp_c;
+  SetDurationRequest req_d;
+  SetDurationResponse resp_d;
+  RebootCompleteRequest req_r;
+  RebootCompleteResponse resp_r;
+  GetBootTimeRequest req_g;
+  GetBootTimeResponse resp_g;
+  int64_t monotime_ms_begin = 0;
+  int64_t monotime_ms_end = 0;
+  int64_t walltime_ms_begin = 0;
+
+  monotime_ms_begin = GetMonoTimeMs();
+  walltime_ms_begin = std::chrono::system_clock::now().time_since_epoch() /
+                      std::chrono::milliseconds(1);
+  req_c.set_name("tc1");
+  req_c.set_timestamp_ms(1000);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c)));
+
+  req_c.set_name("tc2");
+  req_c.set_timestamp_ms(2000);
+  req_c.set_duration_ms(300);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c)));
+
+  // Use internal walltime
+  req_c.set_name("tc3");
+  req_c.set_timestamp_ms(0);
+  req_c.set_duration_ms(0);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c)));
+
+  req_d.set_name("td1");
+  req_d.set_duration_ms(1600);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetDuration(&c, req_d, &resp_d)));
+
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->GetBootTime(&c, req_g, &resp_g)));
+  monotime_ms_end = GetMonoTimeMs();
+
+  // We can't check everything via text proto as reboot_flow.timestamp_mono_ms
+  // is not determined by the caller.
+  EXPECT_EQ(resp_g.stat().is_rebooting(), true);
+  EXPECT_EQ(resp_g.reboot_flow(0).stage(), "tc1");
+  EXPECT_EQ(resp_g.reboot_flow(0).timestamp_wall_ms(), 1000);
+  EXPECT_GE(resp_g.reboot_flow(0).timestamp_mono_ms(), monotime_ms_begin);
+
+  EXPECT_EQ(resp_g.reboot_flow(1).stage(), "tc2:BEGIN");
+  EXPECT_EQ(resp_g.reboot_flow(1).timestamp_wall_ms(), 2000 - 300);
+
+  EXPECT_EQ(resp_g.reboot_flow(2).stage(), "tc2");
+  EXPECT_EQ(resp_g.reboot_flow(2).timestamp_wall_ms(), 2000);
+  EXPECT_EQ(resp_g.reboot_flow(2).timestamp_mono_ms(),
+            resp_g.reboot_flow(1).timestamp_mono_ms() + 300);
+  EXPECT_GE(resp_g.reboot_flow(2).timestamp_mono_ms(),
+            resp_g.reboot_flow(0).timestamp_mono_ms());
+
+  EXPECT_EQ(resp_g.reboot_flow(3).stage(), "tc3");
+  EXPECT_GE(resp_g.reboot_flow(3).timestamp_mono_ms(),
+            resp_g.reboot_flow(2).timestamp_mono_ms());
+  EXPECT_LE(resp_g.reboot_flow(3).timestamp_mono_ms(), monotime_ms_end);
+  EXPECT_GE(resp_g.reboot_flow(3).timestamp_wall_ms(), walltime_ms_begin);
+  EXPECT_LE(resp_g.reboot_flow(3).timestamp_wall_ms(),
+            std::chrono::system_clock::now().time_since_epoch() /
+                std::chrono::milliseconds(1));
+
+  EXPECT_EQ(resp_g.durations(0).name(), "td1");
+  EXPECT_EQ(resp_g.durations(0).duration_ms(), 1600);
+
+  // Verify that single-host port 10166 (instance 2) writes to host0, matching
+  // instance 0 (port 10167).
+  std::shared_ptr<Channel> host0_channel = grpc::CreateChannel(
+      "localhost:10167", grpc::InsecureChannelCredentials());
+  std::unique_ptr<BootTimeService::Stub> host0_stub =
+      BootTimeService::NewStub(host0_channel);
+  GetBootTimeResponse resp_host0;
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(host0_stub->GetBootTime(&c, req_g, &resp_host0)));
+  EXPECT_EQ(resp_host0.reboot_flow_size(), resp_g.reboot_flow_size());
+  EXPECT_EQ(resp_host0.durations_size(), resp_g.durations_size());
+}
+
+TEST_F(BootTimeTest, BootTimeRebootBehavior) {
+  SetCheckpointRequest req_c;
+  SetCheckpointResponse resp_c;
+  SetDurationRequest req_d;
+  SetDurationResponse resp_d;
+  RebootCompleteRequest req_r;
+  RebootCompleteResponse resp_r;
+  GetBootTimeRequest req_g;
+  GetBootTimeResponse resp_g;
+  int64_t monotime_ms_begin = 0;
+  int64_t walltime_ms_begin = 0;
+
+  monotime_ms_begin = GetMonoTimeMs();
+  walltime_ms_begin = std::chrono::system_clock::now().time_since_epoch() /
+                      std::chrono::milliseconds(1);
+  req_c.set_name("tc3");
+  req_c.set_timestamp_ms(20000);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c)));
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->RebootComplete(&c, req_r, &resp_r)));
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->GetBootTime(&c, req_g, &resp_g)));
+
+  EXPECT_EQ(resp_g.stat().is_rebooting(), false);
+  EXPECT_EQ(resp_g.reboot_flow(0).stage(), "tc3");
+  EXPECT_EQ(resp_g.reboot_flow(0).timestamp_wall_ms(), 20000);
+  EXPECT_EQ(resp_g.reboot_flow(1).stage(), "RebootEnd");
+  EXPECT_GE(resp_g.reboot_flow(1).timestamp_mono_ms(), monotime_ms_begin);
+  EXPECT_LE(resp_g.reboot_flow(1).timestamp_mono_ms(), GetMonoTimeMs());
+  EXPECT_GE(resp_g.reboot_flow(1).timestamp_wall_ms(), walltime_ms_begin);
+  EXPECT_LE(resp_g.reboot_flow(1).timestamp_wall_ms(),
+            std::chrono::system_clock::now().time_since_epoch() /
+                std::chrono::milliseconds(1));
+
+  req_d.set_name("td2");
+  req_d.set_duration_ms(1000);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetDuration(&c, req_d, &resp_d)));
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->GetBootTime(&c, req_g, &resp_g)));
+  EXPECT_EQ(resp_g.stat().is_rebooting(), true);
+  EXPECT_EQ(resp_g.reboot_flow_size(), 0);
+  EXPECT_EQ(resp_g.durations(0).name(), "td2");
+  EXPECT_EQ(resp_g.durations(0).duration_ms(), 1000);
+}
+
+TEST_F(BootTimeTest, BootTimeApiRegisterFailure) {
+  SetCheckpointRequest req_c;
+  SetCheckpointResponse resp_c;
+  SetDurationRequest req_d;
+  SetDurationResponse resp_d;
+  RebootCompleteRequest req_r;
+  RebootCompleteResponse resp_r;
+
+  CloseServer();
+  LockFilePermissions(fs::perm_options::remove);
+  SetUpServer();
+
+  req_c.set_name("tc1");
+  req_c.set_timestamp_ms(1000);
+  req_d.set_name("td1");
+  req_d.set_duration_ms(1000);
+
+  TEMP_CLIENT_CONTEXT(
+      c,
+      EXPECT_EQ(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c).error_code(),
+                grpc::StatusCode::INTERNAL));
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_EQ(boottime_stub_->SetDuration(&c, req_d, &resp_d).error_code(),
+                   grpc::StatusCode::INTERNAL));
+  TEMP_CLIENT_CONTEXT(
+      c,
+      EXPECT_EQ(boottime_stub_->RebootComplete(&c, req_r, &resp_r).error_code(),
+                grpc::StatusCode::INTERNAL));
+
+  LockFilePermissions(fs::perm_options::add);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetCheckpoint(&c, req_c, &resp_c)));
+
+  CloseServer();
+  LockFilePermissions(fs::perm_options::remove);
+  SetUpServer();
+  LockFilePermissions(fs::perm_options::add);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->SetDuration(&c, req_d, &resp_d)));
+
+  CloseServer();
+  LockFilePermissions(fs::perm_options::remove);
+  SetUpServer();
+  LockFilePermissions(fs::perm_options::add);
+  TEMP_CLIENT_CONTEXT(
+      c, EXPECT_OK(boottime_stub_->RebootComplete(&c, req_r, &resp_r)));
+}
+
+}  // namespace
+}  // namespace blobs