diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index b16473f367d5be09920f3f399c33ecdba1fc95d7..1bc586e1d65f7ba536a0dd0f7e96c33facaa2650 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -114,6 +114,21 @@ class Unpaid(Exception):
     """
 
 
+@attr.s
+class RedemptionResult(object):
+    """
+    Contain the results of an attempt to redeem a voucher for ZKAP material.
+
+    :ivar list[UnblindedToken] unblinded_tokens: The tokens which resulted
+        from the redemption.
+
+    :ivar unicode public_key: The public key which the server proved was
+        involved in the redemption process.
+    """
+    unblinded_tokens = attr.ib()
+    public_key = attr.ib()
+
+
 class IRedeemer(Interface):
     """
     An ``IRedeemer`` can exchange a voucher for one or more passes.
@@ -150,12 +165,11 @@ class IRedeemer(Interface):
         :param list[RandomToken] random_tokens: The random tokens to use in
             the redemption process.
 
-        :return: A ``Deferred`` which fires with a list of ``UnblindedToken``
-            instances on successful redemption or which fails with any error
-            to allow a retry to be made at some future point.  It may also
-            fail with an ``AlreadySpent`` error to indicate the redemption
-            server considers the voucher to have been redeemed already and
-            will not allow it to be redeemed.
+        :return: A ``Deferred`` which fires with a ``RedemptionResult``
+            instance or which fails with any error to allow a retry to be made
+            at some future point.  It may also fail with an ``AlreadySpent``
+            error to indicate the redemption server considers the voucher to
+            have been redeemed already and will not allow it to be redeemed.
         """
 
     def tokens_to_passes(message, unblinded_tokens):
@@ -290,6 +304,8 @@ class DummyRedeemer(object):
     really redeeming them, it makes up some fake ZKAPs and pretends those are
     the result.
     """
+    _public_key = attr.ib(default=None)
+
     @classmethod
     def make(cls, section_name, node_config, announcement, reactor):
         return cls()
@@ -311,10 +327,13 @@ class DummyRedeemer(object):
             unblinded_value = random_value + b"x" * (96 - len(random_value))
             return UnblindedToken(b64encode(unblinded_value).decode("ascii"))
         return succeed(
-            list(
-                dummy_unblinded_token(token)
-                for token
-                in random_tokens
+            RedemptionResult(
+                list(
+                    dummy_unblinded_token(token)
+                    for token
+                    in random_tokens
+                ),
+                self._public_key,
             ),
         )
 
@@ -483,10 +502,14 @@ class RistrettoRedeemer(object):
                 public_key,
             )
         self._log.info("Validated proof")
