diff --git a/docs/source/interface.rst b/docs/source/interface.rst index d3223d6fe60e9a9af9a009050a8a2b181816fe9d..e771e6702d743a86d9643af813f11235b57a2a18 100644 --- a/docs/source/interface.rst +++ b/docs/source/interface.rst @@ -137,32 +137,20 @@ The response is **OK** with ``application/json`` content-type response body like The elements of the list are objects like the one returned by issuing a **GET** to a child of this collection resource. -``GET /storage-plugins/privatestorageio-zkapauthz-v1/unblinded-token`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This endpoint allows an external agent to retrieve unused unblinded tokens present in the node's database. -Unblinded tokens are returned in a stable order. -This order matches the order in which tokens will be used by the system. -This endpoint accepts several query arguments: +``GET /storage-plugins/privatestorageio-zkapauthz-v1/lease-maintenance`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * limit: An integer limiting the number of unblinded tokens to retrieve. - * position: A string which can be compared against unblinded token values. - Only unblinded tokens which follow this token in the stable order are returned. +This endpoint allows an external agent to retrieve information about automatic spending for lease maintenance. This endpoint accepts no request body. The response is **OK** with ``application/json`` content-type response body like:: - { "total": <integer> - , "spendable": <integer> - , "unblinded-tokens": [<unblinded token string>, ...] + { "spendable": <integer> , "lease-maintenance-spending": <spending object> } -The value associated with ``total`` gives the total number of unblinded tokens in the node's database -(independent of any limit placed on this query). The value associated with ``spendable`` gives the number of unblinded tokens in the node's database which can actually be spent. -The value associated with ``unblinded-tokens`` gives the requested list of unblinded tokens. The ``<spending object>`` may be ``null`` if the lease maintenance process has never run. If it has run, @@ -171,22 +159,6 @@ If it has run, * ``when``: associated with an ISO8601 datetime string giving the approximate time the process ran * ``count``: associated with a number giving the number of passes which would need to be spent to renew leases on all stored objects seen during the lease maintenance activity -``POST /storage-plugins/privatestorageio-zkapauthz-v1/unblinded-token`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This endpoint allows an external agent to insert new unblinded tokens into the node's database. -This allows for restoration of previously backed-up tokens in case the node is lost. -Tokens inserted with this API will be used after any tokens already in the database and in the order they appear in the given list. - -The request body must be ``application/json`` encoded and contain an object like:: - - { "unblinded-tokens": [<unblinded token string>, ...] - } - -The response is **OK** with ``application/json`` content-type response body like:: - - { } - ``POST /storage-plugins/privatestorageio-zkapauthz-v1/calculate-price`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index fcea68b9d50b4c08fe71229a7032dc30a40cc17f..93b34d0bc4b2d7ebbdff64a4b84d2abff112b15c 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -340,7 +340,7 @@ class _LeaseMaintenanceResource(Resource): return dumps_utf8( { "total": self._store.count_unblinded_tokens(), - "lease-maintenance-spending": self._lease_maintenance_activity(), + "spending": self._lease_maintenance_activity(), } ) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 2d827a9addca8e9be652385d36fadc2bb04792aa..96c25c890854e67c4be18e703426c1a9de171f9d 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -472,301 +472,6 @@ class UnblindedTokenTests(TestCase): super(UnblindedTokenTests, self).setUp() self.useFixture(CaptureTwistedLogs()) - @given( - tahoe_configs(), - api_auth_tokens(), - vouchers(), - lists(unblinded_tokens(), unique=True, min_size=1, max_size=1000), - ) - def test_post(self, get_config, api_auth_token, voucher, unblinded_tokens): - """ - When the unblinded token collection receives a **POST**, the unblinded - tokens in the request body are inserted into the system and an OK - response is generated. - """ - config = get_config_with_api_token( - self.useFixture(TempDir()), - get_config, - api_auth_token, - ) - root = root_from_config(config, datetime.now) - agent = RequestTraversalAgent(root) - data = BytesIO( - dumps_utf8( - { - "unblinded-tokens": list( - token.unblinded_token.decode("ascii") - for token in unblinded_tokens - ) - } - ) - ) - - requesting = authorized_request( - api_auth_token, - agent, - b"POST", - b"http://127.0.0.1/unblinded-token", - data=data, - ) - self.assertThat( - requesting, - succeeded( - ok_response(headers=application_json()), - ), - ) - - stored_tokens = root.controller.store.backup()["unblinded-tokens"] - - self.assertThat( - stored_tokens, - Equals( - list( - token.unblinded_token.decode("ascii") for token in unblinded_tokens - ) - ), - ) - - @given( - tahoe_configs(), - api_auth_tokens(), - vouchers(), - maybe_extra_tokens(), - ) - def test_get(self, get_config, api_auth_token, voucher, extra_tokens): - """ - When the unblinded token collection receives a **GET**, the response is - the total number of unblinded tokens in the system, the unblinded - tokens themselves, and information about tokens spent on recent lease - maintenance activity. - """ - config = get_config_with_api_token( - self.useFixture(TempDir()), - get_config, - api_auth_token, - ) - root = root_from_config(config, datetime.now) - if extra_tokens is None: - num_tokens = 0 - else: - num_tokens = root.controller.num_redemption_groups + extra_tokens - # Put in a number of tokens with which to test. - redeeming = root.controller.redeem(voucher, num_tokens) - # Make sure the operation completed before proceeding. - self.assertThat( - redeeming, - succeeded(Always()), - ) - - agent = RequestTraversalAgent(root) - requesting = authorized_request( - api_auth_token, - agent, - b"GET", - b"http://127.0.0.1/unblinded-token", - ) - self.addDetail( - "requesting result", - text_content(f"{vars(requesting.result)}"), - ) - self.assertThat( - requesting, - succeeded_with_unblinded_tokens(num_tokens, num_tokens), - ) - - @given( - tahoe_configs(), - api_auth_tokens(), - vouchers(), - maybe_extra_tokens(), - integers(min_value=0), - ) - def test_get_limit(self, get_config, api_auth_token, voucher, extra_tokens, limit): - """ - When the unblinded token collection receives a **GET** with a **limit** - query argument, it returns no more unblinded tokens than indicated by - the limit. - """ - config = get_config_with_api_token( - self.useFixture(TempDir()), - get_config, - api_auth_token, - ) - root = root_from_config(config, datetime.now) - - if extra_tokens is None: - num_tokens = 0 - else: - num_tokens = root.controller.num_redemption_groups + extra_tokens - # Put in a number of tokens with which to test. - redeeming = root.controller.redeem(voucher, num_tokens) - # Make sure the operation completed before proceeding. - self.assertThat( - redeeming, - succeeded(Always()), - ) - - agent = RequestTraversalAgent(root) - requesting = authorized_request( - api_auth_token, - agent, - b"GET", - "http://127.0.0.1/unblinded-token?limit={}".format(limit).encode("utf-8"), - ) - self.addDetail( - "requesting result", - text_content(f"{vars(requesting.result)}"), - ) - self.assertThat( - requesting, - succeeded_with_unblinded_tokens( - num_tokens, - min(num_tokens, limit), - ), - ) - - @given( - tahoe_configs(), - api_auth_tokens(), - vouchers(), - maybe_extra_tokens(), - text(max_size=64), - ) - def test_get_position( - self, get_config, api_auth_token, voucher, extra_tokens, position - ): - """ - When the unblinded token collection receives a **GET** with a **position** - query argument, it returns all unblinded tokens which sort greater - than the position and no others. - """ - config = get_config_with_api_token( - self.useFixture(TempDir()), - get_config, - api_auth_token, - ) - root = root_from_config(config, datetime.now) - - if extra_tokens is None: - num_tokens = 0 - else: - num_tokens = root.controller.num_redemption_groups + extra_tokens - # Put in a number of tokens with which to test. - redeeming = root.controller.redeem(voucher, num_tokens) - # Make sure the operation completed before proceeding. - self.assertThat( - redeeming, - succeeded(Always()), - ) - - agent = RequestTraversalAgent(root) - requesting = authorized_request( - api_auth_token, - agent, - b"GET", - "http://127.0.0.1/unblinded-token?position={}".format( - quote(position.encode("utf-8"), safe=b""), - ).encode("utf-8"), - ) - self.addDetail( - "requesting result", - text_content(f"{vars(requesting.result)}"), - ) - self.assertThat( - requesting, - succeeded_with_unblinded_tokens_with_matcher( - num_tokens, - Equals(num_tokens), - AllMatch( - MatchesAll( - GreaterThan(position), - IsInstance(str), - ), - ), - matches_lease_maintenance_spending(), - ), - ) - - @given( - tahoe_configs(), - api_auth_tokens(), - vouchers(), - integers(min_value=1, max_value=16), - integers(min_value=1, max_value=128), - ) - def test_get_order_matches_use_order( - self, get_config, api_auth_token, voucher, num_redemption_groups, extra_tokens - ): - """ - The first unblinded token returned in a response to a **GET** request is - the first token to be used to authorize a storage request. - """ - - def after(d, f): - new_d = Deferred() - - def f_and_continue(result): - maybeDeferred(f).chainDeferred(new_d) - return result - - d.addCallback(f_and_continue) - return new_d - - def get_tokens(): - d = authorized_request( - api_auth_token, - agent, - b"GET", - b"http://127.0.0.1/unblinded-token", - ) - d.addCallback(readBody) - d.addCallback( - lambda body: loads(body)["unblinded-tokens"], - ) - return d - - def use_a_token(): - root.store.discard_unblinded_tokens( - root.store.get_unblinded_tokens(1), - ) - - config = get_config_with_api_token( - self.useFixture(TempDir()), - get_config, - api_auth_token, - ) - root = root_from_config(config, datetime.now) - - root.controller.num_redemption_groups = num_redemption_groups - num_tokens = root.controller.num_redemption_groups + extra_tokens - - # Put in a number of tokens with which to test. - redeeming = root.controller.redeem(voucher, num_tokens) - # Make sure the operation completed before proceeding. - self.assertThat( - redeeming, - succeeded(Always()), - ) - - agent = RequestTraversalAgent(root) - getting_initial_tokens = get_tokens() - 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( - check_tokens, - "initial, after (%s): initial[1:] != after", - ), - ), - ) - @given( tahoe_configs(), api_auth_tokens(), @@ -805,11 +510,11 @@ class UnblindedTokenTests(TestCase): api_auth_token, agent, b"GET", - b"http://127.0.0.1/unblinded-token", + b"http://127.0.0.1/lease-maintenance", ) d.addCallback(readBody) d.addCallback( - lambda body: loads(body)["lease-maintenance-spending"], + lambda body: loads(body)["spending"], ) self.assertThat( d, @@ -824,72 +529,10 @@ class UnblindedTokenTests(TestCase): ) -def succeeded_with_unblinded_tokens_with_matcher( - all_token_count, - match_spendable_token_count, - match_unblinded_tokens, - match_lease_maint_spending, -): - """ - :return: A matcher which matches a Deferred which fires with a response - like the one returned by the **unblinded-tokens** endpoint. - - :param int all_token_count: The expected value in the ``total`` field of - the response. - - :param match_unblinded_tokens: A matcher for the ``unblinded-tokens`` - field of the response. - - :param match_lease_maint_spending: A matcher for the - ``lease-maintenance-spending`` field of the response. - """ - return succeeded( - MatchesAll( - ok_response(headers=application_json()), - AfterPreprocessing( - json_content, - succeeded( - ContainsDict( - { - "total": Equals(all_token_count), - "spendable": match_spendable_token_count, - "unblinded-tokens": match_unblinded_tokens, - "lease-maintenance-spending": match_lease_maint_spending, - } - ), - ), - ), - ), - ) - - -def succeeded_with_unblinded_tokens(all_token_count, returned_token_count): - """ - :return: A matcher which matches a Deferred which fires with a response - like the one returned by the **unblinded-tokens** endpoint. - - :param int all_token_count: The expected value in the ``total`` field of - the response. - - :param int returned_token_count: The expected number of tokens in the - ``unblinded-tokens`` field of the response. - """ - return succeeded_with_unblinded_tokens_with_matcher( - all_token_count, - match_spendable_token_count=Equals(all_token_count), - match_unblinded_tokens=MatchesAll( - HasLength(returned_token_count), - AllMatch(IsInstance(str)), - ), - match_lease_maint_spending=matches_lease_maintenance_spending(), - ) - - def matches_lease_maintenance_spending(): """ - :return: A matcher which matches the value of the - *lease-maintenance-spending* key in the ``unblinded-tokens`` endpoint - response. + :return: A matcher which matches the value of the *spending* key in the + ``lease-maintenance`` endpoint response. """ return MatchesAny( Is(None), diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 2f740572cd9f6cec3aa327d3ff82448290ed54b2..baf83b1a976b361f73b784dddaeb07f3ec380733 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -311,88 +311,6 @@ class VoucherStoreTests(TestCase): raises(StoreOpenError), ) - @given(tahoe_configs(), vouchers(), dummy_ristretto_keys(), datetimes(), data()) - def test_spend_order_equals_backup_order( - self, get_config, voucher_value, public_key, now, data - ): - """ - Unblinded tokens returned by ``VoucherStore.backup`` appear in the same - order as they are returned by ``VoucherStore.get_unblinded_tokens``. - """ - backed_up_tokens, spent_tokens, inserted_tokens = self._spend_order_test( - get_config, voucher_value, public_key, now, data - ) - self.assertThat( - backed_up_tokens, - Equals(spent_tokens), - ) - - @given(tahoe_configs(), vouchers(), dummy_ristretto_keys(), datetimes(), data()) - def test_spend_order_equals_insert_order( - self, get_config, voucher_value, public_key, now, data - ): - """ - Unblinded tokens returned by ``VoucherStore.get_unblinded_tokens`` - appear in the same order as they were inserted. - """ - backed_up_tokens, spent_tokens, inserted_tokens = self._spend_order_test( - get_config, voucher_value, public_key, now, data - ) - self.assertThat( - spent_tokens, - Equals(inserted_tokens), - ) - - def _spend_order_test(self, get_config, voucher_value, public_key, now, data): - """ - Insert, backup, and extract some tokens. - - :param get_config: See ``tahoe_configs`` - :param unicode voucher_value: A voucher value to associate with the tokens. - :param unicode public_key: A public key to associate with inserted unblinded tokens. - :param datetime now: A time to pretend is current. - :param data: A Hypothesis data for drawing values from strategies. - - :return: A three-tuple of (backed up tokens, extracted tokens, inserted tokens). - """ - tempdir = self.useFixture(TempDir()) - nodedir = tempdir.join("node") - - config = get_config(nodedir, "tub.port") - - # Create the underlying database file. - store = VoucherStore.from_node_config(config, lambda: now) - - # Put some tokens in it that we can backup and extract - random_tokens, unblinded_tokens = paired_tokens( - data, integers(min_value=1, max_value=5) - ) - store.add(voucher_value, len(random_tokens), 0, lambda: random_tokens) - store.insert_unblinded_tokens_for_voucher( - voucher_value, - public_key, - unblinded_tokens, - completed=data.draw(booleans()), - spendable=True, - ) - - backed_up_tokens = store.backup()["unblinded-tokens"] - extracted_tokens = [] - tokens_remaining = len(unblinded_tokens) - while tokens_remaining > 0: - to_spend = data.draw(integers(min_value=1, max_value=tokens_remaining)) - extracted_tokens.extend( - token.unblinded_token.decode("ascii") - for token in store.get_unblinded_tokens(to_spend) - ) - tokens_remaining -= to_spend - - return ( - backed_up_tokens, - extracted_tokens, - list(token.unblinded_token.decode("ascii") for token in unblinded_tokens), - ) - class UnblindedTokenStateMachine(RuleBasedStateMachine): """ diff --git a/src/_zkapauthorizer/tests/test_spending.py b/src/_zkapauthorizer/tests/test_spending.py index 06f6c6d6701f18a80f591a33dbb255b4447b87c1..3a42678128782aa8b87ddcff9deec484fbde21a7 100644 --- a/src/_zkapauthorizer/tests/test_spending.py +++ b/src/_zkapauthorizer/tests/test_spending.py @@ -76,6 +76,7 @@ class PassGroupTests(TestCase): def _test_token_group_operation( self, operation, + rest_operation, matches_tokens, voucher, num_passes, @@ -108,7 +109,10 @@ class PassGroupTests(TestCase): ) group = pass_factory.get(b"message", num_passes) spent, rest = group.split(spent_indices) + + # Perform the test-specified operations on the two groups. operation(spent) + rest_operation(rest) # Verify the expected outcome of the operation using the supplied # matcher factory. @@ -126,14 +130,14 @@ class PassGroupTests(TestCase): def matches_tokens(num_passes, group): return AfterPreprocessing( - # The use of `backup` here to check is questionable. TODO: - # Straight-up query interface for tokens in different states. - lambda store: store.backup()["unblinded-tokens"], - HasLength(num_passes - len(group.passes)), + lambda store: store.count_unblinded_tokens(), + Equals(num_passes - len(group.passes)), ) return self._test_token_group_operation( lambda group: group.mark_spent(), + # Reset the other group so its tokens are counted above. + lambda group: group.reset(), matches_tokens, voucher, num_passes, @@ -150,15 +154,16 @@ class PassGroupTests(TestCase): """ def matches_tokens(num_passes, group): + expected = num_passes - len(group.passes) return AfterPreprocessing( - # The use of `backup` here to check is questionable. TODO: - # Straight-up query interface for tokens in different states. - lambda store: store.backup()["unblinded-tokens"], - HasLength(num_passes - len(group.passes)), + lambda store: store.count_unblinded_tokens(), + Equals(expected), ) return self._test_token_group_operation( lambda group: group.mark_invalid("reason"), + # Reset the rest so we can count them in our matcher. + lambda group: group.reset(), matches_tokens, voucher, num_passes, @@ -183,6 +188,9 @@ class PassGroupTests(TestCase): return self._test_token_group_operation( lambda group: group.reset(), + # Leave the other group alone so we can see what the effect of the + # above reset was. + lambda group: None, matches_tokens, voucher, num_passes,