Store remote registry file hashes in the lockfile

`MODULE.bazel`, `bazel_registry.json` and `source.json` files obtained from remote registries are stored in the repository cache and their hashes are collected in the lockfile. This speeds up incremental module resolutions, such as after adding a new `bazel_dep`.

Yanked versions are not stored in the lockfile. Their handling will be part of a follow-up PR.

Implements part of https://docs.google.com/document/d/1TjA7-M5njkI1F38IC0pm305S9EOmxcUwaCIvaSmansg/edit
Work towards #20369

Closes #21901.

PiperOrigin-RevId: 631195852
Change-Id: I35c30af7f9c3626bdbcb04c85b8c2502eeaafd3e
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index ce72f16..344180e 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -2917,7 +2917,7 @@
         "bzlTransitiveDigest": "tunTSmgwd2uvTzkCLtdbuCp0AI+WR+ftiPNqZ0rmcZk=",
         "recordedFileInputs": {
           "@@//MODULE.bazel": "eba5503742af5785c2d0d81d88e7407c7f23494b5162c055227435549b8774d1",
-          "@@//src/test/tools/bzlmod/MODULE.bazel.lock": "4315fd0f326ba0b7493bc97ec47982b9dbdd631e12ac799f31016c72a40fdfa8"
+          "@@//src/test/tools/bzlmod/MODULE.bazel.lock": "547b1ca7af37ca0b4e7c7de36093d66b81d46440b58b41c76fe9d6df3af9ea52"
         },
         "recordedDirentsInputs": {},
         "envVariables": {},
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
index ed54ed9..8378eed 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -15,12 +15,16 @@
 
 package com.google.devtools.build.lib.bazel;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
@@ -131,8 +135,8 @@
   public static final String DEFAULT_CACHE_LOCATION = "cache/repos/v1";
 
   // Default list of registries.
-  public static final ImmutableList<String> DEFAULT_REGISTRIES =
-      ImmutableList.of("https://bcr.bazel.build/");
+  public static final ImmutableSet<String> DEFAULT_REGISTRIES =
+      ImmutableSet.of("https://bcr.bazel.build/");
 
   // A map of repository handlers that can be looked up by rule class name.
   private final ImmutableMap<String, RepositoryFunction> repositoryHandlers;
@@ -150,7 +154,7 @@
   private ImmutableMap<String, ModuleOverride> moduleOverrides = ImmutableMap.of();
   private Optional<RootedPath> resolvedFileReplacingWorkspace = Optional.empty();
   private FileSystem filesystem;
-  private List<String> registries;
+  private ImmutableSet<String> registries;
   private final AtomicBoolean ignoreDevDeps = new AtomicBoolean(false);
   private CheckDirectDepsMode checkDirectDepsMode = CheckDirectDepsMode.WARNING;
   private BazelCompatibilityMode bazelCompatibilityMode = BazelCompatibilityMode.ERROR;
@@ -495,7 +499,7 @@
       }
 
       if (repoOptions.registries != null && !repoOptions.registries.isEmpty()) {
-        registries = repoOptions.registries;
+        registries = normalizeRegistries(repoOptions.registries);
       } else {
         registries = DEFAULT_REGISTRIES;
       }
@@ -525,6 +529,14 @@
     }
   }
 
+  private static ImmutableSet<String> normalizeRegistries(List<String> registries) {
+    // Ensure that registries aren't duplicated even after `/modules/...` paths are appended to
+    // them.
+    return registries.stream()
+        .map(url -> CharMatcher.is('/').trimTrailingFrom(url))
+        .collect(toImmutableSet());
+  }
+
   /**
    * If the given path is absolute path, leave it as it is. If the given path is a relative path, it
    * is relative to the current working directory. If the given path starts with '%workspace%, it is
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
index 6df8365..7729210 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
@@ -26,8 +26,6 @@
     ],
     deps = [
         "//src/main/java/com/google/devtools/build/lib/cmdline",
-        "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
-        "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//src/main/java/net/starlark/java/eval",
         "//third_party:auto_value",
         "//third_party:gson",
@@ -77,11 +75,12 @@
         "Registry.java",
         "RegistryFactory.java",
         "RegistryFactoryImpl.java",
+        "RegistryFileDownloadEvent.java",
     ],
     deps = [
         ":common",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/cache",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
-        "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/util:os",
@@ -97,17 +96,15 @@
     name = "bazel_lockfile_module",
     srcs = ["BazelLockFileModule.java"],
     deps = [
-        ":exception",
         ":resolution",
         ":resolution_impl",
         "//src/main/java/com/google/devtools/build/lib:runtime",
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:module_extension",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository:repository_options",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_cluster",
-        "//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
         "//src/main/java/com/google/devtools/build/lib/vfs",
-        "//src/main/java/com/google/devtools/build/skyframe",
         "//third_party:flogger",
         "//third_party:guava",
         "//third_party:jsr305",
@@ -144,6 +141,7 @@
         "RegistryKey.java",
         "RegistryOverride.java",
         "RepoSpecKey.java",
+        "RepoSpecValue.java",
         "SingleExtensionUsagesValue.java",
         "SingleExtensionValue.java",
         "SingleVersionOverride.java",
@@ -160,6 +158,7 @@
         ":root_module_file_fixup",
         "//src/main/java/com/google/devtools/build/docgen/annot",
         "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/packages",
@@ -226,6 +225,7 @@
         "//src/main/java/com/google/devtools/build/lib/analysis:blaze_version_info",
         "//src/main/java/com/google/devtools/build/lib/bazel:bazel_version",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository:repository_options",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/cache",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/starlark",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
@@ -248,7 +248,6 @@
         "//src/main/java/com/google/devtools/build/lib/util:os",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
-        "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//src/main/java/net/starlark/java/annot",
         "//src/main/java/net/starlark/java/eval",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
index e5f53c6..7b11bd8 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
@@ -19,9 +19,9 @@
 import static com.google.common.collect.ImmutableBiMap.toImmutableBiMap;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static java.util.stream.Collectors.counting;
 import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toSet;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -30,6 +30,7 @@
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableTable;
 import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
@@ -49,7 +50,6 @@
 import com.google.devtools.build.skyframe.SkyValue;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 import javax.annotation.Nullable;
 
 /**
@@ -186,7 +186,6 @@
       return null;
     }
 
-    ImmutableList<String> registries = ImmutableList.copyOf(ModuleFileFunction.REGISTRIES.get(env));
     ImmutableMap<String, String> moduleOverrides =
         ModuleFileFunction.MODULE_OVERRIDES.get(env).entrySet().stream()
             .collect(
@@ -202,7 +201,7 @@
     String envYanked = allowedYankedVersionsFromEnv.getValue();
 
     return BzlmodFlagsAndEnvVars.create(
-        registries,
+        ModuleFileFunction.REGISTRIES.get(env),
         moduleOverrides,
         yankedVersions,
         nullToEmpty(envYanked),
@@ -257,14 +256,14 @@
       ImmutableMap<ModuleKey, Module> depGraph) {
     // Find modules with multiple versions in the dep graph. Currently, the only source of such
     // modules is multiple_version_override.
-    Set<String> multipleVersionsModules =
+    ImmutableSet<String> multipleVersionsModules =
         depGraph.keySet().stream()
             .collect(groupingBy(ModuleKey::getName, counting()))
             .entrySet()
             .stream()
             .filter(entry -> entry.getValue() > 1)
             .map(Entry::getKey)
-            .collect(toSet());
+            .collect(toImmutableSet());
 
     // If there is a unique version of this module in the entire dep graph, we elide the version
     // from the canonical repository name. This has a number of benefits:
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunction.java
index 500a577..20206b8 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunction.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
@@ -55,7 +56,7 @@
 
   private static final BzlmodFlagsAndEnvVars EMPTY_FLAGS =
       BzlmodFlagsAndEnvVars.create(
-          ImmutableList.of(), ImmutableMap.of(), ImmutableList.of(), "", false, "", "");
+          ImmutableSet.of(), ImmutableMap.of(), ImmutableList.of(), "", false, "", "");
 
   private static final BazelLockFileValue EMPTY_LOCKFILE =
       BazelLockFileValue.builder()
@@ -65,6 +66,7 @@
           .setLocalOverrideHashes(ImmutableMap.of())
           .setModuleDepGraph(ImmutableMap.of())
           .setModuleExtensions(ImmutableMap.of())
+          .setRegistryFileHashes(ImmutableMap.of())
           .build();
 
   public BazelLockFileFunction(Path rootDirectory) {
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java
index 6116bd5..d5e75e7 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java
@@ -22,11 +22,11 @@
 import com.google.common.flogger.GoogleLogger;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
-import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.Root;
@@ -34,6 +34,7 @@
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import javax.annotation.Nullable;
 
@@ -45,6 +46,7 @@
 
   private SkyframeExecutor executor;
   private Path workspaceRoot;
+  private boolean enabled;
   @Nullable private BazelModuleResolutionEvent moduleResolutionEvent;
 
   private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
@@ -53,20 +55,44 @@
   public void beforeCommand(CommandEnvironment env) {
     executor = env.getSkyframeExecutor();
     workspaceRoot = env.getWorkspace();
-    RepositoryOptions options = env.getOptions().getOptions(RepositoryOptions.class);
-    if (options.lockfileMode.equals(LockfileMode.UPDATE)) {
-      env.getEventBus().register(this);
-    }
+
+    enabled =
+        env.getOptions().getOptions(RepositoryOptions.class).lockfileMode == LockfileMode.UPDATE;
+    moduleResolutionEvent = null;
+    env.getEventBus().register(this);
   }
 
   @Override
-  public void afterCommand() throws AbruptExitException {
-    if (moduleResolutionEvent == null) {
+  public void afterCommand() {
+    if (!enabled || moduleResolutionEvent == null) {
       // Command does not use Bazel modules or the lockfile mode is not update.
       // Since Skyframe caches events, they are replayed even when nothing has changed.
       return;
     }
 
+    BazelDepGraphValue depGraphValue;
+    BazelModuleResolutionValue moduleResolutionValue;
+    try {
+      depGraphValue =
+          (BazelDepGraphValue) executor.getEvaluator().getExistingValue(BazelDepGraphValue.KEY);
+      moduleResolutionValue =
+          (BazelModuleResolutionValue)
+              executor.getEvaluator().getExistingValue(BazelModuleResolutionValue.KEY);
+    } catch (InterruptedException e) {
+      // Not thrown in Bazel.
+      throw new IllegalStateException(e);
+    }
+
+    BazelLockFileValue oldLockfile = moduleResolutionEvent.getOnDiskLockfileValue();
+    ImmutableMap<String, Optional<Checksum>> fileHashes;
+    if (moduleResolutionValue == null) {
+      // BazelDepGraphFunction took the dep graph from the lockfile and didn't cause evaluation of
+      // BazelModuleResolutionFunction. The file hashes in the lockfile are still up-to-date.
+      fileHashes = oldLockfile.getRegistryFileHashes();
+    } else {
+      fileHashes = ImmutableSortedMap.copyOf(moduleResolutionValue.getRegistryFileHashes());
+    }
+
     // All nodes corresponding to module extensions that have been evaluated in the current build
     // are done at this point. Look up entries by eval keys to record results even if validation
     // later fails due to invalid imports.
@@ -88,24 +114,16 @@
                 newExtensionInfos.put(key.argument(), value.getLockFileInfo().get());
               }
             });
+    var combinedExtensionInfos =
+        combineModuleExtensions(
+            oldLockfile.getModuleExtensions(), newExtensionInfos, depGraphValue);
 
-    BazelDepGraphValue depGraphValue;
-    try {
-      depGraphValue =
-          (BazelDepGraphValue) executor.getEvaluator().getExistingValue(BazelDepGraphValue.KEY);
-    } catch (InterruptedException e) {
-      // Not thrown in Bazel.
-      throw new IllegalStateException(e);
-    }
-
-    BazelLockFileValue oldLockfile = moduleResolutionEvent.getOnDiskLockfileValue();
     // Create an updated version of the lockfile, keeping only the extension results from the old
     // lockfile that are still up-to-date and adding the newly resolved extension results.
     BazelLockFileValue newLockfile =
         moduleResolutionEvent.getResolutionOnlyLockfileValue().toBuilder()
-            .setModuleExtensions(
-                combineModuleExtensions(
-                    oldLockfile.getModuleExtensions(), newExtensionInfos, depGraphValue))
+            .setRegistryFileHashes(fileHashes)
+            .setModuleExtensions(combinedExtensionInfos)
             .build();
 
     // Write the new value to the file, but only if needed. This is not just a performance
@@ -115,7 +133,6 @@
     if (!newLockfile.equals(oldLockfile)) {
       updateLockfile(workspaceRoot, newLockfile);
     }
-    this.moduleResolutionEvent = null;
   }
 
   /**
@@ -140,8 +157,6 @@
       var factorToLockedExtension = entry.getValue();
       ModuleExtensionEvalFactors firstEntryFactors =
           factorToLockedExtension.keySet().iterator().next();
-      LockFileModuleExtension firstEntryExtension =
-          factorToLockedExtension.values().iterator().next();
       // All entries for a single extension share the same usages digest, so it suffices to check
       // the first entry.
       if (shouldKeepExtension(
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java
index 67d4db5..cd3eff7 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileValue.java
@@ -20,6 +20,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable;
 import com.google.devtools.build.lib.skyframe.SkyFunctions;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
@@ -27,6 +28,7 @@
 import com.google.devtools.build.skyframe.SkyValue;
 import com.ryanharter.auto.value.gson.GenerateTypeAdapter;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * The result of reading the lockfile. Contains the lockfile version, module hash, definitions of
@@ -44,7 +46,8 @@
   static Builder builder() {
     return new AutoValue_BazelLockFileValue.Builder()
         .setLockFileVersion(LOCK_FILE_VERSION)
-        .setModuleExtensions(ImmutableMap.of());
+        .setModuleExtensions(ImmutableMap.of())
+        .setRegistryFileHashes(ImmutableMap.of());
   }
 
   /** Current version of the lock file */
@@ -62,6 +65,9 @@
   /** The post-selection dep graph retrieved from the lock file. */
   public abstract ImmutableMap<ModuleKey, Module> getModuleDepGraph();
 
+  /** Hashes of files retrieved from registries. */
+  public abstract ImmutableMap<String, Optional<Checksum>> getRegistryFileHashes();
+
   /** Mapping the extension id to the module extension data */
   public abstract ImmutableMap<
           ModuleExtensionId, ImmutableMap<ModuleExtensionEvalFactors, LockFileModuleExtension>>
@@ -82,6 +88,8 @@
 
     public abstract Builder setModuleDepGraph(ImmutableMap<ModuleKey, Module> value);
 
+    public abstract Builder setRegistryFileHashes(ImmutableMap<String, Optional<Checksum>> value);
+
     public abstract Builder setModuleExtensions(
         ImmutableMap<
                 ModuleExtensionId,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunction.java
index 9f121a6..9d863d9 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunction.java
@@ -31,6 +31,7 @@
 import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.BazelCompatibilityMode;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.CheckDirectDepsMode;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.profiler.Profiler;
@@ -46,9 +47,11 @@
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
 import com.google.devtools.build.skyframe.SkyframeLookupResult;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.SequencedMap;
 import java.util.Set;
 import javax.annotation.Nullable;
 
@@ -63,6 +66,14 @@
   public static final Precomputed<BazelCompatibilityMode> BAZEL_COMPATIBILITY_MODE =
       new Precomputed<>("bazel_compatibility_mode");
 
+  private record Result(
+      Selection.Result selectionResult,
+      ImmutableMap<String, Optional<Checksum>> registryFileHashes) {}
+
+  private static class ModuleResolutionComputeState implements Environment.SkyKeyComputeState {
+    Result discoverAndSelectResult;
+  }
+
   @Override
   @Nullable
   public SkyValue compute(SkyKey skyKey, Environment env)
@@ -91,15 +102,17 @@
     }
 
     var state = env.getState(ModuleResolutionComputeState::new);
