diff --git a/.circleci/config.yml b/.circleci/config.yml
index 14fde59987757ba88639fe78fc5b9852b750b52f..4cb017e96e66ed558b786ab5375e725c3bbe3f4e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -134,12 +134,15 @@ jobs:
           environment:
             ZKAPAUTHORIZER_HYPOTHESIS_PROFILE: "ci"
 
-  linux-tests:
+  linux-tests: &LINUX_TESTS
     docker:
       # Run in a highly Nix-capable environment.
-      - image: "nixorg/nix:circleci"
+      - image: "nixos/nix:latest"
 
     environment:
+      # CACHIX_AUTH_TOKEN is manually set in the CircleCI web UI and allows us to push to CACHIX_NAME.
+      CACHIX_NAME: "privatestorage-opensource"
+
       # Specify a revision of PrivateStorageio/nixpkgs to run against.  This
       # essentially pins the majority of the software involved in the build.
       # This revision is selected arbitrarily (it's just new enough to define
@@ -147,75 +150,18 @@ jobs:
       # somewhat current as of the time of this comment.  We can bump it to a
       # newer version when that makes sense.  Meanwhile, the platform won't
       # shift around beneath us unexpectedly.
-      NIXPKGS_REV: "730129887a84a8f84f3b78ffac7add72aeb551b6"
+      NIX_PATH: "nixpkgs=https://github.com/PrivateStorageio/nixpkgs/archive/730129887a84a8f84f3b78ffac7add72aeb551b6.tar.gz"
 
     steps:
       - run:
-          # Get NIX_PATH set for the rest of the job so that the revision of
-          # nixpkgs we selected will be used everywhere Nix pulls in software.
-          # There is no way to set an environment variable containing the
-          # value of another environment variable on CircleCI except to use
-          # the `BASE_ENV` feature as we do here.
-          name: "Setup NIX_PATH Environment Variable"
+          name: "Set up Cachix"
           command: |
-            echo "export NIX_PATH=nixpkgs=https://github.com/PrivateStorageio/nixpkgs/archive/$NIXPKGS_REV.tar.gz" >> $BASH_ENV
+            nix-env -iA nixpkgs.cachix nixpkgs.bash
+            cachix use "${CACHIX_NAME}"
+            nix path-info --all > /tmp/store-path-pre-build
 
       - "checkout"
 
-      - "run":
-          # CircleCI won't let us interpolate NIXPKGS_REV into a cache key.
-          # Only CircleCI's own environment variables or variables set via the
-          # web interface in a "context" can be interpolated into cache keys.
-          # However, we can interpolate the checksum of a file...  Since we
-          # don't care about the exact revision, we just care that a new
-          # revision gives us a new string, we can write the revision to a
-          # file and then put the checksum of that file into the cache key.
-          # This way, we don't have to maintain the nixpkgs revision in two
-          # places and risk having them desynchronize.
-          name: "Prepare For Cache Key"
-          command: |
-            echo "${NIXPKGS_REV}" > nixpkgs.rev
-
-      - restore_cache:
-          # Get all of Nix's state relating to the particular revision of
-          # nixpkgs we're using.  It will always be the same.  CircleCI
-          # artifacts and nixpkgs store objects are probably mostly hosted in
-          # the same place (S3) so there's not a lot of difference for
-          # anything that's pre-built.  For anything we end up building
-          # ourselves, though, this saves us all of the build time (less the
-          # download time).
-          #
-          # Read about caching dependencies: https://circleci.com/docs/2.0/caching/
-          name: "Restore Nix Store Paths"
-          keys:
-            # Construct cache keys that allow sharing as long as nixpkgs
-            # revision is unchanged.
-            #
-            # If nixpkgs changes then potentially a lot of cached packages for
-            # the base system will be invalidated so we may as well drop them
-            # and make a new cache with the new packages.
-            - zkapauthorizer-nix-store-v4-{{ checksum "nixpkgs.rev" }}-ourdeps
-            - zkapauthorizer-nix-store-v4-{{ checksum "nixpkgs.rev" }}-
-            - zkapauthorizer-nix-store-v4-
-
-      - run:
-          name: "Build challenge-bypass-ristretto"
-          command: |
-            # Pre-build this because doing so is somewhat memory intensive and
-            # we want to turn off concurrency for this part.  We want to be
-            # able to leave concurrency on for the rest of the build, though,
-            # where it doesn't cause problems and speeds things up.
-            nix-build --cores 1 --max-jobs 1 \
-              --arg callPackage '(import <nixpkgs> { }).callPackage' \
-              ./python-challenge-bypass-ristretto.nix
-
-      - save_cache:
-          name: "Cache Nix Store Paths"
-          when: "always"
-          key: zkapauthorizer-nix-store-v4-{{ checksum "nixpkgs.rev" }}
-          paths:
-            - "/nix"
-
       - run:
           name: "Run Test Suite"
           command: |