-        returnValue(list(
+        unblinded_tokens = list(
             UnblindedToken(token.encode_base64().decode("ascii"))
             for token
             in clients_unblinded_tokens
+        )
+        returnValue(RedemptionResult(
+            unblinded_tokens,
+            marshaled_public_key,
         ))
 
     def tokens_to_passes(self, message, unblinded_tokens):
@@ -677,14 +700,21 @@ class PaymentController(object):
         tokens = self._get_random_tokens_for_voucher(voucher, num_tokens)
         return self._perform_redeem(voucher, tokens)
 
-    def _redeemSuccess(self, voucher, unblinded_tokens):
+    def _redeemSuccess(self, voucher, result):
         """
         Update the database state to reflect that a voucher was redeemed and to
         store the resulting unblinded tokens (which can be used to construct
         passes later).
         """
-        self._log.info("Inserting redeemed unblinded tokens for a voucher ({voucher}).", voucher=voucher)
-        self.store.insert_unblinded_tokens_for_voucher(voucher, unblinded_tokens)
+        self._log.info(
+            "Inserting redeemed unblinded tokens for a voucher ({voucher}).",
+            voucher=voucher,
+        )
+        self.store.insert_unblinded_tokens_for_voucher(
+            voucher,
+            result.public_key,
+            result.unblinded_tokens,
+        )
 
     def _redeemFailure(self, voucher, reason):
         if reason.check(AlreadySpent):
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index a208fbc9cfc6e1db9a7a1b7bd8594e4da264d5fa..090411660a2873cb98769b2b68f10f86c2935718 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -233,6 +233,18 @@ def memory_connect(path, *a, **kw):
     return _connect(":memory:", *a, **kw)
 
 
+def determine_state_for_redeemed_voucher(existing_vouchers, new_voucher, now):
+    """
+    Choose a state to store in the database for a voucher which was just
+    redeemed.
+
+    This takes into account what is known about previously redeemed vouchers
+    (if any) and watches for suspicious public key changes in the redemption
+    process.
+    """
+    return u"redeemed"
+
+
 @attr.s(frozen=True)
 class VoucherStore(object):
     """
@@ -385,7 +397,7 @@ class VoucherStore(object):
         )
 
     @with_cursor
-    def insert_unblinded_tokens_for_voucher(self, cursor, voucher, unblinded_tokens):
+    def insert_unblinded_tokens_for_voucher(self, cursor, voucher, public_key, unblinded_tokens):
         """
         Store some unblinded tokens.
 
@@ -393,9 +405,32 @@ class VoucherStore(object):
             tokens.  This voucher will be marked as redeemed to indicate it
             has fulfilled its purpose and has no further use for us.
 
+        :param unicode public_key: The encoded public key for the private key
+            which was used to sign these tokens.
+
         :param list[UnblindedToken] unblinded_tokens: The unblinded tokens to
             store.
         """
+        voucher_state = determine_state_for_redeemed_voucher(
+            None,
+            None,
+            None,
+        )
+        cursor.execute(
+            """
+            UPDATE [vouchers]
+            SET [state] = ?
+              , [token-count] = ?
+              , [finished] = ?
+            WHERE [number] = ?
+            """,
+            (
+                voucher_state,
+                len(unblinded_tokens),
+                self.now(),
+                voucher,
+            ),
+        )
         cursor.executemany(
             """
             INSERT INTO [unblinded-tokens] VALUES (?)
@@ -406,16 +441,6 @@ class VoucherStore(object):
                 in unblinded_tokens
             ),
         )
-        cursor.execute(
-            """
-            UPDATE [vouchers]
-            SET [state] = "redeemed"
-              , [token-count] = ?
-              , [finished] = ?
-            WHERE [number] = ?
-            """,
-            (len(unblinded_tokens), self.now(), voucher),
-        )
 
     @with_cursor
     def mark_voucher_double_spent(self, cursor, voucher):
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 0da7adf84af23d599a7ef8f4556d9d5a28bd2c10..e640b4b65edc74b07926aefe11e681cd410f10ad 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -185,6 +185,22 @@ def node_nicknames():
     )
 
 