-    if (state.selectionResult == null) {
-      state.selectionResult = discoverAndSelect(env, root, allowedYankedVersions);
-      if (state.selectionResult == null) {
+    if (state.discoverAndSelectResult == null) {
+      state.discoverAndSelectResult = discoverAndSelect(env, root, allowedYankedVersions);
+      if (state.discoverAndSelectResult == null) {
         return null;
       }
     }
 
+    SequencedMap<String, Optional<Checksum>> registryFileHashes =
+        new LinkedHashMap<>(state.discoverAndSelectResult.registryFileHashes);
     ImmutableSet<RepoSpecKey> repoSpecKeys =
-        state.selectionResult.getResolvedDepGraph().values().stream()
+        state.discoverAndSelectResult.selectionResult.getResolvedDepGraph().values().stream()
             // Modules with a null registry have a non-registry override. We don't need to
             // fetch or store the repo spec in this case.
             .filter(module -> module.getRegistry() != null)
@@ -108,11 +121,12 @@
     SkyframeLookupResult repoSpecResults = env.getValuesAndExceptions(repoSpecKeys);
     ImmutableMap.Builder<ModuleKey, RepoSpec> remoteRepoSpecs = ImmutableMap.builder();
     for (RepoSpecKey repoSpecKey : repoSpecKeys) {
-      RepoSpec repoSpec = (RepoSpec) repoSpecResults.get(repoSpecKey);
-      if (repoSpec == null) {
+      RepoSpecValue repoSpecValue = (RepoSpecValue) repoSpecResults.get(repoSpecKey);
+      if (repoSpecValue == null) {
         return null;
       }
-      remoteRepoSpecs.put(repoSpecKey.getModuleKey(), repoSpec);
+      remoteRepoSpecs.put(repoSpecKey.getModuleKey(), repoSpecValue.repoSpec());
+      registryFileHashes.putAll(repoSpecValue.registryFileHashes());
     }
 
     ImmutableMap<ModuleKey, Module> finalDepGraph;
@@ -120,36 +134,38 @@
         Profiler.instance().profile(ProfilerTask.BZLMOD, "compute final dep graph")) {
       finalDepGraph =
           computeFinalDepGraph(
-              state.selectionResult.getResolvedDepGraph(),
+              state.discoverAndSelectResult.selectionResult.getResolvedDepGraph(),
               root.getOverrides(),
               remoteRepoSpecs.buildOrThrow());
     }
 
     return BazelModuleResolutionValue.create(
-        finalDepGraph, state.selectionResult.getUnprunedDepGraph());
+        finalDepGraph,
+        state.discoverAndSelectResult.selectionResult.getUnprunedDepGraph(),
+        ImmutableMap.copyOf(registryFileHashes));
   }
 
   @Nullable
-  private static Selection.Result discoverAndSelect(
+  private static Result discoverAndSelect(
       Environment env,
       RootModuleFileValue root,
       Optional<ImmutableSet<ModuleKey>> allowedYankedVersions)
       throws BazelModuleResolutionFunctionException, InterruptedException {
-    ImmutableMap<ModuleKey, InterimModule> initialDepGraph;
+    Discovery.Result discoveryResult;
     try (SilentCloseable c = Profiler.instance().profile(ProfilerTask.BZLMOD, "discovery")) {
-      initialDepGraph = Discovery.run(env, root);
+      discoveryResult = Discovery.run(env, root);
     } catch (ExternalDepsException e) {
       throw new BazelModuleResolutionFunctionException(e, Transience.PERSISTENT);
     }
-    if (initialDepGraph == null) {
+    if (discoveryResult == null) {
       return null;
     }
 
-    verifyAllOverridesAreOnExistentModules(initialDepGraph, root.getOverrides());
+    verifyAllOverridesAreOnExistentModules(discoveryResult.depGraph(), root.getOverrides());
 
     Selection.Result selectionResult;
     try (SilentCloseable c = Profiler.instance().profile(ProfilerTask.BZLMOD, "selection")) {
-      selectionResult = Selection.run(initialDepGraph, root.getOverrides());
+      selectionResult = Selection.run(discoveryResult.depGraph(), root.getOverrides());
     } catch (ExternalDepsException e) {
       throw new BazelModuleResolutionFunctionException(e, Transience.PERSISTENT);
     }
@@ -176,7 +192,7 @@
     try (SilentCloseable c =
         Profiler.instance().profile(ProfilerTask.BZLMOD, "verify root module direct deps")) {
       verifyRootModuleDirectDepsAreAccurate(
-          initialDepGraph.get(ModuleKey.ROOT),
+          discoveryResult.depGraph().get(ModuleKey.ROOT),
           resolvedDepGraph.get(ModuleKey.ROOT),
           Objects.requireNonNull(CHECK_DIRECT_DEPENDENCIES.get(env)),
           env.getListener());
@@ -195,7 +211,7 @@
       checkNoYankedVersions(resolvedDepGraph, yankedVersionValues, allowedYankedVersions);
     }
 
-    return selectionResult;
+    return new Result(selectionResult, discoveryResult.registryFileHashes());
   }
 
   private static void verifyAllOverridesAreOnExistentModules(
@@ -337,10 +353,6 @@
     return finalDepGraph.buildOrThrow();
   }
 
-  private static class ModuleResolutionComputeState implements Environment.SkyKeyComputeState {
-    Selection.Result selectionResult;
-  }
-
   static class BazelModuleResolutionFunctionException extends SkyFunctionException {
     BazelModuleResolutionFunctionException(ExternalDepsException e, Transience transience) {
       super(e, transience);
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionValue.java
index 19c642c..6097103 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionValue.java
@@ -17,10 +17,12 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.skyframe.SkyFunctions;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
+import java.util.Optional;
 
 /**
  * The result of the selection process, containing both the pruned and the un-pruned dependency
@@ -46,9 +48,16 @@
    */
   abstract ImmutableMap<ModuleKey, InterimModule> getUnprunedDepGraph();
 
+  /**
+   * Hashes of files obtained (or known to be missing) from registries while performing resolution.
+   */
+  abstract ImmutableMap<String, Optional<Checksum>> getRegistryFileHashes();
+
   static BazelModuleResolutionValue create(
       ImmutableMap<ModuleKey, Module> resolvedDepGraph,
-      ImmutableMap<ModuleKey, InterimModule> unprunedDepGraph) {
-    return new AutoValue_BazelModuleResolutionValue(resolvedDepGraph, unprunedDepGraph);
+      ImmutableMap<ModuleKey, InterimModule> unprunedDepGraph,
+      ImmutableMap<String, Optional<Checksum>> registryFileHashes) {
+    return new AutoValue_BazelModuleResolutionValue(
+        resolvedDepGraph, unprunedDepGraph, registryFileHashes);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodFlagsAndEnvVars.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodFlagsAndEnvVars.java
index a69e537..d425bc4 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodFlagsAndEnvVars.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodFlagsAndEnvVars.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.ryanharter.auto.value.gson.GenerateTypeAdapter;
 
 /** Stores the values of flags and environment variables that affect the resolution */
@@ -25,7 +26,7 @@
 abstract class BzlmodFlagsAndEnvVars {
 
   public static BzlmodFlagsAndEnvVars create(
-      ImmutableList<String> registries,
+      ImmutableSet<String> registries,
       ImmutableMap<String, String> moduleOverrides,
       ImmutableList<String> yankedVersions,
       String envVarYankedVersions,
@@ -43,7 +44,7 @@
   }
 
   /** Registries provided via command line */
-  public abstract ImmutableList<String> cmdRegistries();
+  public abstract ImmutableSet<String> cmdRegistries();
 
   /** ModulesOverride provided via command line */
   public abstract ImmutableMap<String, String> cmdModuleOverrides();
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Discovery.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Discovery.java
index 5da2540..6cc5eae 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Discovery.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Discovery.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.bazel.bzlmod.InterimModule.DepSpec;
 import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.server.FailureDetails;
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -28,10 +29,13 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Queue;
+import java.util.SequencedMap;
 import java.util.Set;
 import javax.annotation.Nullable;
 
@@ -43,13 +47,16 @@
 final class Discovery {
   private Discovery() {}
 
+  public record Result(
+      ImmutableMap<ModuleKey, InterimModule> depGraph,
+      ImmutableMap<String, Optional<Checksum>> registryFileHashes) {}
+
   /**
    * Runs module discovery. This function follows SkyFunction semantics (returns null if a Skyframe
    * dependency is missing and this function needs a restart).
    */
   @Nullable
-  public static ImmutableMap<ModuleKey, InterimModule> run(
-      Environment env, RootModuleFileValue root)
+  public static Result run(Environment env, RootModuleFileValue root)
       throws InterruptedException, ExternalDepsException {
     String rootModuleName = root.getModule().getName();
     ImmutableMap<String, ModuleOverride> overrides = root.getOverrides();
@@ -60,9 +67,11 @@
             .withDepSpecsTransformed(InterimModule.applyOverrides(overrides, rootModuleName)));
     Queue<ModuleKey> unexpanded = new ArrayDeque<>();
     Map<ModuleKey, ModuleKey> predecessors = new HashMap<>();
+    SequencedMap<String, Optional<Checksum>> registryFileHashes =
+        new LinkedHashMap<>(root.getRegistryFileHashes());
     unexpanded.add(ModuleKey.ROOT);
     while (!unexpanded.isEmpty()) {
-      Set<SkyKey> unexpandedSkyKeys = new HashSet<>();
+      Set<SkyKey> unexpandedSkyKeys = new LinkedHashSet<>();
       while (!unexpanded.isEmpty()) {
         InterimModule module = depGraph.get(unexpanded.remove());
         for (DepSpec depSpec : module.getDeps().values()) {
@@ -109,6 +118,7 @@
                   .getModule()
                   .withDepSpecsTransformed(
                       InterimModule.applyOverrides(overrides, rootModuleName)));
+          registryFileHashes.putAll(moduleFileValue.getRegistryFileHashes());
           unexpanded.add(depKey);
         }
       }
@@ -116,6 +126,6 @@
     if (env.valuesMissing()) {
       return null;
     }
-    return ImmutableMap.copyOf(depGraph);
+    return new Result(ImmutableMap.copyOf(depGraph), ImmutableMap.copyOf(registryFileHashes));
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/GsonTypeAdapterUtil.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/GsonTypeAdapterUtil.java
index a90bfb9..912b455 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/GsonTypeAdapterUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/GsonTypeAdapterUtil.java
@@ -26,6 +26,8 @@
 import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.Table;
 import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException;
+import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -475,10 +477,62 @@
             }
           };
 
+  // This can't reuse the existing type adapter factory for Optional as we need to explicitly
+  // serialize null values but don't want to rely on GSON's serializeNulls.
+  private static final class OptionalChecksumTypeAdapterFactory implements TypeAdapterFactory {
+
+    @Nullable
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+      if (typeToken.getRawType() != Optional.class) {
+        return null;
+      }
+      Type type = typeToken.getType();
+      if (!(type instanceof ParameterizedType)) {
+        return null;
+      }
+      Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
+      if (elementType != Checksum.class) {
+        return null;
+      }
+      @SuppressWarnings("unchecked")
+      TypeAdapter<T> typeAdapter = (TypeAdapter<T>) new OptionalChecksumTypeAdapter();
+      return typeAdapter;
+    }
+
+    private static class OptionalChecksumTypeAdapter extends TypeAdapter<Optional<Checksum>> {
+      // This value must not be a valid checksum string.
+      private static final String NOT_FOUND_MARKER = "not found";
+
+      @Override
+      public void write(JsonWriter jsonWriter, Optional<Checksum> checksum) throws IOException {
+        if (checksum.isPresent()) {
+          jsonWriter.value(checksum.get().toString());
+        } else {
+          jsonWriter.value(NOT_FOUND_MARKER);
+        }
+      }
+
+      @Override
+      public Optional<Checksum> read(JsonReader jsonReader) throws IOException {
+        String checksumString = jsonReader.nextString();
+        if (checksumString.equals(NOT_FOUND_MARKER)) {
+          return Optional.empty();
+        }
+        try {
+          return Optional.of(Checksum.fromString(RepositoryCache.KeyType.SHA256, checksumString));
+        } catch (Checksum.InvalidChecksumException e) {
+          throw new JsonParseException(String.format("Invalid checksum: %s", checksumString), e);
+        }
+      }
+    }
+  }
+
   public static Gson createLockFileGson(Path moduleFilePath, Path workspaceRoot) {
     return newGsonBuilder()
         .setPrettyPrinting()
         .registerTypeAdapterFactory(new LocationTypeAdapterFactory(moduleFilePath, workspaceRoot))
+        .registerTypeAdapterFactory(new OptionalChecksumTypeAdapterFactory())
         .create();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java
index e872524..578e78a 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java
@@ -21,8 +21,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
 import com.google.devtools.build.lib.profiler.SilentCloseable;
@@ -49,6 +51,15 @@
  */
 public class IndexRegistry implements Registry {
 
+  /**
+   * How to handle the list of file hashes known from the lockfile when downloading files from the
+   * registry.
+   */
+  public enum KnownFileHashesMode {
+    IGNORE,
+    USE_AND_UPDATE;
+  }
+
   /** The unresolved version of the url. Ex: has %workspace% placeholder */
   private final String unresolvedUri;
 
@@ -56,7 +67,10 @@
   private final DownloadManager downloadManager;
   private final Map<String, String> clientEnv;
   private final Gson gson;
+  private final ImmutableMap<String, Optional<Checksum>> knownFileHashes;
+  private final KnownFileHashesMode knownFileHashesMode;
   private volatile Optional<BazelRegistryJson> bazelRegistryJson;
+  private volatile StoredEventHandler bazelRegistryJsonEvents;
 
   private static final String SOURCE_JSON_FILENAME = "source.json";
 
@@ -64,7 +78,9 @@
       URI uri,
       String unresolvedUri,
       DownloadManager downloadManager,
-      Map<String, String> clientEnv) {
+      Map<String, String> clientEnv,
+      ImmutableMap<String, Optional<Checksum>> knownFileHashes,
+      KnownFileHashesMode knownFileHashesMode) {
     this.uri = uri;
     this.unresolvedUri = unresolvedUri;
     this.downloadManager = downloadManager;
@@ -73,6 +89,8 @@
         new GsonBuilder()
             .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
             .create();
+    this.knownFileHashes = knownFileHashes;
+    this.knownFileHashesMode = knownFileHashesMode;
   }
 
   @Override
@@ -92,14 +110,46 @@
   }
 
   /** Grabs a file from the given URL. Returns {@link Optional#empty} if the file doesn't exist. */
-  private Optional<byte[]> grabFile(String url, ExtendedEventHandler eventHandler)
+  private Optional<byte[]> grabFile(
+      String url, ExtendedEventHandler eventHandler, boolean useChecksum)
       throws IOException, InterruptedException {
+    var maybeContent = doGrabFile(url, eventHandler, useChecksum);
+    if (knownFileHashesMode == KnownFileHashesMode.USE_AND_UPDATE && useChecksum) {
+      eventHandler.post(RegistryFileDownloadEvent.create(url, maybeContent));
+    }
+    return maybeContent;
+  }
+
+  private Optional<byte[]> doGrabFile(
+      String url, ExtendedEventHandler eventHandler, boolean useChecksum)
+      throws IOException, InterruptedException {
+    Optional<Checksum> checksum;
+    if (knownFileHashesMode != KnownFileHashesMode.IGNORE && useChecksum) {
+      Optional<Checksum> knownChecksum = knownFileHashes.get(url);
+      if (knownChecksum == null) {
+        // This is a new file, download without providing a checksum.
+        checksum = Optional.empty();
+      } else if (knownChecksum.isEmpty()) {
+        // The file is known to not exist, so don't attempt to download it.
+        return Optional.empty();
+      } else {
+        // The file is known, download with a checksum to potentially obtain a repository cache hit
+        // and ensure that the remote file hasn't changed.
+        checksum = knownChecksum;
+      }
+    } else {
+      checksum = Optional.empty();
+    }
     try (SilentCloseable c =
         Profiler.instance().profile(ProfilerTask.BZLMOD, () -> "download file: " + url)) {
       return Optional.of(
-          downloadManager.downloadAndReadOneUrl(new URL(url), eventHandler, clientEnv));
+          downloadManager.downloadAndReadOneUrl(new URL(url), eventHandler, clientEnv, checksum));
     } catch (FileNotFoundException e) {
       return Optional.empty();
+    } catch (IOException e) {
+      // Include the URL in the exception message for easier debugging.
+      throw new IOException(
+          "Failed to fetch registry file %s: %s".formatted(url, e.getMessage()), e);
     }
   }
 
@@ -109,7 +159,8 @@
     String url =
         constructUrl(
             uri.toString(), "modules", key.getName(), key.getVersion().toString(), "MODULE.bazel");
-    return grabFile(url, eventHandler).map(content -> ModuleFile.create(content, url));
+    Optional<byte[]> maybeContent = grabFile(url, eventHandler, /* useChecksum= */ true);
+    return maybeContent.map(content -> ModuleFile.create(content, url));
   }
 
   /** Represents fields available in {@code bazel_registry.json} for the registry. */
@@ -153,22 +204,20 @@
    * Grabs a JSON file from the given URL, and returns its content. Returns {@link Optional#empty}
    * if the file doesn't exist.
    */
-  private Optional<String> grabJsonFile(String url, ExtendedEventHandler eventHandler)
+  private Optional<String> grabJsonFile(
+      String url, ExtendedEventHandler eventHandler, boolean useChecksum)
       throws IOException, InterruptedException {
-    Optional<byte[]> bytes = grabFile(url, eventHandler);
-    if (bytes.isEmpty()) {
-      return Optional.empty();
-    }
-    return Optional.of(new String(bytes.get(), UTF_8));
+    return grabFile(url, eventHandler, useChecksum).map(value -> new String(value, UTF_8));
   }
 
   /**
    * Grabs a JSON file from the given URL, and returns it as a parsed object with fields in {@code
    * T}. Returns {@link Optional#empty} if the file doesn't exist.
    */
-  private <T> Optional<T> grabJson(String url, Class<T> klass, ExtendedEventHandler eventHandler)
+  private <T> Optional<T> grabJson(
+      String url, Class<T> klass, ExtendedEventHandler eventHandler, boolean useChecksum)
       throws IOException, InterruptedException {
-    Optional<String> jsonString = grabJsonFile(url, eventHandler);
+    Optional<String> jsonString = grabJsonFile(url, eventHandler, useChecksum);
     if (jsonString.isEmpty() || jsonString.get().isBlank()) {
       return Optional.empty();
     }
@@ -195,7 +244,7 @@
             key.getName(),
             key.getVersion().toString(),
             SOURCE_JSON_FILENAME);
-    Optional<String> jsonString = grabJsonFile(jsonUrl, eventHandler);
+    Optional<String> jsonString = grabJsonFile(jsonUrl, eventHandler, /* useChecksum= */ true);
     if (jsonString.isEmpty()) {
       throw new FileNotFoundException(
           String.format("Module %s's %s not found in registry %s", key, SOURCE_JSON_FILENAME, uri));
@@ -232,14 +281,18 @@
     if (bazelRegistryJson == null) {
       synchronized (this) {
         if (bazelRegistryJson == null) {
+          var storedEventHandler = new StoredEventHandler();
           bazelRegistryJson =
               grabJson(
                   constructUrl(uri.toString(), "bazel_registry.json"),
                   BazelRegistryJson.class,
-                  eventHandler);
+                  storedEventHandler,
+                  /* useChecksum= */ true);
+          bazelRegistryJsonEvents = storedEventHandler;
         }
       }
     }
+    bazelRegistryJsonEvents.replayOn(eventHandler);
     return bazelRegistryJson;
   }
 
@@ -348,7 +401,9 @@
         grabJson(
             constructUrl(uri.toString(), "modules", moduleName, "metadata.json"),
             MetadataJson.class,
-            eventHandler);
+            eventHandler,
+            // metadata.json is not immutable
+            /* useChecksum= */ false);
     if (metadataJson.isEmpty()) {
       return Optional.empty();
     }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
index 542a62e..65f8d99 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java
@@ -22,6 +22,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.bazel.bzlmod.CompiledModuleFile.IncludeStatement;
@@ -34,6 +35,7 @@
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.packages.BazelStarlarkEnvironment;
 import com.google.devtools.build.lib.packages.StarlarkExportable;
 import com.google.devtools.build.lib.profiler.Profiler;
@@ -80,7 +82,8 @@
  */
 public class ModuleFileFunction implements SkyFunction {
 
-  public static final Precomputed<List<String>> REGISTRIES = new Precomputed<>("registries");
+  public static final Precomputed<ImmutableSet<String>> REGISTRIES =
+      new Precomputed<>("registries");
   public static final Precomputed<Boolean> IGNORE_DEV_DEPS =
       new Precomputed<>("ignore_dev_dependency");
 
@@ -157,6 +160,7 @@
     if (getModuleFileResult == null) {
       return null;
     }
+    getModuleFileResult.downloadEventHandler.replayOn(env.getListener());
     String moduleFileHash =
         new Fingerprint().addBytes(getModuleFileResult.moduleFile.getContent()).hexDigestAndReset();
 
@@ -217,8 +221,11 @@
           module.getVersion());
     }
 
