diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 4ba84ddd840e6a56e7cf54fd0ed6ae8bc3449f40..a1935a562ad674d8895951d19d2881977357c192 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -175,12 +175,24 @@ class ZKAPAuthorizer(object): ) - def get_client_resource(self, node_config): + def get_client_resource(self, node_config, default_token_count=None): + """ + Get an ``IZKAPRoot`` for the given node configuration. + + :param allmydata.node._Config node_config: The configuration object + for the relevant node. + + :param int default_token_count: Configure the payment controller with + a default number of tokens to request during voucher redemption. + This is only used if a number of tokens isn't specified at the + point of redemption. + """ from twisted.internet import reactor return resource_from_configuration( node_config, store=self._get_store(node_config), redeemer=self._get_redeemer(node_config, None, reactor), + default_token_count=default_token_count, ) diff --git a/src/_zkapauthorizer/_stack.py b/src/_zkapauthorizer/_stack.py new file mode 100644 index 0000000000000000000000000000000000000000..6fffe2cd73a6d241322269101f32afef7dfc0d73 --- /dev/null +++ b/src/_zkapauthorizer/_stack.py @@ -0,0 +1,47 @@ +# Copyright 2019 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextlib import ( + contextmanager, +) + +try: + from resource import ( + RLIMIT_STACK, + getrlimit, + setrlimit, + ) +except ImportError: + # Not available on Windows, unfortunately. + RLIMIT_STACK = object() + def getrlimit(which): + return (-1, -1) + def setrlimit(which, what): + pass + + + +@contextmanager +def less_limited_stack(): + """ + A context manager which removes the resource limit on stack size, to the + extent possible, for execution of the context. + + More precisely, the soft stack limit is raised to the hard limit. + """ + soft, hard = getrlimit(RLIMIT_STACK) + # We can raise the soft limit to the hard limit and no higher. + setrlimit(RLIMIT_STACK, (hard, hard)) + yield + setrlimit(RLIMIT_STACK, (soft, hard)) diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 5539cea4e03f306f07a388a57a57a07315d05e85..755397108ee0f10ef7cc8931a5818ccfba1da480 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -17,6 +17,10 @@ This module implements controllers (in the MVC sense) for the web interface for the client side of the storage plugin. """ +from __future__ import ( + absolute_import, +) + from sys import ( exc_info, ) @@ -78,6 +82,9 @@ import privacypass from ._base64 import ( urlsafe_b64decode, ) +from ._stack import ( + less_limited_stack, +) from .model import ( RandomToken, @@ -90,9 +97,6 @@ from .model import ( Error as model_Error, ) -# The number of tokens to submit with a voucher redemption. -NUM_TOKENS = 100 - RETRY_INTERVAL = timedelta(minutes=3) class AlreadySpent(Exception): @@ -442,7 +446,12 @@ class RistrettoRedeemer(object): elif reason == u"unpaid": raise Unpaid(voucher) - self._log.info("Redeemed: {public-key} {proof} {signatures}", **result) + self._log.info( + "Redeemed: {public_key} {proof} {count}", + public_key=result[u"public-key"], + proof=result[u"proof"], + count=len(result[u"signatures"]), + ) marshaled_signed_tokens = result[u"signatures"] marshaled_proof = result[u"proof"] @@ -451,6 +460,7 @@ class RistrettoRedeemer(object): public_key = privacypass.PublicKey.decode_base64( marshaled_public_key.encode("ascii"), ) + self._log.info("Decoded public key") clients_signed_tokens = list( privacypass.SignedToken.decode_base64( marshaled_signed_token.encode("ascii"), @@ -458,15 +468,19 @@ class RistrettoRedeemer(object): for marshaled_signed_token in marshaled_signed_tokens ) + self._log.info("Decoded signed tokens") clients_proof = privacypass.BatchDLEQProof.decode_base64( marshaled_proof.encode("ascii"), ) - clients_unblinded_tokens = clients_proof.invalid_or_unblind( - random_tokens, - blinded_tokens, - clients_signed_tokens, - public_key, - ) + with less_limited_stack(): + self._log.info("Decoded batch proof") + clients_unblinded_tokens = clients_proof.invalid_or_unblind( + random_tokens, + blinded_tokens, + clients_signed_tokens, + public_key, + ) + self._log.info("Validated proof") returnValue(list( UnblindedToken(token.encode_base64().decode("ascii")) for token @@ -530,6 +544,10 @@ class PaymentController(object): the voucher. The data store marks the voucher as redeemed and stores the unblinded tokens for use by the storage client. + :ivar int default_token_count: The number of tokens to request when + redeeming a voucher, if no other count is given when the redemption is + started. + :ivar dict[unicode, datetime] _active: A mapping from voucher identifiers which currently have redemption attempts in progress to timestamps when the attempt began. @@ -546,6 +564,7 @@ class PaymentController(object): store = attr.ib() redeemer = attr.ib() + default_token_count = attr.ib() _clock = attr.ib( default=attr.Factory(partial(namedAny, "twisted.internet.reactor")), @@ -595,13 +614,13 @@ class PaymentController(object): for voucher in vouchers: if voucher.state.should_start_redemption(): self._log.info( - "Controller found voucher ({}) at startup that needs redemption.", + "Controller found voucher ({voucher}) at startup that needs redemption.", voucher=voucher.number, ) self.redeem(voucher.number) else: self._log.info( - "Controller found voucher ({}) at startup that does not need redemption.", + "Controller found voucher ({voucher}) at startup that does not need redemption.", voucher=voucher.number, ) @@ -633,7 +652,7 @@ class PaymentController(object): self._log.info("Generating random tokens for a voucher ({voucher}).", voucher=voucher) tokens = self.redeemer.random_tokens_for_voucher(Voucher(voucher), num_tokens) - self._log.info("Persistenting random tokens for a voucher ({voucher}).", voucher=voucher) + self._log.info("Persisting random tokens for a voucher ({voucher}).", voucher=voucher) self.store.add(voucher, tokens) # XXX If the voucher is already in the store then the tokens passed to @@ -659,7 +678,7 @@ class PaymentController(object): # number of passes that can be constructed is still only the size of # the set of random tokens. if num_tokens is None: - num_tokens = NUM_TOKENS + num_tokens = self.default_token_count tokens = self._get_random_tokens_for_voucher(voucher, num_tokens) return self._perform_redeem(voucher, tokens) diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index 6c4931ca2cd08d64856f21b43bbbf37b6b35765d..79c8d3146e82c3d42b5c5a9f404ee8496eaafd85 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -58,6 +58,10 @@ from .controller import ( get_redeemer, ) +# The number of tokens to submit with a voucher redemption. +NUM_TOKENS = 512000 + + class IZKAPRoot(IResource): """ The root of the resource tree of this plugin's client web presence. @@ -66,13 +70,12 @@ class IZKAPRoot(IResource): controller = Attribute("The ``PaymentController`` used by this resource tree.") -def from_configuration(node_config, store, redeemer=None): +def from_configuration(node_config, store, redeemer=None, default_token_count=None): """ Instantiate the plugin root resource using data from its configuration - section in the Tahoe-LAFS configuration file:: - - [storageclient.plugins.privatestorageio-zkapauthz-v1] - # nothing yet + section, **storageclient.plugins.privatestorageio-zkapauthz-v1**, in the + Tahoe-LAFS configuration file. See the configuration documentation for + details of the configuration section. :param _Config node_config: An object representing the overall node configuration. The plugin configuration can be extracted from this. @@ -94,7 +97,9 @@ def from_configuration(node_config, store, redeemer=None): None, None, ) - controller = PaymentController(store, redeemer) + if default_token_count is None: + default_token_count = NUM_TOKENS + controller = PaymentController(store, redeemer, default_token_count) root = Resource() root.store = store root.controller = controller diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 8a24a2c84df5b8225b0772540801bd1f5a1c59cd..f901d020fa1e23d9b26c141369996560d00a0d44 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -153,6 +153,9 @@ from .matchers import ( Provides, ) +# A small number of tokens to work with in the tests. +NUM_TOKENS = 10 + TRANSIENT_ERROR = u"something went wrong, who knows what" # Helper to work-around https://github.com/twisted/treq/issues/161 @@ -248,6 +251,7 @@ def root_from_config(config, now): now, memory_connect, ), + default_token_count=NUM_TOKENS, ) @@ -763,9 +767,7 @@ class VoucherTests(TestCase): created=Equals(now), state=Equals(Redeemed( finished=now, - # Value duplicated from PaymentController.redeem default. - # Should do this better. - token_count=100, + token_count=NUM_TOKENS, )), ), ) @@ -910,10 +912,7 @@ class VoucherTests(TestCase): created=now, state=Redeemed( finished=now, - # Value duplicated from - # PaymentController.redeem - # default. Should do this better. - token_count=100, + token_count=NUM_TOKENS, ), ).marshal() for voucher diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index dec0176991e75bc905b725fd6a08ba165c4b1d3c..0dd8bdd7d982f14eea28abfbe27adb34cc81bd7a 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -139,6 +139,7 @@ class PaymentControllerTests(TestCase): controller = PaymentController( store, NonRedeemer(), + default_token_count=10, ) controller.redeem(voucher) @@ -157,6 +158,7 @@ class PaymentControllerTests(TestCase): controller = PaymentController( store, DummyRedeemer(), + default_token_count=10, ) controller.redeem(voucher) @@ -165,7 +167,7 @@ class PaymentControllerTests(TestCase): persisted_voucher.state, Equals(model_Redeemed( finished=now, - token_count=100, + token_count=10, )), ) @@ -179,6 +181,7 @@ class PaymentControllerTests(TestCase): controller = PaymentController( store, DoubleSpendRedeemer(), + default_token_count=10, ) controller.redeem(voucher) @@ -204,6 +207,7 @@ class PaymentControllerTests(TestCase): unpaid_controller = PaymentController( store, UnpaidRedeemer(), + default_token_count=10, ) unpaid_controller.redeem(voucher) @@ -219,6 +223,7 @@ class PaymentControllerTests(TestCase): success_controller = PaymentController( store, DummyRedeemer(), + default_token_count=10, ) self.assertThat( @@ -247,7 +252,8 @@ class PaymentControllerTests(TestCase): controller = PaymentController( store, UnpaidRedeemer(), - clock, + default_token_count=10, + clock=clock, ) controller.redeem(voucher) # It fails this time. diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index e9c8eecc3ce3290ca8bafb2f286b600442f47c6a..41c2dd43f4b3bd240ec9bbf852fe19e93c328de0 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -461,7 +461,7 @@ class ClientResourceTests(TestCase): nodedir = tempdir.join(b"node") config = get_config(nodedir, b"tub.port") self.assertThat( - storage_server.get_client_resource(config), + storage_server.get_client_resource(config, default_token_count=10), Provides([IResource]), )