From 5310ac76fda44a8f7efe4cae42e0e6dc09eae291 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Thu, 22 Jul 2021 15:22:33 -0400
Subject: [PATCH] Persist unspendable tokens for future inspection

Take care not to spend them, though.
---
 src/_zkapauthorizer/model.py    | 55 +++++++++++++++++++++------------
 src/_zkapauthorizer/resource.py |  2 +-
 src/_zkapauthorizer/schema.py   | 26 ++++++++++++++++
 3 files changed, 62 insertions(+), 21 deletions(-)

diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 764be81..f10552f 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -393,30 +393,33 @@ class VoucherStore(object):
             in refs
         )
 
-    def _insert_unblinded_tokens(self, cursor, unblinded_tokens):
+    def _insert_unblinded_tokens(self, cursor, unblinded_tokens, group_id):
         """
         Helper function to really insert unblinded tokens into the database.
         """
         cursor.executemany(
             """
-            INSERT INTO [unblinded-tokens] VALUES (?)
+            INSERT INTO [unblinded-tokens] ([token], [redemption-group]) VALUES (?, ?)
             """,
             list(
-                (token,)
+                (token, group_id)
                 for token
                 in unblinded_tokens
             ),
         )
 
     @with_cursor
-    def insert_unblinded_tokens(self, cursor, unblinded_tokens):
+    def insert_unblinded_tokens(self, cursor, unblinded_tokens, group_id):
         """
         Store some unblinded tokens, for example as part of a backup-restore
         process.
 
         :param list[unicode] unblinded_tokens: The unblinded tokens to store.
+
+        :param int group_id: The unique identifier of the redemption group to
+            which these tokens belong.
         """
-        self._insert_unblinded_tokens(cursor, unblinded_tokens)
+        self._insert_unblinded_tokens(cursor, unblinded_tokens, group_id)
 
     @with_cursor
     def insert_unblinded_tokens_for_voucher(self, cursor, voucher, public_key, unblinded_tokens, completed, spendable):
@@ -451,6 +454,14 @@ class VoucherStore(object):
             token_count_increase = 0
             sequestered_count_increase = len(unblinded_tokens)
 
+        cursor.execute(
+            """
+            INSERT INTO [redemption-groups] ([voucher], [spendable]) VALUES (?, ?)
+            """,
+            (voucher, spendable),
+        )
+        group_id = cursor.lastrowid
+
         cursor.execute(
             """
             UPDATE [vouchers]
@@ -474,15 +485,15 @@ class VoucherStore(object):
         if cursor.rowcount == 0:
             raise ValueError("Cannot insert tokens for unknown voucher; add voucher first")
 
-        if spendable:
-            self._insert_unblinded_tokens(
-                cursor,
-                list(
-                    t.unblinded_token
-                    for t
-                    in unblinded_tokens
-                ),
-            )
+        self._insert_unblinded_tokens(
+            cursor,
+            list(
+                t.unblinded_token
+                for t
+                in unblinded_tokens
+            ),
+            group_id,
+        )
 
     @with_cursor
     def mark_voucher_double_spent(self, cursor, voucher):
@@ -549,9 +560,11 @@ class VoucherStore(object):
 
         cursor.execute(
             """
-            SELECT [token]
-            FROM [unblinded-tokens]
-            WHERE [token] NOT IN [in-use]
+            SELECT T.[token]
+            FROM   [unblinded-tokens] AS T, [redemption-groups] AS G
+            WHERE  T.[redemption-group] = G.[rowid]
+            AND    G.[spendable] = 1
+            AND    T.[token] NOT IN [in-use]
             LIMIT ?
             """,
             (count,),
@@ -582,8 +595,10 @@ class VoucherStore(object):
         cursor.execute(
             """
             SELECT count(1)
-            FROM   [unblinded-tokens]
-            WHERE  [token] NOT IN [in-use]
+            FROM   [unblinded-tokens] AS T, [redemption-groups] AS G
+            WHERE  T.[redemption-group] = G.[rowid]
+            AND    G.[spendable] = 1
+            AND    T.[token] NOT IN [in-use]
             """,
         )
         (count,) = cursor.fetchone()
@@ -693,7 +708,7 @@ class VoucherStore(object):
         """
         cursor.execute(
             """
-            SELECT [token] FROM [unblinded-tokens]
+            SELECT [token] FROM [unblinded-tokens] ORDER BY [rowid]
             """,
         )
         tokens = cursor.fetchall()
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index cc58494..e675ed2 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -385,7 +385,7 @@ class _UnblindedTokenCollection(Resource):
         """
         application_json(request)
         unblinded_tokens = load(request.content)[u"unblinded-tokens"]
-        self._store.insert_unblinded_tokens(unblinded_tokens)
+        self._store.insert_unblinded_tokens(unblinded_tokens, group_id=0)
         return dumps({})
 
 
diff --git a/src/_zkapauthorizer/schema.py b/src/_zkapauthorizer/schema.py
index 426ca1a..2d66273 100644
--- a/src/_zkapauthorizer/schema.py
+++ b/src/_zkapauthorizer/schema.py
@@ -172,5 +172,31 @@ _UPGRADES = {
         """
         ALTER TABLE [vouchers] ADD COLUMN [sequestered-count] integer NOT NULL DEFAULT 0
         """,
+
+        """
+        -- Create a table where rows represent a single group of unblinded
+        -- tokens all redeemed together.  Some number of these rows represent
+        -- a complete redemption of a voucher.
+        CREATE TABLE [redemption-groups] (
+            -- A unique identifier for this redemption group.
+            [rowid] INTEGER PRIMARY KEY,
+
+            -- The text representation of the voucher this group is associated with.
+            [voucher] text,
+
+            -- A flag indicating whether these tokens can be spent or if
+            -- they're being held for further inspection.
+            [spendable] integer
+        )
+        """,
+
+        """
+        INSERT INTO [redemption-groups] ([voucher], [spendable])
+            SELECT DISTINCT([number]), 1 FROM [vouchers] WHERE [state] = "redeemed"
+        """,
+
+        """
+        ALTER TABLE [unblinded-tokens] ADD COLUMN [redemption-group] integer DEFAULT 0
+        """,
     ],
 }
-- 
GitLab