-
-    return NonRootModuleFileValue.create(module, moduleFileHash);
+    return NonRootModuleFileValue.create(
+        module,
+        moduleFileHash,
+        RegistryFileDownloadEvent.collectToMap(
+            getModuleFileResult.downloadEventHandler.getPosts()));
   }
 
   @Nullable
@@ -529,7 +536,10 @@
    *
    * @param registry can be null if this module has a non-registry override.
    */
-  private record GetModuleFileResult(ModuleFile moduleFile, @Nullable Registry registry) {}
+  private record GetModuleFileResult(
+      ModuleFile moduleFile,
+      @Nullable Registry registry,
+      StoredEventHandler downloadEventHandler) {}
 
   @Nullable
   private GetModuleFileResult getModuleFile(
@@ -560,7 +570,8 @@
           ModuleFile.create(
               readModuleFile(moduleFilePath.asPath()),
               moduleFileLabel.getUnambiguousCanonicalForm()),
-          /* registry= */ null);
+          /* registry= */ null,
+          new StoredEventHandler());
     }
 
     // Otherwise, we should get the module file from a registry.
@@ -573,13 +584,11 @@
               + " non-registry override?",
           key.getName());
     }
-    // TODO(wyv): Move registry object creation to BazelRepositoryModule so we don't repeatedly
-    //   create them, and we can better report the error (is it a flag error or override error?).
-    List<String> registries = Objects.requireNonNull(REGISTRIES.get(env));
+    ImmutableSet<String> registries = Objects.requireNonNull(REGISTRIES.get(env));
     if (override instanceof RegistryOverride registryOverride) {
       String overrideRegistry = registryOverride.getRegistry();
       if (!overrideRegistry.isEmpty()) {
-        registries = ImmutableList.of(overrideRegistry);
+        registries = ImmutableSet.of(overrideRegistry);
       }
     } else if (override != null) {
       // This should never happen.
@@ -607,13 +616,14 @@
 
     // Now go through the list of registries and use the first one that contains the requested
     // module.
+    StoredEventHandler downloadEventHandler = new StoredEventHandler();
     for (Registry registry : registryObjects) {
       try {
-        Optional<ModuleFile> moduleFile = registry.getModuleFile(key, env.getListener());
+        Optional<ModuleFile> moduleFile = registry.getModuleFile(key, downloadEventHandler);
         if (moduleFile.isEmpty()) {
           continue;
         }
-        return new GetModuleFileResult(moduleFile.get(), registry);
+        return new GetModuleFileResult(moduleFile.get(), registry, downloadEventHandler);
       } catch (IOException e) {
         throw errorf(
             Code.ERROR_ACCESSING_REGISTRY, e, "Error accessing registry %s", registry.getUrl());
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java
index 7c7e38f..da64efb 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileValue.java
@@ -18,12 +18,14 @@
 import com.google.auto.value.AutoValue;
 import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.skyframe.SkyFunctions;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.skyframe.SkyFunctionName;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
+import java.util.Optional;
 import javax.annotation.Nullable;
 
 /** The result of {@link ModuleFileFunction}. */
@@ -41,12 +43,22 @@
   /** The hash string of Module.bazel (using SHA256) */
   public abstract String getModuleFileHash();
 
+  /**
+   * Hashes of files obtained (or known to be missing) from registries while obtaining this module
+   * file.
+   */
+  public abstract ImmutableMap<String, Optional<Checksum>> getRegistryFileHashes();
+
   /** The {@link ModuleFileValue} for non-root modules. */
   @AutoValue
   public abstract static class NonRootModuleFileValue extends ModuleFileValue {
 
-    public static NonRootModuleFileValue create(InterimModule module, String moduleFileHash) {
-      return new AutoValue_ModuleFileValue_NonRootModuleFileValue(module, moduleFileHash);
+    public static NonRootModuleFileValue create(
+        InterimModule module,
+        String moduleFileHash,
+        ImmutableMap<String, Optional<Checksum>> registryFileHashes) {
+      return new AutoValue_ModuleFileValue_NonRootModuleFileValue(
+          module, moduleFileHash, registryFileHashes);
     }
   }
 
@@ -77,6 +89,12 @@
      */
     public abstract ImmutableMap<String, CompiledModuleFile> getIncludeLabelToCompiledModuleFile();
 
+    @Override
+    public ImmutableMap<String, Optional<Checksum>> getRegistryFileHashes() {
+      // The root module is not obtained from a registry.
+      return ImmutableMap.of();
+    }
+
     public static RootModuleFileValue create(
         InterimModule module,
         String moduleFileHash,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactory.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactory.java
index 03ba85d..e24b345 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactory.java
@@ -15,7 +15,10 @@
 
 package com.google.devtools.build.lib.bazel.bzlmod;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import java.net.URISyntaxException;
+import java.util.Optional;
 
 /** A factory type for {@link Registry}. */
 public interface RegistryFactory {
@@ -25,5 +28,6 @@
    *
    * <p>Outside of tests, only {@link RegistryFunction} should call this method.
    */
-  Registry createRegistry(String url) throws URISyntaxException;
+  Registry createRegistry(String url, ImmutableMap<String, Optional<Checksum>> fileHashes)
+      throws URISyntaxException;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryImpl.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryImpl.java
index d943e69..06efd40 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryImpl.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryImpl.java
@@ -15,11 +15,15 @@
 
 package com.google.devtools.build.lib.bazel.bzlmod;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.bzlmod.IndexRegistry.KnownFileHashesMode;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
 import com.google.devtools.build.lib.vfs.Path;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Supplier;
 
 /** Prod implementation of {@link RegistryFactory}. */
@@ -38,7 +42,9 @@
   }
 
   @Override
-  public Registry createRegistry(String unresolvedUrl) throws URISyntaxException {
+  public Registry createRegistry(
+      String unresolvedUrl, ImmutableMap<String, Optional<Checksum>> knownFileHashes)
+      throws URISyntaxException {
     URI uri = new URI(unresolvedUrl.replace("%workspace%", workspacePath.getPathString()));
     if (uri.getScheme() == null) {
       throw new URISyntaxException(
@@ -52,10 +58,19 @@
           "Registry URL path is not valid -- did you mean to use file:///foo/bar "
               + "or file:///c:/foo/bar for Windows?");
     }
-    return switch (uri.getScheme()) {
-      case "http", "https", "file" ->
-          new IndexRegistry(uri, unresolvedUrl, downloadManager, clientEnvironmentSupplier.get());
-      default -> throw new URISyntaxException(uri.toString(), "Unrecognized registry URL protocol");
-    };
+    var knownFileHashesMode =
+        switch (uri.getScheme()) {
+          case "http", "https" -> KnownFileHashesMode.USE_AND_UPDATE;
+          case "file" -> KnownFileHashesMode.IGNORE;
+          default ->
+              throw new URISyntaxException(uri.toString(), "Unrecognized registry URL protocol");
+        };
+    return new IndexRegistry(
+        uri,
+        unresolvedUrl,
+        downloadManager,
+        clientEnvironmentSupplier.get(),
+        knownFileHashes,
+        knownFileHashesMode);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFileDownloadEvent.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFileDownloadEvent.java
new file mode 100644
index 0000000..941f023
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFileDownloadEvent.java
@@ -0,0 +1,53 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// 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
+//
+//    http://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.
+//
+
+package com.google.devtools.build.lib.bazel.bzlmod;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.hash.Hashing;
+import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
+import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable;
+import java.util.Collection;
+import java.util.Optional;
+
+/** Event that records the fact that a file has been downloaded from a remote registry. */
+public record RegistryFileDownloadEvent(String uri, Optional<Checksum> checksum)
+    implements Postable {
+
+  public static RegistryFileDownloadEvent create(String uri, Optional<byte[]> content) {
+    return new RegistryFileDownloadEvent(uri, content.map(RegistryFileDownloadEvent::computeHash));
+  }
+
+  static ImmutableMap<String, Optional<Checksum>> collectToMap(Collection<Postable> postables) {
+    ImmutableMap.Builder<String, Optional<Checksum>> builder = ImmutableMap.builder();
+    for (Postable postable : postables) {
+      if (postable instanceof RegistryFileDownloadEvent event) {
+        builder.put(event.uri(), event.checksum());
+      }
+    }
+    return builder.buildKeepingLast();
+  }
+
+  private static Checksum computeHash(byte[] bytes) {
+    try {
+      return Checksum.fromString(
+          RepositoryCache.KeyType.SHA256, Hashing.sha256().hashBytes(bytes).toString());
+    } catch (Checksum.InvalidChecksumException e) {
+      // This can't happen since HashCode.toString() always returns a valid hash.
+      throw new IllegalStateException(e);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFunction.java
index e99fc2b..0c6c12a 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFunction.java
@@ -35,9 +35,14 @@
   @Nullable
   public SkyValue compute(SkyKey skyKey, Environment env)
       throws InterruptedException, RegistryException {
+    BazelLockFileValue lockfile = (BazelLockFileValue) env.getValue(BazelLockFileValue.KEY);
+    if (lockfile == null) {
+      return null;
+    }
+
     RegistryKey key = (RegistryKey) skyKey.argument();
     try {
-      return registryFactory.createRegistry(key.getUrl());
+      return registryFactory.createRegistry(key.getUrl(), lockfile.getRegistryFileHashes());
     } catch (URISyntaxException e) {
       throw new RegistryException(
           ExternalDepsException.withCauseAndMessage(
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpec.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpec.java
index 201312b..d6ad089 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpec.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpec.java
@@ -16,7 +16,6 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.Maps;
-import com.google.devtools.build.skyframe.SkyValue;
 import com.ryanharter.auto.value.gson.GenerateTypeAdapter;
 import javax.annotation.Nullable;
 
@@ -26,7 +25,7 @@
  */
 @AutoValue
 @GenerateTypeAdapter
-public abstract class RepoSpec implements SkyValue {
+public abstract class RepoSpec {
 
   /**
    * The unambiguous canonical label string for the bzl file this repository rule is defined in,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpecFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpecFunction.java
index b9f4369..88d7664 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpecFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpecFunction.java
@@ -15,6 +15,7 @@
 
 package com.google.devtools.build.lib.bazel.bzlmod;
 
+import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
 import com.google.devtools.build.lib.profiler.SilentCloseable;
@@ -43,10 +44,12 @@
       return null;
     }
 
+    StoredEventHandler downloadEvents = new StoredEventHandler();
+    RepoSpec repoSpec;
     try (SilentCloseable c =
         Profiler.instance()
             .profile(ProfilerTask.BZLMOD, () -> "compute repo spec: " + key.getModuleKey())) {
-      return registry.getRepoSpec(key.getModuleKey(), env.getListener());
+      repoSpec = registry.getRepoSpec(key.getModuleKey(), downloadEvents);
     } catch (IOException e) {
       throw new RepoSpecException(
           ExternalDepsException.withCauseAndMessage(
@@ -55,6 +58,9 @@
               "Unable to get module repo spec for %s from registry",
               key.getModuleKey()));
     }
+    downloadEvents.replayOn(env.getListener());
+    return RepoSpecValue.create(
+        repoSpec, RegistryFileDownloadEvent.collectToMap(downloadEvents.getPosts()));
   }
 
   static final class RepoSpecException extends SkyFunctionException {
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpecValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpecValue.java
new file mode 100644
index 0000000..5e37bba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/RepoSpecValue.java
@@ -0,0 +1,35 @@
+// Copyright 2021 The Bazel Authors. All rights reserved.
+//
+// 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
+//
+//    http://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.
+//
+
+package com.google.devtools.build.lib.bazel.bzlmod;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
+import com.google.devtools.build.skyframe.SkyValue;
+import java.util.Optional;
+
+/** The value for {@link RepoSpecFunction}. */
+@AutoValue
+public abstract class RepoSpecValue implements SkyValue {
+  public abstract RepoSpec repoSpec();
+
+  public abstract ImmutableMap<String, Optional<Checksum>> registryFileHashes();
+
+  public static RepoSpecValue create(
+      RepoSpec repoSpec, ImmutableMap<String, Optional<Checksum>> registryFileHashes) {
+    return new AutoValue_RepoSpecValue(repoSpec, registryFileHashes);
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionValue.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionValue.java
index b0a9101..2182ef0 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionValue.java
@@ -46,8 +46,8 @@
   public abstract ImmutableBiMap<RepositoryName, String> getCanonicalRepoNameToInternalNames();
 
   /**
-   * Returns the information stored about the extension in the lockfile. Is empty if the lockfile
-   * mode is not UPDATE.
+   * Returns the information stored about the extension in the lockfile. Non-empty if and only if
+   * the lockfile mode is UPDATE.
    */
   public abstract Optional<LockFileModuleExtension.WithFactors> getLockFileInfo();
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCache.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCache.java
index 402f96d..83e1a19 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCache.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCache.java
@@ -110,9 +110,7 @@
     this.useHardlinks = useHardlinks;
   }
 
-  /**
-   * @return true iff the cache path is set.
-   */
+  /** Returns true iff the cache path is set. */
   public boolean isEnabled() {
     return repositoryCachePath != null;
   }
@@ -157,11 +155,46 @@
    *     entry with the given cacheKey was added with this String given.
    * @return The Path value where the cache value has been copied to. If cache value does not exist,
    *     return null.
-   * @throws IOException
    */
   @Nullable
   public Path get(String cacheKey, Path targetPath, KeyType keyType, String canonicalId)
       throws IOException, InterruptedException {
+    Path cacheValue = findCacheValue(cacheKey, keyType, canonicalId);
+    if (cacheValue == null) {
+      return null;
+    }
+
+    targetPath.getParentDirectory().createDirectoryAndParents();
+    if (useHardlinks) {
+      FileSystemUtils.createHardLink(targetPath, cacheValue);
+    } else {
+      FileSystemUtils.copyFile(cacheValue, targetPath);
+    }
+
+    return targetPath;
+  }
+
+  /**
+   * Get the content of a cached value, if it exists.
+   *
+   * @param cacheKey The string key to cache the value by.
+   * @param keyType The type of key used. See: KeyType
+   * @return The bytes of the cache value. If cache value does not exist, returns null.
+   */
+  @Nullable
+  public byte[] getBytes(String cacheKey, KeyType keyType)
+      throws IOException, InterruptedException {
+    Path cacheValue = findCacheValue(cacheKey, keyType, /* canonicalId= */ null);
+    if (cacheValue == null) {
+      return null;
+    }
+
+    return FileSystemUtils.readContent(cacheValue);
+  }
+
+  @Nullable
+  private Path findCacheValue(String cacheKey, KeyType keyType, String canonicalId)
+      throws IOException, InterruptedException {
     Preconditions.checkState(isEnabled());
 
     assertKeyIsValid(cacheKey, keyType);
@@ -186,33 +219,30 @@
       }
     }
 
-    targetPath.getParentDirectory().createDirectoryAndParents();
-    if (useHardlinks) {
-      FileSystemUtils.createHardLink(targetPath, cacheValue);
-    } else {
-      FileSystemUtils.copyFile(cacheValue, targetPath);
-    }
-
     try {
       FileSystemUtils.touchFile(cacheValue);
     } catch (IOException e) {
       // Ignore, because the cache might be on a read-only volume.
     }
 
-    return targetPath;
+    return cacheValue;
+  }
+
+  interface FileWriter {
+    void writeTo(Path name) throws IOException;
   }
 
   /**
    * Copies a value from a specified path into the cache.
    *
    * @param cacheKey The string key to cache the value by.
-   * @param sourcePath The path of the value to be cached.
+   * @param fileWriter A function that writes the value to a given file.
    * @param keyType The type of key used. See: KeyType
    * @param canonicalId If set to a non-empty String associate the file with this name, allowing
    *     restricted cache lookups later.
-   * @throws IOException
    */
-  public void put(String cacheKey, Path sourcePath, KeyType keyType, String canonicalId)
+  private void storeCacheValue(
+      String cacheKey, FileWriter fileWriter, KeyType keyType, String canonicalId)
       throws IOException {
     Preconditions.checkState(isEnabled());
 
@@ -223,7 +253,7 @@
     Path cacheValue = cacheEntry.getRelative(DEFAULT_CACHE_FILENAME);
     Path tmpName = cacheEntry.getRelative(TMP_PREFIX + UUID.randomUUID());
     cacheEntry.createDirectoryAndParents();
-    FileSystemUtils.copyFile(sourcePath, tmpName);
+    fileWriter.writeTo(tmpName);
     try {
       tmpName.renameTo(cacheValue);
     } catch (FileAccessException e) {
@@ -245,13 +275,52 @@
   }
 
   /**
+   * Copies a value from a specified path into the cache.
+   *
+   * @param cacheKey The string key to cache the value by.
+   * @param sourcePath The path of the value to be cached.
+   * @param keyType The type of key used. See: KeyType
+   * @param canonicalId If set to a non-empty String associate the file with this name, allowing
+   *     restricted cache lookups later.
+   */
+  public void put(String cacheKey, Path sourcePath, KeyType keyType, String canonicalId)
+      throws IOException {
+    storeCacheValue(
+        cacheKey, tmpName -> FileSystemUtils.copyFile(sourcePath, tmpName), keyType, canonicalId);
+  }
+
+  /**
+   * Adds an in-memory value to the cache.
+   *
+   * @param content The byte content of the value to be cached.
+   * @param keyType The type of key used. See: KeyType
+   */
+  public void put(String cacheKey, byte[] content, KeyType keyType) throws IOException {
+    storeCacheValue(
+        cacheKey,
+        tmpName -> FileSystemUtils.writeContent(tmpName, content),
+        keyType,
+        /* canonicalId= */ null);
+  }
+
+  /**
+   * Adds an in-memory value to the cache.
+   *
+   * @param content The byte content of the value to be cached.
+   * @param keyType The type of key used. See: KeyType
+   */
+  public void put(byte[] content, KeyType keyType) throws IOException {
+    String cacheKey = keyType.newHasher().putBytes(content).hash().toString();
+    put(cacheKey, content, keyType);
+  }
+
+  /**
    * Copies a value from a specified path into the cache, computing the cache key itself.
    *
    * @param sourcePath The path of the value to be cached.
    * @param keyType The type of key to be used.
    * @param canonicalId If set to a non-empty String associate the file with this name, allowing
    *     restricted cache lookups later.
-   * @throws IOException
    * @return The key for the cached entry.
    */
   public String put(Path sourcePath, KeyType keyType, String canonicalId)
@@ -301,7 +370,6 @@
    *
    * @param keyType The type of hash function. e.g. SHA-1, SHA-256.
    * @param path The path to the file.
-   * @throws IOException
    */
   public static String getChecksum(KeyType keyType, Path path)
       throws IOException, InterruptedException {
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/Checksum.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/Checksum.java
index af5cf50..8a5ea43 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/Checksum.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/Checksum.java
@@ -120,6 +120,22 @@
     return hashCode.toString();
   }
 
+  @Override
+  public boolean equals(Object other) {
+    if (other == this) {
+      return true;
+    }
+    if (other instanceof Checksum c) {
+      return keyType.equals(c.keyType) && hashCode.equals(c.hashCode);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return hashCode.hashCode() * 31 + keyType.hashCode();
+  }
+
   public HashCode getHashCode() {
     return hashCode;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java
index a56ce63..9737437 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java
@@ -382,16 +382,37 @@
    * @param originalUrl the original URL of the file
    * @param eventHandler CLI progress reporter
    * @param clientEnv environment variables in shell issuing this command
+   * @param checksum checksum of the file used to verify the content and obtain repository cache
+   *     hits
    * @throws IllegalArgumentException on parameter badness, which should be checked beforehand
    * @throws IOException if download was attempted and ended up failing
    * @throws InterruptedException if this thread is being cast into oblivion
    */
   public byte[] downloadAndReadOneUrl(
-      URL originalUrl, ExtendedEventHandler eventHandler, Map<String, String> clientEnv)
+      URL originalUrl,
+      ExtendedEventHandler eventHandler,
+      Map<String, String> clientEnv,
+      Optional<Checksum> checksum)
       throws IOException, InterruptedException {
     if (Thread.interrupted()) {
       throw new InterruptedException();
     }
+
+    if (repositoryCache.isEnabled() && checksum.isPresent()) {
+      String cacheKey = checksum.get().toString();
+      try {
+        byte[] content = repositoryCache.getBytes(cacheKey, checksum.get().getKeyType());
+        if (content != null) {
+          // Cache hit!
+          eventHandler.post(
+              new RepositoryCacheHitEvent("Bazel module fetching", cacheKey, originalUrl));
+          return content;
+        }
+      } catch (IOException e) {
+        // Ignore error trying to get. We'll just download again.
+      }
+    }
+
     Map<URI, Map<String, List<String>>> authHeaders = ImmutableMap.of();
     ImmutableList<URL> rewrittenUrls = ImmutableList.of(originalUrl);
 
@@ -418,15 +439,27 @@
       authHeaders = rewriter.updateAuthHeaders(rewrittenUrlMappings, authHeaders, netrcCreds);
     }
 
+    if (disableDownload) {
+      throw new IOException(
+          String.format("Failed to download %s: download is disabled.", originalUrl));
+    }
+
     if (rewrittenUrls.isEmpty()) {
       throw new IOException(getRewriterBlockedAllUrlsMessage(ImmutableList.of(originalUrl)));
     }
 
     HttpDownloader httpDownloader = new HttpDownloader();
+    byte[] content = null;
     for (int attempt = 0; attempt <= retries; ++attempt) {
       try {
-        return httpDownloader.downloadAndReadOneUrl(
-            rewrittenUrls.get(0), credentialFactory.create(authHeaders), eventHandler, clientEnv);
+        content =
+            httpDownloader.downloadAndReadOneUrl(
+                rewrittenUrls.get(0),
+                credentialFactory.create(authHeaders),
+                checksum,
+                eventHandler,
+                clientEnv);
+        break;
       } catch (ContentLengthMismatchException e) {
         if (attempt == retries) {
           throw e;
@@ -435,8 +468,18 @@
         throw new InterruptedException(e.getMessage());
       }
     }
+    if (content == null) {
+      throw new IllegalStateException("Unexpected error: file should have been downloaded.");
+    }
 
-    throw new IllegalStateException("Unexpected error: file should have been downloaded.");
+    if (repositoryCache.isEnabled()) {
+      if (checksum.isPresent()) {
+        repositoryCache.put(checksum.get().toString(), content, checksum.get().getKeyType());
+      } else {
+        repositoryCache.put(content, KeyType.SHA256);
+      }
+    }
+    return content;
   }
 
   @Nullable
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java
index 35e0ea2..796ac03 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloader.java
@@ -147,6 +147,7 @@
   public byte[] downloadAndReadOneUrl(
       URL url,
       Credentials credentials,
+      Optional<Checksum> checksum,
       ExtendedEventHandler eventHandler,
       Map<String, String> clientEnv)
       throws IOException, InterruptedException {
@@ -155,8 +156,7 @@
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     SEMAPHORE.acquire();
     try (HttpStream payload =
-        multiplexer.connect(
-            url, Optional.empty(), ImmutableMap.of(), credentials, Optional.empty())) {
+        multiplexer.connect(url, checksum, ImmutableMap.of(), credentials, Optional.empty())) {
       ByteStreams.copy(payload, out);
     } catch (SocketTimeoutException e) {
       // SocketTimeoutExceptions are InterruptedIOExceptions; however they do not signify
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/packages/BazelPackageLoader.java b/src/main/java/com/google/devtools/build/lib/skyframe/packages/BazelPackageLoader.java
index ed1ccb5..344048a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/packages/BazelPackageLoader.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/packages/BazelPackageLoader.java
@@ -185,6 +185,9 @@
                       ImmutableMap::of,
                       directories,
                       EXTERNAL_PACKAGE_HELPER))
+              .put(
+                  SkyFunctions.BAZEL_LOCK_FILE,
+                  new BazelLockFileFunction(directories.getWorkspace()))
               .put(SkyFunctions.BAZEL_DEP_GRAPH, new BazelDepGraphFunction())
               .put(SkyFunctions.BAZEL_MODULE_RESOLUTION, new BazelModuleResolutionFunction())
               .put(SkyFunctions.REPO_SPEC, new RepoSpecFunction())
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisMock.java
index 526e2c4..56c441f 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisMock.java
@@ -15,6 +15,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
@@ -208,7 +209,7 @@
             RepositoryDelegatorFunction.FORCE_FETCH_DISABLED),
         PrecomputedValue.injected(RepositoryDelegatorFunction.VENDOR_DIRECTORY, Optional.empty()),
         PrecomputedValue.injected(RepositoryDelegatorFunction.DISABLE_NATIVE_REPO_RULES, false),
-        PrecomputedValue.injected(ModuleFileFunction.REGISTRIES, ImmutableList.of()),
+        PrecomputedValue.injected(ModuleFileFunction.REGISTRIES, ImmutableSet.of()),
         PrecomputedValue.injected(ModuleFileFunction.IGNORE_DEV_DEPS, false),
         PrecomputedValue.injected(ModuleFileFunction.MODULE_OVERRIDES, ImmutableMap.of()),
         PrecomputedValue.injected(YankedVersionsUtil.ALLOWED_YANKED_VERSIONS, ImmutableList.of()),
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java
index 67f0349..42c3ecc 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java
@@ -239,7 +239,7 @@
             .setExtraPrecomputeValues(
                 ImmutableList.of(
                     PrecomputedValue.injected(
-                        ModuleFileFunction.REGISTRIES, ImmutableList.of(registry.getUrl())),
+                        ModuleFileFunction.REGISTRIES, ImmutableSet.of(registry.getUrl())),
                     PrecomputedValue.injected(ModuleFileFunction.IGNORE_DEV_DEPS, false),
                     PrecomputedValue.injected(
                         RepositoryDelegatorFunction.DISABLE_NATIVE_REPO_RULES, false),
@@ -293,7 +293,7 @@
             PrecomputedValue.injected(
                 RepositoryDelegatorFunction.VENDOR_DIRECTORY, Optional.empty()),
             PrecomputedValue.injected(
-                ModuleFileFunction.REGISTRIES, ImmutableList.of(registry.getUrl())),
+                ModuleFileFunction.REGISTRIES, ImmutableSet.of(registry.getUrl())),
             PrecomputedValue.injected(ModuleFileFunction.IGNORE_DEV_DEPS, false),
             PrecomputedValue.injected(RepositoryDelegatorFunction.DISABLE_NATIVE_REPO_RULES, false),
             PrecomputedValue.injected(
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
index eef29a0..89b27e1 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
@@ -308,7 +308,7 @@
             .addAll(analysisMock.getPrecomputedValues())
             .add(
                 PrecomputedValue.injected(
-                    ModuleFileFunction.REGISTRIES, ImmutableList.of(registry.getUrl())))
+                    ModuleFileFunction.REGISTRIES, ImmutableSet.of(registry.getUrl())))
             .addAll(extraPrecomputedValues())
             .build();
     PackageFactory.BuilderForTesting pkgFactoryBuilder =
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
index 2d88b05..1eef645 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
@@ -49,7 +49,6 @@
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/starlark",
         "//src/main/java/com/google/devtools/build/lib/clock",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
-        "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/packages",
         "//src/main/java/com/google/devtools/build/lib/packages/semantics",
@@ -69,7 +68,6 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe:precomputed_function",
         "//src/main/java/com/google/devtools/build/lib/skyframe:precomputed_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:repository_mapping_function",
-        "//src/main/java/com/google/devtools/build/lib/skyframe:repository_mapping_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:sky_functions",
         "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_cluster",
         "//src/main/java/com/google/devtools/build/lib/starlarkbuildapi/repository",
@@ -117,6 +115,8 @@
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:module_extension",
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:registry",
         "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution",
+        "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:resolution_impl",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/packages",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java
index 819c538..a8d61f8 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunctionTest.java
@@ -61,7 +61,6 @@
 import com.google.devtools.build.skyframe.RecordingDifferencer;
 import com.google.devtools.build.skyframe.SequencedRecordingDifferencer;
 import com.google.devtools.build.skyframe.SkyFunction;
-import com.google.devtools.build.skyframe.SkyFunctionException;
 import com.google.devtools.build.skyframe.SkyFunctionName;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
@@ -147,7 +146,7 @@
         differencer,
         StarlarkSemantics.builder().setBool(BuildLanguageOptions.ENABLE_BZLMOD, true).build());
     ModuleFileFunction.IGNORE_DEV_DEPS.set(differencer, false);
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of());
     ModuleFileFunction.MODULE_OVERRIDES.set(differencer, ImmutableMap.of());
     BazelModuleResolutionFunction.CHECK_DIRECT_DEPENDENCIES.set(
         differencer, CheckDirectDepsMode.OFF);
@@ -364,10 +363,8 @@
 
     @Override
     @Nullable
-    public SkyValue compute(SkyKey skyKey, Environment env)
-        throws SkyFunctionException, InterruptedException {
-
-      return BazelModuleResolutionValue.create(depGraph, ImmutableMap.of());
+    public SkyValue compute(SkyKey skyKey, Environment env) {
+      return BazelModuleResolutionValue.create(depGraph, ImmutableMap.of(), ImmutableMap.of());
     }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java
index 0aaf96e..f4dc881 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java
@@ -22,6 +22,7 @@
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
@@ -192,6 +193,7 @@
                                 .setFlags(flags)
                                 .setLocalOverrideHashes(localOverrideHashes)
                                 .setModuleDepGraph(key.depGraph())
+                                .setRegistryFileHashes(ImmutableMap.of())
                                 .build());
 
                         return new SkyValue() {};
@@ -203,7 +205,7 @@
     PrecomputedValue.STARLARK_SEMANTICS.set(
         differencer,
         StarlarkSemantics.builder().setBool(BuildLanguageOptions.ENABLE_BZLMOD, true).build());
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of());
     ModuleFileFunction.IGNORE_DEV_DEPS.set(differencer, true);
     ModuleFileFunction.MODULE_OVERRIDES.set(differencer, ImmutableMap.of());
     YankedVersionsUtil.ALLOWED_YANKED_VERSIONS.set(differencer, ImmutableList.of());
@@ -275,7 +277,7 @@
 
     ImmutableList<String> yankedVersions = ImmutableList.of("2.4", "2.3");
     LocalPathOverride override = LocalPathOverride.create("override_path");
-    ImmutableList<String> registries = ImmutableList.of("registry1", "registry2");
+    ImmutableSet<String> registries = ImmutableSet.of("registry1", "registry2");
     ImmutableMap<String, String> moduleOverride = ImmutableMap.of("my_dep_1", override.getPath());
 
     ModuleFileFunction.IGNORE_DEV_DEPS.set(differencer, true);
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunctionTest.java
index 218a36c..2c4a7b3 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunctionTest.java
@@ -21,6 +21,7 @@
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
@@ -344,7 +345,7 @@
             .addModule(
                 createModuleKey("b", "1.0"),
                 "module(name='b', version='1.0', bazel_compatibility=['<=5.1.4', '-5.1.2']);");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
   }
 
   @Test
@@ -407,7 +408,7 @@
                 "bazel_dep(name='b', version='1.0')")
             .addModule(createModuleKey("b", "1.0"), "module(name='b', version='1.0');")
             .addYankedVersion("b", ImmutableMap.of(Version.parse("1.0"), "1.0 is a bad version!"));
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
   }
 
   @Test
@@ -437,7 +438,7 @@
                 "module(name='b', version='1.1')",
                 "bazel_dep(name='c', version='1.0')");
 
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
     EvaluationResult<BazelModuleResolutionValue> result =
         evaluator.evaluate(ImmutableList.of(BazelModuleResolutionValue.KEY), evaluationContext);
 
@@ -483,9 +484,8 @@
                 "bazel_dep(name='c', version='1.0')",
                 "print('hello from b@1.1')");
 
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
-    EvaluationResult<BazelModuleResolutionValue> result =
-        evaluator.evaluate(ImmutableList.of(BazelModuleResolutionValue.KEY), evaluationContext);
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
+    evaluator.evaluate(ImmutableList.of(BazelModuleResolutionValue.KEY), evaluationContext);
 
     assertContainsEvent("hello from root module");
     assertContainsEvent("hello from overridden a");
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodRepoRuleFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodRepoRuleFunctionTest.java
index e0a1fb2..444ba83 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodRepoRuleFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BzlmodRepoRuleFunctionTest.java
@@ -22,6 +22,7 @@
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
@@ -146,7 +147,7 @@
             differencer);
 
     PrecomputedValue.STARLARK_SEMANTICS.set(differencer, StarlarkSemantics.DEFAULT);
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of());
     ModuleFileFunction.IGNORE_DEV_DEPS.set(differencer, false);
     ModuleFileFunction.MODULE_OVERRIDES.set(differencer, ImmutableMap.of());
     YankedVersionsUtil.ALLOWED_YANKED_VERSIONS.set(differencer, ImmutableList.of());
@@ -170,7 +171,7 @@
                 createModuleKey("bbb", "1.0"),
                 "module(name='bbb', version='1.0');bazel_dep(name='ccc',version='2.0')")
             .addModule(createModuleKey("ccc", "2.0"), "module(name='ccc', version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     RepositoryName repo = RepositoryName.create("ccc~");
     EvaluationResult<BzlmodRepoRuleValue> result =
@@ -201,7 +202,7 @@
                 createModuleKey("bbb", "1.0"),
                 "module(name='bbb', version='1.0');bazel_dep(name='ccc',version='2.0')")
             .addModule(createModuleKey("ccc", "2.0"), "module(name='ccc', version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     RepositoryName repo = RepositoryName.create("ccc~");
     EvaluationResult<BzlmodRepoRuleValue> result =
@@ -234,7 +235,7 @@
                 "module(name='bbb', version='1.0');bazel_dep(name='ccc',version='2.0')")
             .addModule(createModuleKey("ccc", "2.0"), "module(name='ccc', version='2.0')")
             .addModule(createModuleKey("ccc", "3.0"), "module(name='ccc', version='3.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     RepositoryName repo = RepositoryName.create("ccc~");
     EvaluationResult<BzlmodRepoRuleValue> result =
@@ -270,7 +271,7 @@
                 "module(name='ccc', version='2.0');bazel_dep(name='ddd',version='2.0')")
             .addModule(createModuleKey("ddd", "1.0"), "module(name='ddd', version='1.0')")
             .addModule(createModuleKey("ddd", "2.0"), "module(name='ddd', version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     RepositoryName repo = RepositoryName.create("ddd~v2.0");
     EvaluationResult<BzlmodRepoRuleValue> result =
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryTest.java
index 742b5d6..81e4685 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/DiscoveryTest.java
@@ -23,6 +23,8 @@
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
@@ -30,6 +32,7 @@
 import com.google.devtools.build.lib.analysis.util.AnalysisMock;
 import com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.InterimModuleBuilder;
 import com.google.devtools.build.lib.bazel.bzlmod.ModuleFileValue.RootModuleFileValue;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.bazel.repository.starlark.StarlarkRepositoryModule;
 import com.google.devtools.build.lib.clock.BlazeClock;
 import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
@@ -91,11 +94,17 @@
     static final SkyFunctionName FUNCTION_NAME = SkyFunctionName.createHermetic("test_discovery");
     static final SkyKey KEY = () -> FUNCTION_NAME;
 
-    static DiscoveryValue create(ImmutableMap<ModuleKey, InterimModule> depGraph) {
-      return new AutoValue_DiscoveryTest_DiscoveryValue(depGraph);
+    static DiscoveryValue create(
+        ImmutableMap<ModuleKey, InterimModule> depGraph,
+        ImmutableMap<String, Optional<String>> registryFileHashes) {
+      return new AutoValue_DiscoveryTest_DiscoveryValue(depGraph, registryFileHashes);
     }
 
     abstract ImmutableMap<ModuleKey, InterimModule> getDepGraph();
+
+    // Uses Optional<String> rather than Optional<Checksum> for easier testing (Checksum doesn't
+    // implement equals()).
+    abstract ImmutableMap<String, Optional<String>> getRegistryFileHashes();
   }
 
   static class DiscoveryFunction implements SkyFunction {
@@ -107,14 +116,21 @@
       if (root == null) {
         return null;
       }
-      ImmutableMap<ModuleKey, InterimModule> depGraph;
+      Discovery.Result discoveryResult;
       try {
-        depGraph = Discovery.run(env, root);
+        discoveryResult = Discovery.run(env, root);
       } catch (ExternalDepsException e) {
         throw new BazelModuleResolutionFunction.BazelModuleResolutionFunctionException(
             e, SkyFunctionException.Transience.PERSISTENT);
       }
-      return depGraph == null ? null : DiscoveryValue.create(depGraph);
+      return discoveryResult == null
+          ? null
+          : DiscoveryValue.create(
+              discoveryResult.depGraph(),
+              ImmutableMap.copyOf(
+                  Maps.transformValues(
+                      discoveryResult.registryFileHashes(),
+                      value -> value.map(Checksum::toString))));
     }
   }
 
@@ -168,6 +184,7 @@
                         SyscallCache.NO_CACHE,
                         externalFilesHelper))
                 .put(DiscoveryValue.FUNCTION_NAME, new DiscoveryFunction())
+                .put(SkyFunctions.BAZEL_LOCK_FILE, new BazelLockFileFunction(rootDirectory))
                 .put(
                     SkyFunctions.MODULE_FILE,
                     new ModuleFileFunction(
@@ -236,7 +253,7 @@
                 createModuleKey("ddd", "3.0"),
                 // Add a random override here; it should be ignored
                 "module(name='ddd', version='3.0');local_path_override(module_name='ff',path='f')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -260,6 +277,15 @@
                 .setRegistry(registry)
                 .buildEntry(),
             InterimModuleBuilder.create("ddd", "3.0").setRegistry(registry).buildEntry());
+    assertThat(discoveryValue.getRegistryFileHashes())
+        .containsExactly(
+            registry.getUrl() + "/modules/bbb/1.0/MODULE.bazel",
+            Optional.of("3f48e6d8694e0aa0d16617fd97b7d84da0e17ee9932c18cbc71888c12563372d"),
+            registry.getUrl() + "/modules/ccc/2.0/MODULE.bazel",
+            Optional.of("e613d4192495192c3d46ee444dc9882a176a9e7a243d1b5a840ab0f01553e8d6"),
+            registry.getUrl() + "/modules/ddd/3.0/MODULE.bazel",
+            Optional.of("f80d91453520d193b0b79f1501eb902b5b01a991762cc7fb659fc580b95648fd"))
+        .inOrder();
   }
 
   @Test
@@ -278,7 +304,7 @@
                 "bazel_dep(name='ccc',version='2.0',dev_dependency=True)")
             .addModule(createModuleKey("ccc", "1.0"), "module(name='ccc', version='1.0')")
             .addModule(createModuleKey("ccc", "2.0"), "module(name='ccc', version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -313,7 +339,7 @@
                 "bazel_dep(name='ccc',version='2.0',dev_dependency=True)")
             .addModule(createModuleKey("ccc", "1.0"), "module(name='ccc', version='1.0')")
             .addModule(createModuleKey("ccc", "2.0"), "module(name='ccc', version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
     ModuleFileFunction.IGNORE_DEV_DEPS.set(differencer, true);
 
     EvaluationResult<DiscoveryValue> result =
@@ -346,7 +372,7 @@
             .addModule(
                 createModuleKey("ccc", "2.0"),
                 "module(name='ccc', version='2.0');bazel_dep(name='bbb',version='1.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -383,7 +409,7 @@
                 createModuleKey("bbb", "1.0"),
                 "module(name='bbb', version='1.0');bazel_dep(name='aaa',version='2.0')")
             .addModule(createModuleKey("aaa", "2.0"), "module(name='aaa', version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -419,7 +445,7 @@
                 "module(name='bbb', version='0.1');bazel_dep(name='ccc',version='1.0')")
             .addModule(createModuleKey("ccc", "1.0"), "module(name='ccc', version='1.0');")
             .addModule(createModuleKey("ccc", "2.0"), "module(name='ccc', version='2.0');");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -461,7 +487,7 @@
         "module(name='aaa',version='0.1')",
         "bazel_dep(name='bbb',version='0.1')",
         "single_version_override(module_name='ccc',registry='" + registry2.getUrl() + "')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry1.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry1.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -503,7 +529,7 @@
                 createModuleKey("bbb", "0.1"),
                 "module(name='bbb', version='0.1');bazel_dep(name='ccc',version='1.0')")
             .addModule(createModuleKey("ccc", "1.0"), "module(name='ccc', version='1.0');");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -525,6 +551,11 @@
             InterimModuleBuilder.create("ccc", "2.0")
                 .setKey(createModuleKey("ccc", ""))
                 .buildEntry());
