diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 2ed966d8af44e357e789feb8bfc38bb2b92ef4c4..450e25f356083a12b1c79e1be6d5c6e3dc81049e 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -71,7 +71,10 @@ from .model import ( from .resource import ( from_configuration as resource_from_configuration, ) - +from .storage_common import ( + BYTES_PER_PASS, + get_configured_pass_value, +) from .controller import ( get_redeemer, ) @@ -134,6 +137,7 @@ class ZKAPAuthorizer(object): def get_storage_server(self, configuration, get_anonymous_storage_server): kwargs = configuration.copy() root_url = kwargs.pop(u"ristretto-issuer-root-url") + pass_value = kwargs.pop(u"pass-value", BYTES_PER_PASS) signing_key = SigningKey.decode_base64( FilePath( kwargs.pop(u"ristretto-signing-key-path"), @@ -144,7 +148,8 @@ class ZKAPAuthorizer(object): } storage_server = ZKAPAuthorizerStorageServer( get_anonymous_storage_server(), - signing_key, + pass_value=pass_value, + signing_key=signing_key, **kwargs ) return succeed( @@ -170,6 +175,7 @@ class ZKAPAuthorizer(object): return redeemer.tokens_to_passes(message, unblinded_tokens) return ZKAPAuthorizerStorageClient( + get_configured_pass_value(node_config), get_rref, get_passes, ) diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py index a51fbfd1402c74e0f95ebddbd325e99f7ea076f4..6559b732e6a1bcd67396dea3d561162f2ce31c5c 100644 --- a/src/_zkapauthorizer/_storage_client.py +++ b/src/_zkapauthorizer/_storage_client.py @@ -34,7 +34,7 @@ from allmydata.interfaces import ( ) from .storage_common import ( - BYTES_PER_PASS, + pass_value_attribute, required_passes, allocate_buckets_message, add_lease_message, @@ -92,7 +92,7 @@ class ZKAPAuthorizerStorageClient(object): _expected_remote_interface_name = ( "RIPrivacyPassAuthorizedStorageServer.tahoe.privatestorage.io" ) - + _pass_value = pass_value_attribute() _get_rref = attr.ib() _get_passes = attr.ib() @@ -148,7 +148,7 @@ class ZKAPAuthorizerStorageClient(object): "allocate_buckets", self._get_encoded_passes( allocate_buckets_message(storage_index), - required_passes(BYTES_PER_PASS, [allocated_size] * len(sharenums)), + required_passes(self._pass_value, [allocated_size] * len(sharenums)), ), storage_index, renew_secret, @@ -179,7 +179,7 @@ class ZKAPAuthorizerStorageClient(object): storage_index, None, )).values() - num_passes = required_passes(BYTES_PER_PASS, share_sizes) + num_passes = required_passes(self._pass_value, share_sizes) # print("Adding lease to {!r} with sizes {} with {} passes".format( # storage_index, # share_sizes, @@ -206,7 +206,7 @@ class ZKAPAuthorizerStorageClient(object): storage_index, None, )).values() - num_passes = required_passes(BYTES_PER_PASS, share_sizes) + num_passes = required_passes(self._pass_value, share_sizes) returnValue(( yield self._rref.callRemote( "renew_lease", @@ -265,6 +265,7 @@ class ZKAPAuthorizerStorageClient(object): ) # Determine the cost of the new storage for the operation. required_new_passes = get_required_new_passes_for_mutable_write( + self._pass_value, current_sizes, tw_vectors, ) diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index d8c747b451bd1b95ed841ace69094b92e6b2df53..a2d4b9f2c5cce038eff98184ae4859cbde8f8b81 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -86,7 +86,7 @@ from .foolscap import ( RIPrivacyPassAuthorizedStorageServer, ) from .storage_common import ( - BYTES_PER_PASS, + pass_value_attribute, required_passes, allocate_buckets_message, add_lease_message, @@ -153,6 +153,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): LEASE_PERIOD = timedelta(days=31) _original = attr.ib(validator=provides(RIStorageServer)) + _pass_value = pass_value_attribute() _signing_key = attr.ib(validator=instance_of(SigningKey)) _clock = attr.ib( validator=provides(IReactorTime), @@ -217,7 +218,12 @@ class ZKAPAuthorizerStorageServer(Referenceable): allocate_buckets_message(storage_index), passes, ) - check_pass_quantity_for_write(len(valid_passes), sharenums, allocated_size) + check_pass_quantity_for_write( + self._pass_value, + len(valid_passes), + sharenums, + allocated_size, + ) return self._original.remote_allocate_buckets( storage_index, @@ -243,6 +249,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): # print("server add_lease({}, {!r})".format(len(passes), storage_index)) valid_passes = self._validate_passes(add_lease_message(storage_index), passes) check_pass_quantity_for_lease( + self._pass_value, storage_index, valid_passes, self._original, @@ -256,6 +263,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): """ valid_passes = self._validate_passes(renew_lease_message(storage_index), passes) check_pass_quantity_for_lease( + self._pass_value, storage_index, valid_passes, self._original, @@ -324,6 +332,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): renew_leases = True required_new_passes = get_required_new_passes_for_mutable_write( + self._pass_value, current_sizes, tw_vectors, ) @@ -372,23 +381,7 @@ def has_active_lease(storage_server, storage_index, now): ) -def check_pass_quantity_for_lease(storage_index, valid_passes, storage_server): - """ - Check that the given number of passes is sufficient to add or renew a - lease for one period for the given storage index. - """ - allocated_sizes = dict( - get_share_sizes( - storage_server, - storage_index, - list(get_all_share_numbers(storage_server, storage_index)), - ), - ).values() - # print("allocated_sizes: {}".format(allocated_sizes)) - check_pass_quantity(len(valid_passes), allocated_sizes) - # print("Checked out") - -def check_pass_quantity(valid_count, share_sizes): +def check_pass_quantity(pass_value, valid_count, share_sizes): """ Check that the given number of passes is sufficient to cover leases for one period for shares of the given sizes. @@ -402,14 +395,30 @@ def check_pass_quantity(valid_count, share_sizes): :return: ``None`` if the given number of passes is sufficient. """ - required_pass_count = required_passes(BYTES_PER_PASS, share_sizes) + required_pass_count = required_passes(pass_value, share_sizes) if valid_count < required_pass_count: raise MorePassesRequired( valid_count, required_pass_count, ) -def check_pass_quantity_for_write(valid_count, sharenums, allocated_size): + +def check_pass_quantity_for_lease(pass_value, storage_index, valid_passes, storage_server): + """ + Check that the given number of passes is sufficient to add or renew a + lease for one period for the given storage index. + """ + allocated_sizes = dict( + get_share_sizes( + storage_server, + storage_index, + list(get_all_share_numbers(storage_server, storage_index)), + ), + ).values() + check_pass_quantity(pass_value, len(valid_passes), allocated_sizes) + + +def check_pass_quantity_for_write(pass_value, valid_count, sharenums, allocated_size): """ Determine if the given number of valid passes is sufficient for an attempted write. @@ -423,7 +432,7 @@ def check_pass_quantity_for_write(valid_count, sharenums, allocated_size): :return: ``None`` if the number of valid passes given is sufficient. """ - check_pass_quantity(valid_count, [allocated_size] * len(sharenums)) + check_pass_quantity(pass_value, valid_count, [allocated_size] * len(sharenums)) def get_all_share_paths(storage_server, storage_index): diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index fed05e1d489b12864512628bb60e01b0173b25b9..5e391c52c8575f4ecac9a7af0828cba811a22eef 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -27,9 +27,6 @@ from json import ( from datetime import ( datetime, ) -from base64 import ( - b64decode, -) from zope.interface import ( Interface, implementer, @@ -56,8 +53,15 @@ from ._base64 import ( urlsafe_b64decode, ) +from .validators import ( + is_base64_encoded, + has_length, + greater_than, +) + from .storage_common import ( - BYTES_PER_PASS, + pass_value_attribute, + get_configured_pass_value, required_passes, ) @@ -171,6 +175,8 @@ class VoucherStore(object): """ _log = Logger() + pass_value = pass_value_attribute() + database_path = attr.ib(validator=attr.validators.instance_of(FilePath)) now = attr.ib() @@ -196,6 +202,7 @@ class VoucherStore(object): connect=connect, ) return cls( + get_configured_pass_value(node_config), db_path, now, conn, @@ -489,7 +496,7 @@ class VoucherStore(object): :return LeaseMaintenance: A new, started lease maintenance object. """ - m = LeaseMaintenance(self.now, self._connection) + m = LeaseMaintenance(self.pass_value, self.now, self._connection) m.start() return m @@ -533,6 +540,8 @@ class LeaseMaintenance(object): the ``observe`` and ``finish`` methods to persist state about a lease maintenance run. + :ivar int _pass_value: The value of a single ZKAP in byte-months. + :ivar _now: A no-argument callable which returns a datetime giving a time to use as current. @@ -543,6 +552,7 @@ class LeaseMaintenance(object): objects, the database row id that corresponds to the started run. This is used to make sure future updates go to the right row. """ + _pass_value = pass_value_attribute() _now = attr.ib() _connection = attr.ib() _rowid = attr.ib(default=None) @@ -569,7 +579,7 @@ class LeaseMaintenance(object): """ Record a storage shares of the given sizes. """ - count = required_passes(BYTES_PER_PASS, sizes) + count = required_passes(self._pass_value, sizes) cursor.execute( """ UPDATE [lease-maintenance-spending] @@ -612,46 +622,6 @@ class LeaseMaintenanceActivity(object): # x = store.get_latest_lease_maintenance_activity() # xs.started, xs.passes_required, xs.finished -def is_base64_encoded(b64decode=b64decode): - def validate_is_base64_encoded(inst, attr, value): - try: - b64decode(value.encode("ascii")) - except (TypeError, Error): - raise TypeError( - "{name!r} must be base64 encoded unicode, (got {value!r})".format( - name=attr.name, - value=value, - ), - ) - return validate_is_base64_encoded - -def has_length(expected): - def validate_has_length(inst, attr, value): - if len(value) != expected: - raise ValueError( - "{name!r} must have length {expected}, instead has length {actual}".format( - name=attr.name, - expected=expected, - actual=len(value), - ), - ) - return validate_has_length - -def greater_than(expected): - def validate_relation(inst, attr, value): - if value > expected: - return None - - raise ValueError( - "{name!r} must be greater than {expected}, instead it was {actual}".format( - name=attr.name, - expected=expected, - actual=value, - ), - ) - return validate_relation - - @attr.s(frozen=True) class UnblindedToken(object): """ diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index 9bf9435e69e5429cf7bdf596d7e1b18fe0472da1..800f7f0aba9f736e6935bd38e80e6558ae7eb0eb 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -24,6 +24,12 @@ from base64 import ( b64encode, ) +import attr + +from .validators import ( + greater_than, +) + def _message_maker(label): def make_message(storage_index): return u"{label} {storage_index}".format( @@ -43,6 +49,21 @@ slot_testv_and_readv_and_writev_message = _message_maker(u"slot_testv_and_readv_ # submitted. BYTES_PER_PASS = 128 * 1024 +def get_configured_pass_value(node_config): + """ + Determine the configuration-specified value of a single ZKAP. + + If no value is explicitly configured, a default value is returned. The + value is read from the **pass-value** option of the ZKAPAuthorizer plugin + client section. + """ + section_name = u"storageclient.plugins.privatestorageio-zkapauthz-v1" + return int(node_config.get_config( + section=section_name, + option=u"pass-value", + default=BYTES_PER_PASS, + )) + def required_passes(bytes_per_pass, share_sizes): """ Calculate the number of passes that are required to store ``stored_bytes`` @@ -136,10 +157,13 @@ def get_implied_data_length(data_vector, new_length): return min(new_length, data_based_size) -def get_required_new_passes_for_mutable_write(current_sizes, tw_vectors): +def get_required_new_passes_for_mutable_write(pass_value, current_sizes, tw_vectors): + """ + :param int pass_value: The value of a single pass in byte-months. + """ # print("get_required_new_passes_for_mutable_write({}, {})".format(current_sizes, summarize(tw_vectors))) current_passes = required_passes( - BYTES_PER_PASS, + pass_value, current_sizes.values(), ) @@ -155,7 +179,7 @@ def get_required_new_passes_for_mutable_write(current_sizes, tw_vectors): new_sizes.update() new_passes = required_passes( - BYTES_PER_PASS, + pass_value, new_sizes.values(), ) required_new_passes = new_passes - current_passes @@ -180,3 +204,14 @@ def summarize(tw_vectors): for (sharenum, (test_vector, data_vectors, new_length)) in tw_vectors.items() } + +def pass_value_attribute(): + """ + Define an attribute for an attrs-based object which can hold a pass value. + """ + return attr.ib( + validator=attr.validators.and_( + attr.validators.instance_of((int, long)), + greater_than(0), + ), + ) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 6c2089a999b53ae1a655b824de86424144c28752..e2f8934e2ca7a551302cd7f751f12e6589a804c6 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -135,7 +135,6 @@ from ..resource import ( ) from ..storage_common import ( - BYTES_PER_PASS, required_passes, ) @@ -546,7 +545,7 @@ class UnblindedTokenTests(TestCase): total = 0 activity = root.store.start_lease_maintenance() for sizes in size_observations: - total += required_passes(BYTES_PER_PASS, sizes) + total += required_passes(root.store.pass_value, sizes) activity.observe(sizes) activity.finish() diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index e6df76c1ad9921552af6abb0667f341d76f7c57c..13d3dcf85e4192b1b2417f6a0a67117166f980af 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -68,10 +68,6 @@ from twisted.python.runtime import ( platform, ) -from ..storage_common import ( - BYTES_PER_PASS, -) - from ..model import ( StoreOpenError, NotEnoughTokens, @@ -355,8 +351,11 @@ class LeaseMaintenanceTests(TestCase): tuples( # The activity itself, in pass count integers(min_value=1, max_value=2 ** 16 - 1), - # Amount by which to trim back the share sizes - integers(min_value=0, max_value=BYTES_PER_PASS - 1), + # Amount by which to trim back the share sizes. This + # might exceed the value of a single pass but we don't + # know that value yet. We'll map it into a coherent + # range with mod inside the test. + integers(min_value=0), ), ), # How much time passes before this activity finishes @@ -382,8 +381,9 @@ class LeaseMaintenanceTests(TestCase): passes_required = 0 for (num_passes, trim_size) in sizes: passes_required += num_passes + trim_size %= store.pass_value x.observe([ - num_passes * BYTES_PER_PASS - trim_size, + num_passes * store.pass_value - trim_size, ]) now += finish_delay x.finish() diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index 48f8cfcee9ceed36b72dc9df7dd42953c1eee11f..aafecc3df38d45bab480147763b4d3319f1d64bd 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -108,7 +108,6 @@ from ..controller import ( DummyRedeemer, ) from ..storage_common import ( - BYTES_PER_PASS, required_passes, ) from .._storage_client import ( @@ -423,7 +422,7 @@ class ClientPluginTests(TestCase): store, DummyRedeemer(), # Give it enough for the allocate_buckets call below. - required_passes(BYTES_PER_PASS, [size] * len(sharenums)), + required_passes(store.pass_value, [size] * len(sharenums)), ) # Get a token inserted into the store. redeeming = controller.redeem(voucher) diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index 713a0c3e862dddfbf803e409898043f0cd562532..f2b9b6895246583018d498422e84ad2265e4eb4c 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -167,6 +167,8 @@ class ShareTests(TestCase): iteration of the test so far, probably; so make relative comparisons instead of absolute ones). """ + pass_value = 128 * 1024 + def setUp(self): super(ShareTests, self).setUp() self.canary = LocalReferenceable(None) @@ -187,10 +189,12 @@ class ShareTests(TestCase): ) self.server = ZKAPAuthorizerStorageServer( self.anonymous_storage_server, + self.pass_value, self.signing_key, ) self.local_remote_server = LocalRemote(self.server) self.client = ZKAPAuthorizerStorageClient( + self.pass_value, get_rref=lambda: self.local_remote_server, get_passes=get_passes, ) diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py index 55f4402da118c6eac3bf7717a6a690c742c3d835..88ae5a1f1294bc0679787942f7432aa7e08d2291 100644 --- a/src/_zkapauthorizer/tests/test_storage_server.py +++ b/src/_zkapauthorizer/tests/test_storage_server.py @@ -92,7 +92,6 @@ from ..api import ( MorePassesRequired, ) from ..storage_common import ( - BYTES_PER_PASS, required_passes, allocate_buckets_message, add_lease_message, @@ -107,6 +106,8 @@ class PassValidationTests(TestCase): """ Tests for pass validation performed by ``ZKAPAuthorizerStorageServer``. """ + pass_value = 128 * 1024 + @skipIf(platform.isWindows(), "Storage server is not supported on Windows") def setUp(self): super(PassValidationTests, self).setUp() @@ -119,6 +120,7 @@ class PassValidationTests(TestCase): self.signing_key = random_signing_key() self.storage_server = ZKAPAuthorizerStorageServer( self.anonymous_storage_server, + self.pass_value, self.signing_key, self.clock, ) @@ -162,7 +164,7 @@ class PassValidationTests(TestCase): required_passes = 2 share_nums = {3, 7} - allocated_size = int((required_passes * BYTES_PER_PASS) / len(share_nums)) + allocated_size = int((required_passes * self.pass_value) / len(share_nums)) storage_index = b"0123456789" renew_secret = b"x" * 32 cancel_secret = b"y" * 32 @@ -250,7 +252,7 @@ class PassValidationTests(TestCase): :param make_data_vector: A one-argument callable. It will be called with the current length of a slot share. It should return a write vector which will increase the storage requirements of that slot - share by at least BYTES_PER_PASS. + share by at least ``self.pass_value``. """ # hypothesis causes our storage server to be used many times. Clean # up between iterations. @@ -266,6 +268,7 @@ class PassValidationTests(TestCase): # print("test suite") required_pass_count = get_required_new_passes_for_mutable_write( + self.pass_value, dict.fromkeys(tw_vectors.keys(), 0), tw_vectors, ) @@ -355,7 +358,7 @@ class PassValidationTests(TestCase): test_and_write_vectors_for_shares, lambda current_length: ( [], - [(current_length, "x" * BYTES_PER_PASS)], + [(current_length, "x" * self.pass_value)], None, ), ) @@ -387,7 +390,7 @@ class PassValidationTests(TestCase): renew_secret, cancel_secret = secrets - required_count = required_passes(BYTES_PER_PASS, [allocated_size] * len(sharenums)) + required_count = required_passes(self.pass_value, [allocated_size] * len(sharenums)) # Create some shares at a slot which will require lease renewal. write_toy_shares( self.anonymous_storage_server, @@ -524,6 +527,7 @@ class PassValidationTests(TestCase): # Create an initial share to toy with. required_pass_count = get_required_new_passes_for_mutable_write( + self.pass_value, dict.fromkeys(tw_vectors.keys(), 0), tw_vectors, ) diff --git a/src/_zkapauthorizer/validators.py b/src/_zkapauthorizer/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..bd1545144b3a9ed39d10c656ccd9ebbbde549804 --- /dev/null +++ b/src/_zkapauthorizer/validators.py @@ -0,0 +1,60 @@ +# 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. + +""" +This module implements validators for ``attrs``-defined attributes. +""" + +from base64 import ( + b64decode, +) + +def is_base64_encoded(b64decode=b64decode): + def validate_is_base64_encoded(inst, attr, value): + try: + b64decode(value.encode("ascii")) + except TypeError: + raise TypeError( + "{name!r} must be base64 encoded unicode, (got {value!r})".format( + name=attr.name, + value=value, + ), + ) + return validate_is_base64_encoded + +def has_length(expected): + def validate_has_length(inst, attr, value): + if len(value) != expected: + raise ValueError( + "{name!r} must have length {expected}, instead has length {actual}".format( + name=attr.name, + expected=expected, + actual=len(value), + ), + ) + return validate_has_length + +def greater_than(expected): + def validate_relation(inst, attr, value): + if value > expected: + return None + + raise ValueError( + "{name!r} must be greater than {expected}, instead it was {actual}".format( + name=attr.name, + expected=expected, + actual=value, + ), + ) + return validate_relation