@@ -234,12 +180,24 @@ jobs:
               --arg collectCoverage true \
               --attr doc
 
-      - save_cache:
-          name: "Cache Nix Store Paths"
+      - run:
+          name: "Push to Cachix"
           when: "always"
-          key: zkapauthorizer-nix-store-v4-{{ checksum "nixpkgs.rev" }}-ourdeps
-          paths:
-            - "/nix"
+          command: |
+            # Cribbed from
+            # https://circleci.com/blog/managing-secrets-when-you-have-pull-requests-from-outside-contributors/
+            if [ -n "$CIRCLE_PR_NUMBER" ]; then
+              # I'm sure you're thinking "CIRCLE_PR_NUMBER must just be the
+              # number of the PR being built".  Sorry, dear reader, you have
+              # guessed poorly.  It is also conditionally set based on whether
+              # this is a PR from a fork or not.
+              #
+              # https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables
+              echo "Skipping Cachix push for forked PR."
+            else
+              # https://docs.cachix.org/continuous-integration-setup/circleci.html
+              bash -c "comm -13 <(sort /tmp/store-path-pre-build | grep -v '\.drv$') <(nix path-info --all | grep -v '\.drv$' | sort) | cachix push $CACHIX_NAME"
+            fi
 
       - run:
           name: "Report Coverage"
diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py
index dd9a9f49e15bd542aa70cbbbdaadedfde5c64bbb..487c164aa7f5cce69a2e9a66d5cbbfaa475ecd4e 100644
--- a/src/_zkapauthorizer/storage_common.py
+++ b/src/_zkapauthorizer/storage_common.py
@@ -38,7 +38,7 @@ from pyutil.mathutil import (
     div_ceil,
 )
 
-@attr.s(frozen=True)
+@attr.s(frozen=True, str=True)
 class MorePassesRequired(Exception):
     """
     Storage operations fail with ``MorePassesRequired`` when they are not
@@ -50,11 +50,11 @@ class MorePassesRequired(Exception):
     ivar int required_count: The number of valid passes which must be
         presented for the operation to be authorized.
 
-    :ivar list[int] signature_check_failed: Indices into the supplied list of
+    :ivar set[int] signature_check_failed: Indices into the supplied list of
         passes indicating passes which failed the signature check.
     """
-    valid_count = attr.ib()
-    required_count = attr.ib()
+    valid_count = attr.ib(validator=attr.validators.instance_of((int, long)))
+    required_count = attr.ib(validator=attr.validators.instance_of((int, long)))
     signature_check_failed = attr.ib(converter=frozenset)
 
 
diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py
index 1eddf1c2e2c173eda5ad4209c5a5397cf146dccb..314a0cf6ff21465769fcf3b73a8f4d456566e1c3 100644
--- a/src/_zkapauthorizer/tests/test_storage_server.py
+++ b/src/_zkapauthorizer/tests/test_storage_server.py
@@ -33,6 +33,8 @@ from testtools import (
 )
 from testtools.matchers import (
     Equals,
+    AfterPreprocessing,
+    MatchesAll,
 )
 from hypothesis import (
     given,
@@ -157,6 +159,36 @@ class ValidationResultTests(TestCase):
             ),
         )
 
+    def test_raise_for(self):
+        """
+        ``_ValidationResult.raise_for`` raises ``MorePassesRequired`` populated
+        with details of the validation and how it fell short of what was
+        required.
+        """
+        good = [0, 1, 2, 3]
+        badsig = [4]
+        required = 10
+        result = _ValidationResult(good, badsig)
+        try:
+            result.raise_for(required)
+        except MorePassesRequired as exc:
+            self.assertThat(
+                exc,
+                MatchesAll(
+                    Equals(
+                        MorePassesRequired(
+                            len(good),
+                            required,
+                            set(badsig),
+                        ),
+                    ),
+                    AfterPreprocessing(
+                        str,
+                        Equals("MorePassesRequired(valid_count=4, required_count=10, signature_check_failed=frozenset([4]))"),
+                    ),
+                ),
+            )
+
 
 class PassValidationTests(TestCase):
     """