+    assertThat(discoveryValue.getRegistryFileHashes())
+        .containsExactly(
+            registry.getUrl() + "/modules/bbb/0.1/MODULE.bazel",
+            Optional.of("3f9e1a600b4adeee1c1a92b92df9d086eca4bbdde656c122872f48f8f3b874a3"))
+        .inOrder();
   }
 
   @Test
@@ -553,7 +584,7 @@
             .newFakeRegistry("/foo")
             .addModule(createModuleKey("foo", "1.0"), "module(name='foo', version='1.0')")
             .addModule(createModuleKey("foo", "2.0"), "module(name='foo', version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<DiscoveryValue> result =
         evaluator.evaluate(ImmutableList.of(DiscoveryValue.KEY), evaluationContext);
@@ -587,5 +618,13 @@
                 .addDep("local_config_platform", createModuleKey("local_config_platform", ""))
                 .setRegistry(registry)
                 .buildEntry());
+
+    assertThat(discoveryValue.getRegistryFileHashes())
+        .containsExactly(
+            registry.getUrl() + "/modules/foo/2.0/MODULE.bazel",
+            Optional.of("76ecb05b455aecab4ec958c1deb17e4cbbe6e708d9c4e85fceda2317f6c86d7b"),
+            registry.getUrl() + "/modules/foo/1.0/MODULE.bazel",
+            Optional.of("4d887e8dfc1863861e3aa5601eeeebca5d8f110977895f1de4bdb2646e546fb5"))
+        .inOrder();
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/FakeRegistry.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/FakeRegistry.java
index 10500fb..2bcc6d8 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/FakeRegistry.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/FakeRegistry.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.HashMap;
@@ -63,26 +64,32 @@
 
   @Override
   public Optional<ModuleFile> getModuleFile(ModuleKey key, ExtendedEventHandler eventHandler) {
-    return Optional.ofNullable(modules.get(key))
-        .map(value -> value.getBytes(UTF_8))
-        .map(
-            content ->
-                ModuleFile.create(
-                    content,
-                    String.format(
-                        "%s/modules/%s/%s/MODULE.bazel",
-                        url, key.getName(), key.getVersion().toString())));
+    String uri =
+        String.format("%s/modules/%s/%s/MODULE.bazel", url, key.getName(), key.getVersion());
+    var maybeContent = Optional.ofNullable(modules.get(key)).map(value -> value.getBytes(UTF_8));
+    eventHandler.post(RegistryFileDownloadEvent.create(uri, maybeContent));
+    return maybeContent.map(content -> ModuleFile.create(content, uri));
   }
 
   @Override
   public RepoSpec getRepoSpec(ModuleKey key, ExtendedEventHandler eventHandler) {
-    return RepoSpec.builder()
-        .setRuleClassName("local_repository")
-        .setAttributes(
-            AttributeValues.create(
-                ImmutableMap.of(
-                    "path", rootPath + "/" + key.getCanonicalRepoNameWithVersion().getName())))
-        .build();
+    RepoSpec repoSpec =
+        RepoSpec.builder()
+            .setRuleClassName("local_repository")
+            .setAttributes(
+                AttributeValues.create(
+                    ImmutableMap.of(
+                        "path", rootPath + "/" + key.getCanonicalRepoNameWithVersion().getName())))
+            .build();
+    eventHandler.post(
+        RegistryFileDownloadEvent.create(
+            "%s/modules/%s/%s/source.json"
+                .formatted(url, key.getName(), key.getVersion().toString()),
+            Optional.of(
+                GsonTypeAdapterUtil.createSingleExtensionUsagesValueHashGson()
+                    .toJson(repoSpec)
+                    .getBytes(UTF_8))));
+    return repoSpec;
   }
 
   @Override
