diff --git a/src/_zkapauthorizer/config.py b/src/_zkapauthorizer/config.py index 064ff52220e67080b6d3dd587b0fd94190b5ae09..b30b43fbf7e3b49091ba8bc8fd4175000ee755fa 100644 --- a/src/_zkapauthorizer/config.py +++ b/src/_zkapauthorizer/config.py @@ -82,6 +82,40 @@ def lease_maintenance_from_tahoe_config(node_config): ) +def get_configured_lease_duration(node_config): + """ + Return the minimum amount of time for which a newly granted lease will + ensure data is stored. + + The actual lease duration is hard-coded in Tahoe-LAFS in many places. + However, we have local configuration that tells us when to renew a lease. + Since lease renewal discards any remaining time on a current lease and + puts a new lease period in its place, starting from the time of the + operation, the amount of time we effectively get from a lease is based on + Tahoe-LAFS' hard-coded lease duration and our own lease renewal + configuration. + + Since this function only promises to return the *minimum* time a client + can expect a lease to last, we respond with a lease time shortened by our + configuration. + + An excellent goal to pursue in the future would be to change the lease + renewal behavior in Tahoe-LAFS so that we can control the length of leases + and/or add to an existing lease instead of replacing it. The former + option would let us really configure lease durations. The latter would + let us stop worrying so much about what is lost by renewing a lease before + the last second of its validity period. + + :return int: The minimum number of seconds for which a newly acquired + lease will be valid. + """ + # See lots of places in Tahoe-LAFS, eg src/allmydata/storage/server.py + upper_bound = 31 * 24 * 60 * 60 + lease_maint_config = lease_maintenance_from_tahoe_config(node_config) + min_time_remaining = lease_maint_config.min_lease_remaining.total_seconds() + return int(upper_bound - min_time_remaining) + + def _read_duration(cfg, option, default): """ Read an integer number of seconds from the ZKAPAuthorizer section of a diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index 7b81c26579e36108a2a369271841a6f355f528b2..4f955e2c46304260d40be28a091ced5e43223511 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -33,12 +33,12 @@ from zope.interface import Attribute from . import __version__ as _zkapauthorizer_version from ._base64 import urlsafe_b64decode +from .config import get_configured_lease_duration from .controller import PaymentController, get_redeemer from .pricecalculator import PriceCalculator from .private import create_private_tree from .storage_common import ( get_configured_allowed_public_keys, - get_configured_lease_duration, get_configured_pass_value, get_configured_shares_needed, get_configured_shares_total, diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index 605abc1a59a09bda04ea09f6cadb7d0c43a78805..bbe326a292b502024eef0b77b4181977c4f2509d 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -121,17 +121,6 @@ def get_configured_pass_value(node_config): ) -def get_configured_lease_duration(node_config): - """ - Just kidding. Lease duration is hard-coded. - - :return int: The number of seconds after which a newly acquired lease will - be valid. - """ - # See lots of places in Tahoe-LAFS, eg src/allmydata/storage/server.py - return 31 * 24 * 60 * 60 - - def get_configured_allowed_public_keys(node_config): """ Read the set of allowed issuer public keys from the given configuration. diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 6cf2806d56c0f48ac6af30517bf33869284e33c9..dc8763acd552c258d9bab1db243a506b8c62f8b2 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -61,6 +61,58 @@ from ..model import ( Voucher, ) +_POSIX_EPOCH = datetime.utcfromtimestamp(0) + + +def posix_safe_datetimes(): + """ + Build datetime instances in a range that can be represented as floats + without losing microsecond precision. + """ + return datetimes( + # I don't know that time-based parts of the system break down + # before the POSIX epoch but I don't know that they work, either. + # Don't time travel with this code. + min_value=_POSIX_EPOCH, + # Once we get far enough into the future we lose the ability to + # represent a timestamp with microsecond precision in a floating point + # number, which we do with any POSIX timestamp-like API (eg + # twisted.internet.task.Clock). So don't go far enough into the + # future. Furthermore, once we don't fit into an unsigned 4 byte + # integers, we can't round-trip through all the things that expect a + # time_t. Stay back from the absolute top to give tests a little + # space to advance time, too. + max_value=datetime.utcfromtimestamp(2 ** 31), + ) + + +def posix_timestamps(): + """ + Build floats in a range that can represent time without losing microsecond + precision. + """ + return posix_safe_datetimes().map( + lambda when: (when - _POSIX_EPOCH).total_seconds(), + ) + + +def clocks(now=posix_timestamps()): + """ + Build ``twisted.internet.task.Clock`` instances set to a time built by + ``now``. + + :param now: A strategy that builds POSIX timestamps (ie, ints or floats in + the range of time_t). + """ + + def clock_at_time(when): + c = Clock() + c.advance(when) + return c + + return now.map(clock_at_time) + + # Sizes informed by # https://github.com/brave-intl/challenge-bypass-ristretto/blob/2f98b057d7f353c12b2b12d0f5ae9ad115f1d0ba/src/oprf.rs#L18-L33 @@ -237,7 +289,10 @@ def zkapauthz_configuration( ``extra_configurations``. """ - def merge(extra_configuration, allowed_public_keys): + def merge( + extra_configuration, + allowed_public_keys, + ): config = { u"default-token-count": u"32", u"allowed-public-keys": u",".join(allowed_public_keys), @@ -266,21 +321,46 @@ def client_ristrettoredeemer_configurations(): ) -def client_dummyredeemer_configurations(): +def client_dummyredeemer_configurations( + crawl_means=one_of(none(), posix_timestamps()), + crawl_ranges=one_of(none(), posix_timestamps()), + min_times_remaining=one_of(none(), posix_timestamps()), +): """ Build DummyRedeemer-using configuration values for the client-side plugin. """ + def make_lease_config(crawl_mean, crawl_range, min_time_remaining): + config = {} + if crawl_mean is not None: + # Don't allow the mean to be 0 + config["lease.crawl-interval.mean"] = str(int(crawl_mean) + 1) + if crawl_range is not None: + config["lease.crawl-interval.range"] = str(int(crawl_range)) + if min_time_remaining is not None: + config["lease.min-time-remaining"] = str(int(min_time_remaining)) + return config + def share_a_key(allowed_keys): - return zkapauthz_configuration( - just( + lease_configs = builds( + make_lease_config, + crawl_means, + crawl_ranges, + min_times_remaining, + ) + extra_config = lease_configs.map( + lambda config: config.update( { u"redeemer": u"dummy", # Pick out one of the allowed public keys so that the dummy # appears to produce usable tokens. u"issuer-public-key": next(iter(allowed_keys)), } - ), + ) + or config, + ) + return zkapauthz_configuration( + extra_config, allowed_public_keys=just(allowed_keys), ) @@ -895,58 +975,6 @@ def announcements(): ) -_POSIX_EPOCH = datetime.utcfromtimestamp(0) - - -def posix_safe_datetimes(): - """ - Build datetime instances in a range that can be represented as floats - without losing microsecond precision. - """ - return datetimes( - # I don't know that time-based parts of the system break down - # before the POSIX epoch but I don't know that they work, either. - # Don't time travel with this code. - min_value=_POSIX_EPOCH, - # Once we get far enough into the future we lose the ability to - # represent a timestamp with microsecond precision in a floating point - # number, which we do with any POSIX timestamp-like API (eg - # twisted.internet.task.Clock). So don't go far enough into the - # future. Furthermore, once we don't fit into an unsigned 4 byte - # integers, we can't round-trip through all the things that expect a - # time_t. Stay back from the absolute top to give tests a little - # space to advance time, too. - max_value=datetime.utcfromtimestamp(2 ** 31), - ) - - -def posix_timestamps(): - """ - Build floats in a range that can represent time without losing microsecond - precision. - """ - return posix_safe_datetimes().map( - lambda when: (when - _POSIX_EPOCH).total_seconds(), - ) - - -def clocks(now=posix_timestamps()): - """ - Build ``twisted.internet.task.Clock`` instances set to a time built by - ``now``. - - :param now: A strategy that builds POSIX timestamps (ie, ints or floats in - the range of time_t). - """ - - def clock_at_time(when): - c = Clock() - c.advance(when) - return c - - return now.map(clock_at_time) - - @implementer(IFilesystemNode) @attr.s(frozen=True) class _LeafNode(object): diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 954ad66d0dcf99d73bff11e75d443b2d1345dfad..b4e928c6d99231f90a79161561b97f7258a3ffd3 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -88,7 +88,6 @@ from ..pricecalculator import PriceCalculator from ..resource import NUM_TOKENS, from_configuration, get_token_count from ..storage_common import ( get_configured_allowed_public_keys, - get_configured_lease_duration, get_configured_pass_value, required_passes, ) @@ -102,6 +101,7 @@ from .strategies import ( client_nonredeemer_configurations, client_unpaidredeemer_configurations, direct_tahoe_configs, + posix_timestamps, request_paths, requests, share_parameters, @@ -746,12 +746,15 @@ class UnblindedTokenTests(TestCase): using_a_token = after(getting_initial_tokens, use_a_token) getting_tokens_after = after(using_a_token, get_tokens) + def check_tokens(before_and_after): + initial_tokens, tokens_after = before_and_after + return initial_tokens[1:] == tokens_after + self.assertThat( gatherResults([getting_initial_tokens, getting_tokens_after]), succeeded( MatchesPredicate( - lambda (initial_tokens, tokens_after): initial_tokens[1:] - == tokens_after, + check_tokens, u"initial, after (%s): initial[1:] != after", ), ), @@ -1586,30 +1589,36 @@ class CalculatePriceTests(TestCase): ) @given( - # Make the share encoding parameters easily accessible without going - # through the Tahoe-LAFS configuration. - share_parameters().flatmap( - lambda params: tuples( - just(params), - tahoe_configs(shares=just(params)), + tuples( + # Make the share encoding parameters easily accessible without + # going through the Tahoe-LAFS configuration. + share_parameters(), + # Same goes for the minimum lease time remaining configuration. + posix_timestamps().map(int), + ).flatmap( + lambda share_and_lease_time: tuples( + just(share_and_lease_time), + direct_tahoe_configs( + zkapauthz_v1_configuration=client_dummyredeemer_configurations( + min_times_remaining=just(share_and_lease_time[1]), + ), + shares=just(share_and_lease_time[0]), + ), ), ), api_auth_tokens(), lists(integers(min_value=0)), ) - def test_calculated_price( - self, encoding_params_and_get_config, api_auth_token, sizes - ): + def test_calculated_price(self, encoding_params_and_config, api_auth_token, sizes): """ A well-formed request returns the price in ZKAPs as an integer and the storage period (the minimum allowed) that they pay for. """ - encoding_params, get_config = encoding_params_and_get_config + (encoding_params, min_time_remaining), config = encoding_params_and_config shares_needed, shares_happy, shares_total = encoding_params - - config = get_config_with_api_token( - self.useFixture(TempDir()), - get_config, + add_api_token_to_config( + self.useFixture(TempDir()).join(b"tahoe"), + config, api_auth_token, ) root = root_from_config(config, datetime.now) @@ -1639,7 +1648,7 @@ class CalculatePriceTests(TestCase): Equals( { u"price": expected_price, - u"period": get_configured_lease_duration(config), + u"period": 60 * 60 * 24 * 31 - min_time_remaining, } ), ),