diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index ee57951537cb0800d74412e9e8579f0a7ac279df..4e19a3973db20994f044f5cf6b199a87479a6723 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -45,11 +45,6 @@ from twisted.internet.defer import ( succeed, ) -from eliot import ( - MessageType, - Field, -) - from allmydata.interfaces import ( IFoolscapStoragePlugin, IAnnounceableStorageServer, @@ -69,6 +64,10 @@ from .api import ( ZKAPAuthorizerStorageClient, ) +from .eliot import ( + GET_PASSES, +) + from .model import ( VoucherStore, ) @@ -91,24 +90,6 @@ from .lease_maintenance import ( _log = Logger() -PRIVACYPASS_MESSAGE = Field( - u"message", - unicode, - u"The PrivacyPass request-binding data associated with a pass.", -) - -PASS_COUNT = Field( - u"count", - int, - u"A number of passes.", -) - -GET_PASSES = MessageType( - u"zkapauthorizer:get-passes", - [PRIVACYPASS_MESSAGE, PASS_COUNT], - u"Passes are being spent.", -) - @implementer(IAnnounceableStorageServer) @attr.s class AnnounceableStorageServer(object): diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py index 6559b732e6a1bcd67396dea3d561162f2ce31c5c..2c5a7af7393eb6677b86e99222f5d5131d3e94f8 100644 --- a/src/_zkapauthorizer/_storage_client.py +++ b/src/_zkapauthorizer/_storage_client.py @@ -20,20 +20,41 @@ This is the client part of a storage access protocol. The server part is implemented in ``_storage_server.py``. """ +from __future__ import ( + absolute_import, +) + +from functools import ( + partial, + wraps, +) + import attr from zope.interface import ( implementer, ) + +from eliot.twisted import ( + DeferredContext, +) + from twisted.internet.defer import ( inlineCallbacks, returnValue, + maybeDeferred, ) from allmydata.interfaces import ( IStorageServer, ) +from .eliot import ( + SIGNATURE_CHECK_FAILED, + CALL_WITH_PASSES, +) + from .storage_common import ( + MorePassesRequired, pass_value_attribute, required_passes, allocate_buckets_message, @@ -44,7 +65,6 @@ from .storage_common import ( get_required_new_passes_for_mutable_write, ) - class IncorrectStorageServerReference(Exception): """ A Foolscap remote object which should reference a ZKAPAuthorizer storage @@ -64,6 +84,71 @@ class IncorrectStorageServerReference(Exception): ) +def call_with_passes(method, num_passes, get_passes): + """ + Call a method, passing the requested number of passes as the first + argument, and try again if the call fails with an error related to some of + the passes being rejected. + + :param method: A callable which accepts a list of encoded passes as its + only argument and returns a ``Deferred``. If the ``Deferred`` fires + with ``MorePassesRequired`` then the invalid passes will be discarded + and replacement passes will be requested for a new call of ``method``. + This will repeat until no passes remain, the method succeeds, or the + methods fails in a different way. + + :param int num_passes: The number of passes to pass to the call. + + :param (unicode -> int -> [bytes]) get_passes: A function for getting + passes. + + :return: Whatever ``method`` returns. + """ + def get_more_passes(reason): + reason.trap(MorePassesRequired) + num_failed = len(reason.value.signature_check_failed) + if num_failed == 0: + # If no signature checks failed then the call just didn't supply + # enough passes. The exception tells us how many passes we should + # spend so we could try again with that number of passes but for + # now we'll just let the exception propagate. The client should + # always figure out the number of passes right on the first try so + # this case is somewhat suspicious. Err on the side of lack of + # service instead of burning extra passes. + return reason + SIGNATURE_CHECK_FAILED.log(count=num_failed) + new_passes = get_passes(num_failed) + for idx, new_pass in zip(reason.value.signature_check_failed, new_passes): + passes[idx] = new_pass + return go(passes) + + def go(passes): + # Capture the Eliot context for the errback. + d = DeferredContext(maybeDeferred(method, passes)) + d.addErrback(get_more_passes) + # Return the underlying Deferred without finishing the action. + return d.result + + with CALL_WITH_PASSES(count=num_passes).context(): + passes = get_passes(num_passes) + # Finish the Eliot action when this is done. + return DeferredContext(go(passes)).addActionFinish() + + +def with_rref(f): + """ + Decorate a function so that it automatically receives a + ``RemoteReference`` as its first argument when called. + + The ``RemoteReference`` is retrieved by calling ``_rref`` on the first + argument passed to the function (expected to be ``self``). + """ + @wraps(f) + def g(self, *args, **kwargs): + return f(self, self._rref(), *args, **kwargs) + return g + + @implementer(IStorageServer) @attr.s class ZKAPAuthorizerStorageClient(object): @@ -96,7 +181,6 @@ class ZKAPAuthorizerStorageClient(object): _get_rref = attr.ib() _get_passes = attr.ib() - @property def _rref(self): rref = self._get_rref() # rref provides foolscap.ipb.IRemoteReference but in practice it is a @@ -130,13 +214,16 @@ class ZKAPAuthorizerStorageClient(object): in self._get_passes(message.encode("utf-8"), count) ) - def get_version(self): - return self._rref.callRemote( + @with_rref + def get_version(self, rref): + return rref.callRemote( "get_version", ) + @with_rref def allocate_buckets( self, + rref, storage_index, renew_secret, cancel_secret, @@ -144,92 +231,107 @@ class ZKAPAuthorizerStorageClient(object): allocated_size, canary, ): - return self._rref.callRemote( - "allocate_buckets", - self._get_encoded_passes( - allocate_buckets_message(storage_index), - required_passes(self._pass_value, [allocated_size] * len(sharenums)), + message = allocate_buckets_message(storage_index) + num_passes = required_passes(self._pass_value, [allocated_size] * len(sharenums)) + return call_with_passes( + lambda passes: rref.callRemote( + "allocate_buckets", + passes, + storage_index, + renew_secret, + cancel_secret, + sharenums, + allocated_size, + canary, ), - storage_index, - renew_secret, - cancel_secret, - sharenums, - allocated_size, - canary, + num_passes, + partial(self._get_encoded_passes, message), ) + @with_rref def get_buckets( self, + rref, storage_index, ): - return self._rref.callRemote( + return rref.callRemote( "get_buckets", storage_index, ) @inlineCallbacks + @with_rref def add_lease( self, + rref, storage_index, renew_secret, cancel_secret, ): - share_sizes = (yield self._rref.callRemote( + share_sizes = (yield rref.callRemote( "share_sizes", storage_index, None, )).values() num_passes = required_passes(self._pass_value, share_sizes) - # print("Adding lease to {!r} with sizes {} with {} passes".format( - # storage_index, - # share_sizes, - # num_passes, - # )) - returnValue(( - yield self._rref.callRemote( + + result = yield call_with_passes( + lambda passes: rref.callRemote( "add_lease", - self._get_encoded_passes(add_lease_message(storage_index), num_passes), + passes, storage_index, renew_secret, cancel_secret, - ) - )) + ), + num_passes, + partial(self._get_encoded_passes, add_lease_message(storage_index)), + ) + returnValue(result) @inlineCallbacks + @with_rref def renew_lease( self, + rref, storage_index, renew_secret, ): - share_sizes = (yield self._rref.callRemote( + share_sizes = (yield rref.callRemote( "share_sizes", storage_index, None, )).values() num_passes = required_passes(self._pass_value, share_sizes) - returnValue(( - yield self._rref.callRemote( + + result = yield call_with_passes( + lambda passes: rref.callRemote( "renew_lease", - self._get_encoded_passes(renew_lease_message(storage_index), num_passes), + passes, storage_index, renew_secret, - ) - )) + ), + num_passes, + partial(self._get_encoded_passes, renew_lease_message(storage_index)), + ) + returnValue(result) - def stat_shares(self, storage_indexes): - return self._rref.callRemote( + @with_rref + def stat_shares(self, rref, storage_indexes): + return rref.callRemote( "stat_shares", storage_indexes, ) + @with_rref def advise_corrupt_share( self, + rref, share_type, storage_index, shnum, reason, ): - return self._rref.callRemote( + return rref.callRemote( "advise_corrupt_share", share_type, storage_index, @@ -238,15 +340,17 @@ class ZKAPAuthorizerStorageClient(object): ) @inlineCallbacks + @with_rref def slot_testv_and_readv_and_writev( self, + rref, storage_index, secrets, tw_vectors, r_vector, ): - # Non-write operations on slots are free. - passes = [] + # Read operations are free. + num_passes = 0 if has_writes(tw_vectors): # When performing writes, if we're increasing the storage @@ -258,43 +362,44 @@ class ZKAPAuthorizerStorageClient(object): # on the storage server that will give us a really good estimate # of the current size of all of the specified shares (keys of # tw_vectors). - current_sizes = yield self._rref.callRemote( + current_sizes = yield rref.callRemote( "share_sizes", storage_index, set(tw_vectors), ) # Determine the cost of the new storage for the operation. - required_new_passes = get_required_new_passes_for_mutable_write( + num_passes = get_required_new_passes_for_mutable_write( self._pass_value, current_sizes, tw_vectors, ) - # Prepare to pay it. - if required_new_passes: - passes = self._get_encoded_passes( - slot_testv_and_readv_and_writev_message(storage_index), - required_new_passes, - ) - - # Perform the operation with the passes we determined are required. - returnValue(( - yield self._rref.callRemote( + + result = yield call_with_passes( + lambda passes: rref.callRemote( "slot_testv_and_readv_and_writev", passes, storage_index, secrets, tw_vectors, r_vector, - ) - )) + ), + num_passes, + partial( + self._get_encoded_passes, + slot_testv_and_readv_and_writev_message(storage_index), + ), + ) + returnValue(result) + @with_rref def slot_readv( self, + rref, storage_index, shares, r_vector, ): - return self._rref.callRemote( + return rref.callRemote( "slot_readv", storage_index, shares, diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index 73753fe6c4d710af5e633c6ee683c3f195a034f0..bb89df0e0e07e5731120c73d41251aba54a8a98b 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -88,6 +88,7 @@ from .foolscap import ( RIPrivacyPassAuthorizedStorageServer, ) from .storage_common import ( + MorePassesRequired, pass_value_attribute, required_passes, allocate_buckets_message, @@ -102,26 +103,6 @@ from .storage_common import ( SLOT_HEADER_SIZE = 468 LEASE_TRAILER_SIZE = 4 -@attr.s -class MorePassesRequired(Exception): - """ - Storage operations fail with ``MorePassesRequired`` when they are not - accompanied by a sufficient number of valid passes. - - :ivar int valid_count: The number of valid passes presented in the - operation. - - ivar int required_count: The number of valid passes which must be - presented for the operation to be authorized. - - :ivar list[int] signature_check_failed: Indices into the supplied list of - passes indicating passes which failed the signature check. - """ - valid_count = attr.ib() - required_count = attr.ib() - signature_check_failed = attr.ib() - - @attr.s class _ValidationResult(object): """ diff --git a/src/_zkapauthorizer/api.py b/src/_zkapauthorizer/api.py index 365e39a23026e1b7692267d12c895f45cfaeda64..70e4725e5c89d503e6973e2b708cecede48bcd66 100644 --- a/src/_zkapauthorizer/api.py +++ b/src/_zkapauthorizer/api.py @@ -20,8 +20,10 @@ __all__ = [ "ZKAPAuthorizer", ] -from ._storage_server import ( +from .storage_common import ( MorePassesRequired, +) +from ._storage_server import ( LeaseRenewalRequired, ZKAPAuthorizerStorageServer, ) diff --git a/src/_zkapauthorizer/eliot.py b/src/_zkapauthorizer/eliot.py new file mode 100644 index 0000000000000000000000000000000000000000..da3960d07a9f2ef09fb112fed4dd33bae61bc545 --- /dev/null +++ b/src/_zkapauthorizer/eliot.py @@ -0,0 +1,58 @@ +# Copyright 2020 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. + +""" +Eliot field, message, and action definitions for ZKAPAuthorizer. +""" + +from __future__ import ( + absolute_import, +) + +from eliot import ( + Field, + MessageType, + ActionType, +) + +PRIVACYPASS_MESSAGE = Field( + u"message", + unicode, + u"The PrivacyPass request-binding data associated with a pass.", +) + +PASS_COUNT = Field( + u"count", + int, + u"A number of passes.", +) + +GET_PASSES = MessageType( + u"zkapauthorizer:get-passes", + [PRIVACYPASS_MESSAGE, PASS_COUNT], + u"Passes are being spent.", +) + +SIGNATURE_CHECK_FAILED = MessageType( + u"zkapauthorizer:storage-client:signature-check-failed", + [PASS_COUNT], + u"Some passes the client tried to use were rejected for having invalid signatures.", +) + +CALL_WITH_PASSES = ActionType( + u"zkapauthorizer:storage-client:call-with-passes", + [PASS_COUNT], + [], + u"A storage operation is being started which may spend some passes.", +) diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index f00997b1c6db9b240eebd7dade935c5c6dc8e917..80707f226b892c96cfa5fefd278e68b9146dc7e1 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -30,6 +30,26 @@ from .validators import ( greater_than, ) +@attr.s(frozen=True) +class MorePassesRequired(Exception): + """ + Storage operations fail with ``MorePassesRequired`` when they are not + accompanied by a sufficient number of valid passes. + + :ivar int valid_count: The number of valid passes presented in the + operation. + + ivar int required_count: The number of valid passes which must be + presented for the operation to be authorized. + + :ivar list[int] signature_check_failed: Indices into the supplied list of + passes indicating passes which failed the signature check. + """ + valid_count = attr.ib() + required_count = attr.ib() + signature_check_failed = attr.ib(converter=frozenset) + + def _message_maker(label): def make_message(storage_index): return u"{label} {storage_index}".format( diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py new file mode 100644 index 0000000000000000000000000000000000000000..77018c2928f1c7cd991093ed416e12c5ce3a4dff --- /dev/null +++ b/src/_zkapauthorizer/tests/test_storage_client.py @@ -0,0 +1,215 @@ +# Copyright 2020 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. + +""" +Tests for ``_zkapauthorizer._storage_client``. +""" + +import attr + +from itertools import ( + count, + islice, +) + +from testtools import ( + TestCase, +) +from testtools.matchers import ( + Always, + Is, + Equals, + AfterPreprocessing, +) +from testtools.twistedsupport import ( + succeeded, + failed, +) + +from hypothesis import ( + given, +) +from hypothesis.strategies import ( + integers, +) + +from twisted.internet.defer import ( + succeed, + fail, +) + +from ..api import ( + MorePassesRequired, +) + +from .._storage_client import ( + call_with_passes, +) +from .._storage_server import ( + _ValidationResult, +) + +def pass_counts(): + return integers(min_value=1, max_value=2 ** 8) + + +def pass_factory(): + return _PassFactory() + +@attr.s +class _PassFactory(object): + """ + A stateful pass issuer. + + :ivar list spent: All of the passes ever issued. + + :ivar _fountain: A counter for making each new pass issued unique. + """ + spent = attr.ib(default=attr.Factory(list)) + + _fountain = attr.ib(default=attr.Factory(count)) + + def get(self, num_passes): + passes = list(islice(self._fountain, num_passes)) + self.spent.extend(passes) + return passes + + +class CallWithPassesTests(TestCase): + """ + Tests for ``call_with_passes``. + """ + @given(pass_counts()) + def test_success_result(self, num_passes): + """ + ``call_with_passes`` returns a ``Deferred`` that fires with the same + success result as that of the ``Deferred`` returned by the method + passed in. + """ + result = object() + self.assertThat( + call_with_passes( + lambda passes: succeed(result), + num_passes, + pass_factory().get, + ), + succeeded(Is(result)), + ) + + @given(pass_counts()) + def test_failure_result(self, num_passes): + """ + ``call_with_passes`` returns a ``Deferred`` that fires with the same + failure result as that of the ``Deferred`` returned by the method + passed in if that failure is not a ``MorePassesRequired``. + """ + result = Exception() + self.assertThat( + call_with_passes( + lambda passes: fail(result), + num_passes, + pass_factory().get, + ), + failed( + AfterPreprocessing( + lambda f: f.value, + Is(result), + ), + ), + ) + + @given(pass_counts()) + def test_passes(self, num_passes): + """ + ``call_with_passes`` calls the given method with a list of passes + containing ``num_passes`` created by the function passed for + ``get_passes``. + """ + passes = pass_factory() + + self.assertThat( + call_with_passes( + lambda passes: succeed(passes), + num_passes, + passes.get, + ), + succeeded( + Equals( + passes.spent, + ), + ), + ) + + @given(pass_counts()) + def test_retry_on_rejected_passes(self, num_passes): + """ + ``call_with_passes`` tries calling the given method again with a new list + of passes, still of length ```num_passes``, but without the passes + which were rejected on the first try. + """ + passes = pass_factory() + + def reject_even_pass_values(passes): + good_passes = list(idx for (idx, p) in enumerate(passes) if p % 2) + bad_passes = list(idx for (idx, p) in enumerate(passes) if idx not in good_passes) + if len(good_passes) < num_passes: + _ValidationResult( + valid=good_passes, + signature_check_failed=bad_passes, + ).raise_for(num_passes) + return None + + self.assertThat( + call_with_passes( + reject_even_pass_values, + num_passes, + passes.get, + ), + succeeded(Always()), + ) + + @given(pass_counts()) + def test_pass_through_too_few_passes(self, num_passes): + """ + ``call_with_passes`` lets ``MorePassesRequired`` propagate through it if + no passes have been marked as invalid. This happens if all passes + given were valid but too fewer were given. + """ + passes = pass_factory() + + def reject_passes(passes): + _ValidationResult( + valid=range(len(passes)), + signature_check_failed=[], + ).raise_for(len(passes) + 1) + + self.assertThat( + call_with_passes( + reject_passes, + num_passes, + passes.get, + ), + failed( + AfterPreprocessing( + lambda f: f.value, + Equals( + MorePassesRequired( + valid_count=num_passes, + required_count=num_passes + 1, + signature_check_failed=[], + ), + ), + ), + ), + ) diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index a267c0667f3fe1b3101ab9e5a7a9eb10d8091a32..bb79fb25e919110d0454595fe55cb81203dfbac9 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -52,6 +52,7 @@ from hypothesis.strategies import ( lists, tuples, integers, + data as data_strategy, ) from twisted.python.runtime import ( @@ -111,11 +112,13 @@ from .foolscap import ( LocalRemote, ) from ..api import ( + MorePassesRequired, ZKAPAuthorizerStorageServer, ZKAPAuthorizerStorageClient, ) from ..storage_common import ( slot_testv_and_readv_and_writev_message, + allocate_buckets_message, get_implied_data_length, required_passes, ) @@ -164,6 +167,28 @@ class RequiredPassesTests(TestCase): ) +def get_passes(message, count, signing_key): + """ + :param unicode message: Request-binding message for PrivacyPass. + + :param int count: The number of passes to get. + + :param SigningKEy signing_key: The key to use to sign the passes. + + :return list[Pass]: ``count`` new random passes signed with the given key + and bound to the given message. + """ + return list( + Pass(*pass_.split(u" ")) + for pass_ + in make_passes( + signing_key, + message, + list(RandomToken.create() for n in range(count)), + ) + ) + + class ShareTests(TestCase): """ Tests for interaction with shares. @@ -182,17 +207,10 @@ class ShareTests(TestCase): self.signing_key = random_signing_key() self.spent_passes = 0 - def get_passes(message, count): + def counting_get_passes(message, count): self.spent_passes += count - return list( - Pass(*pass_.split(u" ")) - for pass_ - in make_passes( - self.signing_key, - message, - list(RandomToken.create() for n in range(count)), - ) - ) + return get_passes(message, count, self.signing_key) + self.server = ZKAPAuthorizerStorageServer( self.anonymous_storage_server, self.pass_value, @@ -202,7 +220,7 @@ class ShareTests(TestCase): self.client = ZKAPAuthorizerStorageClient( self.pass_value, get_rref=lambda: self.local_remote_server, - get_passes=get_passes, + get_passes=counting_get_passes, ) def test_get_version(self): @@ -215,6 +233,95 @@ class ShareTests(TestCase): succeeded(matches_version_dictionary()), ) + @given( + storage_index=storage_indexes(), + renew_secret=lease_renew_secrets(), + cancel_secret=lease_cancel_secrets(), + sharenums=sharenum_sets(), + size=sizes(), + data=data_strategy(), + ) + def test_rejected_passes_reported(self, storage_index, renew_secret, cancel_secret, sharenums, size, data): + """ + Any passes rejected by the storage server are reported with a + ``MorePassesRequired`` exception sent to the client. + """ + # Hypothesis causes our storage server to be used many times. Clean + # up between iterations. + cleanup_storage_server(self.anonymous_storage_server) + + num_passes = required_passes(self.pass_value, [size] * len(sharenums)) + + # Pick some passes to mess with. + bad_pass_indexes = data.draw( + lists( + integers( + min_value=0, + max_value=num_passes - 1, + ), + min_size=1, + max_size=num_passes, + unique=True, + ), + ) + + # Make some passes with a key untrusted by the server. + bad_passes = get_passes( + allocate_buckets_message(storage_index), + len(bad_pass_indexes), + random_signing_key(), + ) + + # Make some passes with a key trusted by the server. + good_passes = get_passes( + allocate_buckets_message(storage_index), + num_passes - len(bad_passes), + self.signing_key, + ) + + all_passes = [] + for i in range(num_passes): + if i in bad_pass_indexes: + all_passes.append(bad_passes.pop()) + else: + all_passes.append(good_passes.pop()) + + # Sanity checks + self.assertThat(bad_passes, Equals([])) + self.assertThat(good_passes, Equals([])) + self.assertThat(all_passes, HasLength(num_passes)) + + self.assertThat( + # Bypass the client handling of MorePassesRequired so we can see + # it. + self.local_remote_server.callRemote( + "allocate_buckets", + list( + pass_.pass_text.encode("ascii") + for pass_ + in all_passes + ), + storage_index, + renew_secret, + cancel_secret, + sharenums, + size, + canary=self.canary, + ), + failed( + AfterPreprocessing( + lambda f: f.value, + Equals( + MorePassesRequired( + valid_count=num_passes - len(bad_pass_indexes), + required_count=num_passes, + signature_check_failed=bad_pass_indexes, + ), + ), + ), + ), + ) + @given( storage_index=storage_indexes(), renew_secret=lease_renew_secrets(), @@ -814,7 +921,7 @@ class ShareTests(TestCase): # The nice Python API doesn't let you do this so we drop down to # the layer below. We also use positional arguments because they # transit the network differently from keyword arguments. Yay. - d = self.client._rref.callRemote( + d = self.local_remote_server.callRemote( "slot_testv_and_readv_and_writev", # passes self.client._get_encoded_passes(