@@ -118,7 +125,8 @@
     }
 
     @Override
-    public Registry createRegistry(String url) {
+    public Registry createRegistry(
+        String url, ImmutableMap<String, Optional<Checksum>> fileHashes) {
       return Preconditions.checkNotNull(registries.get(url), "unknown registry url: %s", url);
     }
   }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistryTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistryTest.java
index 2401275..a721ba6 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistryTest.java
@@ -15,6 +15,7 @@
 
 package com.google.devtools.build.lib.bazel.bzlmod;
 
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.createModuleKey;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -23,14 +24,17 @@
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.eventbus.Subscribe;
+import com.google.common.hash.Hashing;
 import com.google.devtools.build.lib.authandtls.BasicHttpAuthenticationEncoder;
 import com.google.devtools.build.lib.authandtls.Netrc;
 import com.google.devtools.build.lib.authandtls.NetrcCredentials;
 import com.google.devtools.build.lib.authandtls.NetrcParser;
 import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
+import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
 import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
 import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
-import com.google.devtools.build.lib.bazel.repository.downloader.UnrecoverableHttpException;
 import com.google.devtools.build.lib.testutil.FoundationTestCase;
 import com.google.devtools.build.lib.vfs.Path;
 import java.io.ByteArrayInputStream;
@@ -38,6 +42,8 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 import org.junit.Before;
 import org.junit.Rule;
