about summary refs log tree commit diff
path: root/src/libstore
diff options
context:
space:
mode:
Diffstat (limited to 'src/libstore')
-rw-r--r--src/libstore/binary-cache-store.cc6
-rw-r--r--src/libstore/binary-cache-store.hh11
-rw-r--r--src/libstore/build.cc103
-rw-r--r--src/libstore/crypto.cc25
-rw-r--r--src/libstore/crypto.hh2
-rw-r--r--src/libstore/download.cc23
-rw-r--r--src/libstore/gc.cc31
-rw-r--r--src/libstore/globals.cc3
-rw-r--r--src/libstore/globals.hh3
-rw-r--r--src/libstore/http-binary-cache-store.cc12
-rw-r--r--src/libstore/local-fs-store.cc8
-rw-r--r--src/libstore/local-store.cc731
-rw-r--r--src/libstore/local-store.hh167
-rw-r--r--src/libstore/nar-info.cc24
-rw-r--r--src/libstore/nar-info.hh14
-rw-r--r--src/libstore/remote-store.cc49
-rw-r--r--src/libstore/remote-store.hh6
-rw-r--r--src/libstore/schema.sql9
-rw-r--r--src/libstore/sqlite.cc167
-rw-r--r--src/libstore/sqlite.hh102
-rw-r--r--src/libstore/store-api.cc42
-rw-r--r--src/libstore/store-api.hh38
-rw-r--r--src/libstore/worker-protocol.hh3
23 files changed, 788 insertions, 791 deletions
diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc
index e6b68d486b47..473a0b2614bb 100644
--- a/src/libstore/binary-cache-store.cc
+++ b/src/libstore/binary-cache-store.cc
@@ -156,7 +156,7 @@ bool BinaryCacheStore::isValidPath(const Path & storePath)
     return fileExists(narInfoFileFor(storePath));
 }
 
-void BinaryCacheStore::dumpPath(const Path & storePath, Sink & sink)
+void BinaryCacheStore::narFromPath(const Path & storePath, Sink & sink)
 {
     auto res = readNarInfo(storePath);
 
@@ -189,7 +189,7 @@ void BinaryCacheStore::exportPath(const Path & storePath, bool sign, Sink & sink
 
     auto res = readNarInfo(storePath);
 
-    dumpPath(storePath, sink);
+    narFromPath(storePath, sink);
 
     // FIXME: check integrity of NAR.
 
@@ -271,7 +271,7 @@ Path BinaryCacheStore::addToStore(const string & name, const Path & srcPath,
     StringSink sink;
     Hash h;
     if (recursive) {
-        nix::dumpPath(srcPath, sink, filter);
+        dumpPath(srcPath, sink, filter);
         h = hashString(hashAlgo, *sink.s);
     } else {
         auto s = readFile(srcPath);
diff --git a/src/libstore/binary-cache-store.hh b/src/libstore/binary-cache-store.hh
index f14b4859b04e..9e7b0ad9a384 100644
--- a/src/libstore/binary-cache-store.hh
+++ b/src/libstore/binary-cache-store.hh
@@ -124,7 +124,7 @@ public:
     Path addTextToStore(const string & name, const string & s,
         const PathSet & references, bool repair = false) override;
 
-    void dumpPath(const Path & path, Sink & sink) override;
+    void narFromPath(const Path & path, Sink & sink) override;
 
     void exportPath(const Path & path, bool sign, Sink & sink) override;
 
@@ -156,12 +156,6 @@ public:
     void collectGarbage(const GCOptions & options, GCResults & results) override
     { notImpl(); }
 
-    PathSet queryFailedPaths() override
-    { return {}; }
-
-    void clearFailedPaths(const PathSet & paths) override
-    { }
-
     void optimiseStore() override
     { }
 
@@ -170,6 +164,9 @@ public:
 
     ref<FSAccessor> getFSAccessor() override;
 
+    void addSignatures(const Path & storePath, const StringSet & sigs)
+    { notImpl(); }
+
 };
 
 }
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index ed4e0f659da3..ba3f3a371d8c 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -22,6 +22,7 @@
 #include <sys/stat.h>
 #include <sys/utsname.h>
 #include <sys/select.h>
+#include <sys/resource.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <errno.h>
@@ -239,6 +240,9 @@ private:
     /* Last time the goals in `waitingForAWhile' where woken up. */
     time_t lastWokenUp;
 
+    /* Cache for pathContentsGood(). */
+    std::map<Path, bool> pathContentsGoodCache;
+
 public:
 
     /* Set if at least one derivation had a BuildError (i.e. permanent
@@ -304,6 +308,12 @@ public:
     void waitForInput();
 
     unsigned int exitStatus();
+
+    /* Check whether the given valid path exists and has the right
+       contents. */
+    bool pathContentsGood(const Path & path);
+
+    void markContentsGood(const Path & path);
 };
 
 
@@ -1038,11 +1048,6 @@ void DerivationGoal::haveDerivation()
         return;
     }
 
-    /* Check whether any output previously failed to build.  If so,
-       don't bother. */
-    for (auto & i : invalidOutputs)
-        if (pathFailed(i)) return;
-
     /* Reject doing a hash build of anything other than a fixed-output
        derivation. */
     if (buildMode == bmHash) {
@@ -1159,7 +1164,7 @@ void DerivationGoal::repairClosure()
     /* Check each path (slow!). */
     PathSet broken;
     for (auto & i : outputClosure) {
-        if (worker.store.pathContentsGood(i)) continue;
+        if (worker.pathContentsGood(i)) continue;
         printMsg(lvlError, format("found corrupted or missing path ‘%1%’ in the output closure of ‘%2%’") % i % drvPath);
         Path drvPath2 = outputsToDrv[i];
         if (drvPath2 == "")
@@ -1313,12 +1318,6 @@ void DerivationGoal::tryToBuild()
         deletePath(path);
     }
 
-    /* Check again whether any output previously failed to build,
-       because some other process may have tried and failed before we
-       acquired the lock. */
-    for (auto & i : drv->outputs)
-        if (pathFailed(i.second.path)) return;
-
     /* Don't do a remote build if the derivation has the attribute
        `preferLocalBuild' set.  Also, check and repair modes are only
        supported for local builds. */
@@ -1540,17 +1539,6 @@ void DerivationGoal::buildDone()
                 statusOk(status) ? BuildResult::OutputRejected :
                 fixedOutput || diskFull ? BuildResult::TransientFailure :
                 BuildResult::PermanentFailure;
-
-            /* Register the outputs of this build as "failed" so we
-               won't try to build them again (negative caching).
-               However, don't do this for fixed-output derivations,
-               since they're likely to fail for transient reasons
-               (e.g., fetchurl not being able to access the network).
-               Hook errors (like communication problems with the
-               remote machine) shouldn't be cached either. */
-            if (settings.cacheFailure && !fixedOutput && !diskFull)
-                for (auto & i : drv->outputs)
-                    worker.store.registerFailedPath(i.second.path);
         }
 
         done(st, e.msg());
@@ -2386,6 +2374,12 @@ void DerivationGoal::runChild()
         if (cur != -1) personality(cur | ADDR_NO_RANDOMIZE);
 #endif
 
+        /* Disable core dumps by default. */
+        struct rlimit limit = { 0, RLIM_INFINITY };
+        setrlimit(RLIMIT_CORE, &limit);
+
+        // FIXME: set other limits to deterministic values?
+
         /* Fill in the environment. */
         Strings envStrs;
         for (auto & i : env)
@@ -2743,6 +2737,15 @@ void DerivationGoal::registerOutputs()
                     throw Error(format("derivation ‘%1%’ may not be deterministic: output ‘%2%’ differs")
                         % drvPath % path);
             }
+
+            /* Since we verified the build, it's now ultimately
+               trusted. */
+            if (!info.ultimate) {
+                info.ultimate = true;
+                worker.store.signPathInfo(info);
+                worker.store.registerValidPaths({info});
+            }
+
             continue;
         }
 
@@ -2790,7 +2793,7 @@ void DerivationGoal::registerOutputs()
         if (curRound == nrRounds) {
             worker.store.optimisePath(path); // FIXME: combine with scanForReferences()
 
-            worker.store.markContentsGood(path);
+            worker.markContentsGood(path);
         }
 
         ValidPathInfo info;
@@ -2799,6 +2802,9 @@ void DerivationGoal::registerOutputs()
         info.narSize = hash.second;
         info.references = references;
         info.deriver = drvPath;
+        info.ultimate = true;
+        worker.store.signPathInfo(info);
+
         infos.push_back(info);
     }
 
@@ -2965,30 +2971,13 @@ PathSet DerivationGoal::checkPathValidity(bool returnValid, bool checkHash)
         if (!wantOutput(i.first, wantedOutputs)) continue;
         bool good =
             worker.store.isValidPath(i.second.path) &&
-            (!checkHash || worker.store.pathContentsGood(i.second.path));
+            (!checkHash || worker.pathContentsGood(i.second.path));
         if (good == returnValid) result.insert(i.second.path);
     }
     return result;
 }
 
 
-bool DerivationGoal::pathFailed(const Path & path)
-{
-    if (!settings.cacheFailure) return false;
-
-    if (!worker.store.hasPathFailed(path)) return false;
-
-    printMsg(lvlError, format("builder for ‘%1%’ failed previously (cached)") % path);
-
-    if (settings.printBuildTrace)
-        printMsg(lvlError, format("@ build-failed %1% - cached") % drvPath);
-
-    done(BuildResult::CachedFailure);
-
-    return true;
-}
-
-
 Path DerivationGoal::addHashRewrite(const Path & path)
 {
     string h1 = string(path, settings.nixStore.size() + 1, 32);
@@ -3010,7 +2999,7 @@ void DerivationGoal::done(BuildResult::Status status, const string & msg)
     amDone(result.success() ? ecSuccess : ecFailed);
     if (result.status == BuildResult::TimedOut)
         worker.timedOut = true;
-    if (result.status == BuildResult::PermanentFailure || result.status == BuildResult::CachedFailure)
+    if (result.status == BuildResult::PermanentFailure)
         worker.permanentFailure = true;
 }
 
@@ -3373,7 +3362,7 @@ void SubstitutionGoal::finished()
     outputLock->setDeletion(true);
     outputLock.reset();
 
-    worker.store.markContentsGood(storePath);
+    worker.markContentsGood(storePath);
 
     printMsg(lvlChatty,
         format("substitution of path ‘%1%’ succeeded") % storePath);
@@ -3773,6 +3762,32 @@ unsigned int Worker::exitStatus()
 }
 
 
+bool Worker::pathContentsGood(const Path & path)
+{
+    std::map<Path, bool>::iterator i = pathContentsGoodCache.find(path);
+    if (i != pathContentsGoodCache.end()) return i->second;
+    printMsg(lvlInfo, format("checking path ‘%1%’...") % path);
+    ValidPathInfo info = store.queryPathInfo(path);
+    bool res;
+    if (!pathExists(path))
+        res = false;
+    else {
+        HashResult current = hashPath(info.narHash.type, path);
+        Hash nullHash(htSHA256);
+        res = info.narHash == nullHash || info.narHash == current.first;
+    }
+    pathContentsGoodCache[path] = res;
+    if (!res) printMsg(lvlError, format("path ‘%1%’ is corrupted or missing!") % path);
+    return res;
+}
+
+
+void Worker::markContentsGood(const Path & path)
+{
+    pathContentsGoodCache[path] = true;
+}
+
+
 //////////////////////////////////////////////////////////////////////
 
 
diff --git a/src/libstore/crypto.cc b/src/libstore/crypto.cc
index caba22c1e274..747483afb30b 100644
--- a/src/libstore/crypto.cc
+++ b/src/libstore/crypto.cc
@@ -1,5 +1,6 @@
 #include "crypto.hh"
 #include "util.hh"
+#include "globals.hh"
 
 #if HAVE_SODIUM
 #include <sodium.h>
