diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 95a45a4388485c136e00087d29db949046e84271..1f96295374e30064d0c51e9ade02bfab3cc135df 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -140,7 +140,8 @@ class ZKAPAuthorizer(object): registry=registry, ) storage_server = ZKAPAuthorizerStorageServer( - anonymous_storage_server, + # unwrap the Foolscap layer, we'll do it ourselves. + anonymous_storage_server._server, pass_value=pass_value, signing_key=signing_key, spender=spender, diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index a2c8e1f596fdab64f01706a131b9973cf50a3947..ff61dbe17f5f8faf14120aa2d01e3ac516466371 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -27,12 +27,17 @@ from functools import partial from os import listdir, stat from os.path import join from struct import calcsize, unpack -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import attr from allmydata.interfaces import RIStorageServer, TestAndWriteVectorsForShares from allmydata.storage.common import storage_index_to_dir -from allmydata.storage.immutable import ShareFile, FoolscapBucketWriter +from allmydata.storage.immutable import ( + BucketWriter, + FoolscapBucketReader, + FoolscapBucketWriter, + ShareFile, +) from allmydata.storage.lease import LeaseInfo from allmydata.storage.mutable import MutableShareFile from allmydata.storage.server import StorageServer @@ -47,6 +52,7 @@ from challenge_bypass_ristretto import ( ) from eliot import log_call, start_action from foolscap.api import Referenceable +from foolscap.ipb import IRemoteReference from prometheus_client import CollectorRegistry, Histogram from twisted.internet.defer import Deferred from twisted.internet.interfaces import IReactorTime @@ -189,7 +195,10 @@ class ZKAPAuthorizerStorageServer(Referenceable): # control it ourselves. LEASE_PERIOD = timedelta(days=31) - _original = attr.ib(validator=provides(RIStorageServer)) + # A StorageServer instance, but not validated because of the fake used in + # the test suite. + _original = attr.ib() + _pass_value = pass_value_attribute() _signing_key = attr.ib(validator=instance_of(SigningKey)) _spender = attr.ib(validator=provides(ISpender)) @@ -203,6 +212,12 @@ class ZKAPAuthorizerStorageServer(Referenceable): ) _public_key = attr.ib(init=False) _metric_spending_successes = attr.ib(init=False) + _bucket_writer_disconnect_markers: Dict[ + BucketWriter, Tuple[IRemoteReference, Any] + ] = attr.ib( + init=False, + default=attr.Factory(dict), + ) @_public_key.default def _get_public_key(self): @@ -211,6 +226,14 @@ class ZKAPAuthorizerStorageServer(Referenceable): # so that `self._signing_key` will be assigned when this runs. return PublicKey.from_signing_key(self._signing_key) + def _bucket_writer_closed(self, bw): + if bw in self._bucket_writer_disconnect_markers: + canary, disconnect_marker = self._bucket_writer_disconnect_markers.pop(bw) + canary.dontNotifyOnDisconnect(disconnect_marker) + + def __attrs_post_init__(self): + self._original.register_bucket_writer_close_handler(self._bucket_writer_closed) + def _get_spending_histogram_buckets(self): """ Create the upper bounds for the ZKAP spending histogram. @@ -253,7 +276,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): Pass-through without pass check to allow clients to learn about our version and configuration in case it helps them decide how to behave. """ - return self._original._server.get_version() + return self._original.get_version() def remote_allocate_buckets( self, @@ -302,7 +325,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): allocated_size, ) - alreadygot, bucketwriters = self._original._server.allocate_buckets( + alreadygot, bucketwriters = self._original.allocate_buckets( storage_index, renew_secret, cancel_secret, @@ -330,7 +353,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): # StorageServer.remote_allocate_buckets. for bw in bucketwriters.values(): disconnect_marker = canary.notifyOnDisconnect(bw.disconnected) - self._original._bucket_writer_disconnect_markers[bw] = ( + self._bucket_writer_disconnect_markers[bw] = ( canary, disconnect_marker, ) @@ -339,8 +362,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): validation.valid[:spent_passes], ) return alreadygot, { - k: FoolscapBucketWriter(bw) - for (k, bw) in bucketwriters.items() + k: FoolscapBucketWriter(bw) for (k, bw) in bucketwriters.items() } def remote_get_buckets(self, storage_index): @@ -348,7 +370,10 @@ class ZKAPAuthorizerStorageServer(Referenceable): Pass-through without pass check to let clients read immutable shares as long as those shares exist. """ - return self._original.remote_get_buckets(storage_index) + return { + k: FoolscapBucketReader(bucket) + for (k, bucket) in self._original.get_buckets(storage_index).items() + } def remote_add_lease(self, passes, storage_index, *a, **kw): """ @@ -364,9 +389,9 @@ class ZKAPAuthorizerStorageServer(Referenceable): self._pass_value, storage_index, validation, - self._original._server, + self._original, ) - result = self._original._server.add_lease(storage_index, *a, **kw) + result = self._original.add_lease(storage_index, *a, **kw) self._spender.mark_as_spent( self._public_key, validation.valid, @@ -379,7 +404,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): Pass-through without a pass check to let clients inform us of possible issues with the system without incurring any cost to themselves. """ - return self._original._server.advise_corrupt_share(*a, **kw) + return self._original.advise_corrupt_share(*a, **kw) def remote_share_sizes(self, storage_index_or_slot, sharenums): with start_action( @@ -387,13 +412,13 @@ class ZKAPAuthorizerStorageServer(Referenceable): storage_index_or_slot=storage_index_or_slot, ): return dict( - get_share_sizes(self._original._server, storage_index_or_slot, sharenums) + get_share_sizes(self._original, storage_index_or_slot, sharenums) ) def remote_stat_shares(self, storage_indexes_or_slots): # type: (List[bytes]) -> List[Dict[int, ShareStat]] return list( - dict(get_share_stats(self._original._server, storage_index_or_slot, None)) + dict(get_share_stats(self._original, storage_index_or_slot, None)) for storage_index_or_slot in storage_indexes_or_slots ) @@ -472,7 +497,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): # Inspect the operation to determine its price based on any # allocations. required_new_passes = get_writev_price( - self._original._server, + self._original, self._pass_value, storage_index, tw_vectors, @@ -485,7 +510,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): validation.raise_for(required_new_passes) # Perform the operation. - result = self._original._server.slot_testv_and_readv_and_writev( + result = self._original.slot_testv_and_readv_and_writev( storage_index, secrets, tw_vectors, @@ -506,7 +531,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): # difference but this only grants storage for the remainder of the # existing lease period. This results in the client being overcharged # somewhat. - add_leases_for_writev(self._original._server, storage_index, secrets, tw_vectors, now) + add_leases_for_writev(self._original, storage_index, secrets, tw_vectors, now) self._spender.mark_as_spent( self._public_key, @@ -524,7 +549,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): Pass-through without a pass check to let clients read mutable shares as long as those shares exist. """ - return self._original._server.slot_readv(*a, **kw) + return self._original.slot_readv(*a, **kw) def check_pass_quantity(pass_value, validation, share_sizes): diff --git a/src/_zkapauthorizer/tests/fixtures.py b/src/_zkapauthorizer/tests/fixtures.py index aa967e27fdcf791a14bd249f4c40af82634cfe50..a01dc6ff430eef143a73c0b0242767514b136dbb 100644 --- a/src/_zkapauthorizer/tests/fixtures.py +++ b/src/_zkapauthorizer/tests/fixtures.py @@ -19,7 +19,7 @@ Common fixtures to let the test suite focus on application logic. from base64 import b64encode import attr -from allmydata.storage.server import StorageServer, FoolscapStorageServer +from allmydata.storage.server import StorageServer from fixtures import Fixture, TempDir from twisted.internet.task import Clock from twisted.python.filepath import FilePath @@ -38,9 +38,6 @@ class AnonymousStorageServer(Fixture): :ivar backend: The protocol-agnostic storage server backend. - :ivar anonymous_foolscap_server: The Foolscap-based server wrapped around - the backend. - :ivar clock: The ``IReactorTime`` provider to supply to ``StorageServer`` for its time-checking needs. """ @@ -49,7 +46,6 @@ class AnonymousStorageServer(Fixture): tempdir: FilePath = attr.ib(default=None) backend: StorageServer = attr.ib(default=None) - anonymous_foolscap_server: FoolscapStorageServer = attr.ib(default=None) def _setUp(self): self.tempdir = FilePath(self.useFixture(TempDir()).join(u"storage")) @@ -58,9 +54,6 @@ class AnonymousStorageServer(Fixture): b"x" * 20, clock=self.clock, ) - self.anonymous_foolscap_server = FoolscapStorageServer( - self.backend, - ) @attr.s diff --git a/src/_zkapauthorizer/tests/foolscap.py b/src/_zkapauthorizer/tests/foolscap.py index 8ad3345c45c44634052bb680032c4f8c0c18ede1..dba7e9fdd965eab41304ed1c1acfaa774f2b2156 100644 --- a/src/_zkapauthorizer/tests/foolscap.py +++ b/src/_zkapauthorizer/tests/foolscap.py @@ -18,6 +18,7 @@ Testing helpers related to Foolscap. import attr from allmydata.interfaces import RIStorageServer +from allmydata.storage.server import StorageServer from foolscap.api import Any, Copyable, Referenceable, RemoteInterface from foolscap.copyable import CopyableSlicer, ICopyable from twisted.internet.defer import fail, succeed @@ -33,13 +34,19 @@ class RIEcho(RemoteInterface): return Any() +class StubStorageBackend(object): + def register_bucket_writer_close_handler(self, handler): + pass + + @implementer(RIStorageServer) -class StubStorageServer(object): - pass +@attr.s +class StubFoolscapStorageServer(object): + _server = attr.ib(default=attr.Factory(StubStorageBackend)) def get_anonymous_storage_server(): - return StubStorageServer() + return StubFoolscapStorageServer() class BrokenCopyable(Copyable): diff --git a/src/_zkapauthorizer/tests/storage_common.py b/src/_zkapauthorizer/tests/storage_common.py index a168395ca073ba197ece412a39dbcbf346c2a3f6..6538b3bc4afee1cab06b5f9b57c601eb79dbefd8 100644 --- a/src/_zkapauthorizer/tests/storage_common.py +++ b/src/_zkapauthorizer/tests/storage_common.py @@ -60,7 +60,6 @@ def write_toy_shares( cancel_secret, sharenums, size, - canary, ): """ Write some immutable shares to the given storage server. @@ -71,19 +70,17 @@ def write_toy_shares( :param bytes cancel_secret: :param set[int] sharenums: :param int size: - :param IRemoteReference canary: """ - _, allocated = storage_server.remote_allocate_buckets( + _, allocated = storage_server.allocate_buckets( storage_index, renew_secret, cancel_secret, sharenums, size, - canary=canary, ) for (sharenum, writer) in allocated.items(): - writer.remote_write(0, bytes_for_share(sharenum, size)) - writer.remote_close() + writer.write(0, bytes_for_share(sharenum, size)) + writer.close() def whitebox_write_sparse_share(sharepath, version, size, leases, now): diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index b6a8c4dace48c96dedaf29c6de6da9d8c6e898aa..2ee75aaae77a34dadf1c239c2397ceddcd852fc7 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -160,7 +160,7 @@ class ShareTests(TestCase): self.spending_recorder, spender = RecordingSpender.make() self.server = ZKAPAuthorizerStorageServer( - self.storage.anonymous_foolscap_server, + self.storage.backend, self.pass_value, self.signing_key, spender, @@ -410,13 +410,12 @@ class ShareTests(TestCase): # Create some shares to alter the behavior of the next # allocate_buckets. write_toy_shares( - self.storage.anonymous_foolscap_server, + self.storage.backend, storage_index, renew_secret, cancel_secret, existing_sharenums, size, - canary=self.canary, ) # Let some time pass so leases added after this point will look @@ -504,13 +503,12 @@ class ShareTests(TestCase): # Create a share we can toy with. write_toy_shares( - self.storage.anonymous_foolscap_server, + self.storage.backend, storage_index, add_lease_secret, cancel_secret, sharenums, size, - canary=self.canary, ) self.assertThat( @@ -592,13 +590,12 @@ class ShareTests(TestCase): when, leases, lambda storage, storage_index, sharenums, size, canary: write_toy_shares( - storage.anonymous_foolscap_server, + storage.backend, storage_index, renew_secret, cancel_secret, sharenums, size, - canary, ), ) @@ -819,13 +816,12 @@ class ShareTests(TestCase): """ # Create a share we can toy with. write_toy_shares( - self.storage.anonymous_foolscap_server, + self.storage.backend, storage_index, renew_secret, cancel_secret, {sharenum}, size, - canary=self.canary, ) self.assertThat( @@ -956,9 +952,7 @@ class ShareTests(TestCase): def leases(): return list( lease.to_mutable_data() - for lease in self.storage.backend.get_slot_leases( - storage_index - ) + for lease in self.storage.backend.get_slot_leases(storage_index) ) def write(): diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py index 055a4be53be1b01e29c21432171e21f4648ad0a0..defab2b6842a34ffc513cc9a444a61a6ea0dd890 100644 --- a/src/_zkapauthorizer/tests/test_storage_server.py +++ b/src/_zkapauthorizer/tests/test_storage_server.py @@ -19,8 +19,8 @@ Tests for ``_zkapauthorizer._storage_server``. from random import shuffle from time import time -from allmydata.storage.mutable import MutableShareFile from allmydata.interfaces import NoSpace +from allmydata.storage.mutable import MutableShareFile from challenge_bypass_ristretto import PublicKey, random_signing_key from foolscap.referenceable import LocalReferenceable from hypothesis import given, note @@ -202,7 +202,7 @@ class PassValidationTests(TestCase): self.signing_key ).encode_base64() self.storage_server = ZKAPAuthorizerStorageServer( - self.storage.anonymous_foolscap_server, + self.storage.backend, self.pass_value, self.signing_key, spender, @@ -532,13 +532,12 @@ class PassValidationTests(TestCase): ) # Create some shares at a slot which will require lease renewal. write_toy_shares( - self.storage.anonymous_foolscap_server, + self.storage.backend, storage_index, renew_secret, cancel_secret, sharenums, allocated_size, - LocalReferenceable(None), ) # Advance time to a point where the lease is expired. This simplifies @@ -806,13 +805,12 @@ class PassValidationTests(TestCase): # the subsequent `allocate_buckets` operation - but of which the # client is unaware. write_toy_shares( - self.storage.anonymous_foolscap_server, + self.storage.backend, storage_index, renew_secret, cancel_secret, existing_sharenums, size, - LocalReferenceable(None), ) # The client will present this many passes. @@ -877,13 +875,12 @@ class PassValidationTests(TestCase): ): # Create some shares at a slot which will require lease renewal. write_toy_shares( - self.storage.anonymous_foolscap_server, + self.storage.backend, storage_index, renew_secret, cancel_secret, sharenums, allocated_size, - LocalReferenceable(None), ) num_passes = required_passes( @@ -941,13 +938,12 @@ class PassValidationTests(TestCase): # Put some shares up there to target with the add_lease operation. write_toy_shares( - self.storage.anonymous_foolscap_server, + self.storage.backend, storage_index, renew_secret, cancel_secret, sharenums, allocated_size, - LocalReferenceable(None), ) num_passes = required_passes(