@@ -49,18 +55,38 @@
 /** Tests for {@link IndexRegistry}. */
 @RunWith(JUnit4.class)
 public class IndexRegistryTest extends FoundationTestCase {
+  private static class EventRecorder {
+    private final List<RegistryFileDownloadEvent> downloadEvents = new ArrayList<>();
+
+    @Subscribe
+    public void onRegistryFileDownloadEvent(RegistryFileDownloadEvent downloadEvent) {
+      downloadEvents.add(downloadEvent);
+    }
+
+    public ImmutableMap<String, Optional<Checksum>> getRecordedHashes() {
+      return downloadEvents.stream()
+          .collect(
+              toImmutableMap(RegistryFileDownloadEvent::uri, RegistryFileDownloadEvent::checksum));
+    }
+  }
+
   private final String authToken =
       BasicHttpAuthenticationEncoder.encode("rinne", "rinnepass", UTF_8);
   private DownloadManager downloadManager;
+  private EventRecorder eventRecorder;
   @Rule public final TestHttpServer server = new TestHttpServer(authToken);
   @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
 
   private RegistryFactory registryFactory;
+  private RepositoryCache repositoryCache;
 
   @Before
   public void setUp() throws Exception {
+    eventRecorder = new EventRecorder();
+    eventBus.register(eventRecorder);
     Path workspaceRoot = scratch.dir("/ws");
-    downloadManager = new DownloadManager(new RepositoryCache(), new HttpDownloader());
+    repositoryCache = new RepositoryCache();
+    downloadManager = new DownloadManager(repositoryCache, new HttpDownloader());
     registryFactory =
         new RegistryFactoryImpl(
             workspaceRoot, downloadManager, Suppliers.ofInstance(ImmutableMap.of()));
@@ -71,7 +97,8 @@
     server.serve("/myreg/modules/foo/1.0/MODULE.bazel", "lol");
     server.start();
 
-    Registry registry = registryFactory.createRegistry(server.getUrl() + "/myreg");
+    Registry registry =
+        registryFactory.createRegistry(server.getUrl() + "/myreg", ImmutableMap.of());
     assertThat(registry.getModuleFile(createModuleKey("foo", "1.0"), reporter))
         .hasValue(
             ModuleFile.create(
@@ -87,13 +114,18 @@
         NetrcParser.parseAndClose(
             new ByteArrayInputStream(
                 "machine [::1] login rinne password rinnepass\n".getBytes(UTF_8)));
-    Registry registry = registryFactory.createRegistry(server.getUrl() + "/myreg");
+    Registry registry =
+        registryFactory.createRegistry(server.getUrl() + "/myreg", ImmutableMap.of());
 
-    UnrecoverableHttpException e =
+    var e =
         assertThrows(
-            UnrecoverableHttpException.class,
+            IOException.class,
             () -> registry.getModuleFile(createModuleKey("foo", "1.0"), reporter));
-    assertThat(e).hasMessageThat().contains("GET returned 401 Unauthorized");
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to fetch registry file %s: GET returned 401 Unauthorized"
+                .formatted(server.getUrl() + "/myreg/modules/foo/1.0/MODULE.bazel"));
 
     downloadManager.setNetrcCreds(new NetrcCredentials(netrc));
     assertThat(registry.getModuleFile(createModuleKey("foo", "1.0"), reporter))
@@ -113,7 +145,7 @@
 
     Registry registry =
         registryFactory.createRegistry(
-            new File(tempFolder.getRoot(), "fakereg").toURI().toString());
+            new File(tempFolder.getRoot(), "fakereg").toURI().toString(), ImmutableMap.of());
     assertThat(registry.getModuleFile(createModuleKey("foo", "1.0"), reporter))
         .hasValue(ModuleFile.create("lol".getBytes(UTF_8), file.toURI().toString()));
     assertThat(registry.getModuleFile(createModuleKey("bar", "1.0"), reporter)).isEmpty();
@@ -149,7 +181,7 @@
         "}");
     server.start();
 
-    Registry registry = registryFactory.createRegistry(server.getUrl());
+    Registry registry = registryFactory.createRegistry(server.getUrl(), ImmutableMap.of());
     assertThat(registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter))
         .isEqualTo(
             new ArchiveRepoSpecBuilder()
@@ -193,7 +225,7 @@
         "}");
     server.start();
 
-    Registry registry = registryFactory.createRegistry(server.getUrl());
+    Registry registry = registryFactory.createRegistry(server.getUrl(), ImmutableMap.of());
     assertThat(registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter))
         .isEqualTo(
             RepoSpec.builder()
@@ -215,7 +247,7 @@
         "  \"strip_prefix\": \"pref\"",
         "}");
 
-    Registry registry = registryFactory.createRegistry(server.getUrl());
+    Registry registry = registryFactory.createRegistry(server.getUrl(), ImmutableMap.of());
     assertThat(registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter))
         .isEqualTo(
             new ArchiveRepoSpecBuilder()
@@ -246,7 +278,7 @@
         "}");
     server.start();
 
-    Registry registry = registryFactory.createRegistry(server.getUrl());
+    Registry registry = registryFactory.createRegistry(server.getUrl(), ImmutableMap.of());
     assertThrows(
         IOException.class, () -> registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter));
   }
@@ -273,7 +305,7 @@
             + "    }\n"
             + "}");
     server.start();
-    Registry registry = registryFactory.createRegistry(server.getUrl());
+    Registry registry = registryFactory.createRegistry(server.getUrl(), ImmutableMap.of());
     Optional<ImmutableMap<Version, String>> yankedVersion =
         registry.getYankedVersions("red-pill", reporter);
     assertThat(yankedVersion)
@@ -294,7 +326,7 @@
         "}");
     server.start();
 
-    Registry registry = registryFactory.createRegistry(server.getUrl());
+    Registry registry = registryFactory.createRegistry(server.getUrl(), ImmutableMap.of());
     assertThat(registry.getRepoSpec(createModuleKey("archive_type", "1.0"), reporter))
         .isEqualTo(
             new ArchiveRepoSpecBuilder()
@@ -306,4 +338,230 @@
                 .setRemotePatchStrip(0)
                 .build());
   }
+
+  @Test
+  public void testGetModuleFileChecksums() throws Exception {
+    repositoryCache.setRepositoryCachePath(scratch.dir("cache"));
+
+    server.serve("/myreg/modules/foo/1.0/MODULE.bazel", "old");
+    server.serve("/myreg/modules/foo/2.0/MODULE.bazel", "new");
+    server.start();
+
+    var knownFiles =
+        ImmutableMap.of(
+            server.getUrl() + "/myreg/modules/foo/1.0/MODULE.bazel",
+            Optional.of(sha256("old")),
+            server.getUrl() + "/myreg/modules/unused/1.0/MODULE.bazel",
+            Optional.of(sha256("unused")));
+    Registry registry = registryFactory.createRegistry(server.getUrl() + "/myreg", knownFiles);
+    assertThat(registry.getModuleFile(createModuleKey("foo", "1.0"), reporter))
+        .hasValue(
+            ModuleFile.create(
+                "old".getBytes(UTF_8), server.getUrl() + "/myreg/modules/foo/1.0/MODULE.bazel"));
+    assertThat(registry.getModuleFile(createModuleKey("foo", "2.0"), reporter))
+        .hasValue(
+            ModuleFile.create(
+                "new".getBytes(UTF_8), server.getUrl() + "/myreg/modules/foo/2.0/MODULE.bazel"));
+    assertThat(registry.getModuleFile(createModuleKey("bar", "1.0"), reporter)).isEmpty();
+
+    var recordedChecksums = eventRecorder.getRecordedHashes();
+    assertThat(
+            Maps.transformValues(
+                recordedChecksums, maybeChecksum -> maybeChecksum.map(Checksum::toString)))
+        .containsExactly(
+            server.getUrl() + "/myreg/modules/foo/1.0/MODULE.bazel",
+            Optional.of(sha256("old").toString()),
+            server.getUrl() + "/myreg/modules/foo/2.0/MODULE.bazel",
+            Optional.of(sha256("new").toString()),
+            server.getUrl() + "/myreg/modules/bar/1.0/MODULE.bazel",
+            Optional.empty())
+        .inOrder();
+
+    registry = registryFactory.createRegistry(server.getUrl() + "/myreg", recordedChecksums);
+    // Test that the recorded hashes are used for repo cache hits even when the server content
+    // changes.
+    server.unserve("/myreg/modules/foo/1.0/MODULE.bazel");
+    server.unserve("/myreg/modules/foo/2.0/MODULE.bazel");
+    server.serve("/myreg/modules/bar/1.0/MODULE.bazel", "no longer 404");
+    assertThat(registry.getModuleFile(createModuleKey("foo", "1.0"), reporter))
+        .hasValue(
+            ModuleFile.create(
+                "old".getBytes(UTF_8), server.getUrl() + "/myreg/modules/foo/1.0/MODULE.bazel"));
+    assertThat(registry.getModuleFile(createModuleKey("foo", "2.0"), reporter))
+        .hasValue(
+            ModuleFile.create(
+                "new".getBytes(UTF_8), server.getUrl() + "/myreg/modules/foo/2.0/MODULE.bazel"));
+    assertThat(registry.getModuleFile(createModuleKey("bar", "1.0"), reporter)).isEmpty();
+  }
+
+  @Test
+  public void testGetModuleFileChecksumMismatch() throws Exception {
+    repositoryCache.setRepositoryCachePath(scratch.dir("cache"));
+
+    server.serve("/myreg/modules/foo/1.0/MODULE.bazel", "fake");
+    server.start();
+
+    var knownFiles =
+        ImmutableMap.of(
+            server.getUrl() + "/myreg/modules/foo/1.0/MODULE.bazel",
+            Optional.of(sha256("original")));
+    Registry registry = registryFactory.createRegistry(server.getUrl() + "/myreg", knownFiles);
+    var e =
+        assertThrows(
+            IOException.class,
+            () -> registry.getModuleFile(createModuleKey("foo", "1.0"), reporter));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to fetch registry file %s: Checksum was %s but wanted %s"
+                .formatted(
+                    server.getUrl() + "/myreg/modules/foo/1.0/MODULE.bazel",
+                    sha256("fake"),
+                    sha256("original")));
+  }
+
+  @Test
+  public void testGetRepoSpecChecksum() throws Exception {
+    repositoryCache.setRepositoryCachePath(scratch.dir("cache"));
+
+    String registryJson =
+        """
+        {
+          "module_base_path": "/hello/foo"
+        }
+        """;
+    server.serve("/bazel_registry.json", registryJson);
+    String sourceJson =
+        """
+        {
+          "type": "local_path",
+          "path": "../bar/project_x"
+        }
+        """;
+    server.serve("/modules/foo/1.0/source.json", sourceJson.getBytes(UTF_8));
+    server.start();
+
+    var knownFiles =
+        ImmutableMap.of(
+            server.getUrl() + "/modules/foo/2.0/source.json", Optional.of(sha256("unused")));
+    Registry registry = registryFactory.createRegistry(server.getUrl(), knownFiles);
+    assertThat(registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter))
+        .isEqualTo(
+            RepoSpec.builder()
+                .setRuleClassName("local_repository")
+                .setAttributes(
+                    AttributeValues.create(ImmutableMap.of("path", "/hello/bar/project_x")))
+                .build());
+
+    var recordedChecksums = eventRecorder.getRecordedHashes();
+    assertThat(
+            Maps.transformValues(recordedChecksums, checksum -> checksum.map(Checksum::toString)))
+        .containsExactly(
+            server.getUrl() + "/bazel_registry.json",
+            Optional.of(sha256(registryJson).toString()),
+            server.getUrl() + "/modules/foo/1.0/source.json",
+            Optional.of(sha256(sourceJson).toString()));
+
+    registry = registryFactory.createRegistry(server.getUrl(), recordedChecksums);
+    // Test that the recorded hashes are used for repo cache hits even when the server content
+    // changes.
+    server.unserve("/bazel_registry.json");
+    server.unserve("/modules/foo/1.0/source.json");
+    assertThat(registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter))
+        .isEqualTo(
+            RepoSpec.builder()
+                .setRuleClassName("local_repository")
+                .setAttributes(
+                    AttributeValues.create(ImmutableMap.of("path", "/hello/bar/project_x")))
+                .build());
+  }
+
+  @Test
+  public void testGetRepoSpecChecksumMismatch() throws Exception {
+    repositoryCache.setRepositoryCachePath(scratch.dir("cache"));
+
+    String registryJson =
+        """
+        {
+          "module_base_path": "/hello/foo"
+        }
+        """;
+    server.serve("/bazel_registry.json", registryJson.getBytes(UTF_8));
+    String sourceJson =
+        """
+        {
+          "type": "local_path",
+          "path": "../bar/project_x"
+        }
+        """;
+    String maliciousSourceJson = sourceJson.replace("project_x", "malicious");
+    server.serve("/modules/foo/1.0/source.json", maliciousSourceJson.getBytes(UTF_8));
+    server.start();
+
+    var knownFiles =
+        ImmutableMap.of(
+            server.getUrl() + "/bazel_registry.json",
+            Optional.of(sha256(registryJson)),
+            server.getUrl() + "/modules/foo/1.0/source.json",
+            Optional.of(sha256(sourceJson)));
+    Registry registry = registryFactory.createRegistry(server.getUrl(), knownFiles);
+    var e =
+        assertThrows(
+            IOException.class, () -> registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to fetch registry file %s: Checksum was %s but wanted %s"
+                .formatted(
+                    server.getUrl() + "/modules/foo/1.0/source.json",
+                    sha256(maliciousSourceJson),
+                    sha256(sourceJson)));
+  }
+
+  @Test
+  public void testBazelRegistryChecksumMismatch() throws Exception {
+    repositoryCache.setRepositoryCachePath(scratch.dir("cache"));
+
+    String registryJson =
+        """
+        {
+          "module_base_path": "/hello/foo"
+        }
+        """;
+    String maliciousRegistryJson = registryJson.replace("foo", "malicious");
+    server.serve("/bazel_registry.json", maliciousRegistryJson.getBytes(UTF_8));
+    String sourceJson =
+        """
+        {
+          "type": "local_path",
+          "path": "../bar/project_x"
+        }
+        """;
+    server.serve("/modules/foo/1.0/source.json", sourceJson.getBytes(UTF_8));
+    server.start();
+
+    var knownFiles =
+        ImmutableMap.of(
+            server.getUrl() + "/bazel_registry.json",
+            Optional.of(sha256(registryJson)),
+            server.getUrl() + "/modules/foo/1.0/source.json",
+            Optional.of(sha256(sourceJson)));
+    Registry registry = registryFactory.createRegistry(server.getUrl(), knownFiles);
+    var e =
+        assertThrows(
+            IOException.class, () -> registry.getRepoSpec(createModuleKey("foo", "1.0"), reporter));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to fetch registry file %s: Checksum was %s but wanted %s"
+                .formatted(
+                    server.getUrl() + "/bazel_registry.json",
+                    sha256(maliciousRegistryJson),
+                    sha256(registryJson)));
+  }
+
+  private static Checksum sha256(String content) throws Checksum.InvalidChecksumException {
+    return Checksum.fromString(
+        RepositoryCache.KeyType.SHA256, Hashing.sha256().hashString(content, UTF_8).toString());
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java
index fbbdb8e..6607062 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java
@@ -278,7 +278,7 @@
     ModuleFileFunction.IGNORE_DEV_DEPS.set(differencer, false);
     ModuleFileFunction.MODULE_OVERRIDES.set(differencer, ImmutableMap.of());
     YankedVersionsUtil.ALLOWED_YANKED_VERSIONS.set(differencer, ImmutableList.of());
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
     BazelModuleResolutionFunction.CHECK_DIRECT_DEPENDENCIES.set(
         differencer, CheckDirectDepsMode.WARNING);
     BazelModuleResolutionFunction.BAZEL_COMPATIBILITY_MODE.set(
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
index cea99f3..bf4d985 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunctionTest.java
@@ -142,6 +142,7 @@
                             new TimestampGranularityMonitor(BlazeClock.instance())),
                         SyscallCache.NO_CACHE,
                         externalFilesHelper))