@@ -98,4 +99,28 @@ bool verifyDetached(const std::string & data, const std::string & sig,
 #endif
 }
 
+PublicKeys getDefaultPublicKeys()
+{
+    PublicKeys publicKeys;
+
+    // FIXME: filter duplicates
+
+    for (auto s : settings.get("binary-cache-public-keys", Strings())) {
+        PublicKey key(s);
+        publicKeys.emplace(key.name, key);
+    }
+
+    for (auto secretKeyFile : settings.get("secret-key-files", Strings())) {
+        try {
+            SecretKey secretKey(readFile(secretKeyFile));
+            publicKeys.emplace(secretKey.name, secretKey.toPublicKey());
+        } catch (SysError & e) {
+            /* Ignore unreadable key files. That's normal in a
+               multi-user installation. */
+        }
+    }
+
+    return publicKeys;
+}
+
 }
diff --git a/src/libstore/crypto.hh b/src/libstore/crypto.hh
index 38d5fe2a80c6..9110af3aa9e5 100644
--- a/src/libstore/crypto.hh
+++ b/src/libstore/crypto.hh
@@ -49,4 +49,6 @@ typedef std::map<std::string, PublicKey> PublicKeys;
 bool verifyDetached(const std::string & data, const std::string & sig,
     const PublicKeys & publicKeys);
 
+PublicKeys getDefaultPublicKeys();
+
 }
diff --git a/src/libstore/download.cc b/src/libstore/download.cc
index 4776d0091685..7277751b48e7 100644
--- a/src/libstore/download.cc
+++ b/src/libstore/download.cc
@@ -18,6 +18,14 @@ double getTime()
     return tv.tv_sec + (tv.tv_usec / 1000000.0);
 }
 
