def test_unpack_request(self): client_id = 4 req_seq_num = 1 read_only = True timeout_milli = 5000 span_context = b'span context' msg = b'hello' cid = str(req_seq_num) packed = bft_msgs.pack_request(client_id, req_seq_num, read_only, timeout_milli, cid, msg, pre_process=False, span_context=span_context) header, unpacked_span_context, unpacked_msg, unpacked_cid = bft_msgs.unpack_request( packed) self.assertEqual(len(span_context), header.span_context_size) self.assertEqual(client_id, header.client_id) self.assertEqual(1, header.flags) # read_only = True self.assertEqual(req_seq_num, header.req_seq_num) self.assertEqual(len(msg), header.length) self.assertEqual(timeout_milli, header.timeout_milli) self.assertEqual(len(cid), header.cid) self.assertEqual(span_context, unpacked_span_context) self.assertEqual(msg, unpacked_msg) self.assertEqual(cid, unpacked_cid)
async def sendSync(self, msg, read_only): """ Send a client request and wait for a quorum (2F+C+1) of replies. Return a single reply message if a quorum of replies matches. Otherwise, raise a trio.TooSlowError indicating the request timed out. Retry Strategy: If the request is a write and the primary is known then send only to the primary on the first attempt. Otherwise, if the request is read only or the primary is unknown, then send to all replicas on the first attempt. After `config.retry_timeout_milli` without receiving a quorum of identical replies, then clear the replies and send to all replicas. Continue this strategy every `retry_timeout_milli` until `config.req_timeout_milli` elapses. If `config.req_timeout_milli` elapses then a trio.TooSlowError is raised. Note that this method also binds the socket to an appropriate port if not already bound. """ if not self.sock_bound: await self.bind() seq = self.req_seq_num.next() data = bft_msgs.pack_request(self.client_id, seq, read_only, msg) # Raise a trio.TooSlowError exception if a quorum of replies with trio.fail_after(self.config.req_timeout_milli / 1000): self.reset_on_new_request() self.retries = 0 return await self.send_loop(data, read_only)
async def sendSync(self, msg, read_only, seq_num=None, cid=None, pre_process=False, m_of_n_quorum=None, \ reconfiguration=False, include_ro=False, corrupt_params={}, no_retries=False): """ Send a client request and wait for a m_of_n_quorum (if None, it will set to 2F+C+1 quorum) of replies. Return a single reply message if a quorum of replies matches. Otherwise, raise a trio.TooSlowError indicating the request timed out. Retry Strategy: If the request is a write and the primary is known then send only to the primary on the first attempt. Otherwise, if the request is read only or the primary is unknown, then send to all replicas on the first attempt. After `config.retry_timeout_milli` without receiving a quorum of identical replies, then clear the replies and send to all replicas. Continue this strategy every `retry_timeout_milli` until `config.req_timeout_milli` elapses. If `config.req_timeout_milli` elapses then a trio.TooSlowError is raised. Note that this method also binds the socket to an appropriate port if not already bound. """ # Call an abstract function to allow each client type to set-up its communication before starting if not self.comm_prepared: await self._comm_prepare() if seq_num is None: seq_num = self.req_seq_num.next() if cid is None: cid = str(seq_num) signature = b'' client_id = self.client_id if self.signing_key: h = SHA256.new(msg) signature = pkcs1_15.new(self.signing_key).sign(h) if corrupt_params: msg, signature, client_id = self._corrupt_signing_params(msg, signature, client_id, corrupt_params) data = bft_msgs.pack_request(client_id, seq_num, read_only, self.config.req_timeout_milli, cid, msg, pre_process, reconfiguration=reconfiguration, signature=signature) if m_of_n_quorum is None: m_of_n_quorum = MofNQuorum.LinearizableQuorum(self.config, [r.id for r in self.replicas]) # Raise a trio.TooSlowError exception if a quorum of replies try: with trio.fail_after(self.config.req_timeout_milli / 1000): self._reset_on_new_request([seq_num]) replies = await self._send_receive_loop( data, read_only, m_of_n_quorum, include_ro=include_ro, no_retries=no_retries) return next(iter(self.replies.values())).get_common_data() if replies else None except trio.TooSlowError: print("TooSlowError thrown from client_id", self.client_id, "for seq_num", seq_num) raise trio.TooSlowError finally: pass
def test_unpack_request(self): msg = b'hello' client_id = 4 req_seq_num = 1 read_only = True packed = bft_msgs.pack_request(client_id, req_seq_num, read_only, msg) (header, unpacked_msg) = bft_msgs.unpack_request(packed) self.assertEqual(client_id, header.client_id) self.assertEqual(1, header.flags) # read_only = True self.assertEqual(req_seq_num, header.req_seq_num) self.assertEqual(msg, unpacked_msg)
async def write_batch(self, msg_batch, batch_seq_nums=None, m_of_n_quorum=None): if not self.comm_prepared: await self._comm_prepare() cid = str(self.req_seq_num.next()) batch_size = len(msg_batch) if batch_seq_nums is None: batch_seq_nums = [] for n in range(batch_size): batch_seq_nums.append(self.req_seq_num.next()) msg_data = b'' for n in range(batch_size): msg = msg_batch[n] msg_seq_num = batch_seq_nums[n] msg_cid = str(msg_seq_num) msg_data = b''.join([ msg_data, bft_msgs.pack_request(self.client_id, msg_seq_num, False, self.config.req_timeout_milli, msg_cid, msg, True) ]) data = bft_msgs.pack_batch_request(self.client_id, batch_size, msg_data, cid) if m_of_n_quorum is None: m_of_n_quorum = MofNQuorum.LinearizableQuorum( self.config, [r.id for r in self.replicas]) # Raise a trio.TooSlowError exception if a quorum of replies try: with trio.fail_after(batch_size * self.config.req_timeout_milli / 1000): self._reset_on_new_request(batch_seq_nums) return await self._send_receive_loop( data, False, m_of_n_quorum, batch_size * self.config.retry_timeout_milli / 1000) except trio.TooSlowError: print( f"TooSlowError thrown from client_id {self.client_id}, for batch msg {cid} {batch_seq_nums}" ) raise trio.TooSlowError finally: pass
async def write_batch(self, msg_batch, batch_seq_nums=None, m_of_n_quorum=None, corrupt_params=None, no_retries=False): if not self.comm_prepared: await self._comm_prepare() cid = str(self.req_seq_num.next()) batch_size = len(msg_batch) if batch_seq_nums is None: batch_seq_nums = [] for n in range(batch_size): batch_seq_nums.append(self.req_seq_num.next()) msg_data = b'' req_index_to_corrupt = random.randint(1, batch_size-1) # don't corrupt the 1st for n in range(batch_size): msg = msg_batch[n] msg_seq_num = batch_seq_nums[n] msg_cid = str(msg_seq_num) signature = b'' client_id = self.client_id if self.signing_key: h = SHA256.new(msg) signature = pkcs1_15.new(self.signing_key).sign(h) if corrupt_params and (req_index_to_corrupt == n): msg, signature, client_id = self._corrupt_signing_params(msg, signature, client_id, corrupt_params) msg_data = b''.join([msg_data, bft_msgs.pack_request( self.client_id, msg_seq_num, False, self.config.req_timeout_milli, msg_cid, msg, True, reconfiguration=False, span_context=b'', signature=signature)]) data = bft_msgs.pack_batch_request( self.client_id, batch_size, msg_data, cid) if m_of_n_quorum is None: m_of_n_quorum = MofNQuorum.LinearizableQuorum(self.config, [r.id for r in self.replicas]) # Raise a trio.TooSlowError exception if a quorum of replies try: with trio.fail_after(batch_size * self.config.req_timeout_milli / 1000): self._reset_on_new_request(batch_seq_nums) return await self._send_receive_loop(data, False, m_of_n_quorum, batch_size * self.config.retry_timeout_milli / 1000, no_retries=no_retries) except trio.TooSlowError: print(f"TooSlowError thrown from client_id {self.client_id}, for batch msg {cid} {batch_seq_nums}") raise trio.TooSlowError finally: pass
async def sendSync(self, msg, read_only, seq_num=None, cid=None, pre_process=False, m_of_n_quorum=None): """ Send a client request and wait for a m_of_n_quorum (if None, it will set to 2F+C+1 quorum) of replies. Return a single reply message if a quorum of replies matches. Otherwise, raise a trio.TooSlowError indicating the request timed out. Retry Strategy: If the request is a write and the primary is known then send only to the primary on the first attempt. Otherwise, if the request is read only or the primary is unknown, then send to all replicas on the first attempt. After `config.retry_timeout_milli` without receiving a quorum of identical replies, then clear the replies and send to all replicas. Continue this strategy every `retry_timeout_milli` until `config.req_timeout_milli` elapses. If `config.req_timeout_milli` elapses then a trio.TooSlowError is raised. Note that this method also binds the socket to an appropriate port if not already bound. """ if not self.sock_bound: await self.bind() if seq_num is None: seq_num = self.req_seq_num.next() if cid is None: cid = str(seq_num) data = bft_msgs.pack_request( self.client_id, seq_num, read_only, self.config.req_timeout_milli, cid, msg, pre_process) if m_of_n_quorum is None: m_of_n_quorum = MofNQuorum.LinearizableQuorum(self.config, [r.id for r in self.replicas]) # Raise a trio.TooSlowError exception if a quorum of replies try: with trio.fail_after(self.config.req_timeout_milli / 1000): self.reset_on_new_request() self.retries = 0 return await self.send_loop(data, read_only, m_of_n_quorum) except trio.TooSlowError: print("TooSlowError thrown from client_id", self.client_id, "for seq_num", seq_num) raise trio.TooSlowError finally: pass