+                .put(SkyFunctions.BAZEL_LOCK_FILE, new BazelLockFileFunction(rootDirectory))
                 .put(
                     SkyFunctions.MODULE_FILE,
                     new ModuleFileFunction(
@@ -223,7 +224,7 @@
         "multiple_version_override(module_name='fff',versions=['1.0','2.0'])",
         "archive_override(module_name='ggg',urls=['https://hello.com/world.zip'])");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -271,7 +272,7 @@
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "bazel_dep(name='bbb',version='1.0')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -297,7 +298,7 @@
         "module(name='aaa')",
         "single_version_override(module_name='aaa',version='7')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -318,7 +319,7 @@
         "module(name='aaa')",
         "local_path_override(module_name='bazel_tools',path='./bazel_tools_new')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -352,7 +353,7 @@
         rootDirectory.getRelative("python/toolchains/toolchains.MODULE.bazel").getPathString(),
         "register_toolchains('//:python-whatever')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -389,7 +390,7 @@
         "module(name='aaa')",
         "include('@haha//java:java.MODULE.bazel')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -405,7 +406,7 @@
         "module(name='aaa')",
         "include(':relative.MODULE.bazel')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -421,7 +422,7 @@
         "module(name='aaa')",
         "include('//:MODULE.bazel.segment')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -437,7 +438,7 @@
         "module(name='aaa')",
         "include('//haha/:::.MODULE.bazel')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     EvaluationResult<RootModuleFileValue> result =
@@ -458,7 +459,7 @@
         "module(name='bet-you-didnt-expect-this-didya')",
         "bazel_dep(name='java-foo', version='1.0', repo_name='foo')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     EvaluationResult<RootModuleFileValue> result =
@@ -484,7 +485,7 @@
         rootDirectory.getRelative("python/python.MODULE.bazel").getPathString(),
         "bazel_dep(name='python-foo', version='1.0', repo_name='foo')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     EvaluationResult<RootModuleFileValue> result =
@@ -506,7 +507,7 @@
         rootDirectory.getRelative("java/java.MODULE.bazel").getPathString(),
         "bazel_dep(name=FOO_NAME, version='1.0')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     EvaluationResult<RootModuleFileValue> result =
@@ -519,7 +520,7 @@
   @Test
   public void forgotVersion() throws Exception {
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     SkyKey skyKey = ModuleFileValue.key(createModuleKey("bbb", ""), null);
     EvaluationResult<ModuleFileValue> result =
@@ -547,7 +548,7 @@
                 createModuleKey("bbb", "1.0"),
                 "module(name='bbb',version='1.0');bazel_dep(name='ddd',version='3.0')");
     ModuleFileFunction.REGISTRIES.set(
-        differencer, ImmutableList.of(registry1.getUrl(), registry2.getUrl(), registry3.getUrl()));
+        differencer, ImmutableSet.of(registry1.getUrl(), registry2.getUrl(), registry3.getUrl()));
 
     SkyKey skyKey = ModuleFileValue.key(createModuleKey("bbb", "1.0"), null);
     EvaluationResult<ModuleFileValue> result =
@@ -573,7 +574,7 @@
                 createModuleKey("foo", "1.0"),
                 "module(name='foo',version='1.0')",
                 "include('//java:MODULE.bazel.segment')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -602,7 +603,7 @@
             .addModule(
                 createModuleKey("bbb", "1.0"),
                 "module(name='bbb',version='1.0');bazel_dep(name='ccc',version='3.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     // The version is empty here due to the override.
     SkyKey skyKey =
@@ -653,7 +654,7 @@
             .addModule(
                 createModuleKey("bbb", "1.0"),
                 "module(name='bbb',version='1.0');bazel_dep(name='ccc',version='3.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     // The version is empty here due to the override.
     SkyKey skyKey =
@@ -688,7 +689,7 @@
                 createModuleKey("bbb", "1.0"),
                 "module(name='bbb',version='1.0',compatibility_level=6)",
                 "bazel_dep(name='ccc',version='3.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry1.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry1.getUrl()));
 
     // Override the registry for B to be registry2 (instead of the default registry1).
     SkyKey skyKey =
@@ -731,7 +732,7 @@
                 "maven.dep(coord='junit')",
                 "use_repo(maven, 'junit', 'guava')",
                 "maven.dep(coord='guava')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     ModuleKey myMod = createModuleKey("mymod", "1.0");
     SkyKey skyKey = ModuleFileValue.key(myMod, null);
@@ -873,7 +874,7 @@
         "myext4 = use_extension('//:defs.bzl','myext')",
         "myext4.tag(name = 'tag4')",
         "use_repo(myext4, 'delta')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of());
 
     SkyKey skyKey = ModuleFileValue.KEY_FOR_ROOT_MODULE;
     EvaluationResult<ModuleFileValue> result =
@@ -972,7 +973,7 @@
                 "myext4 = use_extension('//:defs.bzl','myext')",
                 "myext4.tag(name = 'tag4')",
                 "use_repo(myext4, 'delta')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     ModuleKey myMod = createModuleKey("mymod", "1.0");
     SkyKey skyKey = ModuleFileValue.key(myMod, null);
@@ -1039,7 +1040,7 @@
                 "module(name='mymod',version='1.0')",
                 "myext = use_extension('//:defs.bzl','myext')",
                 "use_repo(myext, mymod='some_repo')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     SkyKey skyKey = ModuleFileValue.key(createModuleKey("mymod", "1.0"), null);
     reporter.removeHandler(failFastHandler); // expect failures
@@ -1059,7 +1060,7 @@
                 "module(name='mymod',version='1.0')",
                 "myext = use_extension('//:defs.bzl','myext')",
                 "use_repo(myext, 'some_repo', again='some_repo')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     SkyKey skyKey = ModuleFileValue.key(createModuleKey("mymod", "1.0"), null);
     reporter.removeHandler(failFastHandler); // expect failures
@@ -1078,7 +1079,7 @@
         "http_archive = use_repo_rule('@bazel_tools//:http.bzl','http_archive')",
         "http_archive(name='guava',url='guava.com')",
         "http_archive(name='vuaga',url='vuaga.com',dev_dependency=True)");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of());
 
     SkyKey skyKey = ModuleFileValue.KEY_FOR_ROOT_MODULE;
     EvaluationResult<ModuleFileValue> result =
@@ -1272,7 +1273,7 @@
     scratch.overwriteFile(
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "bazel_dep(name='foo',version='1.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of());
 
     SkyKey skyKey = ModuleFileValue.KEY_FOR_ROOT_MODULE;
     EvaluationResult<RootModuleFileValue> result =
@@ -1308,7 +1309,7 @@
         rootDirectory.getRelative("tools/MODULE.bazel").getPathString(),
         "module(name='bazel_tools',version='1.0')",
         "bazel_dep(name='foo',version='2.0')");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of());
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of());
 
     SkyKey skyKey =
         ModuleFileValue.key(createModuleKey("bazel_tools", ""), builtinModules.get("bazel_tools"));
@@ -1333,7 +1334,7 @@
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "module(name='aaa',version='0.1',repo_name='bbb')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -1357,7 +1358,7 @@
         "module(name='aaa',version='0.1',repo_name='bbb')",
         "bazel_dep(name='bbb',version='1.0')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     evaluator.evaluate(ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
@@ -1372,7 +1373,7 @@
         "module(name='aaa',version='0.1',repo_name='bbb')",
         "module(name='aaa',version='0.1',repo_name='bbb')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     evaluator.evaluate(ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
@@ -1387,7 +1388,7 @@
         "use_extension('//:extensions.bzl', 'my_ext')",
         "module(name='aaa',version='0.1',repo_name='bbb')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     evaluator.evaluate(ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
@@ -1401,7 +1402,7 @@
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "if 3+5>7: module(name='aaa',version='0.1',repo_name='bbb')");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     evaluator.evaluate(ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
@@ -1424,7 +1425,7 @@
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "isolated_ext = use_extension('//:extensions.bzl', 'my_ext', isolate = True)");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     EvaluationResult<RootModuleFileValue> result =
         evaluator.evaluate(
@@ -1451,7 +1452,7 @@
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "use_extension('//:extensions.bzl', 'my_ext', isolate = True)");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     evaluator.evaluate(ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
@@ -1466,7 +1467,7 @@
         rootDirectory.getRelative("MODULE.bazel").getPathString(),
         "use_extension('//:extensions.bzl', 'my_ext', isolate = True)");
     FakeRegistry registry = registryFactory.newFakeRegistry("/foo");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     reporter.removeHandler(failFastHandler); // expect failures
     evaluator.evaluate(ImmutableList.of(ModuleFileValue.KEY_FOR_ROOT_MODULE), evaluationContext);
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryTest.java
index 4c6c632..c56ad70 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/RegistryFactoryTest.java
@@ -43,10 +43,14 @@
             new DownloadManager(new RepositoryCache(), new HttpDownloader()),
             Suppliers.ofInstance(ImmutableMap.of()));
     Throwable exception =
-        assertThrows(URISyntaxException.class, () -> registryFactory.createRegistry("/home/www"));
+        assertThrows(
+            URISyntaxException.class,
+            () -> registryFactory.createRegistry("/home/www", ImmutableMap.of()));
     assertThat(exception).hasMessageThat().contains("Registry URL has no scheme");
     exception =
-        assertThrows(URISyntaxException.class, () -> registryFactory.createRegistry("foo://bar"));
+        assertThrows(
+            URISyntaxException.class,
+            () -> registryFactory.createRegistry("foo://bar", ImmutableMap.of()));
     assertThat(exception).hasMessageThat().contains("Unrecognized registry URL protocol");
   }
 
@@ -61,7 +65,9 @@
     Throwable exception =
         assertThrows(
             URISyntaxException.class,
-            () -> registryFactory.createRegistry("file:c:/path/to/workspace/registry"));
+            () ->
+                registryFactory.createRegistry(
+                    "file:c:/path/to/workspace/registry", ImmutableMap.of()));
     assertThat(exception).hasMessageThat().contains("Registry URL path is not valid");
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TestHttpServer.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TestHttpServer.java
index 629e4be..8a7628f 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TestHttpServer.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/TestHttpServer.java
@@ -78,6 +78,10 @@
     serve(path, JOINER.join(lines).getBytes(UTF_8));
   }
 
+  public void unserve(String path) {
+    server.removeContext(path);
+  }
+
   public String getUrl() throws MalformedURLException {
     return new URL("http", "[::1]", server.getAddress().getPort(), "").toString();
   }
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloaderTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloaderTest.java
index 0e59d41..2a47760 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloaderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpDownloaderTest.java
@@ -26,6 +26,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
 import com.google.devtools.build.lib.authandtls.StaticCredentials;
 import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
@@ -516,6 +517,7 @@
                   httpDownloader.downloadAndReadOneUrl(
                       new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
                       StaticCredentials.EMPTY,
+                      Optional.empty(),
                       eventHandler,
                       Collections.emptyMap()),
                   UTF_8))
@@ -551,12 +553,91 @@
               httpDownloader.downloadAndReadOneUrl(
                   new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
                   StaticCredentials.EMPTY,
+                  Optional.empty(),
                   eventHandler,
                   Collections.emptyMap()));
     }
   }
 
   @Test