+std::string resolveUri(const std::string & uri)
+{
+    if (uri.compare(0, 8, "channel:") == 0)
+        return "https://nixos.org/channels/" + std::string(uri, 8) + "/nixexprs.tar.xz";
+    else
+        return uri;
+}
+
 struct CurlDownloader : public Downloader
 {
     CURL * curl;
@@ -179,11 +187,9 @@ struct CurlDownloader : public Downloader
         if (res == CURLE_WRITE_ERROR && etag == options.expectedETag) return false;
 
         long httpStatus = -1;
-        if (res == CURLE_HTTP_RETURNED_ERROR)
-            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);
+        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);
 
         if (res != CURLE_OK) {
-            long httpStatus = 0;
             Error err =
                 httpStatus == 404 ? NotFound :
                 httpStatus == 403 ? Forbidden : Misc;
@@ -199,7 +205,7 @@ struct CurlDownloader : public Downloader
     DownloadResult download(string url, const DownloadOptions & options) override
     {
         DownloadResult res;
-        if (fetch(url, options)) {
+        if (fetch(resolveUri(url), options)) {
             res.cached = false;
             res.data = data;
         } else
@@ -209,15 +215,15 @@ struct CurlDownloader : public Downloader
     }
 };
 
-
 ref<Downloader> makeDownloader()
 {
     return make_ref<CurlDownloader>();
 }
 
-
-Path Downloader::downloadCached(ref<Store> store, const string & url, bool unpack)
+Path Downloader::downloadCached(ref<Store> store, const string & url_, bool unpack)
 {
+    auto url = resolveUri(url_);
+
     Path cacheDir = getEnv("XDG_CACHE_HOME", getEnv("HOME", "") + "/.cache") + "/nix/tarballs";
     createDirs(cacheDir);
 
@@ -302,10 +308,11 @@ Path Downloader::downloadCached(ref<Store> store, const string & url, bool unpac
 
 bool isUri(const string & s)
 {
+    if (s.compare(0, 8, "channel:") == 0) return true;
     size_t pos = s.find("://");
     if (pos == string::npos) return false;
     string scheme(s, 0, pos);
-    return scheme == "http" || scheme == "https" || scheme == "file";
+    return scheme == "http" || scheme == "https" || scheme == "file" || scheme == "channel";
 }
 
 
diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc
index e082f67143a3..52afa1b14e03 100644
--- a/src/libstore/gc.cc
+++ b/src/libstore/gc.cc
@@ -147,35 +147,36 @@ Path Store::addPermRoot(const Path & _storePath,
 
 void LocalStore::addTempRoot(const Path & path)
 {
+    auto state(_state.lock());
+
     /* Create the temporary roots file for this process. */
-    if (fdTempRoots == -1) {
+    if (state->fdTempRoots == -1) {
 
         while (1) {
             Path dir = (format("%1%/%2%") % settings.nixStateDir % tempRootsDir).str();
             createDirs(dir);
 
-            fnTempRoots = (format("%1%/%2%")
-                % dir % getpid()).str();
+            state->fnTempRoots = (format("%1%/%2%") % dir % getpid()).str();
 
             AutoCloseFD fdGCLock = openGCLock(ltRead);
 
-            if (pathExists(fnTempRoots))
+            if (pathExists(state->fnTempRoots))
                 /* It *must* be stale, since there can be no two
                    processes with the same pid. */
-                unlink(fnTempRoots.c_str());
+                unlink(state->fnTempRoots.c_str());
 
-            fdTempRoots = openLockFile(fnTempRoots, true);
+            state->fdTempRoots = openLockFile(state->fnTempRoots, true);
 
             fdGCLock.close();
 
-            debug(format("acquiring read lock on ‘%1%’") % fnTempRoots);
-            lockFile(fdTempRoots, ltRead, true);
+            debug(format("acquiring read lock on ‘%1%’") % state->fnTempRoots);
+            lockFile(state->fdTempRoots, ltRead, true);
 
             /* Check whether the garbage collector didn't get in our
                way. */
             struct stat st;
-            if (fstat(fdTempRoots, &st) == -1)
-                throw SysError(format("statting ‘%1%’") % fnTempRoots);
+            if (fstat(state->fdTempRoots, &st) == -1)
+                throw SysError(format("statting ‘%1%’") % state->fnTempRoots);
             if (st.st_size == 0) break;
 
             /* The garbage collector deleted this file before we could
@@ -187,15 +188,15 @@ void LocalStore::addTempRoot(const Path & path)
 
     /* Upgrade the lock to a write lock.  This will cause us to block
        if the garbage collector is holding our lock. */
-    debug(format("acquiring write lock on ‘%1%’") % fnTempRoots);
-    lockFile(fdTempRoots, ltWrite, true);
+    debug(format("acquiring write lock on ‘%1%’") % state->fnTempRoots);
+    lockFile(state->fdTempRoots, ltWrite, true);
 
     string s = path + '\0';
-    writeFull(fdTempRoots, s);
+    writeFull(state->fdTempRoots, s);
 
     /* Downgrade to a read lock. */
-    debug(format("downgrading to read lock on ‘%1%’") % fnTempRoots);
-    lockFile(fdTempRoots, ltRead, true);
+    debug(format("downgrading to read lock on ‘%1%’") % state->fnTempRoots);
+    lockFile(state->fdTempRoots, ltRead, true);
 }
 
 
diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc
index e704837e8798..b6078253319f 100644
--- a/src/libstore/globals.cc
+++ b/src/libstore/globals.cc
@@ -52,7 +52,6 @@ Settings::Settings()
     keepLog = true;
     compressLog = true;
     maxLogSize = 0;
-    cacheFailure = false;
     pollInterval = 5;
     checkRootReachability = false;
     gcKeepOutputs = false;
@@ -175,7 +174,6 @@ void Settings::update()
     _get(keepLog, "build-keep-log");
     _get(compressLog, "build-compress-log");
     _get(maxLogSize, "build-max-log-size");
-    _get(cacheFailure, "build-cache-failure");
     _get(pollInterval, "build-poll-interval");
     _get(checkRootReachability, "gc-check-reachability");
     _get(gcKeepOutputs, "gc-keep-outputs");
@@ -196,7 +194,6 @@ void Settings::update()
         if (getEnv("NIX_OTHER_STORES") != "")
             substituters.push_back(nixLibexecDir + "/nix/substituters/copy-from-other-stores.pl");
 #endif
-        substituters.push_back(nixLibexecDir + "/nix/substituters/download-using-manifests.pl");
         substituters.push_back(nixLibexecDir + "/nix/substituters/download-from-binary-cache.pl");
         if (useSshSubstituter && !sshSubstituterHosts.empty())
             substituters.push_back(nixLibexecDir + "/nix/substituters/download-via-ssh");
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 60b11afe6088..572fa7188c14 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -168,9 +168,6 @@ struct Settings {
        before being killed (0 means no limit). */
     unsigned long maxLogSize;
 
-    /* Whether to cache build failures. */
-    bool cacheFailure;
-
     /* How often (in seconds) to poll for locks. */
     unsigned int pollInterval;
 
diff --git a/src/libstore/http-binary-cache-store.cc b/src/libstore/http-binary-cache-store.cc
index 1d707c043a72..8a719db150aa 100644
--- a/src/libstore/http-binary-cache-store.cc
+++ b/src/libstore/http-binary-cache-store.cc
@@ -10,7 +10,7 @@ private:
 
     Path cacheUri;
 
-    ref<Downloader> downloader;
+    Pool<Downloader> downloaders;
 
 public:
 
@@ -18,7 +18,9 @@ public:
         const Path & secretKeyFile, const Path & _cacheUri)
         : BinaryCacheStore(localStore, secretKeyFile)
         , cacheUri(_cacheUri)
-        , downloader(makeDownloader())
+        , downloaders(
+            std::numeric_limits<size_t>::max(),
+            []() { return makeDownloader(); })
     {
         if (cacheUri.back() == '/')
             cacheUri.pop_back();
@@ -36,13 +38,16 @@ protected:
     bool fileExists(const std::string & path) override
     {
         try {
+            auto downloader(downloaders.get());
             DownloadOptions options;
             options.showProgress = DownloadOptions::no;
             options.head = true;
             downloader->download(cacheUri + "/" + path, options);
             return true;
         } catch (DownloadError & e) {
-            if (e.error == Downloader::NotFound)
+            /* S3 buckets return 403 if a file doesn't exist and the
+               bucket is unlistable, so treat 403 as 404. */
+            if (e.error == Downloader::NotFound || e.error == Downloader::Forbidden)
                 return false;
             throw;
         }
@@ -55,6 +60,7 @@ protected:
 
     std::string getFile(const std::string & path) override
     {
+        auto downloader(downloaders.get());
         DownloadOptions options;
         options.showProgress = DownloadOptions::no;
         return downloader->download(cacheUri + "/" + path, options).data;
diff --git a/src/libstore/local-fs-store.cc b/src/libstore/local-fs-store.cc
index 87c216ec9f95..303c3af27b8d 100644
--- a/src/libstore/local-fs-store.cc
+++ b/src/libstore/local-fs-store.cc
@@ -23,7 +23,7 @@ struct LocalStoreAccessor : public FSAccessor
 
         struct stat st;
         if (lstat(path.c_str(), &st)) {
-            if (errno == ENOENT) return {Type::tMissing, 0, false};
+            if (errno == ENOENT || errno == ENOTDIR) return {Type::tMissing, 0, false};
             throw SysError(format("getting status of ‘%1%’") % path);
         }
 
@@ -69,9 +69,11 @@ ref<FSAccessor> LocalFSStore::getFSAccessor()
     return make_ref<LocalStoreAccessor>(ref<Store>(shared_from_this()));
 }
 
-void LocalFSStore::dumpPath(const Path & path, Sink & sink)
+void LocalFSStore::narFromPath(const Path & path, Sink & sink)
 {
-    nix::dumpPath(path, sink);
+    if (!isValidPath(path))
+        throw Error(format("path ‘%s’ is not valid") % path);
+    dumpPath(path, sink);
 }
 
 }
diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc
index 532d511ef8bf..d6e1e4d7e0d4 100644
--- a/src/libstore/local-store.cc
+++ b/src/libstore/local-store.cc
@@ -10,6 +10,7 @@
 #include <iostream>
 #include <algorithm>
 #include <cstring>
+#include <atomic>
 
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -36,168 +37,6 @@
 namespace nix {
 
 
-MakeError(SQLiteError, Error);
-MakeError(SQLiteBusy, SQLiteError);
-
-
-[[noreturn]] static void throwSQLiteError(sqlite3 * db, const format & f)
-{
-    int err = sqlite3_errcode(db);
-    if (err == SQLITE_BUSY || err == SQLITE_PROTOCOL) {
-        if (err == SQLITE_PROTOCOL)
-            printMsg(lvlError, "warning: SQLite database is busy (SQLITE_PROTOCOL)");
-        else {
-            static bool warned = false;
-            if (!warned) {
-                printMsg(lvlError, "warning: SQLite database is busy");
-                warned = true;
-            }
-        }
-        /* Sleep for a while since retrying the transaction right away
-           is likely to fail again. */
-#if HAVE_NANOSLEEP
-        struct timespec t;
-        t.tv_sec = 0;
-        t.tv_nsec = (random() % 100) * 1000 * 1000; /* <= 0.1s */
-        nanosleep(&t, 0);
-#else
-        sleep(1);
-#endif
-        throw SQLiteBusy(format("%1%: %2%") % f.str() % sqlite3_errmsg(db));
-    }
-    else
-        throw SQLiteError(format("%1%: %2%") % f.str() % sqlite3_errmsg(db));
-}
-
-
-/* Convenience macros for retrying a SQLite transaction. */
-#define retry_sqlite while (1) { try {
-#define end_retry_sqlite break; } catch (SQLiteBusy & e) { } }
-
-
-SQLite::~SQLite()
-{
-    try {
-        if (db && sqlite3_close(db) != SQLITE_OK)
-            throwSQLiteError(db, "closing database");
-    } catch (...) {
-        ignoreException();
-    }
-}
-
-
-void SQLiteStmt::create(sqlite3 * db, const string & s)
-{
-    checkInterrupt();
-    assert(!stmt);
-    if (sqlite3_prepare_v2(db, s.c_str(), -1, &stmt, 0) != SQLITE_OK)
-        throwSQLiteError(db, "creating statement");
-    this->db = db;
-}
-
-
-void SQLiteStmt::reset()
-{
-    assert(stmt);
-    /* Note: sqlite3_reset() returns the error code for the most
-       recent call to sqlite3_step().  So ignore it. */
-    sqlite3_reset(stmt);
-    curArg = 1;
-}
-
-
-SQLiteStmt::~SQLiteStmt()
-{
-    try {
-        if (stmt && sqlite3_finalize(stmt) != SQLITE_OK)
-            throwSQLiteError(db, "finalizing statement");
-    } catch (...) {
-        ignoreException();
-    }
-}
-
-
-void SQLiteStmt::bind(const string & value)
-{
-    if (sqlite3_bind_text(stmt, curArg++, value.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
-        throwSQLiteError(db, "binding argument");
-}
-
-
-void SQLiteStmt::bind(int value)
-{
-    if (sqlite3_bind_int(stmt, curArg++, value) != SQLITE_OK)
-        throwSQLiteError(db, "binding argument");
-}
-
-
-void SQLiteStmt::bind64(long long value)
-{
-    if (sqlite3_bind_int64(stmt, curArg++, value) != SQLITE_OK)
-        throwSQLiteError(db, "binding argument");
-}
-
-
-void SQLiteStmt::bind()
-{
-    if (sqlite3_bind_null(stmt, curArg++) != SQLITE_OK)
-        throwSQLiteError(db, "binding argument");
-}
-
-
-/* Helper class to ensure that prepared statements are reset when
-   leaving the scope that uses them.  Unfinished prepared statements
-   prevent transactions from being aborted, and can cause locks to be
-   kept when they should be released. */
-struct SQLiteStmtUse
-{
-    SQLiteStmt & stmt;
-    SQLiteStmtUse(SQLiteStmt & stmt) : stmt(stmt)
-    {
-        stmt.reset();
-    }
-    ~SQLiteStmtUse()
-    {
-        try {
-            stmt.reset();
-        } catch (...) {
-            ignoreException();
-        }
-    }
-};
-
-
-struct SQLiteTxn
-{
-    bool active;
-    sqlite3 * db;
-
-    SQLiteTxn(sqlite3 * db) : active(false) {
-        this->db = db;
-        if (sqlite3_exec(db, "begin;", 0, 0, 0) != SQLITE_OK)
-            throwSQLiteError(db, "starting transaction");
-        active = true;
-    }
-
-    void commit()
-    {
-        if (sqlite3_exec(db, "commit;", 0, 0, 0) != SQLITE_OK)
-            throwSQLiteError(db, "committing transaction");
-        active = false;
-    }
-
-    ~SQLiteTxn()
-    {
-        try {
-            if (active && sqlite3_exec(db, "rollback;", 0, 0, 0) != SQLITE_OK)
-                throwSQLiteError(db, "aborting transaction");
-        } catch (...) {
-            ignoreException();
-        }
-    }
-};
-
-
 void checkStoreNotSymlink()
 {
     if (getEnv("NIX_IGNORE_SYMLINK_STORE") == "1") return;
@@ -217,20 +56,21 @@ void checkStoreNotSymlink()
 
 
 LocalStore::LocalStore()
-    : reservedPath(settings.nixDBPath + "/reserved")
-    , didSetSubstituterEnv(false)
+    : linksDir(settings.nixStore + "/.links")
+    , reservedPath(settings.nixDBPath + "/reserved")
+    , schemaPath(settings.nixDBPath + "/schema")
 {
-    schemaPath = settings.nixDBPath + "/schema";
+    auto state(_state.lock());
 
     if (settings.readOnlyMode) {
-        openDB(false);
+        openDB(*state, false);
         return;
     }
 
     /* Create missing state directories if they don't already exist. */
     createDirs(settings.nixStore);
     makeStoreWritable();
-    createDirs(linksDir = settings.nixStore + "/.links");
+    createDirs(linksDir);
     Path profilesDir = settings.nixStateDir + "/profiles";
     createDirs(profilesDir);
     createDirs(settings.nixStateDir + "/temproots");
@@ -302,7 +142,7 @@ LocalStore::LocalStore()
     } catch (SysError & e) {
         if (e.errNo != EACCES) throw;
         settings.readOnlyMode = true;
-        openDB(false);
+        openDB(*state, false);
         return;
     }
 
@@ -320,7 +160,7 @@ LocalStore::LocalStore()
 
     else if (curSchema == 0) { /* new store */
         curSchema = nixSchemaVersion;
-        openDB(true);
+        openDB(*state, true);
         writeFile(schemaPath, (format("%1%") % nixSchemaVersion).str());
     }
 
@@ -346,21 +186,41 @@ LocalStore::LocalStore()
            have performed the upgrade already. */
         curSchema = getSchema();
 
-        if (curSchema < 7) { upgradeStore7(); openDB(true); }
+        if (curSchema < 7) { upgradeStore7(); }
+
+        openDB(*state, false);
+
+        if (curSchema < 8) {
+            SQLiteTxn txn(state->db);
+            if (sqlite3_exec(state->db, "alter table ValidPaths add column ultimate integer", 0, 0, 0) != SQLITE_OK)
+                throwSQLiteError(state->db, "upgrading database schema");
+            if (sqlite3_exec(state->db, "alter table ValidPaths add column sigs text", 0, 0, 0) != SQLITE_OK)
+                throwSQLiteError(state->db, "upgrading database schema");
+            txn.commit();
+        }
+
+        if (curSchema < 9) {
+            SQLiteTxn txn(state->db);
+            if (sqlite3_exec(state->db, "drop table FailedPaths", 0, 0, 0) != SQLITE_OK)
+                throwSQLiteError(state->db, "upgrading database schema");
+            txn.commit();
+        }
 
         writeFile(schemaPath, (format("%1%") % nixSchemaVersion).str());
 
         lockFile(globalLock, ltRead, true);
     }
 
-    else openDB(false);
+    else openDB(*state, false);
 }
 
 
 LocalStore::~LocalStore()
 {
+    auto state(_state.lock());
+
     try {
-        for (auto & i : runningSubstituters) {
+        for (auto & i : state->runningSubstituters) {
             if (i.second.disabled) continue;
             i.second.to.close();
             i.second.from.close();
@@ -373,9 +233,9 @@ LocalStore::~LocalStore()
     }
 
     try {
-        if (fdTempRoots != -1) {
-            fdTempRoots.close();
-            unlink(fnTempRoots.c_str());
+        if (state->fdTempRoots != -1) {
+            state->fdTempRoots.close();
+            unlink(state->fnTempRoots.c_str());
         }
     } catch (...) {
         ignoreException();
@@ -401,13 +261,14 @@ bool LocalStore::haveWriteAccess()
 }
 
 
-void LocalStore::openDB(bool create)
+void LocalStore::openDB(State & state, bool create)
 {
     if (!haveWriteAccess())
         throw SysError(format("Nix database directory ‘%1%’ is not writable") % settings.nixDBPath);
 
     /* Open the Nix database. */
     string dbPath = settings.nixDBPath + "/db.sqlite";
+    auto & db(state.db);
     if (sqlite3_open_v2(dbPath.c_str(), &db.db,
             SQLITE_OPEN_READWRITE | (create ? SQLITE_OPEN_CREATE : 0), 0) != SQLITE_OK)
         throw Error(format("cannot open Nix database ‘%1%’") % dbPath);
@@ -460,40 +321,31 @@ void LocalStore::openDB(bool create)
     }
 
     /* Prepare SQL statements. */
-    stmtRegisterValidPath.create(db,
-        "insert into ValidPaths (path, hash, registrationTime, deriver, narSize) values (?, ?, ?, ?, ?);");
-    stmtUpdatePathInfo.create(db,
-        "update ValidPaths set narSize = ?, hash = ? where path = ?;");
-    stmtAddReference.create(db,
+    state.stmtRegisterValidPath.create(db,
+        "insert into ValidPaths (path, hash, registrationTime, deriver, narSize, ultimate, sigs) values (?, ?, ?, ?, ?, ?, ?);");
+    state.stmtUpdatePathInfo.create(db,
+        "update ValidPaths set narSize = ?, hash = ?, ultimate = ?, sigs = ? where path = ?;");
+    state.stmtAddReference.create(db,
         "insert or replace into Refs (referrer, reference) values (?, ?);");
-    stmtQueryPathInfo.create(db,
-        "select id, hash, registrationTime, deriver, narSize from ValidPaths where path = ?;");
-    stmtQueryReferences.create(db,
+    state.stmtQueryPathInfo.create(db,
+        "select id, hash, registrationTime, deriver, narSize, ultimate, sigs from ValidPaths where path = ?;");
+    state.stmtQueryReferences.create(db,
         "select path from Refs join ValidPaths on reference = id where referrer = ?;");
-    stmtQueryReferrers.create(db,
+    state.stmtQueryReferrers.create(db,
         "select path from Refs join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?);");
-    stmtInvalidatePath.create(db,
+    state.stmtInvalidatePath.create(db,
         "delete from ValidPaths where path = ?;");
-    stmtRegisterFailedPath.create(db,
-        "insert or ignore into FailedPaths (path, time) values (?, ?);");
-    stmtHasPathFailed.create(db,
-        "select time from FailedPaths where path = ?;");
-    stmtQueryFailedPaths.create(db,
-        "select path from FailedPaths;");
-    // If the path is a derivation, then clear its outputs.
-    stmtClearFailedPath.create(db,
-        "delete from FailedPaths where ?1 = '*' or path = ?1 "
-        "or path in (select d.path from DerivationOutputs d join ValidPaths v on d.drv = v.id where v.path = ?1);");
-    stmtAddDerivationOutput.create(db,
+    state.stmtAddDerivationOutput.create(db,
         "insert or replace into DerivationOutputs (drv, id, path) values (?, ?, ?);");
-    stmtQueryValidDerivers.create(db,
+    state.stmtQueryValidDerivers.create(db,
         "select v.id, v.path from DerivationOutputs d join ValidPaths v on d.drv = v.id where d.path = ?;");
-    stmtQueryDerivationOutputs.create(db,
+    state.stmtQueryDerivationOutputs.create(db,
         "select id, path from DerivationOutputs where drv = ?;");
     // Use "path >= ?" with limit 1 rather than "path like '?%'" to
     // ensure efficient lookup.
-    stmtQueryPathFromHashPart.create(db,
+    state.stmtQueryPathFromHashPart.create(db,
         "select path from ValidPaths where path >= ? limit 1;");
+    state.stmtQueryValidPaths.create(db, "select path from ValidPaths");
 }
 
 
@@ -688,23 +540,19 @@ void LocalStore::checkDerivationOutputs(const Path & drvPath, const Derivation &
 }
 
 
-unsigned long long LocalStore::addValidPath(const ValidPathInfo & info, bool checkOutputs)
+uint64_t LocalStore::addValidPath(State & state,
+    const ValidPathInfo & info, bool checkOutputs)
 {
-    SQLiteStmtUse use(stmtRegisterValidPath);
-    stmtRegisterValidPath.bind(info.path);
-    stmtRegisterValidPath.bind("sha256:" + printHash(info.narHash));
-    stmtRegisterValidPath.bind(info.registrationTime == 0 ? time(0) : info.registrationTime);
-    if (info.deriver != "")
-        stmtRegisterValidPath.bind(info.deriver);
-    else
-        stmtRegisterValidPath.bind(); // null
-    if (info.narSize != 0)
-        stmtRegisterValidPath.bind64(info.narSize);
-    else
-        stmtRegisterValidPath.bind(); // null
-    if (sqlite3_step(stmtRegisterValidPath) != SQLITE_DONE)
-        throwSQLiteError(db, format("registering valid path ‘%1%’ in database") % info.path);
-    unsigned long long id = sqlite3_last_insert_rowid(db);
+    state.stmtRegisterValidPath.use()
+        (info.path)
+        ("sha256:" + printHash(info.narHash))
+        (info.registrationTime == 0 ? time(0) : info.registrationTime)
+        (info.deriver, info.deriver != "")
+        (info.narSize, info.narSize != 0)
+        (info.ultimate ? 1 : 0, info.ultimate)
+        (concatStringsSep(" ", info.sigs), !info.sigs.empty())
+        .exec();
+    uint64_t id = sqlite3_last_insert_rowid(state.db);
 
     /* If this is a derivation, then store the derivation outputs in
        the database.  This is useful for the garbage collector: it can
@@ -721,12 +569,11 @@ unsigned long long LocalStore::addValidPath(const ValidPathInfo & info, bool che
         if (checkOutputs) checkDerivationOutputs(info.path, drv);
 
         for (auto & i : drv.outputs) {
-            SQLiteStmtUse use(stmtAddDerivationOutput);
-            stmtAddDerivationOutput.bind(id);
-            stmtAddDerivationOutput.bind(i.first);
-            stmtAddDerivationOutput.bind(i.second.path);
-            if (sqlite3_step(stmtAddDerivationOutput) != SQLITE_DONE)
-                throwSQLiteError(db, format("adding derivation output for ‘%1%’ in database") % info.path);
+            state.stmtAddDerivationOutput.use()
+                (id)
+                (i.first)
+                (i.second.path)
+                .exec();
         }
     }
 
@@ -734,79 +581,6 @@ unsigned long long LocalStore::addValidPath(const ValidPathInfo & info, bool che
 }
 
 
-void LocalStore::addReference(unsigned long long referrer, unsigned long long reference)
-{
-    SQLiteStmtUse use(stmtAddReference);
-    stmtAddReference.bind(referrer);
-    stmtAddReference.bind(reference);
-    if (sqlite3_step(stmtAddReference) != SQLITE_DONE)
-        throwSQLiteError(db, "adding reference to database");
-}
-
-
-void LocalStore::registerFailedPath(const Path & path)
-{
-    retry_sqlite {
-        SQLiteStmtUse use(stmtRegisterFailedPath);
-        stmtRegisterFailedPath.bind(path);
-        stmtRegisterFailedPath.bind(time(0));
-        if (sqlite3_step(stmtRegisterFailedPath) != SQLITE_DONE)
-            throwSQLiteError(db, format("registering failed path ‘%1%’") % path);
-    } end_retry_sqlite;
-}
-
-
-bool LocalStore::hasPathFailed(const Path & path)
-{
-    retry_sqlite {
-        SQLiteStmtUse use(stmtHasPathFailed);
-        stmtHasPathFailed.bind(path);
-        int res = sqlite3_step(stmtHasPathFailed);
-        if (res != SQLITE_DONE && res != SQLITE_ROW)
-            throwSQLiteError(db, "querying whether path failed");
-        return res == SQLITE_ROW;
-    } end_retry_sqlite;
-}
-
-
-PathSet LocalStore::queryFailedPaths()
-{
-    retry_sqlite {
-        SQLiteStmtUse use(stmtQueryFailedPaths);
-
-        PathSet res;
-        int r;
-        while ((r = sqlite3_step(stmtQueryFailedPaths)) == SQLITE_ROW) {
-            const char * s = (const char *) sqlite3_column_text(stmtQueryFailedPaths, 0);
-            assert(s);
-            res.insert(s);
-        }
-
-        if (r != SQLITE_DONE)
-            throwSQLiteError(db, "error querying failed paths");
-
-        return res;
-    } end_retry_sqlite;
-}
-
-
-void LocalStore::clearFailedPaths(const PathSet & paths)
-{
-    retry_sqlite {
-        SQLiteTxn txn(db);
-
-        for (auto & i : paths) {
-            SQLiteStmtUse use(stmtClearFailedPath);
-            stmtClearFailedPath.bind(i);
-            if (sqlite3_step(stmtClearFailedPath) != SQLITE_DONE)
-                throwSQLiteError(db, format("clearing failed path ‘%1%’ in database") % i);
-        }
-
-        txn.commit();
-    } end_retry_sqlite;
-}
-
-
 Hash parseHashField(const Path & path, const string & s)
 {
     string::size_type colon = s.find(':');
@@ -828,153 +602,117 @@ ValidPathInfo LocalStore::queryPathInfo(const Path & path)
 
     assertStorePath(path);
 
-    retry_sqlite {
+    return retrySQLite<ValidPathInfo>([&]() {
+        auto state(_state.lock());
 
         /* Get the path info. */
-        SQLiteStmtUse use1(stmtQueryPathInfo);
+        auto useQueryPathInfo(state->stmtQueryPathInfo.use()(path));
 
-        stmtQueryPathInfo.bind(path);
+        if (!useQueryPathInfo.next())
+            throw Error(format("path ‘%1%’ is not valid") % path);
 
-        int r = sqlite3_step(stmtQueryPathInfo);
-        if (r == SQLITE_DONE) throw Error(format("path ‘%1%’ is not valid") % path);
-        if (r != SQLITE_ROW) throwSQLiteError(db, "querying path in database");
+        info.id = useQueryPathInfo.getInt(0);
 
-        info.id = sqlite3_column_int(stmtQueryPathInfo, 0);
+        info.narHash = parseHashField(path, useQueryPathInfo.getStr(1));
 
-        const char * s = (const char *) sqlite3_column_text(stmtQueryPathInfo, 1);
-        assert(s);
-        info.narHash = parseHashField(path, s);
+        info.registrationTime = useQueryPathInfo.getInt(2);
 
-        info.registrationTime = sqlite3_column_int(stmtQueryPathInfo, 2);
-
-        s = (const char *) sqlite3_column_text(stmtQueryPathInfo, 3);
+        auto s = (const char *) sqlite3_column_text(state->stmtQueryPathInfo, 3);
         if (s) info.deriver = s;
 
         /* Note that narSize = NULL yields 0. */
-        info.narSize = sqlite3_column_int64(stmtQueryPathInfo, 4);
+        info.narSize = useQueryPathInfo.getInt(4);
 
-        /* Get the references. */
-        SQLiteStmtUse use2(stmtQueryReferences);
+        info.ultimate = useQueryPathInfo.getInt(5) == 1;
 
-        stmtQueryReferences.bind(info.id);
+        s = (const char *) sqlite3_column_text(state->stmtQueryPathInfo, 6);
+        if (s) info.sigs = tokenizeString<StringSet>(s, " ");
 
-        while ((r = sqlite3_step(stmtQueryReferences)) == SQLITE_ROW) {
-            s = (const char *) sqlite3_column_text(stmtQueryReferences, 0);
-            assert(s);
-            info.references.insert(s);
-        }
+        /* Get the references. */
+        auto useQueryReferences(state->stmtQueryReferences.use()(info.id));
 
-        if (r != SQLITE_DONE)
-            throwSQLiteError(db, format("error getting references of ‘%1%’") % path);
+        while (useQueryReferences.next())
+            info.references.insert(useQueryReferences.getStr(0));
 
         return info;
-    } end_retry_sqlite;
+    });
 }
 
 
-/* Update path info in the database.  Currently only updates the
-   narSize field. */
-void LocalStore::updatePathInfo(const ValidPathInfo & info)
+/* Update path info in the database. */
+void LocalStore::updatePathInfo(State & state, const ValidPathInfo & info)
 {
-    SQLiteStmtUse use(stmtUpdatePathInfo);
-    if (info.narSize != 0)
-        stmtUpdatePathInfo.bind64(info.narSize);
-    else
-        stmtUpdatePathInfo.bind(); // null
-    stmtUpdatePathInfo.bind("sha256:" + printHash(info.narHash));
-    stmtUpdatePathInfo.bind(info.path);
-    if (sqlite3_step(stmtUpdatePathInfo) != SQLITE_DONE)
-        throwSQLiteError(db, format("updating info of path ‘%1%’ in database") % info.path);
+    state.stmtUpdatePathInfo.use()
+        (info.narSize, info.narSize != 0)
+        ("sha256:" + printHash(info.narHash))
+        (info.ultimate ? 1 : 0, info.ultimate)
+        (concatStringsSep(" ", info.sigs), !info.sigs.empty())
+        (info.path)
+        .exec();
 }
 
 
-unsigned long long LocalStore::queryValidPathId(const Path & path)
+uint64_t LocalStore::queryValidPathId(State & state, const Path & path)
 {
-    SQLiteStmtUse use(stmtQueryPathInfo);
-    stmtQueryPathInfo.bind(path);
-    int res = sqlite3_step(stmtQueryPathInfo);
-    if (res == SQLITE_ROW) return sqlite3_column_int(stmtQueryPathInfo, 0);
-    if (res == SQLITE_DONE) throw Error(format("path ‘%1%’ is not valid") % path);
-    throwSQLiteError(db, "querying path in database");
+    auto use(state.stmtQueryPathInfo.use()(path));
+    if (!use.next())
+        throw Error(format("path ‘%1%’ is not valid") % path);
+    return use.getInt(0);
 }
 
 
-bool LocalStore::isValidPath_(const Path & path)
+bool LocalStore::isValidPath(State & state, const Path & path)
 {
-    SQLiteStmtUse use(stmtQueryPathInfo);
-    stmtQueryPathInfo.bind(path);
-    int res = sqlite3_step(stmtQueryPathInfo);
-    if (res != SQLITE_DONE && res != SQLITE_ROW)
-        throwSQLiteError(db, "querying path in database");
-    return res == SQLITE_ROW;
+    return state.stmtQueryPathInfo.use()(path).next();
 }
 
 
 bool LocalStore::isValidPath(const Path & path)
 {
-    retry_sqlite {
-        return isValidPath_(path);
-    } end_retry_sqlite;
+    return retrySQLite<bool>([&]() {
+        auto state(_state.lock());
+        return isValidPath(*state, path);
+    });
 }
 
 
 PathSet LocalStore::queryValidPaths(const PathSet & paths)
 {
-    retry_sqlite {
-        PathSet res;
-        for (auto & i : paths)
-            if (isValidPath_(i)) res.insert(i);
-        return res;
-    } end_retry_sqlite;
+    PathSet res;
+    for (auto & i : paths)
+        if (isValidPath(i)) res.insert(i);
+    return res;
 }
 
 
 PathSet LocalStore::queryAllValidPaths()
 {
-    retry_sqlite {
-        SQLiteStmt stmt;
-        stmt.create(db, "select path from ValidPaths");
-
+    return retrySQLite<PathSet>([&]() {
+        auto state(_state.lock());
+        auto use(state->stmtQueryValidPaths.use());
         PathSet res;
-        int r;
-        while ((r = sqlite3_step(stmt)) == SQLITE_ROW) {
-            const char * s = (const char *) sqlite3_column_text(stmt, 0);
-            assert(s);
-            res.insert(s);
-        }
-
-        if (r != SQLITE_DONE)
-            throwSQLiteError(db, "error getting valid paths");
-
+        while (use.next()) res.insert(use.getStr(0));
         return res;
-    } end_retry_sqlite;
+    });
 }
 
 
-void LocalStore::queryReferrers_(const Path & path, PathSet & referrers)
+void LocalStore::queryReferrers(State & state, const Path & path, PathSet & referrers)
 {
-    SQLiteStmtUse use(stmtQueryReferrers);
-
-    stmtQueryReferrers.bind(path);
-
-    int r;
-    while ((r = sqlite3_step(stmtQueryReferrers)) == SQLITE_ROW) {
-        const char * s = (const char *) sqlite3_column_text(stmtQueryReferrers, 0);
-        assert(s);
-        referrers.insert(s);
-    }
+    auto useQueryReferrers(state.stmtQueryReferrers.use()(path));
 
-    if (r != SQLITE_DONE)
-        throwSQLiteError(db, format("error getting references of ‘%1%’") % path);
+    while (useQueryReferrers.next())
+        referrers.insert(useQueryReferrers.getStr(0));
 }
 
 
 void LocalStore::queryReferrers(const Path & path, PathSet & referrers)
 {
     assertStorePath(path);
-    retry_sqlite {
-        queryReferrers_(path, referrers);
-    } end_retry_sqlite;
+    return retrySQLite<void>([&]() {
+        auto state(_state.lock());
+        queryReferrers(*state, path, referrers);
+    });
 }
 
 
@@ -988,67 +726,51 @@ PathSet LocalStore::queryValidDerivers(const Path & path)
 {
     assertStorePath(path);
 
-    retry_sqlite {
-        SQLiteStmtUse use(stmtQueryValidDerivers);
-        stmtQueryValidDerivers.bind(path);
+    return retrySQLite<PathSet>([&]() {
+        auto state(_state.lock());
 
-        PathSet derivers;
-        int r;
-        while ((r = sqlite3_step(stmtQueryValidDerivers)) == SQLITE_ROW) {
-            const char * s = (const char *) sqlite3_column_text(stmtQueryValidDerivers, 1);
-            assert(s);
-            derivers.insert(s);
-        }
+        auto useQueryValidDerivers(state->stmtQueryValidDerivers.use()(path));
 
-        if (r != SQLITE_DONE)
-            throwSQLiteError(db, format("error getting valid derivers of ‘%1%’") % path);
+        PathSet derivers;
+        while (useQueryValidDerivers.next())
+            derivers.insert(useQueryValidDerivers.getStr(1));
 
         return derivers;
-    } end_retry_sqlite;
+    });
 }
 
 
 PathSet LocalStore::queryDerivationOutputs(const Path & path)
 {
-    retry_sqlite {
-        SQLiteStmtUse use(stmtQueryDerivationOutputs);
-        stmtQueryDerivationOutputs.bind(queryValidPathId(path));
+    return retrySQLite<PathSet>([&]() {
+        auto state(_state.lock());
 
-        PathSet outputs;
-        int r;
-        while ((r = sqlite3_step(stmtQueryDerivationOutputs)) == SQLITE_ROW) {
-            const char * s = (const char *) sqlite3_column_text(stmtQueryDerivationOutputs, 1);
-            assert(s);
-            outputs.insert(s);
-        }
+        auto useQueryDerivationOutputs(state->stmtQueryDerivationOutputs.use()
+            (queryValidPathId(*state, path)));
 
-        if (r != SQLITE_DONE)
-            throwSQLiteError(db, format("error getting outputs of ‘%1%’") % path);
+        PathSet outputs;
+        while (useQueryDerivationOutputs.next())
+            outputs.insert(useQueryDerivationOutputs.getStr(1));
 
         return outputs;
-    } end_retry_sqlite;
+    });
 }
 
 
 StringSet LocalStore::queryDerivationOutputNames(const Path & path)
 {
-    retry_sqlite {
-        SQLiteStmtUse use(stmtQueryDerivationOutputs);
-        stmtQueryDerivationOutputs.bind(queryValidPathId(path));
+    return retrySQLite<StringSet>([&]() {
+        auto state(_state.lock());
 
-        StringSet outputNames;
-        int r;
-        while ((r = sqlite3_step(stmtQueryDerivationOutputs)) == SQLITE_ROW) {
-            const char * s = (const char *) sqlite3_column_text(stmtQueryDerivationOutputs, 0);
-            assert(s);
-            outputNames.insert(s);
-        }
+        auto useQueryDerivationOutputs(state->stmtQueryDerivationOutputs.use()
+            (queryValidPathId(*state, path)));
 
-        if (r != SQLITE_DONE)
-            throwSQLiteError(db, format("error getting output names of ‘%1%’") % path);
+        StringSet outputNames;
+        while (useQueryDerivationOutputs.next())
+            outputNames.insert(useQueryDerivationOutputs.getStr(0));
 
         return outputNames;
-    } end_retry_sqlite;
+    });
 }
 
 
@@ -1058,29 +780,28 @@ Path LocalStore::queryPathFromHashPart(const string & hashPart)
 
     Path prefix = settings.nixStore + "/" + hashPart;
 
-    retry_sqlite {
-        SQLiteStmtUse use(stmtQueryPathFromHashPart);
-        stmtQueryPathFromHashPart.bind(prefix);
+    return retrySQLite<Path>([&]() {
+        auto state(_state.lock());
+
+        auto useQueryPathFromHashPart(state->stmtQueryPathFromHashPart.use()(prefix));
 
-        int res = sqlite3_step(stmtQueryPathFromHashPart);
-        if (res == SQLITE_DONE) return "";
-        if (res != SQLITE_ROW) throwSQLiteError(db, "finding path in database");
+        if (!useQueryPathFromHashPart.next()) return "";
 
-        const char * s = (const char *) sqlite3_column_text(stmtQueryPathFromHashPart, 0);
+        const char * s = (const char *) sqlite3_column_text(state->stmtQueryPathFromHashPart, 0);
         return s && prefix.compare(0, prefix.size(), s, prefix.size()) == 0 ? s : "";
-    } end_retry_sqlite;
+    });
 }
 
 
 void LocalStore::setSubstituterEnv()
 {
-    if (didSetSubstituterEnv) return;
+    static std::atomic_flag done;
+
+    if (done.test_and_set()) return;
 
     /* Pass configuration options (including those overridden with
        --option) to substituters. */
     setenv("_NIX_OPTIONS", settings.pack().c_str(), 1);
-
-    didSetSubstituterEnv = true;
 }
 
 
@@ -1202,10 +923,12 @@ template<class T> T LocalStore::getIntLineFromSubstituter(RunningSubstituter & r
 
 PathSet LocalStore::querySubstitutablePaths(const PathSet & paths)
 {
+    auto state(_state.lock());
+
     PathSet res;
     for (auto & i : settings.substituters) {
         if (res.size() == paths.size()) break;
-        RunningSubstituter & run(runningSubstituters[i]);
+        RunningSubstituter & run(state->runningSubstituters[i]);
         startSubstituter(i, run);
         if (run.disabled) continue;
         string s = "have ";
@@ -1222,6 +945,7 @@ PathSet LocalStore::querySubstitutablePaths(const PathSet & paths)
             res.insert(path);
         }
     }
+
     return res;
 }
 
@@ -1229,7 +953,9 @@ PathSet LocalStore::querySubstitutablePaths(const PathSet & paths)
 void LocalStore::querySubstitutablePathInfos(const Path & substituter,
     PathSet & paths, SubstitutablePathInfos & infos)
 {
-    RunningSubstituter & run(runningSubstituters[substituter]);
+    auto state(_state.lock());
+
+    RunningSubstituter & run(state->runningSubstituters[substituter]);
     startSubstituter(substituter, run);
     if (run.disabled) return;
 
@@ -1286,28 +1012,31 @@ void LocalStore::registerValidPath(const ValidPathInfo & info)
 
 void LocalStore::registerValidPaths(const ValidPathInfos & infos)
 {
-    /* SQLite will fsync by default, but the new valid paths may not be fsync-ed.
-     * So some may want to fsync them before registering the validity, at the
-     * expense of some speed of the path registering operation. */
+    /* SQLite will fsync by default, but the new valid paths may not
+       be fsync-ed.  So some may want to fsync them before registering
+       the validity, at the expense of some speed of the path
+       registering operation. */
     if (settings.syncBeforeRegistering) sync();
 
-    retry_sqlite {
-        SQLiteTxn txn(db);
+    return retrySQLite<void>([&]() {
+        auto state(_state.lock());
+
+        SQLiteTxn txn(state->db);
         PathSet paths;
 
         for (auto & i : infos) {
             assert(i.narHash.type == htSHA256);
-            if (isValidPath_(i.path))
-                updatePathInfo(i);
+            if (isValidPath(*state, i.path))
+                updatePathInfo(*state, i);
             else
-                addValidPath(i, false);
+                addValidPath(*state, i, false);
             paths.insert(i.path);
         }
 
         for (auto & i : infos) {
-            unsigned long long referrer = queryValidPathId(i.path);
+            auto referrer = queryValidPathId(*state, i.path);
             for (auto & j : i.references)
-                addReference(referrer, queryValidPathId(j));
+                state->stmtAddReference.use()(referrer)(queryValidPathId(*state, j)).exec();
         }
 
         /* Check that the derivation outputs are correct.  We can't do
@@ -1328,24 +1057,17 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos)
         topoSortPaths(paths);
 
         txn.commit();
-    } end_retry_sqlite;
+    });
 }
 
 
 /* Invalidate a path.  The caller is responsible for checking that
    there are no referrers. */
-void LocalStore::invalidatePath(const Path & path)
+void LocalStore::invalidatePath(State & state, const Path & path)
 {
     debug(format("invalidating path ‘%1%’") % path);
 
-    drvHashes.erase(path);
-
-    SQLiteStmtUse use(stmtInvalidatePath);
-
-    stmtInvalidatePath.bind(path);
-
-    if (sqlite3_step(stmtInvalidatePath) != SQLITE_DONE)
-        throwSQLiteError(db, format("invalidating path ‘%1%’ in database") % path);
+    state.stmtInvalidatePath.use()(path).exec();
 
     /* Note that the foreign key constraints on the Refs table take
        care of deleting the references entries for `path'. */
@@ -1397,6 +1119,7 @@ Path LocalStore::addToStoreFromDump(const string & dump, const string & name,
             info.path = dstPath;
             info.narHash = hash.first;
             info.narSize = hash.second;
+            info.ultimate = true;
             registerValidPath(info);
         }
 
@@ -1411,14 +1134,13 @@ Path LocalStore::addToStore(const string & name, const Path & _srcPath,
     bool recursive, HashType hashAlgo, PathFilter & filter, bool repair)
 {
     Path srcPath(absPath(_srcPath));
-    debug(format("adding ‘%1%’ to the store") % srcPath);
 
     /* Read the whole path into memory. This is not a very scalable
        method for very large paths, but `copyPath' is mainly used for
        small files. */
     StringSink sink;
     if (recursive)
-        nix::dumpPath(srcPath, sink, filter);
+        dumpPath(srcPath, sink, filter);
     else
         sink.s = make_ref<std::string>(readFile(srcPath));
 
@@ -1456,6 +1178,7 @@ Path LocalStore::addTextToStore(const string & name, const string & s,
             info.narHash = hash;
             info.narSize = sink.s->size();
             info.references = references;
+            info.ultimate = true;
             registerValidPath(info);
         }
 
@@ -1712,20 +1435,22 @@ void LocalStore::invalidatePathChecked(const Path & path)
 {
     assertStorePath(path);
 
-    retry_sqlite {
-        SQLiteTxn txn(db);
+    retrySQLite<void>([&]() {
+        auto state(_state.lock());
+
+        SQLiteTxn txn(state->db);
 
-        if (isValidPath_(path)) {
-            PathSet referrers; queryReferrers_(path, referrers);
+        if (isValidPath(*state, path)) {
+            PathSet referrers; queryReferrers(*state, path, referrers);
             referrers.erase(path); /* ignore self-references */
             if (!referrers.empty())
                 throw PathInUse(format("cannot delete path ‘%1%’ because it is in use by %2%")
                     % path % showPaths(referrers));
-            invalidatePath(path);
+            invalidatePath(*state, path);
         }
 
         txn.commit();
-    } end_retry_sqlite;
+    });
 }
 
 
@@ -1790,7 +1515,10 @@ bool LocalStore::verifyStore(bool checkContents, bool repair)
                         update = true;
                     }
 
-                    if (update) updatePathInfo(info);
+                    if (update) {
+                        auto state(_state.lock());
+                        updatePathInfo(*state, info);
+                    }
 
                 }
 
@@ -1820,7 +1548,8 @@ void LocalStore::verifyPath(const Path & path, const PathSet & store,
 
     if (!isStorePath(path)) {
         printMsg(lvlError, format("path ‘%1%’ is not in the Nix store") % path);
-        invalidatePath(path);
+        auto state(_state.lock());
+        invalidatePath(*state, path);
         return;
     }
 
@@ -1838,7 +1567,8 @@ void LocalStore::verifyPath(const Path & path, const PathSet & store,
 
         if (canInvalidate) {
             printMsg(lvlError, format("path ‘%1%’ disappeared, removing from database...") % path);
-            invalidatePath(path);
+            auto state(_state.lock());
+            invalidatePath(*state, path);
         } else {
             printMsg(lvlError, format("path ‘%1%’ disappeared, but it still has valid referrers!") % path);
             if (repair)
@@ -1858,32 +1588,6 @@ void LocalStore::verifyPath(const Path & path, const PathSet & store,
 }
 
 
-bool LocalStore::pathContentsGood(const Path & path)
-{
-    std::map<Path, bool>::iterator i = pathContentsGoodCache.find(path);
-    if (i != pathContentsGoodCache.end()) return i->second;
-    printMsg(lvlInfo, format("checking path ‘%1%’...") % path);
-    ValidPathInfo info = queryPathInfo(path);
-    bool res;
-    if (!pathExists(path))
-        res = false;
-    else {
-        HashResult current = hashPath(info.narHash.type, path);
-        Hash nullHash(htSHA256);
-        res = info.narHash == nullHash || info.narHash == current.first;
-    }
-    pathContentsGoodCache[path] = res;
-    if (!res) printMsg(lvlError, format("path ‘%1%’ is corrupted or missing!") % path);
-    return res;
-}
-
-
-void LocalStore::markContentsGood(const Path & path)
-{
-    pathContentsGoodCache[path] = true;
-}
-
-
 #if defined(FS_IOC_SETFLAGS) && defined(FS_IOC_GETFLAGS) && defined(FS_IMMUTABLE_FL)
 
 static void makeMutable(const Path & path)
@@ -1938,8 +1642,41 @@ void LocalStore::upgradeStore7()
 
 void LocalStore::vacuumDB()
 {
-    if (sqlite3_exec(db, "vacuum;", 0, 0, 0) != SQLITE_OK)
-        throwSQLiteError(db, "vacuuming SQLite database");
+    auto state(_state.lock());
+
+    if (sqlite3_exec(state->db, "vacuum;", 0, 0, 0) != SQLITE_OK)
+        throwSQLiteError(state->db, "vacuuming SQLite database");
+}
+
+
+void LocalStore::addSignatures(const Path & storePath, const StringSet & sigs)
+{
+    retrySQLite<void>([&]() {
+        auto state(_state.lock());
+
+        SQLiteTxn txn(state->db);
+
+        auto info = queryPathInfo(storePath);
+
+        info.sigs.insert(sigs.begin(), sigs.end());
+
+        updatePathInfo(*state, info);
+
+        txn.commit();
+    });
+}
+
+
+void LocalStore::signPathInfo(ValidPathInfo & info)
+{
+    // FIXME: keep secret keys in memory.
+
+    auto secretKeyFiles = settings.get("secret-key-files", Strings());
+
+    for (auto & secretKeyFile : secretKeyFiles) {
+        SecretKey secretKey(readFile(secretKeyFile));
+        info.sign(secretKey);
+    }
 }
 
 
diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh
index c7ea9e5038b4..14ff92c35cc5 100644
--- a/src/libstore/local-store.hh
+++ b/src/libstore/local-store.hh
@@ -1,15 +1,14 @@
 #pragma once
 
-#include <string>
-#include <unordered_set>
+#include "sqlite.hh"
 
+#include "pathlocks.hh"
 #include "store-api.hh"
+#include "sync.hh"
 #include "util.hh"
-#include "pathlocks.hh"
-
 
-class sqlite3;
-class sqlite3_stmt;
+#include <string>
+#include <unordered_set>
 
 
 namespace nix {
@@ -18,8 +17,8 @@ namespace nix {
 /* Nix store and database schema version.  Version 1 (or 0) was Nix <=
    0.7.  Version 2 was Nix 0.8 and 0.9.  Version 3 is Nix 0.10.
    Version 4 is Nix 0.11.  Version 5 is Nix 0.12-0.16.  Version 6 is
-   Nix 1.0.  Version 7 is Nix 1.3. */
-const int nixSchemaVersion = 7;
+   Nix 1.0.  Version 7 is Nix 1.3. Version 9 is 1.12. */
+const int nixSchemaVersion = 9;
 
 
 extern string drvsLogDir;
@@ -52,43 +51,46 @@ struct RunningSubstituter
 };
 
 
-/* Wrapper object to close the SQLite database automatically. */
-struct SQLite
-{
-    sqlite3 * db;
-    SQLite() { db = 0; }
-    ~SQLite();
-    operator sqlite3 * () { return db; }
-};
-
-
-/* Wrapper object to create and destroy SQLite prepared statements. */
-struct SQLiteStmt
-{
-    sqlite3 * db;
-    sqlite3_stmt * stmt;
-    unsigned int curArg;
-    SQLiteStmt() { stmt = 0; }
-    void create(sqlite3 * db, const string & s);
-    void reset();
-    ~SQLiteStmt();
-    operator sqlite3_stmt * () { return stmt; }
-    void bind(const string & value);
-    void bind(int value);
-    void bind64(long long value);
-    void bind();
-};
-
-
 class LocalStore : public LocalFSStore
 {
 private:
-    typedef std::map<Path, RunningSubstituter> RunningSubstituters;
-    RunningSubstituters runningSubstituters;
 
-    Path linksDir;
+    /* Lock file used for upgrading. */
+    AutoCloseFD globalLock;
 
-    Path reservedPath;
+    struct State
+    {
+        /* The SQLite database object. */
+        SQLite db;
+
+        /* Some precompiled SQLite statements. */
+        SQLiteStmt stmtRegisterValidPath;
+        SQLiteStmt stmtUpdatePathInfo;
+        SQLiteStmt stmtAddReference;
+        SQLiteStmt stmtQueryPathInfo;
+        SQLiteStmt stmtQueryReferences;
+        SQLiteStmt stmtQueryReferrers;
+        SQLiteStmt stmtInvalidatePath;
+        SQLiteStmt stmtAddDerivationOutput;
+        SQLiteStmt stmtQueryValidDerivers;
+        SQLiteStmt stmtQueryDerivationOutputs;
+        SQLiteStmt stmtQueryPathFromHashPart;
+        SQLiteStmt stmtQueryValidPaths;
+
+        /* The file to which we write our temporary roots. */
+        Path fnTempRoots;
+        AutoCloseFD fdTempRoots;
+
+        typedef std::map<Path, RunningSubstituter> RunningSubstituters;
+        RunningSubstituters runningSubstituters;
+
+    };
+
+    Sync<State, std::recursive_mutex> _state;
+
+    const Path linksDir;
+    const Path reservedPath;
+    const Path schemaPath;
 
 public:
 
@@ -188,90 +190,31 @@ public:
 
     void registerValidPaths(const ValidPathInfos & infos);
 
-    /* Register that the build of a derivation with output `path' has
-       failed. */
-    void registerFailedPath(const Path & path);
-
-    /* Query whether `path' previously failed to build. */
-    bool hasPathFailed(const Path & path);
-
-    PathSet queryFailedPaths() override;
-
-    void clearFailedPaths(const PathSet & paths) override;
-
     void vacuumDB();
 
     /* Repair the contents of the given path by redownloading it using
        a substituter (if available). */
     void repairPath(const Path & path);
 
-    /* Check whether the given valid path exists and has the right
-       contents. */
-    bool pathContentsGood(const Path & path);
-
-    void markContentsGood(const Path & path);
-
     void setSubstituterEnv();
 
-private:
-
-    Path schemaPath;
-
-    /* Lock file used for upgrading. */
-    AutoCloseFD globalLock;
-
-    /* The SQLite database object. */
-    SQLite db;
-
-    /* Some precompiled SQLite statements. */
-    SQLiteStmt stmtRegisterValidPath;
-    SQLiteStmt stmtUpdatePathInfo;
-    SQLiteStmt stmtAddReference;
-    SQLiteStmt stmtQueryPathInfo;
-    SQLiteStmt stmtQueryReferences;
-    SQLiteStmt stmtQueryReferrers;
-    SQLiteStmt stmtInvalidatePath;
-    SQLiteStmt stmtRegisterFailedPath;
-    SQLiteStmt stmtHasPathFailed;
-    SQLiteStmt stmtQueryFailedPaths;
-    SQLiteStmt stmtClearFailedPath;
-    SQLiteStmt stmtAddDerivationOutput;
-    SQLiteStmt stmtQueryValidDerivers;
-    SQLiteStmt stmtQueryDerivationOutputs;
-    SQLiteStmt stmtQueryPathFromHashPart;
-
-    /* Cache for pathContentsGood(). */
-    std::map<Path, bool> pathContentsGoodCache;
-
-    bool didSetSubstituterEnv;
-
-    /* The file to which we write our temporary roots. */
-    Path fnTempRoots;
-    AutoCloseFD fdTempRoots;
-
-    int getSchema();
-
-public:
+    void addSignatures(const Path & storePath, const StringSet & sigs) override;
 
     static bool haveWriteAccess();
 
 private:
 
-    void openDB(bool create);
-
-    void makeStoreWritable();
-
-    unsigned long long queryValidPathId(const Path & path);
+    int getSchema();
 
-    unsigned long long addValidPath(const ValidPathInfo & info, bool checkOutputs = true);
+    void openDB(State & state, bool create);
 
-    void addReference(unsigned long long referrer, unsigned long long reference);
+    void makeStoreWritable();
 
-    void appendReferrer(const Path & from, const Path & to, bool lock);
+    uint64_t queryValidPathId(State & state, const Path & path);
 
-    void rewriteReferrers(const Path & path, bool purge, PathSet referrers);
+    uint64_t addValidPath(State & state, const ValidPathInfo & info, bool checkOutputs = true);
 
-    void invalidatePath(const Path & path);
+    void invalidatePath(State & state, const Path & path);
 
     /* Delete a path from the Nix store. */
     void invalidatePathChecked(const Path & path);
@@ -279,7 +222,7 @@ private:
     void verifyPath(const Path & path, const PathSet & store,
         PathSet & done, PathSet & validPaths, bool repair, bool & errors);
 
-    void updatePathInfo(const ValidPathInfo & info);
+    void updatePathInfo(State & state, const ValidPathInfo & info);
 
     void upgradeStore6();
     void upgradeStore7();
@@ -327,8 +270,14 @@ private:
     void optimisePath_(OptimiseStats & stats, const Path & path, InodeHash & inodeHash);
 
     // Internal versions that are not wrapped in retry_sqlite.
-    bool isValidPath_(const Path & path);
-    void queryReferrers_(const Path & path, PathSet & referrers);
+    bool isValidPath(State & state, const Path & path);
+    void queryReferrers(State & state, const Path & path, PathSet & referrers);
+
+    /* Add signatures to a ValidPathInfo using the secret keys
+       specified by the ‘secret-key-files’ option. */
+    void signPathInfo(ValidPathInfo & info);
+
+    friend class DerivationGoal;
 };
 
 
diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc
index 9028370ac707..680facdcfeb8 100644
--- a/src/libstore/nar-info.cc
+++ b/src/libstore/nar-info.cc
@@ -1,4 +1,3 @@
-#include "crypto.hh"
 #include "globals.hh"
 #include "nar-info.hh"
 
@@ -104,15 +103,6 @@ std::string NarInfo::to_string() const
     return res;
 }
 
-std::string NarInfo::fingerprint() const
-{
-    return
-        "1;" + path + ";"
-        + printHashType(narHash.type) + ":" + printHash32(narHash) + ";"
-        + std::to_string(narSize) + ";"
-        + concatStringsSep(",", references);
-}
-
 Strings NarInfo::shortRefs() const
 {
     Strings refs;
@@ -121,18 +111,4 @@ Strings NarInfo::shortRefs() const
     return refs;
 }
 
-void NarInfo::sign(const SecretKey & secretKey)
-{
-    sigs.insert(secretKey.signDetached(fingerprint()));
-}
-
-unsigned int NarInfo::checkSignatures(const PublicKeys & publicKeys) const
-{
-    unsigned int good = 0;
-    for (auto & sig : sigs)
-        if (verifyDetached(fingerprint(), sig, publicKeys))
-            good++;
-    return good;
-}
-
 }
diff --git a/src/libstore/nar-info.hh b/src/libstore/nar-info.hh
index 2d04e452611e..3c783cf83fef 100644
--- a/src/libstore/nar-info.hh
+++ b/src/libstore/nar-info.hh
@@ -20,20 +20,6 @@ struct NarInfo : ValidPathInfo
 
     std::string to_string() const;
 
-    /*  Return a fingerprint of the store path to be used in binary
-        cache signatures. It contains the store path, the base-32
-        SHA-256 hash of the NAR serialisation of the path, the size of
-        the NAR, and the sorted references. The size field is strictly
-        speaking superfluous, but might prevent endless/excessive data
-        attacks. */
-    std::string fingerprint() const;
-
-    void sign(const SecretKey & secretKey);
-
-    /* Return the number of signatures on this .narinfo that were
-       produced by one of the specified keys. */
-    unsigned int checkSignatures(const PublicKeys & publicKeys) const;
-
 private:
 
     Strings shortRefs() const;
diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc
index 3c417b4a6d06..761e835481a8 100644
--- a/src/libstore/remote-store.cc
+++ b/src/libstore/remote-store.cc
@@ -61,27 +61,15 @@ ref<RemoteStore::Connection> RemoteStore::openConnection()
 
     string socketPath = settings.nixDaemonSocketFile;
 
-    /* Urgh, sockaddr_un allows path names of only 108 characters.  So
-       chdir to the socket directory so that we can pass a relative
-       path name.  !!! this is probably a bad idea in multi-threaded
-       applications... */
-    AutoCloseFD fdPrevDir = open(".", O_RDONLY);
-    if (fdPrevDir == -1) throw SysError("couldn't open current directory");
-    if (chdir(dirOf(socketPath).c_str()) == -1) throw SysError(format("couldn't change to directory of ‘%1%’") % socketPath);
-    Path socketPathRel = "./" + baseNameOf(socketPath);
-
     struct sockaddr_un addr;
     addr.sun_family = AF_UNIX;
-    if (socketPathRel.size() >= sizeof(addr.sun_path))
-        throw Error(format("socket path ‘%1%’ is too long") % socketPathRel);
-    strcpy(addr.sun_path, socketPathRel.c_str());
+    if (socketPath.size() + 1 >= sizeof(addr.sun_path))
+        throw Error(format("socket path ‘%1%’ is too long") % socketPath);
+    strcpy(addr.sun_path, socketPath.c_str());
 
     if (connect(conn->fd, (struct sockaddr *) &addr, sizeof(addr)) == -1)
         throw SysError(format("cannot connect to daemon at ‘%1%’") % socketPath);
 
-    if (fchdir(fdPrevDir) == -1)
-        throw SysError("couldn't change back to previous directory");
-
     conn->from.fd = conn->fd;
     conn->to.fd = conn->fd;
 
@@ -264,6 +252,10 @@ ValidPathInfo RemoteStore::queryPathInfo(const Path & path)
     info.references = readStorePaths<PathSet>(conn->from);
     info.registrationTime = readInt(conn->from);
     info.narSize = readLongLong(conn->from);
+    if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 16) {
+        info.ultimate = readInt(conn->from) != 0;
+        info.sigs = readStrings<StringSet>(conn->from);
+    }
     return info;
 }
 
@@ -366,7 +358,7 @@ Path RemoteStore::addToStore(const string & name, const Path & _srcPath,
     try {
         conn->to.written = 0;
         conn->to.warn = true;
-        nix::dumpPath(srcPath, conn->to, filter);
+        dumpPath(srcPath, conn->to, filter);
         conn->to.warn = false;
         conn->processStderr();
     } catch (SysError & e) {
@@ -528,37 +520,30 @@ void RemoteStore::collectGarbage(const GCOptions & options, GCResults & results)
 }
 
 
-PathSet RemoteStore::queryFailedPaths()
+void RemoteStore::optimiseStore()
 {
     auto conn(connections->get());
-    conn->to << wopQueryFailedPaths;
+    conn->to << wopOptimiseStore;
     conn->processStderr();
-    return readStorePaths<PathSet>(conn->from);
+    readInt(conn->from);
 }
 
 
-void RemoteStore::clearFailedPaths(const PathSet & paths)
+bool RemoteStore::verifyStore(bool checkContents, bool repair)
 {
     auto conn(connections->get());
-    conn->to << wopClearFailedPaths << paths;
+    conn->to << wopVerifyStore << checkContents << repair;
     conn->processStderr();
-    readInt(conn->from);
+    return readInt(conn->from) != 0;
 }
 
-void RemoteStore::optimiseStore()
-{
-    auto conn(connections->get());
-    conn->to << wopOptimiseStore;
-    conn->processStderr();
-    readInt(conn->from);
-}
 
-bool RemoteStore::verifyStore(bool checkContents, bool repair)
+void RemoteStore::addSignatures(const Path & storePath, const StringSet & sigs)
 {
     auto conn(connections->get());
-    conn->to << wopVerifyStore << checkContents << repair;
+    conn->to << wopAddSignatures << storePath << sigs;
     conn->processStderr();
-    return readInt(conn->from) != 0;
+    readInt(conn->from);
 }
 
 
diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh
index 85c8292c7698..45bc41804ccf 100644
--- a/src/libstore/remote-store.hh
+++ b/src/libstore/remote-store.hh
@@ -85,14 +85,12 @@ public:
 
     void collectGarbage(const GCOptions & options, GCResults & results) override;
 
-    PathSet queryFailedPaths() override;
-
-    void clearFailedPaths(const PathSet & paths) override;
-
     void optimiseStore() override;
 
     bool verifyStore(bool checkContents, bool repair) override;
 
+    void addSignatures(const Path & storePath, const StringSet & sigs) override;
+
 private:
 
     struct Connection
diff --git a/src/libstore/schema.sql b/src/libstore/schema.sql
index c1b4a689afcb..91878af1580d 100644
--- a/src/libstore/schema.sql
+++ b/src/libstore/schema.sql
@@ -4,7 +4,9 @@ create table if not exists ValidPaths (
     hash             text not null,
     registrationTime integer not null,
     deriver          text,
-    narSize          integer
+    narSize          integer,
+    ultimate         integer, -- null implies "false"
+    sigs             text -- space-separated
 );
 
 create table if not exists Refs (
@@ -37,8 +39,3 @@ create table if not exists DerivationOutputs (
 );
 
 create index if not exists IndexDerivationOutputs on DerivationOutputs(path);
-
-create table if not exists FailedPaths (
-    path text primary key not null,
-    time integer not null
-);
diff --git a/src/libstore/sqlite.cc b/src/libstore/sqlite.cc
new file mode 100644
index 000000000000..f93fa0857588
--- /dev/null
+++ b/src/libstore/sqlite.cc
@@ -0,0 +1,167 @@
+#include "sqlite.hh"
+#include "util.hh"
+
+#include <sqlite3.h>
+
+namespace nix {
+
+[[noreturn]] void throwSQLiteError(sqlite3 * db, const format & f)
+{
+    int err = sqlite3_errcode(db);
+    if (err == SQLITE_BUSY || err == SQLITE_PROTOCOL) {
+        if (err == SQLITE_PROTOCOL)
+            printMsg(lvlError, "warning: SQLite database is busy (SQLITE_PROTOCOL)");
+        else {
+            static bool warned = false;
+            if (!warned) {
+                printMsg(lvlError, "warning: SQLite database is busy");
+                warned = true;
+            }
+        }
+        /* Sleep for a while since retrying the transaction right away
+           is likely to fail again. */
+        checkInterrupt();
+#if HAVE_NANOSLEEP
+        struct timespec t;
+        t.tv_sec = 0;
+        t.tv_nsec = (random() % 100) * 1000 * 1000; /* <= 0.1s */
+        nanosleep(&t, 0);
+#else
+        sleep(1);
+#endif
+        throw SQLiteBusy(format("%1%: %2%") % f.str() % sqlite3_errmsg(db));
+    }
+    else
+        throw SQLiteError(format("%1%: %2%") % f.str() % sqlite3_errmsg(db));
+}
+
+SQLite::~SQLite()
+{
+    try {
+        if (db && sqlite3_close(db) != SQLITE_OK)
+            throwSQLiteError(db, "closing database");
+    } catch (...) {
+        ignoreException();
+    }
+}
+
+void SQLiteStmt::create(sqlite3 * db, const string & s)
+{
+    checkInterrupt();
+    assert(!stmt);
+    if (sqlite3_prepare_v2(db, s.c_str(), -1, &stmt, 0) != SQLITE_OK)
+        throwSQLiteError(db, "creating statement");
+    this->db = db;
+}
+
+SQLiteStmt::~SQLiteStmt()
+{
+    try {
+        if (stmt && sqlite3_finalize(stmt) != SQLITE_OK)
+            throwSQLiteError(db, "finalizing statement");
+    } catch (...) {
+        ignoreException();
+    }
+}
+
+SQLiteStmt::Use::Use(SQLiteStmt & stmt)
+    : stmt(stmt)
+{
+    assert(stmt.stmt);
+    /* Note: sqlite3_reset() returns the error code for the most
+       recent call to sqlite3_step().  So ignore it. */
+    sqlite3_reset(stmt);
+}
+
+SQLiteStmt::Use::~Use()
+{
+    sqlite3_reset(stmt);
+}
+
+SQLiteStmt::Use & SQLiteStmt::Use::operator () (const std::string & value, bool notNull)
+{
+    if (notNull) {
+        if (sqlite3_bind_text(stmt, curArg++, value.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
+            throwSQLiteError(stmt.db, "binding argument");
+    } else
+        bind();
+    return *this;
+}
+
+SQLiteStmt::Use & SQLiteStmt::Use::operator () (int64_t value, bool notNull)
+{
+    if (notNull) {
+        if (sqlite3_bind_int64(stmt, curArg++, value) != SQLITE_OK)
+            throwSQLiteError(stmt.db, "binding argument");
+    } else
+        bind();
+    return *this;
+}
+
+SQLiteStmt::Use & SQLiteStmt::Use::bind()
+{
+    if (sqlite3_bind_null(stmt, curArg++) != SQLITE_OK)
+        throwSQLiteError(stmt.db, "binding argument");
+    return *this;
+}
+
+int SQLiteStmt::Use::step()
+{
+    return sqlite3_step(stmt);
+}
+
+void SQLiteStmt::Use::exec()
+{
+    int r = step();
+    assert(r != SQLITE_ROW);
+    if (r != SQLITE_DONE)
+        throwSQLiteError(stmt.db, "executing SQLite statement");
+}
+
+bool SQLiteStmt::Use::next()
+{
+    int r = step();
+    if (r != SQLITE_DONE && r != SQLITE_ROW)
+        throwSQLiteError(stmt.db, "executing SQLite query");
+    return r == SQLITE_ROW;
+}
+
+std::string SQLiteStmt::Use::getStr(int col)
+{
+    auto s = (const char *) sqlite3_column_text(stmt, col);
+    assert(s);
+    return s;
+}
+
+int64_t SQLiteStmt::Use::getInt(int col)
+{
+    // FIXME: detect nulls?
+    return sqlite3_column_int64(stmt, col);
+}
+
+SQLiteTxn::SQLiteTxn(sqlite3 * db)
+{
+    this->db = db;
+    if (sqlite3_exec(db, "begin;", 0, 0, 0) != SQLITE_OK)
+        throwSQLiteError(db, "starting transaction");
+    active = true;
+}
+
+void SQLiteTxn::commit()
+{
+    if (sqlite3_exec(db, "commit;", 0, 0, 0) != SQLITE_OK)
+        throwSQLiteError(db, "committing transaction");
+    active = false;
+}
+
+SQLiteTxn::~SQLiteTxn()
+{
+    try {
+        if (active && sqlite3_exec(db, "rollback;", 0, 0, 0) != SQLITE_OK)
+            throwSQLiteError(db, "aborting transaction");
+    } catch (...) {
+        ignoreException();
+    }
+}
+
+}
diff --git a/src/libstore/sqlite.hh b/src/libstore/sqlite.hh
new file mode 100644
index 000000000000..326e4a4855b7
--- /dev/null
+++ b/src/libstore/sqlite.hh
@@ -0,0 +1,102 @@
+#pragma once
+
+#include <functional>
+#include <string>
+
+#include "types.hh"
+
+class sqlite3;
+class sqlite3_stmt;
+
+namespace nix {
+
+/* RAII wrapper to close a SQLite database automatically. */
+struct SQLite
+{
+    sqlite3 * db;
+    SQLite() { db = 0; }
+    ~SQLite();
+    operator sqlite3 * () { return db; }
+};
+
+/* RAII wrapper to create and destroy SQLite prepared statements. */
+struct SQLiteStmt
+{
+    sqlite3 * db = 0;
+    sqlite3_stmt * stmt = 0;
+    SQLiteStmt() { }
+    void create(sqlite3 * db, const std::string & s);
+    ~SQLiteStmt();
+    operator sqlite3_stmt * () { return stmt; }
+
+    /* Helper for binding / executing statements. */
+    class Use
+    {
+        friend struct SQLiteStmt;
+    private:
+        SQLiteStmt & stmt;
+        unsigned int curArg = 1;
+        Use(SQLiteStmt & stmt);
+
+    public:
+
+        ~Use();
+
+        /* Bind the next parameter. */
+        Use & operator () (const std::string & value, bool notNull = true);
+        Use & operator () (int64_t value, bool notNull = true);
+        Use & bind(); // null
+
+        int step();
+
+        /* Execute a statement that does not return rows. */
+        void exec();
+
+        /* For statements that return 0 or more rows. Returns true iff
+           a row is available. */
+        bool next();
+
+        std::string getStr(int col);
+        int64_t getInt(int col);
+    };
+
+    Use use()
+    {
+        return Use(*this);
+    }
+};
+
+/* RAII helper that ensures transactions are aborted unless explicitly
+   committed. */
+struct SQLiteTxn
+{
+    bool active = false;
+    sqlite3 * db;
+
+    SQLiteTxn(sqlite3 * db);
+
+    void commit();
+
+    ~SQLiteTxn();
+};
+
+
+MakeError(SQLiteError, Error);
+MakeError(SQLiteBusy, SQLiteError);
+
+[[noreturn]] void throwSQLiteError(sqlite3 * db, const format & f);
+
+/* Convenience function for retrying a SQLite transaction when the
+   database is busy. */
+template<typename T>
+T retrySQLite(std::function<T()> fun)
+{
+    while (true) {
+        try {
+            return fun();
+        } catch (SQLiteBusy & e) {
+        }
+    }
+}
+
+}
diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc
index 378233654225..cc91ed287768 100644
--- a/src/libstore/store-api.cc
+++ b/src/libstore/store-api.cc
@@ -1,5 +1,6 @@
-#include "store-api.hh"
+#include "crypto.hh"
 #include "globals.hh"
+#include "store-api.hh"
 #include "util.hh"
 
 
@@ -135,14 +136,14 @@ void checkStoreName(const string & name)
      if <type> = "source":
        the serialisation of the path from which this store path is
        copied, as returned by hashPath()
-     if <type> = "output:out":
+     if <type> = "output:<id>":
        for non-fixed derivation outputs:
          the derivation (see hashDerivationModulo() in
          primops.cc)
        for paths copied by addToStore() or produced by fixed-output
        derivations:
          the string "fixed:out:<rec><algo>:<hash>:", where
-           <rec> = "r:" for recursive (path) hashes, or "" or flat
+           <rec> = "r:" for recursive (path) hashes, or "" for flat
              (file) hashes
            <algo> = "md5", "sha1" or "sha256"
            <hash> = base-16 representation of the path or flat hash of
@@ -309,6 +310,41 @@ void Store::exportPaths(const Paths & paths,
 }
 
 
+std::string ValidPathInfo::fingerprint() const
+{
+    if (narSize == 0 || narHash.type == htUnknown)
+        throw Error(format("cannot calculate fingerprint of path ‘%s’ because its size/hash is not known")
+            % path);
+    return
+        "1;" + path + ";"
+        + printHashType(narHash.type) + ":" + printHash32(narHash) + ";"
+        + std::to_string(narSize) + ";"
+        + concatStringsSep(",", references);
+}
+
+
+void ValidPathInfo::sign(const SecretKey & secretKey)
+{
+    sigs.insert(secretKey.signDetached(fingerprint()));
+}
+
+
+unsigned int ValidPathInfo::checkSignatures(const PublicKeys & publicKeys) const
+{
+    unsigned int good = 0;
+    for (auto & sig : sigs)
+        if (checkSignature(publicKeys, sig))
+            good++;
+    return good;
+}
+
+
+bool ValidPathInfo::checkSignature(const PublicKeys & publicKeys, const std::string & sig) const
+{
+    return verifyDetached(fingerprint(), sig, publicKeys);
+}
+
+
 }
 
 
diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh
index 1a7440148322..ae5631ba0b7c 100644
--- a/src/libstore/store-api.hh
+++ b/src/libstore/store-api.hh
@@ -2,6 +2,7 @@
 
 #include "hash.hh"
 #include "serialise.hh"
+#include "crypto.hh"
 
 #include <string>
 #include <limits>
@@ -95,8 +96,8 @@ struct ValidPathInfo
     Hash narHash;
     PathSet references;
     time_t registrationTime = 0;
-    unsigned long long narSize = 0; // 0 = unknown
-    unsigned long long id; // internal use only
+    uint64_t narSize = 0; // 0 = unknown
+    uint64_t id; // internal use only
 
     /* Whether the path is ultimately trusted, that is, it was built
        locally or is content-addressable (e.g. added via addToStore()
@@ -112,6 +113,23 @@ struct ValidPathInfo
             && narHash == i.narHash
             && references == i.references;
     }
+
+    /*  Return a fingerprint of the store path to be used in binary
+        cache signatures. It contains the store path, the base-32
+        SHA-256 hash of the NAR serialisation of the path, the size of
+        the NAR, and the sorted references. The size field is strictly
+        speaking superfluous, but might prevent endless/excessive data
+        attacks. */
+    std::string fingerprint() const;
+
+    void sign(const SecretKey & secretKey);
+
+    /* Return the number of signatures on this .narinfo that were
+       produced by one of the specified keys. */
+    unsigned int checkSignatures(const PublicKeys & publicKeys) const;
+
+    /* Verify a single signature. */
+    bool checkSignature(const PublicKeys & publicKeys, const std::string & sig) const;
 };
 
 typedef list<ValidPathInfo> ValidPathInfos;
@@ -130,7 +148,6 @@ struct BuildResult
         InputRejected,
         OutputRejected,
         TransientFailure, // possibly transient
-        CachedFailure,
         TimedOut,
         MiscFailure,
         DependencyFailed,
@@ -223,7 +240,7 @@ public:
         const PathSet & references, bool repair = false) = 0;
 
     /* Write a NAR dump of a store path. */
-    virtual void dumpPath(const Path & path, Sink & sink) = 0;
+    virtual void narFromPath(const Path & path, Sink & sink) = 0;
 
     /* Export a store path, that is, create a NAR dump of the store
        path and append its references and its deriver.  Optionally, a
@@ -307,13 +324,6 @@ public:
     /* Perform a garbage collection. */
     virtual void collectGarbage(const GCOptions & options, GCResults & results) = 0;
 
-    /* Return the set of paths that have failed to build.*/
-    virtual PathSet queryFailedPaths() = 0;
-
-    /* Clear the "failed" status of the given paths.  The special
-       value `*' causes all failed paths to be cleared. */
-    virtual void clearFailedPaths(const PathSet & paths) = 0;
-
     /* Return a string representing information about the path that
        can be loaded into the database using `nix-store --load-db' or
        `nix-store --register-validity'. */
@@ -331,6 +341,10 @@ public:
     /* Return an object to access files in the Nix store. */
     virtual ref<FSAccessor> getFSAccessor() = 0;
 
+    /* Add signatures to the specified store path. The signatures are
+       not verified. */
+    virtual void addSignatures(const Path & storePath, const StringSet & sigs) = 0;
+
     /* Utility functions. */
 
     /* Read a derivation, after ensuring its existence through
@@ -365,7 +379,7 @@ public:
 class LocalFSStore : public Store
 {
 public:
-    void dumpPath(const Path & path, Sink & sink) override;
+    void narFromPath(const Path & path, Sink & sink) override;
     ref<FSAccessor> getFSAccessor() override;
 };
 
diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh
index 7d9bcb58a249..c10598d5d301 100644
--- a/src/libstore/worker-protocol.hh
+++ b/src/libstore/worker-protocol.hh
@@ -6,7 +6,7 @@ namespace nix {
 #define WORKER_MAGIC_1 0x6e697863
 #define WORKER_MAGIC_2 0x6478696f
 
-#define PROTOCOL_VERSION 0x10f
+#define PROTOCOL_VERSION 0x110
 #define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00)
 #define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff)
 
@@ -45,6 +45,7 @@ typedef enum {
     wopOptimiseStore = 34,
     wopVerifyStore = 35,
     wopBuildDerivation = 36,
+    wopAddSignatures = 37,
 } WorkerOp;