+def dummy_ristretto_keys():
+    """
+    Build string values which one could imagine might be Ristretto-flavored
+    PrivacyPass signing or public keys.
+
+    They're not really because they're entirely random rather than points on
+    the curve.
+    """
+    return binary(
+        min_size=32,
+        max_size=32,
+    ).map(
+        b64encode,
+    )
+
+
 def server_configurations(signing_key_path):
     """
     Build configuration values for the server-side plugin.
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 37997eecd565be664e62887ed89244aec5f1bae7..9333ed1d7b5c589e244ae77a9c6aa9e15a3b1acc 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -319,11 +319,16 @@ class RistrettoRedeemerTests(TestCase):
         self.assertThat(
             d,
             succeeded(
-                MatchesAll(
-                    AllMatch(
-                        IsInstance(UnblindedToken),
+                MatchesStructure(
+                    unblinded_tokens=MatchesAll(
+                        AllMatch(
+                            IsInstance(UnblindedToken),
+                        ),
+                        HasLength(num_tokens),
+                    ),
+                    public_key=Equals(
+                        PublicKey.from_signing_key(signing_key).encode_base64(),
                     ),
-                    HasLength(num_tokens),
                 ),
             ),
         )
@@ -431,8 +436,8 @@ class RistrettoRedeemerTests(TestCase):
             voucher,
             random_tokens,
         )
-        def unblinded_tokens_to_passes(unblinded_tokens):
-            passes = redeemer.tokens_to_passes(message, unblinded_tokens)
+        def unblinded_tokens_to_passes(result):
+            passes = redeemer.tokens_to_passes(message, result.unblinded_tokens)
             return passes
         d.addCallback(unblinded_tokens_to_passes)
 
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 00dc6eca2395628b38bc56c1d1a7ef7a20ddb321..9ea41591dfadd3eac428726d22856fead977f00b 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -96,6 +96,7 @@ from .strategies import (
     random_tokens,
     unblinded_tokens,
     posix_safe_datetimes,
+    dummy_ristretto_keys,
 )
 from .fixtures import (
     TemporaryVoucherStore,
@@ -352,13 +353,19 @@ class UnblindedTokenStoreTests(TestCase):
     """
     Tests for ``UnblindedToken``-related functionality of ``VoucherStore``.
     """
-    @given(tahoe_configs(), datetimes(), vouchers(), lists(unblinded_tokens(), unique=True))
-    def test_unblinded_tokens_round_trip(self, get_config, now, voucher_value, tokens):
+    @given(
+        tahoe_configs(),
+        datetimes(),
+        vouchers(),
+        dummy_ristretto_keys(),
+        lists(unblinded_tokens(), unique=True),
+    )
+    def test_unblinded_tokens_round_trip(self, get_config, now, voucher_value, public_key, tokens):
         """
         Unblinded tokens that are added to the store can later be retrieved.
         """
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
-        store.insert_unblinded_tokens_for_voucher(voucher_value, tokens)
+        store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, tokens)
         retrieved_tokens = store.extract_unblinded_tokens(len(tokens))
         self.expectThat(tokens, AfterPreprocessing(sorted, Equals(retrieved_tokens)))
 
@@ -370,10 +377,11 @@ class UnblindedTokenStoreTests(TestCase):
         tahoe_configs(),
         datetimes(),
         vouchers(),
+        dummy_ristretto_keys(),
         integers(min_value=1, max_value=100),
         data(),
     )
-    def test_mark_vouchers_redeemed(self, get_config, now, voucher_value, num_tokens, data):
+    def test_mark_vouchers_redeemed(self, get_config, now, voucher_value, public_key, num_tokens, data):
         """
         The voucher for unblinded tokens that are added to the store is marked as
         redeemed.
@@ -397,7 +405,7 @@ class UnblindedTokenStoreTests(TestCase):
 
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
         store.add(voucher_value, lambda: random)
-        store.insert_unblinded_tokens_for_voucher(voucher_value, unblinded)
+        store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded)
         loaded_voucher = store.get(voucher_value)
         self.assertThat(
             loaded_voucher,
@@ -437,10 +445,11 @@ class UnblindedTokenStoreTests(TestCase):
         tahoe_configs(),
         datetimes(),
         vouchers(),
+        dummy_ristretto_keys(),
         integers(min_value=1, max_value=100),
         data(),
     )
-    def test_mark_spent_vouchers_double_spent(self, get_config, now, voucher_value, num_tokens, data):
+    def test_mark_spent_vouchers_double_spent(self, get_config, now, voucher_value, public_key, num_tokens, data):
         """
         A voucher which has already been spent cannot be marked as double-spent.
         """
@@ -462,7 +471,7 @@ class UnblindedTokenStoreTests(TestCase):
         )
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
         store.add(voucher_value, lambda: random)
-        store.insert_unblinded_tokens_for_voucher(voucher_value, unblinded)
+        store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded)
         try:
             result = store.mark_voucher_double_spent(voucher_value)
         except ValueError: