pw_kvs: Transcation ID and key descriptor refactor

- Switch from per-key versions to a monotonically increasing KVS-global
  transcation ID.
- Move KeyDescriptor and the hash function to their own files.

Change-Id: I05287137579d4fe2d72c6e176969d46006c2aae6
diff --git a/pw_kvs/BUILD b/pw_kvs/BUILD
index 50601da..6adb71a 100644
--- a/pw_kvs/BUILD
+++ b/pw_kvs/BUILD
@@ -30,6 +30,8 @@
         "entry.cc",
         "flash_memory.cc",
         "key_value_store.cc",
+        "public/pw_kvs/internal/hash.h",
+        "public/pw_kvs/internal/key_descriptor.h",
         "public/pw_kvs/internal/sector_descriptor.h",
         "pw_kvs_private/entry.h",
         "pw_kvs_private/macros.h",
diff --git a/pw_kvs/BUILD.gn b/pw_kvs/BUILD.gn
index a083724..e626bbb 100644
--- a/pw_kvs/BUILD.gn
+++ b/pw_kvs/BUILD.gn
@@ -34,6 +34,8 @@
     "entry.cc",
     "flash_memory.cc",
     "key_value_store.cc",
+    "public/pw_kvs/internal/hash.h",
+    "public/pw_kvs/internal/key_descriptor.h",
     "public/pw_kvs/internal/sector_descriptor.h",
     "pw_kvs_private/entry.h",
     "pw_kvs_private/macros.h",
diff --git a/pw_kvs/entry.cc b/pw_kvs/entry.cc
index 33e86a8..ad81b12 100644
--- a/pw_kvs/entry.cc
+++ b/pw_kvs/entry.cc
@@ -20,7 +20,7 @@
 #include "pw_kvs_private/macros.h"
 #include "pw_log/log.h"
 
-namespace pw::kvs {
+namespace pw::kvs::internal {
 
 using std::byte;
 using std::string_view;
@@ -60,7 +60,7 @@
              span<const byte> value,
              uint16_t value_size_bytes,
              size_t alignment_bytes,
-             uint32_t key_version)
+             uint32_t transaction_id)
     : Entry(&partition,
             address,
             {.magic = magic,
@@ -68,7 +68,7 @@
              .alignment_units = alignment_bytes_to_units(alignment_bytes),
              .key_length_bytes = static_cast<uint8_t>(key.size()),
              .value_size_bytes = value_size_bytes,
-             .key_version = key_version}) {
+             .transaction_id = transaction_id}) {
   if (algorithm != nullptr) {
     span<const byte> checksum = CalculateChecksum(algorithm, key, value);
     std::memcpy(&header_.checksum,
@@ -207,4 +207,4 @@
   return algorithm->Finish();
 }
 
-}  // namespace pw::kvs
+}  // namespace pw::kvs::internal
diff --git a/pw_kvs/entry_test.cc b/pw_kvs/entry_test.cc
index 5022034..2c5f7c4 100644
--- a/pw_kvs/entry_test.cc
+++ b/pw_kvs/entry_test.cc
@@ -24,7 +24,7 @@
 #include "pw_kvs_private/byte_utils.h"
 #include "pw_span/span.h"
 
-namespace pw::kvs {
+namespace pw::kvs::internal {
 namespace {
 
 using std::byte;
@@ -60,7 +60,7 @@
   EXPECT_FALSE(entry.deleted());
   EXPECT_EQ(entry.magic(), 9u);
   EXPECT_EQ(entry.value_size(), sizeof("123"));
-  EXPECT_EQ(entry.key_version(), 9876u);
+  EXPECT_EQ(entry.transaction_id(), 9876u);
 }
 
 TEST(Entry, Construct_Tombstone) {
@@ -72,7 +72,7 @@
   EXPECT_TRUE(entry.deleted());
   EXPECT_EQ(entry.magic(), 99u);
   EXPECT_EQ(entry.value_size(), 0u);
-  EXPECT_EQ(entry.key_version(), 123u);
+  EXPECT_EQ(entry.transaction_id(), 123u);
 }
 
 constexpr auto kHeader1 = ByteStr(
@@ -111,7 +111,7 @@
   EXPECT_EQ(entry_.magic(), 0x600DF00Du);
   EXPECT_EQ(entry_.key_length(), 5u);
   EXPECT_EQ(entry_.value_size(), 6u);
-  EXPECT_EQ(entry_.key_version(), 0x96979899u);
+  EXPECT_EQ(entry_.transaction_id(), 0x96979899u);
   EXPECT_FALSE(entry_.deleted());
 }
 
@@ -229,7 +229,7 @@
   EXPECT_EQ(entry_.magic(), 0x600DF00Du);
   EXPECT_EQ(entry_.key_length(), 1u);
   EXPECT_EQ(entry_.value_size(), 0u);
-  EXPECT_EQ(entry_.key_version(), 0x03020100u);
+  EXPECT_EQ(entry_.transaction_id(), 0x03020100u);
   EXPECT_TRUE(entry_.deleted());
 }
 
@@ -300,4 +300,4 @@
   EXPECT_EQ(Status::OK, entry.VerifyChecksumInFlash(&checksum));
 }
 }  // namespace
-}  // namespace pw::kvs
+}  // namespace pw::kvs::internal
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
index 2fe9504..934b296 100644
--- a/pw_kvs/key_value_store.cc
+++ b/pw_kvs/key_value_store.cc
@@ -27,6 +27,7 @@
 namespace pw::kvs {
 namespace {
 
+using internal::Entry;
 using std::byte;
 using std::string_view;
 
@@ -45,8 +46,7 @@
       entry_header_format_(format),
       options_(options),
       key_descriptors_(key_descriptor_list),
-      sectors_(sector_descriptor_list),
-      last_new_sector_(sectors_.data()) {}
+      sectors_(sector_descriptor_list) {}
 
 Status KeyValueStore::Init() {
   INF("Initializing key value store");
@@ -62,10 +62,6 @@
   sectors_.resize(partition_.sector_count());
   key_descriptors_.clear();
 
-  // TODO: init last_new_sector_ to a random sector. Since the on-flash stored
-  // information does not allow recovering the previous last_new_sector_ after
-  // clean start, random is a good second choice.
-
   const size_t sector_size_bytes = partition_.sector_size_bytes();
 
   if (working_buffer_.size() < sector_size_bytes) {
@@ -132,12 +128,27 @@
   }
 
   DBG("Second pass: Count valid bytes in each sector");
+  const KeyDescriptor* newest_key = nullptr;
+  last_transaction_id_ = 0;
+
   // For every valid key, increment the valid bytes for that sector.
   for (KeyDescriptor& key_descriptor : key_descriptors_) {
     Entry entry;
-    TRY(Entry::Read(partition_, key_descriptor.address, &entry));
-    SectorFromAddress(key_descriptor.address)->AddValidBytes(entry.size());
+    TRY(Entry::Read(partition_, key_descriptor.address(), &entry));
+    SectorFromKey(key_descriptor)->AddValidBytes(entry.size());
+
+    if (key_descriptor.IsNewerThan(last_transaction_id_)) {
+      last_transaction_id_ = key_descriptor.transaction_id();
+      newest_key = &key_descriptor;
+    }
   }
+
+  if (newest_key == nullptr) {
+    last_new_sector_ = sectors_.begin();
+  } else {
+    last_new_sector_ = SectorFromKey(newest_key);
+  }
+
   initialized_ = true;
 
   INF("KeyValueStore init complete: active keys %zu, deleted keys %zu, sectors "
@@ -171,18 +182,7 @@
   const string_view key(key_buffer.data(), key_length);
 
   TRY(entry.VerifyChecksumInFlash(entry_header_format_.checksum));
-
-  KeyDescriptor key_descriptor(
-      key,
-      entry.key_version(),
-      entry_address,
-      entry.deleted() ? KeyDescriptor::kDeleted : KeyDescriptor::kValid);
-
-  DBG("Key hash: %zx (%zu)",
-      size_t(key_descriptor.key_hash),
-      size_t(key_descriptor.key_hash));
-
-  TRY(AppendNewOrOverwriteStaleExistingDescriptor(key_descriptor));
+  TRY(AppendNewOrOverwriteStaleExistingDescriptor(entry.descriptor(key)));
 
   *next_entry_address = entry.next_address();
   return Status::OK;
@@ -196,7 +196,7 @@
     const KeyDescriptor& key_descriptor) {
   // With the new key descriptor, either add it to the descriptor table or
   // overwrite an existing entry with an older version of the key.
-  KeyDescriptor* existing_descriptor = FindDescriptor(key_descriptor.key_hash);
+  KeyDescriptor* existing_descriptor = FindDescriptor(key_descriptor.hash());
 
   // Write a new entry.
   if (existing_descriptor == nullptr) {
@@ -204,15 +204,18 @@
       return Status::RESOURCE_EXHAUSTED;
     }
     key_descriptors_.push_back(key_descriptor);
-  } else if (existing_descriptor->key_version < key_descriptor.key_version) {
+  } else if (key_descriptor.IsNewerThan(
+                 existing_descriptor->transaction_id())) {
     // Existing entry is old; replace the existing entry with the new one.
     *existing_descriptor = key_descriptor;
   } else {
-    // Otherwise, check for data integrity and leave the existing entry.
-    if (existing_descriptor->key_version == key_descriptor.key_version) {
-      ERR("Data loss: Duplicated old(=%zu) and new(=%zu) version",
-          size_t(existing_descriptor->key_version),
-          size_t(key_descriptor.key_version));
+    // Otherwise, check if the entries have a duplicate transaction ID, which is
+    // not valid.
+    if (existing_descriptor->transaction_id() ==
+        key_descriptor.transaction_id()) {
+      ERR("Data loss: Duplicated old(=%zu) and new(=%zu) transaction ID",
+          size_t(existing_descriptor->transaction_id()),
+          size_t(key_descriptor.transaction_id()));
       return Status::DATA_LOSS;
     }
     DBG("Found stale entry when appending; ignoring");
@@ -222,7 +225,7 @@
 
 KeyValueStore::KeyDescriptor* KeyValueStore::FindDescriptor(uint32_t hash) {
   for (KeyDescriptor& key_descriptor : key_descriptors_) {
-    if (key_descriptor.key_hash == hash) {
+    if (key_descriptor.hash() == hash) {
       return &key_descriptor;
     }
   }
@@ -238,7 +241,7 @@
   TRY_WITH_SIZE(FindExistingKeyDescriptor(key, &key_descriptor));
 
   Entry entry;
-  TRY_WITH_SIZE(Entry::Read(partition_, key_descriptor->address, &entry));
+  TRY_WITH_SIZE(Entry::Read(partition_, key_descriptor->address(), &entry));
 
   StatusWithSize result = entry.ReadValue(value_buffer, offset_bytes);
   if (result.ok() && options_.verify_on_read && offset_bytes == 0u) {
@@ -276,8 +279,8 @@
 
   if (status.ok()) {
     DBG("Overwriting entry for key %#08" PRIx32 " in sector %u",
-        key_descriptor->key_hash,
-        SectorIndex(SectorFromAddress(key_descriptor->address)));
+        key_descriptor->hash(),
+        SectorIndex(SectorFromKey(key_descriptor)));
     return WriteEntryForExistingKey(
         key_descriptor, KeyDescriptor::kValid, key, value);
   }
@@ -296,8 +299,8 @@
   TRY(FindExistingKeyDescriptor(key, &key_descriptor));
 
   DBG("Writing tombstone for key %#08" PRIx32 " in sector %u",
-      key_descriptor->key_hash,
-      SectorIndex(SectorFromAddress(key_descriptor->address)));
+      key_descriptor->hash(),
+      SectorIndex(SectorFromKey(key_descriptor)));
   return WriteEntryForExistingKey(
       key_descriptor, KeyDescriptor::kDeleted, key, {});
 }
@@ -314,7 +317,7 @@
   std::memset(item_.key_buffer_.data(), 0, item_.key_buffer_.size());
 
   Entry entry;
-  if (Entry::Read(item_.kvs_.partition_, descriptor().address, &entry).ok()) {
+  if (Entry::Read(item_.kvs_.partition_, descriptor().address(), &entry).ok()) {
     entry.ReadKey(item_.key_buffer_);
   }
 
@@ -351,23 +354,11 @@
   TRY_WITH_SIZE(FindExistingKeyDescriptor(key, &key_descriptor));
 
   Entry entry;
-  TRY_WITH_SIZE(Entry::Read(partition_, key_descriptor->address, &entry));
+  TRY_WITH_SIZE(Entry::Read(partition_, key_descriptor->address(), &entry));
 
   return StatusWithSize(entry.value_size());
 }
 
-uint32_t KeyValueStore::HashKey(string_view string) {
-  uint32_t hash = 0;
-  uint32_t coefficient = 65599u;
-
-  for (char ch : string) {
-    hash += coefficient * unsigned(ch);
-    coefficient *= 65599u;
-  }
-
-  return hash;
-}
-
 Status KeyValueStore::FixedSizeGet(std::string_view key,
                                    byte* value,
                                    size_t size_bytes) const {
@@ -405,13 +396,13 @@
 //
 Status KeyValueStore::FindKeyDescriptor(string_view key,
                                         const KeyDescriptor** result) const {
-  const uint32_t hash = HashKey(key);
+  const uint32_t hash = internal::Hash(key);
   Entry::KeyBuffer key_buffer;
 
   for (auto& descriptor : key_descriptors_) {
-    if (descriptor.key_hash == hash) {
+    if (descriptor.hash() == hash) {
       TRY(Entry::ReadKey(
-          partition_, descriptor.address, key.size(), key_buffer.data()));
+          partition_, descriptor.address(), key.size(), key_buffer.data()));
 
       if (key == string_view(key_buffer.data(), key.size())) {
         DBG("Found match for key hash 0x%08" PRIx32, hash);
@@ -451,8 +442,8 @@
                                                span<const byte> value) {
   // Find the original entry and sector to update the sector's valid_bytes.
   Entry original_entry;
-  TRY(Entry::Read(partition_, key_descriptor->address, &original_entry));
-  SectorDescriptor* old_sector = SectorFromAddress(key_descriptor->address);
+  TRY(Entry::Read(partition_, key_descriptor->address(), &original_entry));
+  SectorDescriptor* old_sector = SectorFromKey(key_descriptor);
 
   SectorDescriptor* sector;
   TRY(FindOrRecoverSectorWithSpace(&sector,
@@ -461,13 +452,13 @@
       SectorIndex(sector),
       SectorBaseAddress(sector));
 
-  if (old_sector != SectorFromAddress(key_descriptor->address)) {
+  if (old_sector != SectorFromKey(key_descriptor)) {
     DBG("Sector for old entry (size %zu) was garbage collected. Old entry "
         "relocated to sector %u",
         original_entry.size(),
-        SectorIndex(SectorFromAddress(key_descriptor->address)));
+        SectorIndex(SectorFromKey(key_descriptor)));
 
-    old_sector = SectorFromAddress(key_descriptor->address);
+    old_sector = SectorFromKey(key_descriptor);
   }
 
   TRY(AppendEntry(sector, key_descriptor, key, value, new_state));
@@ -484,14 +475,14 @@
     return Status::RESOURCE_EXHAUSTED;
   }
 
-  // Create the KeyDescriptor that will be added to the list. The version and
-  // address will be set by AppendEntry.
-  KeyDescriptor key_descriptor(key, 0, 0);
-
   SectorDescriptor* sector;
   TRY(FindOrRecoverSectorWithSpace(&sector,
                                    Entry::size(partition_, key, value)));
   DBG("Writing new entry; found sector: %u", SectorIndex(sector));
+
+  // Create the KeyDescriptor that will be added to the list. The transaction ID
+  // and address will be set by AppendEntry.
+  KeyDescriptor key_descriptor(key);
   TRY(AppendEntry(sector, &key_descriptor, key, value, KeyDescriptor::kValid));
 
   // Only add the entry when we are certain the write succeeded.
@@ -512,7 +503,7 @@
   // store the key and value in the TempEntry stored in the static allocated
   // working_buffer_.
   Entry entry;
-  TRY(Entry::Read(partition_, key_descriptor.address, &entry));
+  TRY(Entry::Read(partition_, key_descriptor.address(), &entry));
   TRY_ASSIGN(size_t key_length, entry.ReadKey(temp_entry->key));
   string_view key = string_view(temp_entry->key.data(), key_length);
   auto result = entry.ReadValue(as_writable_bytes(span(temp_entry->value)));
@@ -524,13 +515,16 @@
   TRY(entry.VerifyChecksum(
       entry_header_format_.checksum, key, as_bytes(value)));
 
-  SectorDescriptor* old_sector = SectorFromAddress(key_descriptor.address);
+  SectorDescriptor* old_sector = SectorFromKey(key_descriptor);
 
   // Find a new sector for the entry and write it to the new location.
   SectorDescriptor* new_sector;
   TRY(FindSectorWithSpace(&new_sector, entry.size(), old_sector, true));
-  TRY(AppendEntry(
-      new_sector, &key_descriptor, key, as_bytes(value), key_descriptor.state));
+  TRY(AppendEntry(new_sector,
+                  &key_descriptor,
+                  key,
+                  as_bytes(value),
+                  key_descriptor.state()));
 
   // Do the valid bytes accounting for the sector the entry was relocated out
   // of.
@@ -677,7 +671,7 @@
   // Step 2: Move any valid entries in the GC sector to other sectors
   if (sector_to_gc->valid_bytes() != 0) {
     for (auto& descriptor : key_descriptors_) {
-      if (AddressInSector(*sector_to_gc, descriptor.address)) {
+      if (AddressInSector(*sector_to_gc, descriptor.address())) {
         DBG("  Relocate entry");
         TRY(RelocateEntry(descriptor));
       }
@@ -703,36 +697,16 @@
 
 Status KeyValueStore::AppendEntry(SectorDescriptor* sector,
                                   KeyDescriptor* key_descriptor,
-                                  const string_view key,
+                                  string_view key,
                                   span<const byte> value,
                                   KeyDescriptor::State new_state) {
   const Address address = NextWritableAddress(sector);
-  DBG("Appending to address: %#zx", size_t(address));
+  Entry entry = CreateEntry(address, key, value, new_state);
 
-  Entry entry;
-
-  if (new_state == KeyDescriptor::kDeleted) {
-    entry = Entry::Tombstone(partition_,
-                             address,
-                             entry_header_format_.magic,
-                             entry_header_format_.checksum,
-                             key,
-                             partition_.alignment_bytes(),
-                             key_descriptor->key_version + 1);
-  } else {
-    entry = Entry::Valid(partition_,
-                         address,
-                         entry_header_format_.magic,
-                         entry_header_format_.checksum,
-                         key,
-                         value,
-                         partition_.alignment_bytes(),
-                         key_descriptor->key_version + 1);
-  }
-
-  DBG("Appending %zu B entry with key version: %x",
+  DBG("Appending %zu B entry with transaction ID %" PRIu32 " to address %#zx",
       entry.size(),
-      unsigned(entry.key_version()));
+      entry.transaction_id(),
+      size_t(address));
 
   TRY_ASSIGN(const size_t written, entry.Write(key, value));
 
@@ -740,14 +714,36 @@
     TRY(entry.VerifyChecksumInFlash(entry_header_format_.checksum));
   }
 
-  key_descriptor->address = address;
-  key_descriptor->key_version = entry.key_version();
-  key_descriptor->state = new_state;
-
+  entry.UpdateDescriptor(key_descriptor);
   sector->MarkValidBytesWritten(written);
   return Status::OK;
 }
 
+Entry KeyValueStore::CreateEntry(Address address,
+                                 std::string_view key,
+                                 span<const byte> value,
+                                 KeyDescriptor::State state) {
+  const uint32_t transaction_id = ++last_transaction_id_;
+
+  if (state == KeyDescriptor::kDeleted) {
+    return Entry::Tombstone(partition_,
+                            address,
+                            entry_header_format_.magic,
+                            entry_header_format_.checksum,
+                            key,
+                            partition_.alignment_bytes(),
+                            transaction_id);
+  }
+  return Entry::Valid(partition_,
+                      address,
+                      entry_header_format_.magic,
+                      entry_header_format_.checksum,
+                      key,
+                      value,
+                      partition_.alignment_bytes(),
+                      transaction_id);
+}
+
 void KeyValueStore::LogDebugInfo() {
   const size_t sector_size_bytes = partition_.sector_size_bytes();
   DBG("====================== KEY VALUE STORE DUMP =========================");
@@ -769,10 +765,10 @@
     const KeyDescriptor& kd = key_descriptors_[i];
     DBG("   |%3zu: | %8zx  |%8zu  | %8zu | %8zx",
         i,
-        size_t(kd.key_hash),
-        size_t(kd.key_version),
-        size_t(kd.address),
-        size_t(kd.address));
+        size_t(kd.hash()),
+        size_t(kd.transaction_id()),
+        size_t(kd.address()),
+        size_t(kd.address()));
   }
   DBG(" ");
 
@@ -838,11 +834,11 @@
 void KeyValueStore::LogKeyDescriptor() const {
   DBG("Key descriptors: count %zu", key_descriptors_.size());
   for (auto& key : key_descriptors_) {
-    DBG("  - Key: %s, hash %#zx, version %zu, address %#zx",
+    DBG("  - Key: %s, hash %#zx, transaction ID %zu, address %#zx",
         key.deleted() ? "Deleted" : "Valid",
-        static_cast<size_t>(key.key_hash),
-        static_cast<size_t>(key.key_version),
-        static_cast<size_t>(key.address));
+        static_cast<size_t>(key.hash()),
+        static_cast<size_t>(key.transaction_id()),
+        static_cast<size_t>(key.address()));
   }
 }
 
diff --git a/pw_kvs/key_value_store_map_test.cc b/pw_kvs/key_value_store_map_test.cc
index 6f3a5a4..2e04fc0 100644
--- a/pw_kvs/key_value_store_map_test.cc
+++ b/pw_kvs/key_value_store_map_test.cc
@@ -118,7 +118,8 @@
 
         // Either add a new key or replace an existing one.
         if (empty() || random_int() % 2 == 0) {
-          key = random_string(random_int() % (Entry::kMaxKeyLength + 1));
+          key = random_string(random_int() %
+                              (internal::Entry::kMaxKeyLength + 1));
         } else {
           key = RandomPresentKey();
         }
@@ -221,7 +222,7 @@
 
     Status result = kvs_.Put(key, as_bytes(span(value)));
 
-    if (key.empty() || key.size() > Entry::kMaxKeyLength) {
+    if (key.empty() || key.size() > internal::Entry::kMaxKeyLength) {
       EXPECT_EQ(Status::INVALID_ARGUMENT, result);
     } else if (map_.size() == kvs_.max_size()) {
       EXPECT_EQ(Status::RESOURCE_EXHAUSTED, result);
@@ -251,7 +252,7 @@
 
     Status result = kvs_.Delete(key);
 
-    if (key.empty() || key.size() > Entry::kMaxKeyLength) {
+    if (key.empty() || key.size() > internal::Entry::kMaxKeyLength) {
       EXPECT_EQ(Status::INVALID_ARGUMENT, result);
     } else if (map_.count(key) == 0) {
       EXPECT_EQ(Status::NOT_FOUND, result);
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
index 0c29979..d47eed4 100644
--- a/pw_kvs/key_value_store_test.cc
+++ b/pw_kvs/key_value_store_test.cc
@@ -45,6 +45,7 @@
 namespace pw::kvs {
 namespace {
 
+using internal::EntryHeader;
 using std::byte;
 
 constexpr size_t kMaxEntries = 256;
diff --git a/pw_kvs/public/pw_kvs/internal/hash.h b/pw_kvs/public/pw_kvs/internal/hash.h
new file mode 100644
index 0000000..5e97e69
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/internal/hash.h
@@ -0,0 +1,34 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <string_view>
+
+namespace pw::kvs::internal {
+
+// The hash function used to hash keys.
+constexpr uint32_t Hash(std::string_view string) {
+  uint32_t hash = 0;
+  uint32_t coefficient = 65599u;
+
+  for (char ch : string) {
+    hash += coefficient * uint32_t(ch);
+    coefficient *= 65599u;
+  }
+
+  return hash;
+}
+
+}  // namespace pw::kvs::internal
diff --git a/pw_kvs/public/pw_kvs/internal/key_descriptor.h b/pw_kvs/public/pw_kvs/internal/key_descriptor.h
new file mode 100644
index 0000000..bc4d2f8
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/internal/key_descriptor.h
@@ -0,0 +1,67 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <string_view>
+
+#include "pw_kvs/flash_memory.h"
+#include "pw_kvs/internal/hash.h"
+
+namespace pw::kvs::internal {
+
+// Caches information about a key-value entry. Facilitates quickly finding
+// entries without having to read flash.
+class KeyDescriptor {
+ public:
+  enum State { kValid, kDeleted };
+
+  constexpr KeyDescriptor(std::string_view key)
+      : KeyDescriptor(key, 0, 0, kValid) {}
+
+  uint32_t hash() const { return key_hash_; }
+  uint32_t transaction_id() const { return transaction_id_; }
+  uint32_t address() const { return address_; }
+  State state() const { return state_; }
+
+  // True if the KeyDesctiptor's transaction ID is newer than the specified ID.
+  bool IsNewerThan(uint32_t other_transaction_id) const {
+    // TODO: Consider handling rollover.
+    return transaction_id() > other_transaction_id;
+  }
+
+  bool deleted() const { return state_ == kDeleted; }
+
+ private:
+  friend class Entry;
+
+  constexpr KeyDescriptor(std::string_view key,
+                          uint32_t version,
+                          FlashPartition::Address address,
+                          State initial_state)
+      : key_hash_(Hash(key)),
+        transaction_id_(version),
+        address_(address),
+        state_(initial_state) {}
+
+  uint32_t key_hash_;
+  uint32_t transaction_id_;
+  FlashPartition::Address address_;
+
+  // TODO: This information could be packed into the above fields to save RAM.
+  State state_;
+};
+
+}  // namespace pw::kvs::internal
diff --git a/pw_kvs/public/pw_kvs/key_value_store.h b/pw_kvs/public/pw_kvs/key_value_store.h
index da40270..d6a6768 100644
--- a/pw_kvs/public/pw_kvs/key_value_store.h
+++ b/pw_kvs/public/pw_kvs/key_value_store.h
@@ -22,6 +22,7 @@
 #include "pw_containers/vector.h"
 #include "pw_kvs/checksum.h"
 #include "pw_kvs/flash_memory.h"
+#include "pw_kvs/internal/key_descriptor.h"
 #include "pw_kvs/internal/sector_descriptor.h"
 #include "pw_span/span.h"
 #include "pw_status/status.h"
@@ -30,6 +31,8 @@
 namespace pw::kvs {
 namespace internal {
 
+class Entry;
+
 template <typename T, typename = decltype(span(std::declval<T>()))>
 constexpr bool ConvertsToSpan(int) {
   return true;
@@ -49,9 +52,6 @@
 using ConvertsToSpan =
     std::bool_constant<internal::ConvertsToSpan<std::remove_reference_t<T>>(0)>;
 
-// Internal-only persistent storage header format.
-class Entry;
-
 struct EntryHeaderFormat {
   // Magic is a unique constant identifier for entries.
   //
@@ -82,9 +82,6 @@
 };
 
 class KeyValueStore {
- protected:
-  struct KeyDescriptor;
-
  public:
   // TODO: Make this configurable.
   static constexpr size_t kWorkingBufferSizeBytes = (4 * 1024);
@@ -210,7 +207,7 @@
     constexpr iterator(const KeyValueStore& kvs, size_t index)
         : item_(kvs), index_(index) {}
 
-    const KeyDescriptor& descriptor() const {
+    const internal::KeyDescriptor& descriptor() const {
       return item_.kvs_.key_descriptors_[index_];
     }
 
@@ -232,30 +229,9 @@
 
  protected:
   using Address = FlashPartition::Address;
+  using KeyDescriptor = internal::KeyDescriptor;
   using SectorDescriptor = internal::SectorDescriptor;
 
-  struct KeyDescriptor {
-    enum State { kValid, kDeleted };
-
-    KeyDescriptor(std::string_view key,
-                  uint32_t version,
-                  Address addr,
-                  State initial_state = kValid)
-        : key_hash(HashKey(key)),
-          key_version(version),
-          address(addr),
-          state(initial_state) {}
-
-    bool deleted() const { return state == kDeleted; }
-
-    uint32_t key_hash;
-    uint32_t key_version;
-    Address address;  // In partition address.
-
-    // TODO: This information should be packed into the above fields to save RAM
-    State state;
-  };
-
   // In the future, will be able to provide additional EntryHeaderFormats for
   // backwards compatibility.
   KeyValueStore(FlashPartition* partition,
@@ -265,8 +241,6 @@
                 const Options& options);
 
  private:
-  static uint32_t HashKey(std::string_view string);
-
   Status FixedSizeGet(std::string_view key,
                       std::byte* value,
                       size_t size_bytes) const;
@@ -339,18 +313,27 @@
     return SectorIndex(sector) * partition_.sector_size_bytes();
   }
 
-  SectorDescriptor* SectorFromAddress(Address address) {
-    const size_t index = address / partition_.sector_size_bytes();
+  SectorDescriptor* SectorFromKey(const KeyDescriptor& descriptor) {
+    const size_t index = descriptor.address() / partition_.sector_size_bytes();
     // TODO: Add boundary checking once asserts are supported.
     // DCHECK_LT(index, sector_map_size_);
     return &sectors_[index];
   }
 
+  SectorDescriptor* SectorFromKey(const KeyDescriptor* descriptor) {
+    return SectorFromKey(*descriptor);
+  }
+
   Address NextWritableAddress(const SectorDescriptor* sector) const {
     return SectorBaseAddress(sector) + partition_.sector_size_bytes() -
            sector->writable_bytes();
   }
 
+  internal::Entry CreateEntry(Address address,
+                              std::string_view key,
+                              span<const std::byte> value,
+                              KeyDescriptor::State state);
+
   void LogSectors() const;
   void LogKeyDescriptor() const;
 
@@ -379,6 +362,7 @@
   // Use SectorDescriptor* for the persistent storage rather than sector index
   // because SectorDescriptor* is the standard way to identify a sector.
   SectorDescriptor* last_new_sector_;
+  uint32_t last_transaction_id_;
 
   bool initialized_ = false;
 
diff --git a/pw_kvs/pw_kvs_private/entry.h b/pw_kvs/pw_kvs_private/entry.h
index f42df7c..d898f56 100644
--- a/pw_kvs/pw_kvs_private/entry.h
+++ b/pw_kvs/pw_kvs_private/entry.h
@@ -24,9 +24,10 @@
 #include "pw_kvs/alignment.h"
 #include "pw_kvs/checksum.h"
 #include "pw_kvs/flash_memory.h"
+#include "pw_kvs/internal/key_descriptor.h"
 #include "pw_span/span.h"
 
-namespace pw::kvs {
+namespace pw::kvs::internal {
 
 // Disk format of the header used for each key-value entry.
 struct EntryHeader {
@@ -50,8 +51,8 @@
   // or 0xFFFF) is reserved to indicate this is a tombstone (deleted) entry.
   uint16_t value_size_bytes;
 
-  // The version of the key. Monotonically increasing.
-  uint32_t key_version;
+  // The transaction ID for this key. Monotonically increasing.
+  uint32_t transaction_id;
 };
 
 static_assert(sizeof(EntryHeader) == 16, "EntryHeader must not have padding");
@@ -90,7 +91,7 @@
                      std::string_view key,
                      span<const std::byte> value,
                      size_t alignment_bytes,
-                     uint32_t key_version) {
+                     uint32_t transaction_id) {
     return Entry(partition,
                  address,
                  magic,
@@ -99,7 +100,7 @@
                  value,
                  value.size(),
                  alignment_bytes,
-                 key_version);
+                 transaction_id);
   }
 
   // Creates a new Entry for a tombstone entry, which marks a deleted key.
@@ -109,7 +110,7 @@
                          ChecksumAlgorithm* algorithm,
                          std::string_view key,
                          size_t alignment_bytes,
-                         uint32_t key_version) {
+                         uint32_t transaction_id) {
     return Entry(partition,
                  address,
                  magic,
@@ -118,11 +119,25 @@
                  {},
                  kDeletedValueLength,
                  alignment_bytes,
-                 key_version);
+                 transaction_id);
   }
 
   Entry() = default;
 
+  KeyDescriptor descriptor(std::string_view key) const {
+    return KeyDescriptor(
+        key,
+        transaction_id(),
+        address_,
+        deleted() ? KeyDescriptor::kDeleted : KeyDescriptor::kValid);
+  }
+
+  void UpdateDescriptor(KeyDescriptor* kd) {
+    kd->transaction_id_ = transaction_id();
+    kd->address_ = address_;
+    kd->state_ = deleted() ? KeyDescriptor::kDeleted : KeyDescriptor::kValid;
+  }
+
   StatusWithSize Write(std::string_view key, span<const std::byte> value) const;
 
   // Reads a key into a buffer, which must be large enough for a max-length key.
@@ -169,7 +184,7 @@
 
   uint32_t magic() const { return header_.magic; }
 
-  uint32_t key_version() const { return header_.key_version; }
+  uint32_t transaction_id() const { return header_.transaction_id; }
 
   // True if this is a tombstone entry.
   bool deleted() const {
@@ -200,7 +215,7 @@
         span<const std::byte> value,
         uint16_t value_size_bytes,
         size_t alignment_bytes,
-        uint32_t key_version);
+        uint32_t transaction_id);
 
   constexpr Entry(FlashPartition* partition,
                   Address address,
@@ -224,4 +239,4 @@
   EntryHeader header_;
 };
 
-}  // namespace pw::kvs
+}  // namespace pw::kvs::internal