+  public void downloadAndReadOneUrl_checksumProvided()
+      throws IOException, Checksum.InvalidChecksumException, InterruptedException {
+    try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
+      @SuppressWarnings("unused")
+      Future<?> possiblyIgnoredError =
+          executor.submit(
+              () -> {
+                try (Socket socket = server.accept()) {
+                  readHttpRequest(socket.getInputStream());
+                  sendLines(
+                      socket,
+                      "HTTP/1.1 200 OK",
+                      "Date: Fri, 31 Dec 1999 23:59:59 GMT",
+                      "Connection: close",
+                      "Content-Type: text/plain",
+                      "Content-Length: 5",
+                      "",
+                      "hello");
+                }
+                return null;
+              });
+
+      assertThat(
+              new String(
+                  httpDownloader.downloadAndReadOneUrl(
+                      new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
+                      StaticCredentials.EMPTY,
+                      Optional.of(
+                          Checksum.fromString(
+                              RepositoryCache.KeyType.SHA256,
+                              Hashing.sha256().hashString("hello", UTF_8).toString())),
+                      eventHandler,
+                      ImmutableMap.of()),
+                  UTF_8))
+          .isEqualTo("hello");
+    }
+  }
+
+  @Test
+  public void downloadAndReadOneUrl_checksumMismatch() throws IOException {
+    try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getByName(null))) {
+      @SuppressWarnings("unused")
+      Future<?> possiblyIgnoredError =
+          executor.submit(
+              () -> {
+                try (Socket socket = server.accept()) {
+                  readHttpRequest(socket.getInputStream());
+                  sendLines(
+                      socket,
+                      "HTTP/1.1 200 OK",
+                      "Date: Fri, 31 Dec 1999 23:59:59 GMT",
+                      "Connection: close",
+                      "Content-Type: text/plain",
+                      "Content-Length: 9",
+                      "",
+                      "malicious");
+                }
+                return null;
+              });
+
+      var e =
+          assertThrows(
+              UnrecoverableHttpException.class,
+              () ->
+                  httpDownloader.downloadAndReadOneUrl(
+                      new URL(String.format("http://localhost:%d/foo", server.getLocalPort())),
+                      StaticCredentials.EMPTY,
+                      Optional.of(
+                          Checksum.fromString(
+                              RepositoryCache.KeyType.SHA256,
+                              Hashing.sha256().hashUnencodedChars("hello").toString())),
+                      eventHandler,
+                      ImmutableMap.of()));
+      assertThat(e).hasMessageThat().contains("Checksum was");
+    }
+  }
+
+  @Test
   public void download_contentLengthMismatch_propagateErrorIfNotRetry() throws Exception {
     Downloader downloader = mock(Downloader.class);
     DownloadManager downloadManager = new DownloadManager(repositoryCache, downloader);
diff --git a/src/test/java/com/google/devtools/build/lib/query2/testutil/SkyframeQueryHelper.java b/src/test/java/com/google/devtools/build/lib/query2/testutil/SkyframeQueryHelper.java
index af85d6a..8f60ae2 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/testutil/SkyframeQueryHelper.java
+++ b/src/test/java/com/google/devtools/build/lib/query2/testutil/SkyframeQueryHelper.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.actions.ActionKeyContext;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
@@ -380,7 +381,7 @@
                     PrecomputedValue.injected(
                         ModuleFileFunction.MODULE_OVERRIDES, ImmutableMap.of()),
                     PrecomputedValue.injected(
-                        ModuleFileFunction.REGISTRIES, ImmutableList.of(registry.getUrl())),
+                        ModuleFileFunction.REGISTRIES, ImmutableSet.of(registry.getUrl())),
                     PrecomputedValue.injected(ModuleFileFunction.IGNORE_DEV_DEPS, false),
                     PrecomputedValue.injected(
                         BazelModuleResolutionFunction.CHECK_DIRECT_DEPENDENCIES,
@@ -417,7 +418,7 @@
             PrecomputedValue.injected(
                 RepositoryDelegatorFunction.VENDOR_DIRECTORY, Optional.empty()),
             PrecomputedValue.injected(
-                ModuleFileFunction.REGISTRIES, ImmutableList.of(registry.getUrl())),
+                ModuleFileFunction.REGISTRIES, ImmutableSet.of(registry.getUrl())),
             PrecomputedValue.injected(ModuleFileFunction.IGNORE_DEV_DEPS, false),
             PrecomputedValue.injected(RepositoryDelegatorFunction.DISABLE_NATIVE_REPO_RULES, false),
             PrecomputedValue.injected(
diff --git a/src/test/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorTest.java b/src/test/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorTest.java
index 943afea..0c88fc5 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorTest.java
@@ -175,7 +175,7 @@
             .addModule(
                 createModuleKey("bazel_tools", "1.0"),
                 "module(name='bazel_tools', version='1.0');");
-    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableList.of(registry.getUrl()));
+    ModuleFileFunction.REGISTRIES.set(differencer, ImmutableSet.of(registry.getUrl()));
 
     HashFunction hashFunction = fileSystem.getDigestFunction().getHashFunction();
     evaluator =
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunctionTest.java
index 4d64bb2..b7da221 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunctionTest.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.eventbus.EventBus;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.bazel.bzlmod.BazelLockFileFunction;
@@ -268,7 +269,7 @@
     registry = FakeRegistry.DEFAULT_FACTORY.newFakeRegistry(moduleRoot.getPathString());
     return ImmutableList.of(
         PrecomputedValue.injected(
-            ModuleFileFunction.REGISTRIES, ImmutableList.of(registry.getUrl())),
+            ModuleFileFunction.REGISTRIES, ImmutableSet.of(registry.getUrl())),
         PrecomputedValue.injected(ModuleFileFunction.IGNORE_DEV_DEPS, false),
         PrecomputedValue.injected(
             BazelModuleResolutionFunction.CHECK_DIRECT_DEPENDENCIES, CheckDirectDepsMode.WARNING),
diff --git a/src/test/py/bazel/bzlmod/bazel_lockfile_test.py b/src/test/py/bazel/bzlmod/bazel_lockfile_test.py
index fb04b52..6118fb8 100644
--- a/src/test/py/bazel/bzlmod/bazel_lockfile_test.py
+++ b/src/test/py/bazel/bzlmod/bazel_lockfile_test.py
@@ -133,26 +133,68 @@
     # hence find no errors
     self.RunBazel(['build', '--nobuild', '//:all'])
 
-  def testChangeFlagWithLockfile(self):
-    # Add module 'sss' to the registry with dep on 'aaa'
-    self.main_registry.createCcModule('sss', '1.3', {'aaa': '1.1'})
-    # Create a project with deps on 'sss'
     self.ScratchFile(
         'MODULE.bazel',
         [
             'bazel_dep(name = "sss", version = "1.3")',
+            'bazel_dep(name = "bbb", version = "1.1")',
+        ],
+    )
+    # Shutdown bazel to empty any cache of the deps tree
+    self.RunBazel(['shutdown'])
+    # Even adding a new dependency should not fail due to the registry change
+    self.RunBazel(['build', '--nobuild', '//:all'])
+
+  def testAddModuleToRegistryWithLockfile(self):
+    # Create a project with deps on the BCR's 'platforms' module
+    self.ScratchFile(
+        'MODULE.bazel',
+        [
+            'bazel_dep(name = "platforms", version = "0.0.9")',
         ],
     )
     self.ScratchFile('BUILD', ['filegroup(name = "hello")'])
     self.RunBazel(['build', '--nobuild', '//:all'])
 
-    # Change registry -> update 'sss' module file (corrupt it)
-    module_dir = self.main_registry.root.joinpath('modules', 'sss', '1.3')
+    # Add a broken 'platforms' module to the first registry
+    module_dir = self.main_registry.root.joinpath(
+        'modules', 'platforms', '0.0.9'
+    )
     scratchFile(module_dir.joinpath('MODULE.bazel'), ['whatever!'])
 
     # Shutdown bazel to empty any cache of the deps tree
     self.RunBazel(['shutdown'])
-    # Running with the lockfile, but adding a flag should cause resolution rerun
+    # Running with the lockfile, should not recognize the registry changes
+    # hence find no errors
+    self.RunBazel(['build', '--nobuild', '//:all'])
+
+    self.ScratchFile(
+        'MODULE.bazel',
+        [
+            'bazel_dep(name = "platforms", version = "0.0.9")',
+            'bazel_dep(name = "bbb", version = "1.1")',
+        ],
+    )
+    # Shutdown bazel to empty any cache of the deps tree
+    self.RunBazel(['shutdown'])
+    # Even adding a new dependency should not fail due to the registry change
+    self.RunBazel(['build', '--nobuild', '//:all'])
+
+  def testChangeFlagWithLockfile(self):
+    # Create a project with an outdated direct dep on aaa
+    self.ScratchFile(
+        'MODULE.bazel',
+        [
+            'bazel_dep(name = "aaa", version = "1.0")',
+            'bazel_dep(name = "bbb", version = "1.1")',
+        ],
+    )
+    self.ScratchFile('BUILD', ['filegroup(name = "hello")'])
+    self.RunBazel(['build', '--nobuild', '//:all'])
+
+    # Shutdown bazel to empty any cache of the deps tree
+    self.RunBazel(['shutdown'])
+    # Running with the lockfile, but the changed flag value should be honored
     exit_code, _, stderr = self.RunBazel(
         [
             'build',
@@ -163,9 +205,10 @@
         allow_failure=True,
     )
     self.AssertExitCode(exit_code, 48, stderr)
-    self.assertRegex(
+    self.assertIn(
+        "ERROR: For repository 'aaa', the root module requires module version"
+        ' aaa@1.0, but got aaa@1.1',
         '\n'.join(stderr),
-        "ERROR: .*/sss/1.3/MODULE.bazel:1:9: invalid character: '!'",
     )
 
   def testLockfileErrorMode(self):
@@ -1104,6 +1147,62 @@
 
     self.assertEqual(old_data, new_data)
 
+  def testLockfileExtensionsUpdatedIncrementally(self):
+    self.ScratchFile(
+        'MODULE.bazel',
+        [
+            'lockfile_ext1 = use_extension("extension.bzl", "lockfile_ext1")',
+            'use_repo(lockfile_ext1, "hello1")',
+            'lockfile_ext2 = use_extension("extension.bzl", "lockfile_ext2")',
+            'use_repo(lockfile_ext2, "hello2")',
+        ],
+    )
+    self.ScratchFile('BUILD.bazel')
+    self.ScratchFile(
+        'extension.bzl',
+        [
+            'def _repo_rule_impl(ctx):',
+            '    ctx.file("BUILD", "filegroup(name=\'lala\')")',
+            '',
+            'repo_rule = repository_rule(implementation=_repo_rule_impl)',
+            '',
+            'def _module_ext1_impl(ctx):',
+            '    print("Hello from ext1!")',
+            '    repo_rule(name="hello1")',
+            '',
+            'lockfile_ext1 = module_extension(',
+            '    implementation=_module_ext1_impl,',
+            ')',
+            '',
+            'def _module_ext2_impl(ctx):',
+            '    print("Hello from ext2!")',
+            '    repo_rule(name="hello2")',
+            '',
+            'lockfile_ext2 = module_extension(',
+            '    implementation=_module_ext2_impl,',
+            ')',
+        ],
+    )
+
+    _, _, stderr = self.RunBazel(['build', '@hello1//:all'])
+    stderr = '\n'.join(stderr)
+    self.assertIn('Hello from ext1!', stderr)
+    self.assertNotIn('Hello from ext2!', stderr)
+
+    self.RunBazel(['shutdown'])
+
+    _, _, stderr = self.RunBazel(['build', '@hello1//:all', '@hello2//:all'])
+    stderr = '\n'.join(stderr)
+    self.assertNotIn('Hello from ext1!', stderr)
+    self.assertIn('Hello from ext2!', stderr)
+
+    self.RunBazel(['shutdown'])
+
+    _, _, stderr = self.RunBazel(['build', '@hello1//:all', '@hello2//:all'])
+    stderr = '\n'.join(stderr)
+    self.assertNotIn('Hello from ext1!', stderr)
+    self.assertNotIn('Hello from ext2!', stderr)
+
   def testExtensionOsAndArch(self):
     self.ScratchFile(
         'MODULE.bazel',
diff --git a/src/test/shell/bazel/starlark_repository_test.sh b/src/test/shell/bazel/starlark_repository_test.sh
index a53803f..5742bb5 100755
--- a/src/test/shell/bazel/starlark_repository_test.sh
+++ b/src/test/shell/bazel/starlark_repository_test.sh
@@ -2327,6 +2327,9 @@
 )
 EOF
 
+  # Ensure that all Bzlmod-related downloads have happened before disabling
+  # downloads.
+  bazel mod deps || fail "Failed to cache Bazel modules"
   bazel build --repository_disable_download //:it > "${TEST_log}" 2>&1 \
       && fail "Expected failure" || :
   expect_log "Failed to download repository @.*: download is disabled"
diff --git a/src/test/tools/bzlmod/MODULE.bazel.lock b/src/test/tools/bzlmod/MODULE.bazel.lock
index 5f745e0..43fb80e 100644
--- a/src/test/tools/bzlmod/MODULE.bazel.lock
+++ b/src/test/tools/bzlmod/MODULE.bazel.lock
@@ -1074,6 +1074,63 @@
       }
     }
   },
+  "registryFileHashes": {
+    "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
+    "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
+    "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
+    "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/source.json": "7e3a9adf473e9af076ae485ed649d5641ad50ec5c11718103f34de03170d94ad",
+    "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef",
+    "https://bcr.bazel.build/modules/apple_support/1.5.0/source.json": "eb98a7627c0bc486b57f598ad8da50f6625d974c8f723e9ea71bd39f709c9862",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/source.json": "2e0e90f6788740b72f0db3c19c46155a82ec01cfc1527c35b58f3f8f0180da29",
+    "https://bcr.bazel.build/modules/buildozer/7.1.1.1/MODULE.bazel": "21c6a7d08e3171d3e13b003407caefe7ebe007693e217a053cc1f49f008ce010",
+    "https://bcr.bazel.build/modules/buildozer/7.1.1.1/source.json": "a9ced884dedcf1c45d11052d53d854e368b05aa8fbbf0f983037fbed4d3ea4c6",
+    "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
+    "https://bcr.bazel.build/modules/googletest/1.11.0/source.json": "c73d9ef4268c91bd0c1cd88f1f9dfa08e814b1dbe89b5f594a9f08ba0244d206",
+    "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
+    "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
+    "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
+    "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
+    "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
+    "https://bcr.bazel.build/modules/platforms/0.0.9/source.json": "cd74d854bf16a9e002fb2ca7b1a421f4403cda29f824a765acd3a8c56f8d43e6",
+    "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
+    "https://bcr.bazel.build/modules/protobuf/21.7/source.json": "bbe500720421e582ff2d18b0802464205138c06056f443184de39fbb8187b09b",
+    "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
+    "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430",
+    "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
+    "https://bcr.bazel.build/modules/rules_java/7.5.0/MODULE.bazel": "b329bf9aa07a58bd1ccb37bfdcd9528acf6f12712efb38c3a8553c2cc2494806",
+    "https://bcr.bazel.build/modules/rules_java/7.5.0/source.json": "72762e4e144dd1bc54e18b90be52e31a4ca9cf11d7358a06e1b87b74e839e9ad",
+    "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
+    "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/source.json": "a075731e1b46bc8425098512d038d416e966ab19684a10a34f4741295642fc35",
+    "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
+    "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
+    "https://bcr.bazel.build/modules/rules_license/0.0.7/source.json": "355cc5737a0f294e560d52b1b7a6492d4fff2caf0bef1a315df5a298fca2d34a",
+    "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
+    "https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c",
+    "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
+    "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
+    "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9",
+    "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
+    "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7",
+    "https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014",
+    "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
+    "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
+    "https://bcr.bazel.build/modules/stardoc/0.5.1/source.json": "a96f95e02123320aa015b956f29c00cb818fa891ef823d55148e1a362caacf29",
+    "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
+    "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/source.json": "f1ef7d3f9e0e26d4b23d1c39b5f5de71f584dd7d1b4ef83d9bbba6ec7a6a6459",
+    "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
+    "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
+    "https://bcr.bazel.build/modules/zlib/1.3/MODULE.bazel": "6a9c02f19a24dcedb05572b2381446e27c272cd383aed11d41d99da9e3167a72",
+    "https://bcr.bazel.build/modules/zlib/1.3/source.json": "b6b43d0737af846022636e6e255fd4a96fee0d34f08f3830e6e0bac51465c37c"
+  },
   "moduleExtensions": {
     "@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
       "general": {