diff --git a/src/_zkapauthorizer/__init__.py b/src/_zkapauthorizer/__init__.py index 5bfc4ba480c1699f0f936d27517cc40d8430dca8..b68f4ad55a090c8032082ca2986b8163b8a7098e 100644 --- a/src/_zkapauthorizer/__init__.py +++ b/src/_zkapauthorizer/__init__.py @@ -15,6 +15,11 @@ __all__ = [ "__version__", "__revision__", + "NAME", ] +# The identifier for this plugin. This appears in URLs for resources the +# client plugin exposes, configuration files, etc. +NAME = "privatestorageio-zkapauthz-v2" + from ._version import __revision__, __version__ diff --git a/src/_zkapauthorizer/api.py b/src/_zkapauthorizer/api.py index 4076c7ca5a671762ffcd3f87228ec94965b2c097..f4bb2c21bfbbf1fcce7ca906538b1a802c677043 100644 --- a/src/_zkapauthorizer/api.py +++ b/src/_zkapauthorizer/api.py @@ -20,10 +20,6 @@ __all__ = [ "ZKAPAuthorizer", ] -# The identifier for this plugin. This appears in URLs for resources the -# client plugin exposes, configuration files, etc. -NAME = "privatestorageio-zkapauthz-v2" - from ._storage_client import ZKAPAuthorizerStorageClient from ._storage_server import LeaseRenewalRequired, ZKAPAuthorizerStorageServer from .storage_common import MorePassesRequired diff --git a/src/_zkapauthorizer/config.py b/src/_zkapauthorizer/config.py index f92cde0fa46a45dc490899a18edab5c70cf9fa2f..a3ab5882e7aef071d8117c28a4c609a068cf924c 100644 --- a/src/_zkapauthorizer/config.py +++ b/src/_zkapauthorizer/config.py @@ -21,7 +21,7 @@ from typing import Optional from allmydata.node import _Config -from .api import NAME +from . import NAME from .lease_maintenance import LeaseMaintenanceConfig diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index bccbf3d28d54d3a9cc01eb01e9b1ecea4b6180c6..dab10313767a4fef69d0b4da7dce332449a41f3f 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -290,20 +290,22 @@ class VoucherStore(object): if not isinstance(now, datetime): raise TypeError("{} returned {}, expected datetime".format(self.now, now)) + voucher_text = voucher.decode("ascii") + cursor.execute( """ SELECT [text] FROM [tokens] WHERE [voucher] = ? AND [counter] = ? """, - (voucher.decode("ascii"), counter), + (voucher_text, counter), ) rows = cursor.fetchall() if len(rows) > 0: self._log.info( "Loaded {count} random tokens for a voucher ({voucher}[{counter}]).", count=len(rows), - voucher=voucher, + voucher=voucher_text, counter=counter, ) tokens = list( @@ -314,14 +316,14 @@ class VoucherStore(object): self._log.info( "Persisting {count} random tokens for a voucher ({voucher}[{counter}]).", count=len(tokens), - voucher=voucher.decode("ascii"), + voucher=voucher_text, counter=counter, ) cursor.execute( """ INSERT OR IGNORE INTO [vouchers] ([number], [expected-tokens], [created]) VALUES (?, ?, ?) """, - (voucher.decode("ascii"), expected_tokens, self.now()), + (voucher_text, expected_tokens, self.now()), ) cursor.executemany( """ @@ -329,7 +331,7 @@ class VoucherStore(object): """, list( ( - voucher.decode("ascii"), + voucher_text, counter, token.token_value.decode("ascii"), ) @@ -392,11 +394,13 @@ class VoucherStore(object): token_count_increase = 0 sequestered_count_increase = len(unblinded_tokens) + voucher_text = voucher.decode("ascii") + cursor.execute( """ INSERT INTO [redemption-groups] ([voucher], [public-key], [spendable]) VALUES (?, ?, ?) """, - (voucher.decode("ascii"), public_key, spendable), + (voucher_text, public_key, spendable), ) group_id = cursor.lastrowid @@ -422,7 +426,7 @@ class VoucherStore(object): token_count_increase, sequestered_count_increase, self.now(), - voucher.decode("ascii"), + voucher_text, ), ) if cursor.rowcount == 0: @@ -430,6 +434,14 @@ class VoucherStore(object): "Cannot insert tokens for unknown voucher; add voucher first" ) + cursor.execute( + """ + SELECT [counter] FROM [vouchers] WHERE [number] = ? + """, + (voucher_text,), + ) + (new_counter,) = cursor.fetchone() + cursor.executemany( """ INSERT INTO [unblinded-tokens] ([token], [redemption-group]) VALUES (?, ?) @@ -439,6 +451,19 @@ class VoucherStore(object): for token in unblinded_tokens ), ) + self._delete_corresponding_tokens(cursor, voucher_text, new_counter - 1) + + def _delete_corresponding_tokens(self, cursor, voucher: str, counter: int) -> None: + """ + Delete rows from the [tokens] table corresponding to the given redemption + group. + """ + cursor.execute( + """ + DELETE FROM [tokens] WHERE [voucher] = ? AND [counter] = ? + """, + (voucher, counter), + ) @with_cursor def mark_voucher_double_spent(self, cursor, voucher): @@ -496,7 +521,7 @@ class VoucherStore(object): of tokens available to be spent. In this case, all tokens remain available to future calls and do not need to be reset. - :return list[UnblindedTokens]: The removed unblinded tokens. + :return list[UnblindedToken]: The removed unblinded tokens. """ if count > _SQLITE3_INTEGER_MAX: # An unreasonable number of tokens and also large enough to @@ -526,6 +551,17 @@ class VoucherStore(object): ) return list(UnblindedToken(t.encode("ascii")) for (t,) in texts) + @with_cursor + def count_random_tokens(self, cursor) -> int: + """ + :return: The number of random tokens present in the database. This is + usually not interesting but it is exposed so the test suite can check + invariants related to it. + """ + cursor.execute("SELECT count(1) FROM [tokens]") + (count,) = cursor.fetchone() + return count + @with_cursor def count_unblinded_tokens(self, cursor): """ diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index a06c862cb9bf0c8d3c9dd2183b6d44bbf83cdc20..d0edfb3d2ce0ff1e4e23bd55bae6edd77e86538a 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -29,10 +29,10 @@ from twisted.web.resource import ErrorPage, IResource, NoResource, Resource from twisted.web.server import NOT_DONE_YET from zope.interface import Attribute +from . import NAME from . import __version__ as _zkapauthorizer_version from ._base64 import urlsafe_b64decode from ._json import dumps_utf8 -from .api import NAME from .config import get_configured_lease_duration from .controller import PaymentController, get_redeemer from .pricecalculator import PriceCalculator diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index c8e8786776d4414675848f400e294bdbe805c2ac..175039d8807fd5580a56ab05ca5d45d7b1c8ac38 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -22,7 +22,7 @@ from typing import Callable import attr from pyutil.mathutil import div_ceil -from .api import NAME +from . import NAME from .eliot import MUTABLE_PASSES_REQUIRED from .validators import greater_than diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 45d278012d75fcfcacdd5ec3976476baa6b7d6fe..2a03c8b17ff65f3484e7f13c80c14886b6a1bb15 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -46,7 +46,7 @@ from twisted.internet.task import Clock from twisted.web.test.requesthelper import DummyRequest from zope.interface import implementer -from ..api import NAME +from .. import NAME from ..configutil import config_string_from_sections from ..lease_maintenance import LeaseMaintenanceConfig, lease_maintenance_config_to_dict from ..model import ( diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 61e741112e376d771107b90c17bdaf0ae7455e98..10f7bbcc1c8b9ef933473ad77d932fa6ff3b46a8 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -68,11 +68,11 @@ from twisted.web.http import BAD_REQUEST, NOT_FOUND, NOT_IMPLEMENTED, OK, UNAUTH from twisted.web.http_headers import Headers from twisted.web.resource import IResource, getChildForRequest +from .. import NAME from .. import __file__ as package_init_file from .. import __version__ as zkapauthorizer_version from .._base64 import urlsafe_b64decode from .._json import dumps_utf8 -from ..api import NAME from ..configutil import config_string_from_sections from ..model import ( DoubleSpend, diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 494e0af7940f1ecca5ae5fef42044a5069cfe1bf..7bd9aecb33ed096b1cc0bdf9b9707349caada4e6 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -452,6 +452,21 @@ class UnblindedTokenStateMachine(RuleBasedStateMachine): self.available += len(self.using) del self.using[:] + @invariant() + def random_token_count(self): + """ + ``VoucherStore.random_token_count`` returns ``0``. + + The state machine currently jumps over all intermediate states where + this function could legitimately return non-zero. It might be nice to + split up the ``get_passes`` rule so that we could see other values + sometimes. + """ + self.case.assertThat( + self.configless.store.count_random_tokens(), + Equals(0), + ) + @invariant() def unblinded_token_count(self): """ diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index f1fc566d211f985b965583ea91ede11d9046e48b..1c50a993b3d5d9a485268ba8395dcb055c10c888 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -67,9 +67,9 @@ from twisted.web.resource import IResource from twisted.plugins.zkapauthorizer import storage_server +from .. import NAME from .._plugin import get_root_nodes, load_signing_key from .._storage_client import IncorrectStorageServerReference -from ..api import NAME from ..controller import DummyRedeemer, IssuerConfigurationMismatch, PaymentController from ..foolscap import RIPrivacyPassAuthorizedStorageServer from ..lease_maintenance import SERVICE_NAME, LeaseMaintenanceConfig diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py index 7b56840cbd5f175ff96debb3836dbe51fa5f1ef1..a793c37f7f7dd01c154a2b864010b87089652f04 100644 --- a/src/_zkapauthorizer/tests/test_storage_client.py +++ b/src/_zkapauthorizer/tests/test_storage_client.py @@ -36,9 +36,10 @@ from testtools.matchers import ( from testtools.twistedsupport import failed, succeeded from twisted.internet.defer import fail, succeed +from .. import NAME from .._storage_client import call_with_passes from .._storage_server import _ValidationResult -from ..api import NAME, MorePassesRequired +from ..api import MorePassesRequired from ..model import NotEnoughTokens from ..storage_common import ( get_configured_allowed_public_keys, diff --git a/src/twisted/plugins/zkapauthorizer.py b/src/twisted/plugins/zkapauthorizer.py index ed116120a6cab7542701c2a8895e27a40d4deabc..041daaeb1f520a026b07777c2e2a786767a63f1d 100644 --- a/src/twisted/plugins/zkapauthorizer.py +++ b/src/twisted/plugins/zkapauthorizer.py @@ -16,6 +16,7 @@ A drop-in to supply plugins to the Twisted plugin system. """ -from _zkapauthorizer.api import NAME, ZKAPAuthorizer +from _zkapauthorizer import NAME +from _zkapauthorizer.api import ZKAPAuthorizer storage_server = ZKAPAuthorizer(name=NAME)