def __enter__(self): n, t, my_id = self.n, self.t, self.my_id send, recv = self.get_send_recv(f"{self.tag}-AVSS") g, h, pks, sk = get_avss_params(n, t, my_id) crs = [g, h] self.avss_instance = HbAvssLight(pks, sk, crs, n, t, my_id, send, recv) self.avss_instance.__enter__() self.tasks.append(asyncio.create_task(self._runner())) send, recv = self.get_send_recv(f"{self.tag}-AVSS_VALUE_PROCESSOR") pk, sks = dealer(n, t + 1, seed=17) self.avss_value_processor = AvssValueProcessor( pk, sks[my_id], n, t, my_id, send, recv, self.avss_instance.output_queue.get, self.avss_value_processor_chunk_size, ) self.avss_value_processor.__enter__() self.tasks.append(asyncio.create_task(self._extract())) return self
async def test_add_to_output_queue( n, t, b, input_next_ids, output_next_ids, per_dealer_input, output_queue_vals ): """ Each row is a test case. This test runs on only one node. This is to test the part of code which runs after ACS i.e. all nodes see the same view of the output list and they have to add a set of values in the output queue in the same order such that each batch of values has values from at least `n-t` dealers. input_next_idx: This denotes the index which has the next value to be added to the output queue for a particular dealer. Basically this represents the state of already outputted values. output_next_ids: This denotes the next index to return after the values have been added to the output queue. This is to verify the output. per_dealer_input: This is the count of values that have been received by this node per dealer. We take this list and add as many values as the counts in the output list. The output list is the one which gets updated after ACS and all nodes have the same view of the output list. output_queue_vals: This is for verification. This denotes the order in which the output values should be available in the queue. THe first character is the node who dealt the value and the second character is the count starting from 0 for each dealer. We want to verify the AVSS Value Proessor Output Order. | 0 | 1 | 2 | 3 | ------------------------- | 00 | 10 | 20 | 30 | => Each row is a batch. | 01 | 11 | 21 | 31 | | | 12 | 22 | 32 | | | | 23 | 33 | | | | | 34 | | | | | 35 | Counts => [2, 3, 4, 6] Sorted in ascending order: max_values_to_output_per_dealer = pending_counts[t] = t_th idx value = 3 Batch 1 => 00, 10, 20, 30 Batch 2 => 01, 11, 21, 31 Batch 3 => 12, 22, 32 AVSS Value Proessor Output Order => 00, 10, 20, 30, 01, 11, 21, 31, 12, 22, 32 """ avss_proc = AvssValueProcessor(None, None, n, t, 0, None, None, None, b) avss_proc.next_idx_to_return_per_dealer = input_next_ids for i in range(n): for j in range(per_dealer_input[i]): avss_proc.outputs_per_dealer[i].append(f"{i}{j}") avss_proc._add_to_output_queue() assert output_next_ids == avss_proc.next_idx_to_return_per_dealer assert len(output_queue_vals) == avss_proc.output_queue.qsize() for val in output_queue_vals: assert val == avss_proc.output_queue.get_nowait()
async def test_with_agreed_values_on_another_node_with_input(k, acs_outputs): n, t, sender_id = 4, 1, 1 input_q = asyncio.Queue() with AvssValueProcessor(None, None, n, t, 0, None, None, input_q.get) as proc: proc._process_acs_output(acs_outputs) # 0th node has not received any AVSSed value from node 1 yet assert [len(proc.inputs_per_dealer[i]) for i in range(n)] == [0, 0, 0, 0] # 0th node should however know that one value sent by 1st node has been agreed assert [len(proc.outputs_per_dealer[i]) for i in range(n)] == [0, k, 0, 0] for i in range(k): assert type(proc.outputs_per_dealer[sender_id][i]) is asyncio.Future # This value is not yet available assert not proc.outputs_per_dealer[sender_id][i].done() # This is set by another method and should not have been updated. assert all(proc.next_idx_to_return_per_dealer[i] == 0 for i in range(n)) for i in range(k): value = (sender_id, i, i) # dealer_id, avss_id, value input_q.put_nowait(value) # Make the 0th node receive the value now await asyncio.sleep(0.1) # Give the recv loop a chance to run # 0th node has received the AVSSed value from node 1 assert [len(proc.inputs_per_dealer[i]) for i in range(n)] == [0, k, 0, 0] # 0th node already knows that one value sent by 1st node has been agreed assert [len(proc.outputs_per_dealer[i]) for i in range(n)] == [0, k, 0, 0] for i in range(k): assert proc.outputs_per_dealer[sender_id][i].done() assert (await proc.outputs_per_dealer[sender_id][i]) == i # This is set by another method and should not have been updated. assert all(proc.next_idx_to_return_per_dealer[i] == 0 for i in range(n))
async def test_acs_output(n, t, output_counts, next_idx, acs_outputs): my_id = 0 input_q = asyncio.Queue() with AvssValueProcessor(None, None, n, t, my_id, None, None, input_q.get) as proc: proc._process_acs_output(acs_outputs) assert [len(proc.outputs_per_dealer[i]) for i in range(n)] == output_counts for i in range(n): for output in proc.outputs_per_dealer[i]: assert type(output) is asyncio.Future # These are set by another method and shouldn't have been updated. assert all(len(proc.inputs_per_dealer[i]) == 0 for i in range(n)) assert proc.next_idx_to_return_per_dealer == next_idx
async def test_with_agreed_values_on_same_node_with_input(k, acs_outputs): n, t, my_id = 4, 1, 0 input_q = asyncio.Queue() with AvssValueProcessor(None, None, n, t, my_id, None, None, input_q.get) as proc: for i in range(k): value = (my_id, i, i) # dealer_id, avss_id, value input_q.put_nowait(value) await asyncio.sleep(0.1) # Give the recv loop a chance to run proc._process_acs_output(acs_outputs) assert [len(proc.inputs_per_dealer[i]) for i in range(n)] == [k, 0, 0, 0] assert [len(proc.outputs_per_dealer[i]) for i in range(n)] == [k, 0, 0, 0] for i in range(k): assert type(proc.outputs_per_dealer[my_id][i]) is asyncio.Future assert proc.outputs_per_dealer[my_id][i].done() assert (await proc.outputs_per_dealer[my_id][i]) == i # This is set by another method and should not have been updated. assert all(proc.next_idx_to_return_per_dealer[i] == 0 for i in range(n))
async def test_avss_value_processor_with_diff_inputs(test_router): n, t = 4, 1 sends, recvs, _ = test_router(n) node_inputs = [ [(0, 0, "00"), (1, 0, "10"), (2, 0, "20")], [(0, 0, "01")], [(0, 0, "02"), (2, 0, "22"), (3, 0, "32")], [(3, 0, "33")], ] get_tasks = [None] * n pk, sks = dealer(n, t + 1) avss_value_procs = [None] * n input_qs = [None] * n with ExitStack() as stack: for i in range(n): input_qs[i] = asyncio.Queue() for node_input in node_inputs[i]: input_qs[i].put_nowait(node_input) avss_value_procs[i] = AvssValueProcessor( pk, sks[i], n, t, i, sends[i], recvs[i], input_qs[i].get ) stack.enter_context(avss_value_procs[i]) get_tasks[i] = asyncio.create_task(avss_value_procs[i].get()) futures = await asyncio.gather(*get_tasks) for i, future in enumerate(futures): assert type(future) is asyncio.Future if i == 3: # node 3 does not receive the value dealt from node 0 assert not future.done() else: # all other nodes have received the value dealt from node 0 assert (await future) == f"0{i}" # this is based on node_inputs inputs = [[1, 1, 1, 0], [1, 0, 0, 0], [1, 0, 1, 1], [0, 0, 0, 1]] # this is based on the fact that only values dealt by 0 and 2 have been agreed outputs = [[1, 0, 1, 1], [1, 0, 1, 1], [1, 0, 1, 1], [1, 0, 1, 1]] for j, proc in enumerate(avss_value_procs): assert [len(proc.inputs_per_dealer[i]) for i in range(n)] == inputs[j] assert [len(proc.outputs_per_dealer[i]) for i in range(n)] == outputs[j] # 1 value was already retrieved, two values and 1 batch delimiter expected assert proc.output_queue.qsize() == 3 # The values from 0, 2 and 3 have been added to the queue so their # next indices should be updated assert proc.next_idx_to_return_per_dealer == [1, 0, 1, 1] for i in range(n): if i in [0, 2]: # only nodes 0 and 2 have received the value dealt by 2 # executing this sequentially also ensurs that ACS is not run again # since this value is already available assert (await (await avss_value_procs[i].get())) == f"2{i}" else: # nodes 1 and 3 have not received the value dealt by 2 assert not (await avss_value_procs[i].get()).done() for j, proc in enumerate(avss_value_procs): assert [len(proc.inputs_per_dealer[i]) for i in range(n)] == inputs[j] assert [len(proc.outputs_per_dealer[i]) for i in range(n)] == outputs[j] # values from node 0 and 1 have been requested assert proc.next_idx_to_return_per_dealer == [1, 0, 1, 1]
class PreProcessingBase(ABC): PERIOD_IN_SECONDS = 3 def __init__( self, n, t, my_id, send, recv, tag, batch_size=10, avss_value_processor_chunk_size=1, ): self.n, self.t, self.my_id = n, t, my_id self.tag = tag self.avss_value_processor_chunk_size = avss_value_processor_chunk_size # Batch size of values to AVSS from a node self.batch_size = batch_size # Minimum number of values before triggering another set of AVSSes self.low_watermark = self.batch_size self.output_queue = asyncio.Queue() # Create a mechanism to split the `send` and `recv` channels based on `tag` subscribe_recv_task, subscribe = subscribe_recv(recv) self.tasks = [subscribe_recv_task] def _get_send_recv(tag): return wrap_send(tag, send), subscribe(tag) self.get_send_recv = _get_send_recv async def get(self): return await self.output_queue.get() @abstractmethod def _get_input_batch(self): raise NotImplementedError async def _trigger_and_wait_for_avss(self, avss_id): inputs = self._get_input_batch() assert type(inputs) in [tuple, list] avss_tasks = [] avss_tasks.append( asyncio.create_task( self.avss_instance.avss_parallel( avss_id, len(inputs), values=inputs, dealer_id=self.my_id ) ) ) for i in range(self.n): if i != self.my_id: avss_tasks.append( asyncio.create_task( self.avss_instance.avss_parallel( avss_id, len(inputs), dealer_id=i ) ) ) await asyncio.gather(*avss_tasks) async def _runner(self): counter = 0 logging.debug("[%d] Starting preprocessing runner: %s", self.my_id, self.tag) while True: # If the number of values in the output queue are below the lower # watermark then we want to trigger the next set of AVSSes. if self.output_queue.qsize() < self.low_watermark: logging.debug("[%d] Starting AVSS Batch: %d", self.my_id, counter) await self._trigger_and_wait_for_avss(counter) logging.debug("[%d] AVSS Batch Completed: %d", self.my_id, counter) counter += 1 # Wait for sometime before checking again. await asyncio.sleep(PreProcessingBase.PERIOD_IN_SECONDS) async def _get_output_batch(self, group_size=1): for i in range(self.batch_size): batch = [] while True: value = await self.avss_value_processor.get() if value is None: break batch.append(value) assert len(batch) / group_size >= self.n - self.t assert len(batch) / group_size <= self.n yield batch async def _extract(self): raise NotImplementedError def __enter__(self): n, t, my_id = self.n, self.t, self.my_id send, recv = self.get_send_recv(f"{self.tag}-AVSS") g, h, pks, sk = get_avss_params(n, t, my_id) crs = [g, h] self.avss_instance = HbAvssLight(pks, sk, crs, n, t, my_id, send, recv) self.avss_instance.__enter__() self.tasks.append(asyncio.create_task(self._runner())) send, recv = self.get_send_recv(f"{self.tag}-AVSS_VALUE_PROCESSOR") pk, sks = dealer(n, t + 1, seed=17) self.avss_value_processor = AvssValueProcessor( pk, sks[my_id], n, t, my_id, send, recv, self.avss_instance.output_queue.get, self.avss_value_processor_chunk_size, ) self.avss_value_processor.__enter__() self.tasks.append(asyncio.create_task(self._extract())) return self def __exit__(self, *args): for task in self.tasks: task.cancel() self.avss_instance.__exit__(*args) self.avss_value_processor.__exit__(*args)