heap.cpp 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. #include <functional>
  2. #include "types.hpp"
  3. #include "duoram.hpp"
  4. #include "cell.hpp"
  5. #include "rdpf.hpp"
  6. #include "shapes.hpp"
  7. #include "heap.hpp"
  8. /*
  9. The heap datastructure is stored in an array with the starting index as 1 (and not 0)
  10. For nodes stored in index i of the array, the parent is stored at i/2 and
  11. The left and right children are stored at 2i and 2i + 1
  12. All the unused array indicies have MAX_INT stored in them
  13. x1
  14. / \
  15. x2 x3
  16. / \ / \
  17. x4 x5 x6 ()
  18. A Heap like above is stored in array like below.
  19. NULL| x1 | x2 | x3 | x4 | x5 | x6 | MAXINT |
  20. */
  21. /*
  22. The Optimized Insert Protocol
  23. Takes in the additive share of the value to be inserted
  24. and adds the the value into the heap while keeping the heap property intact
  25. _Protocol 4_ from PRAC: Round-Efficient 3-Party MPC for Dynamic Data Structures
  26. Consider the following insertion path with: x0 < x1 < x2 < NewElement < x3 < x4
  27. x0 x0 x0
  28. / \ / \ / \
  29. x1 x1 x1
  30. / / /
  31. x2 x2 x2
  32. \ \ \
  33. x3 ( ) NewElement
  34. \ \ \
  35. x4 x3 x3
  36. / / /
  37. ( ) x4 x4
  38. (Path with new element) (binary search to determine (After insertion)
  39. the point where New Element
  40. should be and shift the elements
  41. from that point down the path
  42. from the point)
  43. The insert protocol begins by adding an empty node at the end of the heap array
  44. The key observation is that after the insert operation, the only entries that might change are the ones on the path from the root to the new node
  45. The path from the root to the new node is determined based on the number of entries in the heap, which is publicly known
  46. The observation is that this path starts off sorted and will end up with the new element (NewElement) inserted into the correct position, preserving the sorted property of the path
  47. The length of the path is logarithmic with respect to the heap size (path length = log(heap size))
  48. To find the appropriate insertion position, we use binary search with a single IDPF of height logarithmic with respect to the logarithm of the heap size (IDPF height = log(log(heap size)))
  49. The advice bits of the IDPF correspond to the bit shares of a vector 'flag' with a single '1' indicating the position where the new value (insertval) must be inserted.
  50. The shares of 'flag' are locally converted to shares of a vector 'u = [000011111]' using running XORs.
  51. The bits of 'flag' and 'u' are then used in parallel Flag-Word multiplications, totaling 2 times the logarithm of the heap size, to shift the elements greater than 'insertval' down one position
  52. And write 'insertval' into the resulting empty location in the path
  53. This process requires a single message of communication
  54. The protocol requires one binary search on a database of size log(heap size) (height of the tree)
  55. Overall, the insert protocol achieves efficient insertion of a new element into the heap, with a complexity of log(log(heap size)) oblivious comparisons
  56. and 2 x log(heap size) flag multiplications. The flag multiplications
  57. are all done in a single round.
  58. */
  59. void MinHeap::insert_optimized(MPCTIO &tio, yield_t & yield, RegAS val) {
  60. auto HeapArray = oram.flat(tio, yield);
  61. num_items++;
  62. typename Duoram<RegAS>::Path P(HeapArray, tio, yield, num_items);
  63. const RegXS foundidx = P.binary_search(val);
  64. size_t childindex = num_items;
  65. // height is the number of nodes on the path from root to the leaf
  66. uint64_t height = P.size();
  67. RegAS zero;
  68. HeapArray[childindex] = zero;
  69. #ifdef HEAP_VERBOSE
  70. uint64_t val_reconstruction = mpc_reconstruct(tio, yield, val);
  71. std::cout << "val_reconstruction = " << val_reconstruction << std::endl;
  72. #endif
  73. uint64_t logheight = std::floor(double(std::log2(height))) + 1;
  74. std::vector<RegBS> flag;
  75. std::vector<RegBS> u(height);
  76. typename Duoram<RegAS>::template OblivIndex<RegXS,1> oidx(tio, yield, foundidx, logheight);
  77. flag = oidx.unit_vector(tio, yield, height, foundidx);
  78. #ifdef HEAP_VERBOSE
  79. uint64_t foundidx_reconstruction = mpc_reconstruct(tio, yield, foundidx);
  80. std::cout << "foundidx_reconstruction = " << foundidx_reconstruction << std::endl;
  81. std::cout << std::endl << " =============== " << std::endl;
  82. for (size_t j = 0; j < height; ++j) {
  83. uint64_t reconstruction = mpc_reconstruct(tio, yield, flag[j]);
  84. std::cout << " --->> flag[" << j << "] = " << reconstruction << std::endl;
  85. }
  86. #endif
  87. for (size_t j = 0; j < height; ++j) {
  88. if(j > 0) {
  89. u[j] = flag[j] ^ u[j-1];
  90. } else {
  91. u[j] = flag[j];
  92. }
  93. }
  94. #ifdef HEAP_VERBOSE
  95. for (size_t j = 0; j < height; ++j) {
  96. uint64_t reconstruction = mpc_reconstruct(tio, yield, u[j]);
  97. std::cout << " --->> [0000111111]][" << j << "] = " << reconstruction << std::endl;
  98. }
  99. #endif
  100. std::vector<RegAS> path(height);
  101. std::vector<RegAS> w(height);
  102. std::vector<RegAS> v(height);
  103. for (size_t j = 0; j < height; ++j) path[j] = P[j];
  104. std::vector<coro_t> coroutines;
  105. for (size_t j = 0; j < height; ++j) {
  106. if (j > 0) {
  107. coroutines.emplace_back(
  108. [&tio, &w, &u, &path, j](yield_t &yield) {
  109. mpc_flagmult(tio, yield, w[j], u[j-1], path[j-1]-path[j]);
  110. }
  111. );
  112. }
  113. coroutines.emplace_back(
  114. [&tio, &v, flag, val, &path, j](yield_t &yield) {
  115. mpc_flagmult(tio, yield, v[j], flag[j], val - path[j]);
  116. }
  117. );
  118. }
  119. run_coroutines(tio, coroutines);
  120. #ifdef HEAP_VERBOSE
  121. std::cout << "\n\n=================Before===========\n\n";
  122. auto path_rec_before = P.reconstruct();
  123. for (size_t j = 0; j < height; ++j) {
  124. std::cout << j << " --->: " << path_rec_before[j].share() << std::endl;
  125. }
  126. std::cout << "\n\n============================\n\n";
  127. #endif
  128. coroutines.clear();
  129. for (size_t j = 0; j < height; ++j) {
  130. coroutines.emplace_back( [&tio, &v, &w, &P, j](yield_t &yield) {
  131. auto Pcoro = P.context(yield);
  132. Pcoro[j] += (w[j] + v[j]);
  133. });
  134. }
  135. run_coroutines(tio, coroutines);
  136. #ifdef HEAP_VERBOSE
  137. std::cout << "\n\n=================After===========\n\n";
  138. auto path_rec_after = P.reconstruct();
  139. for (size_t j = 0; j < height; ++j) {
  140. std::cout << j << " --->: " << path_rec_after[j].share() << std::endl;
  141. }
  142. std::cout << "\n\n============================\n\n";
  143. #endif
  144. }
  145. // The Basic Insert Protocol
  146. // Takes in the additive share of the value to be inserted
  147. // And adds the the value into the heap while keeping the heap property intact
  148. // The insert protocol works as follows:
  149. // Step 1: Add a new element to the last entry of the array.
  150. // This new element becomes a leaf in the heap.
  151. // Step 2: Starting from the leaf (the newly added element), compare it with its parent.
  152. // Perform 1 oblivious comparison to determine if the parent is greater than the child.
  153. // Step 3: If the parent is greater than the child, swap them obliviously to maintain the heap property.
  154. // This swap ensures that the parent is always greater than both its children.
  155. // Step 4: Continue moving up the tree by repeating steps 2 and 3 until we reach the root.
  156. // This process ensures that the newly inserted element is correctly positioned in the heap.
  157. // The total cost of the insert protocol is log(num_items) oblivious comparisons and log(num_items) oblivious swaps.
  158. // This protocol follows the approach described as Protocol 3 in the paper "PRAC: Round-Efficient 3-Party MPC for Dynamic Data Structures."
  159. void MinHeap::insert(MPCTIO &tio, yield_t & yield, RegAS val) {
  160. auto HeapArray = oram.flat(tio, yield);
  161. num_items++;
  162. size_t childindex = num_items;
  163. size_t parentindex = childindex / 2;
  164. #ifdef HEAP_VERBOSE
  165. std::cout << "childindex = " << childindex << std::endl;
  166. std::cout << "parentindex = " << parentindex << std::endl;
  167. #endif
  168. HeapArray[num_items] = val;
  169. while (parentindex > 0) {
  170. RegAS sharechild = HeapArray[childindex];
  171. RegAS shareparent = HeapArray[parentindex];
  172. CDPF cdpf = tio.cdpf(yield);
  173. RegAS diff = sharechild - shareparent;
  174. auto[lt, eq, gt] = cdpf.compare(tio, yield, diff, tio.aes_ops());
  175. mpc_oswap(tio, yield, sharechild, shareparent, lt);
  176. HeapArray[childindex] = sharechild;
  177. HeapArray[parentindex] = shareparent;
  178. childindex = parentindex;
  179. parentindex = parentindex / 2;
  180. }
  181. }
  182. // Note: This function is intended for testing purposes only.
  183. // The purpose of this function is to verify that the heap property is satisfied.
  184. // The function checks if the heap property holds for the given heap structure. It ensures that for each node in the heap, the value of the parent node is less than or equal to the values of its children.
  185. // By calling this function during debugging, you can validate the integrity of the heap structure and ensure that the heap property is maintained correctly.
  186. // It is important to note that this function is not meant for production use and should be used solely for testing purposes.
  187. void MinHeap::verify_heap_property(MPCTIO &tio, yield_t & yield) {
  188. #ifdef HEAP_VERBOSE
  189. std::cout << std::endl << std::endl << "verify_heap_property is being called " << std::endl;
  190. #endif
  191. auto HeapArray = oram.flat(tio, yield);
  192. auto heapreconstruction = HeapArray.reconstruct();
  193. #ifdef HEAP_VERBOSE
  194. for (size_t j = 1; j < num_items + 1; ++j) {
  195. if(tio.player() < 2) std::cout << j << " -----> heapreconstruction[" << j << "] = " << heapreconstruction[j].share() << std::endl;
  196. }
  197. #endif
  198. for (size_t j = 2; j <= num_items; ++j) {
  199. if (heapreconstruction[j/2].share() > heapreconstruction[j].share()) {
  200. std::cout << "heap property failure\n\n";
  201. std::cout << "j = " << j << std::endl;
  202. std::cout << heapreconstruction[j].share() << std::endl;
  203. std::cout << "j/2 = " << j/2 << std::endl;
  204. std::cout << heapreconstruction[j/2].share() << std::endl;
  205. }
  206. assert(heapreconstruction[j/2].share() <= heapreconstruction[j].share());
  207. }
  208. }
  209. #ifdef HEAP_DEBUG
  210. // Note: This function is intended for debugging purposes only.
  211. // The purpose of this function is to assert the fact that the reconstruction values of both the left child and right child are greater than or equal to the reconstruction value of the parent.
  212. // The function performs an assertion check to validate this condition. If the condition is not satisfied, an assertion error will be triggered.
  213. // This function is useful for verifying the correctness of reconstruction values during debugging and ensuring the integrity of the heap structure.
  214. // It is important to note that this function is not meant for production use and should be used solely for debugging purposes.
  215. static void verify_parent_children_heaps(MPCTIO &tio, yield_t & yield, RegAS parent, RegAS leftchild, RegAS rightchild) {
  216. uint64_t parent_reconstruction = mpc_reconstruct(tio, yield, parent);
  217. uint64_t leftchild_reconstruction = mpc_reconstruct(tio, yield, leftchild);
  218. uint64_t rightchild_reconstruction = mpc_reconstruct(tio, yield, rightchild);
  219. std::cout << "parent_reconstruction = " << parent_reconstruction << std::endl;
  220. std::cout << "leftchild_reconstruction = " << leftchild_reconstruction << std::endl;
  221. std::cout << "rightchild_reconstruction = " << rightchild_reconstruction << std::endl << std::endl << std::endl;
  222. assert(parent_reconstruction <= leftchild_reconstruction);
  223. assert(parent_reconstruction <= rightchild_reconstruction);
  224. }
  225. #endif
  226. /*
  227. Protocol 6 from PRAC: Round-Efficient 3-Party MPC for Dynamic Data Structures
  228. Takes in as an input the XOR shares of the index at which the heap property has to be restored
  229. Returns the XOR shares of the index of the smaller child
  230. Basic restore heap property has the following functionality:
  231. Before restoring heap property: z
  232. / \
  233. y x
  234. After restoring heap property: if(y < x AND z < y) if(y < x AND z > y) if(y > x AND z < x) if(y > x AND z > x)
  235. z y z x
  236. / \ / \ / \ / \
  237. y x z x y x y z
  238. The function is relying on the "unused" entries in the heap being MAXINT
  239. The protocol works as follows:
  240. Step 1: Compare the left and right children.
  241. Step 2: Compare the smaller child with the parent.
  242. If the smaller child is smaller than the parent, swap the smaller child with the root.
  243. The protocol requires three DORAM (Distributed Oblivious RAM) reads performed in parallel:
  244. - Read the parent, left child, and right child.
  245. Two comparisons are performed:
  246. a) Comparison between the left and right child.
  247. b) Comparison between the smaller child and the parent.
  248. Two MPC-selects are performed in parallel:
  249. - Computing the smaller child and its index using MPC-select operations.
  250. Next, the offsets by which the parent and children need to be updated are computed.
  251. Offset computation involves:
  252. - One flag-flag multiplication.
  253. - Two flag-word multiplications performed in parallel.
  254. Three DORAM update operations are performed in parallel:
  255. - Update the parent, left child, and right child.
  256. The function returns the XOR-share of the smaller child's index.
  257. The total cost of the protocol includes:
  258. - 3 DORAM reads (performed in parallel).
  259. - 2 comparisons.
  260. - 2 MPC-selects (performed in parallel).
  261. - 1 flag-flag multiplication.
  262. - 2 flag-word multiplications (performed in parallel).
  263. - 3 DORAM updates (performed in parallel).
  264. */
  265. RegXS MinHeap::restore_heap_property(MPCTIO &tio, yield_t & yield, RegXS index) {
  266. RegAS smallest;
  267. auto HeapArray = oram.flat(tio, yield);
  268. RegXS leftchildindex = index;
  269. leftchildindex = index << 1;
  270. RegXS rightchildindex;
  271. rightchildindex.xshare = leftchildindex.xshare ^ (!tio.player());
  272. RegAS parent, leftchild, rightchild;
  273. #ifdef HEAP_VERBOSE
  274. auto index_reconstruction = mpc_reconstruct(tio, yield, index);
  275. auto leftchildindex_reconstruction = mpc_reconstruct(tio, yield, leftchildindex);
  276. auto rightchildindex_reconstruction = mpc_reconstruct(tio, yield, rightchildindex);
  277. std::cout << "index_reconstruction = " << index_reconstruction << std::endl;
  278. std::cout << "leftchildindex_reconstruction = " << leftchildindex_reconstruction << std::endl;
  279. std::cout << "rightchildindex_reconstruction = " << rightchildindex_reconstruction << std::endl;
  280. #endif
  281. run_coroutines(tio, [&tio, &parent, &HeapArray, index](yield_t &yield) {
  282. auto Acoro = HeapArray.context(yield);
  283. parent = Acoro[index];},
  284. [&tio, &HeapArray, &leftchild, leftchildindex](yield_t &yield) {
  285. auto Acoro = HeapArray.context(yield);
  286. leftchild = Acoro[leftchildindex];},
  287. [&tio, &rightchild, &HeapArray, rightchildindex](yield_t &yield) {
  288. auto Acoro = HeapArray.context(yield);
  289. rightchild = Acoro[rightchildindex];});
  290. CDPF cdpf = tio.cdpf(yield);
  291. auto[lt_c, eq_c, gt_c] = cdpf.compare(tio, yield, leftchild - rightchild, tio.aes_ops());
  292. RegXS smallerindex;
  293. RegAS smallerchild;
  294. run_coroutines(tio, [&tio, &smallerindex, lt_c, rightchildindex, leftchildindex](yield_t &yield) {
  295. mpc_select(tio, yield, smallerindex, lt_c, rightchildindex, leftchildindex);
  296. }, [&tio, &smallerchild, lt_c, rightchild, leftchild](yield_t &yield) {
  297. mpc_select(tio, yield, smallerchild, lt_c, rightchild, leftchild);
  298. }
  299. );
  300. CDPF cdpf0 = tio.cdpf(yield);
  301. auto[lt_p, eq_p, gt_p] = cdpf0.compare(tio, yield, smallerchild - parent, tio.aes_ops());
  302. RegBS ltlt1;
  303. mpc_and(tio, yield, ltlt1, lt_c, lt_p);
  304. RegAS update_index_by, update_leftindex_by;
  305. run_coroutines(tio, [&tio, &update_leftindex_by, ltlt1, parent, leftchild](yield_t &yield) {
  306. mpc_flagmult(tio, yield, update_leftindex_by, ltlt1, parent - leftchild);
  307. }, [&tio, &update_index_by, lt_p, parent, smallerchild](yield_t &yield) {
  308. mpc_flagmult(tio, yield, update_index_by, lt_p, smallerchild - parent);
  309. }
  310. );
  311. run_coroutines(tio, [&tio, &HeapArray, index, update_index_by](yield_t &yield) {
  312. auto Acoro = HeapArray.context(yield);
  313. Acoro[index] += update_index_by;},
  314. [&tio, &HeapArray, leftchildindex, update_leftindex_by](yield_t &yield) {
  315. auto Acoro = HeapArray.context(yield);
  316. Acoro[leftchildindex] += update_leftindex_by;},
  317. [&tio, &HeapArray, rightchildindex, update_index_by, update_leftindex_by](yield_t &yield) {
  318. auto Acoro = HeapArray.context(yield);
  319. Acoro[rightchildindex] += -(update_index_by + update_leftindex_by);});
  320. #ifdef HEAP_DEBUG
  321. verify_parent_children_heaps(tio, yield, HeapArray[index], HeapArray[leftchildindex] , HeapArray[rightchildindex]);
  322. #endif
  323. return smallerindex;
  324. }
  325. // This Protocol 7 is derived from PRAC: Round-Efficient 3-Party MPC for Dynamic Data Structures
  326. // Takes in as an input the XOR shares of the index at which
  327. // the heap property has to be restored
  328. // Returns the XOR shares of the index of the smaller child and
  329. // comparison between the left and right child
  330. // This protocol represents an optimized version of restoring the heap property
  331. // The key difference between the optimized and basic versions is that the optimized version utilizes a wide DPF (Distributed Point Function) for reads and writes
  332. // In addition to restoring the heap property, the function also returns
  333. // shares of the index of the smaller child, and the result of the
  334. // comparison (leftchild > rightchild)
  335. // The (leftchild > rightchild) comparison is utilized in the extract_min operation to increment the oblivindx by a certain value
  336. // The function restores the heap property at node index
  337. // The parameter layer is the height at which the node at index lies
  338. // The optimized version achieves improved efficiency by leveraging wide DPF operations for read and write operations
  339. std::pair<RegXS, RegBS> MinHeap::restore_heap_property_optimized(MPCTIO &tio, yield_t & yield, RegXS index, size_t layer, typename Duoram < RegAS > ::template OblivIndex < RegXS, 3 > oidx) {
  340. auto HeapArray = oram.flat(tio, yield);
  341. RegXS leftchildindex = index;
  342. leftchildindex = index << 1;
  343. RegXS rightchildindex;
  344. rightchildindex.xshare = leftchildindex.xshare ^ (!tio.player());
  345. typename Duoram < RegAS > ::Flat P(HeapArray, tio, yield, 1 << layer, 1 << layer);
  346. typename Duoram < RegAS > ::Flat C(HeapArray, tio, yield, 2 << layer, 2 << layer);
  347. typename Duoram < RegAS > ::Stride L(C, tio, yield, 0, 2);
  348. typename Duoram < RegAS > ::Stride R(C, tio, yield, 1, 2);
  349. RegAS parent, leftchild, rightchild;
  350. run_coroutines(tio, [&tio, &parent, &P, &oidx](yield_t &yield) {
  351. auto Pcoro = P.context(yield);
  352. parent = Pcoro[oidx]; },
  353. [&tio, &L, &leftchild, &oidx](yield_t &yield) {
  354. auto Lcoro = L.context(yield);
  355. leftchild = Lcoro[oidx];},
  356. [&tio, &R, &rightchild, &oidx](yield_t &yield) {
  357. auto Rcoro = R.context(yield);
  358. rightchild = Rcoro[oidx];
  359. });
  360. CDPF cdpf = tio.cdpf(yield);
  361. auto[lt_c, eq_c, gt_c] = cdpf.compare(tio, yield, leftchild - rightchild, tio.aes_ops());
  362. RegXS smallerindex;
  363. RegAS smallerchild;
  364. run_coroutines(tio, [&tio, &smallerindex, lt_c, rightchildindex, leftchildindex](yield_t &yield) {
  365. mpc_select(tio, yield, smallerindex, lt_c, rightchildindex, leftchildindex);
  366. }, [&tio, &smallerchild, lt_c, rightchild, leftchild](yield_t &yield) {
  367. mpc_select(tio, yield, smallerchild, lt_c, rightchild, leftchild);
  368. }
  369. );
  370. CDPF cdpf0 = tio.cdpf(yield);
  371. auto[lt_p, eq_p, gt_p] = cdpf0.compare(tio, yield, smallerchild - parent, tio.aes_ops());
  372. RegBS ltlt1;
  373. mpc_and(tio, yield, ltlt1, lt_c, lt_p);
  374. RegAS update_index_by, update_leftindex_by;
  375. run_coroutines(tio, [&tio, &update_leftindex_by, ltlt1, parent, leftchild](yield_t &yield) {
  376. mpc_flagmult(tio, yield, update_leftindex_by, ltlt1, parent - leftchild);
  377. }, [&tio, &update_index_by, lt_p, parent, smallerchild](yield_t &yield) {
  378. mpc_flagmult(tio, yield, update_index_by, lt_p, smallerchild - parent);
  379. }
  380. );
  381. run_coroutines(tio, [&tio, &P, &oidx, update_index_by](yield_t &yield) {
  382. auto Pcoro = P.context(yield);
  383. Pcoro[oidx] += update_index_by;},
  384. [&tio, &L, &oidx, update_leftindex_by](yield_t &yield) {
  385. auto Lcoro = L.context(yield);
  386. Lcoro[oidx] += update_leftindex_by;},
  387. [&tio, &R, &oidx, update_leftindex_by, update_index_by](yield_t &yield) {
  388. auto Rcoro = R.context(yield);
  389. Rcoro[oidx] += -(update_leftindex_by + update_index_by);
  390. });
  391. auto gteq = gt_c ^ eq_c;
  392. return {smallerindex, gteq};
  393. }
  394. // Intializes the heap array with 0x7fffffffffffffff
  395. void MinHeap::init(MPCTIO &tio, yield_t & yield) {
  396. auto HeapArray = oram.flat(tio, yield);
  397. HeapArray.init(0x7fffffffffffffff);
  398. num_items = 0;
  399. }
  400. // This function simply inits a heap with values 100,200,...,100*n
  401. // We use this function only to set up our heap
  402. // to do timing experiments on insert and extractmins
  403. void MinHeap::init(MPCTIO &tio, yield_t & yield, size_t n) {
  404. auto HeapArray = oram.flat(tio, yield);
  405. num_items = n;
  406. HeapArray.init([n](size_t i) {
  407. if (i >= 1 && i <= n) {
  408. return i*100;
  409. } else {
  410. return size_t(0x7fffffffffffffff);
  411. }
  412. });
  413. }
  414. // Note: This function is intended for debugging purposes only.
  415. // The purpose of this function is to reconstruct the heap and print its contents.
  416. // The function performs the necessary operations to reconstruct the heap, ensuring that the heap property is satisfied. It then prints the contents of the reconstructed heap.
  417. // This function is useful for debugging and inspecting the state of the heap at a particular point in the program execution.
  418. // It is important to note that this function is not meant for production use and should be used solely for debugging purposes.
  419. void MinHeap::print_heap(MPCTIO &tio, yield_t & yield) {
  420. auto HeapArray = oram.flat(tio, yield);
  421. auto Pjreconstruction = HeapArray.reconstruct();
  422. for (size_t j = 1; j <= num_items; ++j) {
  423. if(2 * j <= num_items) {
  424. std::cout << j << "-->> HeapArray[" << j << "] = " << Pjreconstruction[j].share() << ", children are: " << Pjreconstruction[2 * j].share() << " and " << Pjreconstruction[2 * j + 1].share() << std::endl;
  425. } else {
  426. std::cout << j << "-->> HeapArray[" << j << "] = " << std::dec << Pjreconstruction[j].share() << " is a LEAF " << std::endl;
  427. }
  428. }
  429. }
  430. /*
  431. Restore the head property at an explicit index (typically the root).
  432. the only reason this function exists is because at the root level
  433. the indices to read (the root and its two children) are explicit and not shared
  434. Restore heap property at an index in clear
  435. Takes in as an input the index (in clear) at which
  436. the heap property has to be restored
  437. root
  438. / \
  439. leftchild rightchild
  440. After restoring heap property:
  441. if(leftchild < rightchild AND root < leftchild) if(leftchild < rightchild AND root > leftchild) if(leftchild > rightchild AND root < rightchild) if(leftchild > rightchild AND root > rightchild)
  442. root leftchild root rightchild
  443. / \ / \ / \ / \
  444. leftchild rightchild root rightchild leftchild rightchild leftchild root
  445. The restore_heap_property_at_explicit_index protocol works as follows:
  446. Step 1: Compare the left and right children.
  447. Step 2: Compare the smaller child with the root.
  448. If the smaller child is smaller than the root, swap the smaller child with the root.
  449. Unlike the restore_heap_property protocol, restore_heap_property_at_explicit_index begins with three explicit-index (non-DORAM) read operations:
  450. - Read the parent, left child, and right child.
  451. Two comparisons are performed:
  452. a) Comparison between the left and right child.
  453. b) Comparison between the smaller child and the parent.
  454. The above comparisons have to be sequential because we need to find the smallerindex and smallerchild,
  455. which is dependent on the first comparison
  456. Next, the offsets by which the parent and children need to be updated are computed.
  457. Offset computation involves:
  458. - One flag-flag multiplication.
  459. - Two flag-word multiplications.
  460. Three explicit-index (non-DORAM) update operations are required (performed in parallel) to update the parent, left child, and right child.
  461. In total, this protocol requires:
  462. - 2 comparisons.
  463. - 1 flag-flag multiplication.
  464. - 2 flag-word multiplications.
  465. - 3 explicit-index (non-DORAM) reads and updates.
  466. The function returns a pair of a) XOR-share of the index of the smaller child and b) the comparison between left and right children
  467. */
  468. std::pair<RegXS, RegBS> MinHeap::restore_heap_property_at_explicit_index(MPCTIO &tio, yield_t & yield, size_t index = 1) {
  469. auto HeapArray = oram.flat(tio, yield);
  470. RegAS parent = HeapArray[index];
  471. RegAS leftchild = HeapArray[2 * index];
  472. RegAS rightchild = HeapArray[2 * index + 1];
  473. CDPF cdpf = tio.cdpf(yield);
  474. auto[lt, eq, gt] = cdpf.compare(tio, yield, leftchild - rightchild, tio.aes_ops());
  475. auto gteq = gt ^ eq;
  476. RegAS smallerchild;
  477. mpc_select(tio, yield, smallerchild, lt, rightchild, leftchild);
  478. uint64_t leftchildindex = (2 * index);
  479. uint64_t rightchildindex = (2 * index) + 1;
  480. RegXS smallerindex = (RegXS(lt) & leftchildindex) ^ (RegXS(gteq) & rightchildindex);
  481. CDPF cdpf0 = tio.cdpf(yield);
  482. auto[lt1, eq1, gt1] = cdpf0.compare(tio, yield, smallerchild - parent, tio.aes_ops());
  483. RegBS ltlt1;
  484. mpc_and(tio, yield, ltlt1, lt, lt1);
  485. RegAS update_index_by, update_leftindex_by;
  486. run_coroutines(tio, [&tio, &update_leftindex_by, ltlt1, parent, leftchild](yield_t &yield) {
  487. mpc_flagmult(tio, yield, update_leftindex_by, ltlt1, parent - leftchild);
  488. }, [&tio, &update_index_by, lt1, parent, smallerchild](yield_t &yield) {
  489. mpc_flagmult(tio, yield, update_index_by, lt1, smallerchild - parent);});
  490. run_coroutines(tio,
  491. [&tio, &HeapArray, &update_index_by, index](yield_t &yield) {
  492. auto HeapArraycoro = HeapArray.context(yield);
  493. HeapArraycoro[index] += update_index_by;
  494. },
  495. [&tio, &HeapArray, &update_leftindex_by, leftchildindex](yield_t &yield) {
  496. auto HeapArraycoro = HeapArray.context(yield);
  497. HeapArraycoro[leftchildindex] += update_leftindex_by;
  498. },
  499. [&tio, &HeapArray, &update_index_by, &update_leftindex_by, rightchildindex](yield_t &yield) {
  500. auto HeapArraycoro = HeapArray.context(yield);
  501. HeapArraycoro[rightchildindex] += -(update_index_by + update_leftindex_by);
  502. });
  503. #ifdef HEAP_VERBOSE
  504. RegAS new_parent = HeapArray[index];
  505. RegAS new_left = HeapArray[leftchildindex];
  506. RegAS new_right = HeapArray[rightchildindex];
  507. uint64_t parent_R = mpc_reconstruct(tio, yield, new_parent);
  508. uint64_t left_R = mpc_reconstruct(tio, yield, new_left);
  509. uint64_t right_R = mpc_reconstruct(tio, yield, new_right);
  510. std::cout << "parent_R = " << parent_R << std::endl;
  511. std::cout << "left_R = " << left_R << std::endl;
  512. std::cout << "right_R = " << right_R << std::endl;
  513. #endif
  514. #ifdef HEAP_DEBUG
  515. verify_parent_children_heaps(tio, yield, HeapArray[index], HeapArray[leftchildindex] , HeapArray[rightchildindex]);
  516. #endif
  517. return {smallerindex, gteq};
  518. }
  519. // This Protocol 5 from PRAC: Round-Efficient 3-Party MPC for Dynamic Data Structures
  520. // The extractmin protocol returns the minimum element (the root), removes it
  521. // and restores the heap property
  522. // The function extract_min cannot be called on an empty heap
  523. // Like in the paper, there is only one version of extract_min
  524. // and takes in a boolean parameter to decide if the basic or the optimized version needs to be run
  525. // the optimized version calls the optimized restore_heap_property with everything else remaing the same
  526. // The extractmin algorithm removes the root and replaces it with last leaf node
  527. // After extracting the minimum element from the heap, the heap property is temporarily violated.
  528. // To restore the heap property, we begin at the root layer.
  529. // Step 1: Swap the root with the smaller child if the smaller child is less than the root.
  530. // This step is performed by the function restore_heap_property_at_explicit_index.
  531. // Step 2: Proceed down the tree along the path of the smaller child.
  532. // Repeat the process of swapping the parent with the smaller child if the parent is greater than the smaller child.
  533. // After the swap, make the smaller child the new parent.
  534. // The choice of whether to use restore_heap_property or restore_heap_property_optimized
  535. // depends on whether it is a basic or optimized extraction of the minimum element.
  536. // These functions ensure that the heap property is maintained throughout the tree.
  537. RegAS MinHeap::extract_min(MPCTIO &tio, yield_t & yield, int is_optimized) {
  538. size_t height = std::log2(num_items);
  539. RegAS minval;
  540. auto HeapArray = oram.flat(tio, yield);
  541. minval = HeapArray[1];
  542. HeapArray[1] = RegAS(HeapArray[num_items]);
  543. RegAS v;
  544. v.ashare = 0x7fffffffffffffff * !tio.player();
  545. HeapArray[num_items] = v;
  546. num_items--;
  547. // If this was the last item, just return it
  548. if (num_items == 0) {
  549. return minval;
  550. }
  551. auto outroot = restore_heap_property_at_explicit_index(tio, yield);
  552. RegXS smaller = outroot.first;
  553. if(is_optimized > 0) {
  554. typename Duoram < RegAS > ::template OblivIndex < RegXS, 3 > oidx(tio, yield, height);
  555. oidx.incr(outroot.second);
  556. for (size_t i = 0; i < height-1; ++i) {
  557. auto out = restore_heap_property_optimized(tio, yield, smaller, i + 1, oidx);
  558. smaller = out.first;
  559. oidx.incr(out.second);
  560. }
  561. }
  562. if(is_optimized == 0) {
  563. for (size_t i = 0; i < height - 1; ++i) {
  564. smaller = restore_heap_property(tio, yield, smaller);
  565. }
  566. }
  567. return minval;
  568. }
  569. void Heap(MPCIO & mpcio, const PRACOptions & opts, char ** args) {
  570. MPCTIO tio(mpcio, 0, opts.num_cpu_threads);
  571. int nargs = 0;
  572. while (args[nargs] != nullptr) {
  573. ++nargs;
  574. }
  575. int maxdepth = 0;
  576. int heapdepth = 0;
  577. size_t n_inserts = 0;
  578. size_t n_extracts = 0;
  579. int is_optimized = 0;
  580. int run_sanity = 0;
  581. for (int i = 0; i < nargs; i += 2) {
  582. std::string option = args[i];
  583. if (option == "-m" && i + 1 < nargs) {
  584. maxdepth = std::atoi(args[i + 1]);
  585. } else if (option == "-d" && i + 1 < nargs) {
  586. heapdepth = std::atoi(args[i + 1]);
  587. } else if (option == "-i" && i + 1 < nargs) {
  588. n_inserts = std::atoi(args[i + 1]);
  589. } else if (option == "-e" && i + 1 < nargs) {
  590. n_extracts = std::atoi(args[i + 1]);
  591. } else if (option == "-opt" && i + 1 < nargs) {
  592. is_optimized = std::atoi(args[i + 1]);
  593. } else if (option == "-s" && i + 1 < nargs) {
  594. run_sanity = std::atoi(args[i + 1]);
  595. }
  596. }
  597. run_coroutines(tio, [ & tio, maxdepth, heapdepth, n_inserts, n_extracts, is_optimized, run_sanity, &mpcio](yield_t & yield) {
  598. size_t size = size_t(1) << maxdepth;
  599. MinHeap tree(tio.player(), size);
  600. // This form of init with a third parameter of n sets the heap
  601. // to contain 100, 200, 300, ..., 100*n.
  602. tree.init(tio, yield, (size_t(1) << heapdepth) - 1);
  603. std::cout << "\n===== Init Stats =====\n";
  604. tio.sync_lamport();
  605. mpcio.dump_stats(std::cout);
  606. mpcio.reset_stats();
  607. tio.reset_lamport();
  608. for (size_t j = 0; j < n_inserts; ++j) {
  609. RegAS inserted_val;
  610. inserted_val.randomize(8);
  611. #ifdef HEAP_VERBOSE
  612. inserted_val.ashare = inserted_val.ashare;
  613. uint64_t inserted_val_rec = mpc_reconstruct(tio, yield, inserted_val);
  614. std::cout << "inserted_val_rec = " << inserted_val_rec << std::endl << std::endl;
  615. #endif
  616. if(is_optimized > 0) tree.insert_optimized(tio, yield, inserted_val);
  617. if(is_optimized == 0) tree.insert(tio, yield, inserted_val);
  618. }
  619. std::cout << "\n===== Insert Stats =====\n";
  620. tio.sync_lamport();
  621. mpcio.dump_stats(std::cout);
  622. if(run_sanity == 1 && n_inserts != 0) tree.verify_heap_property(tio, yield);
  623. mpcio.reset_stats();
  624. tio.reset_lamport();
  625. #ifdef HEAP_VERBOSE
  626. tree.print_heap(tio, yield);
  627. #endif
  628. bool have_lastextract = false;
  629. uint64_t lastextract = 0;
  630. for (size_t j = 0; j < n_extracts; ++j) {
  631. if(run_sanity == 1) {
  632. RegAS minval = tree.extract_min(tio, yield, is_optimized);
  633. uint64_t minval_reconstruction = mpc_reconstruct(tio, yield, minval);
  634. std::cout << "minval_reconstruction = " << minval_reconstruction << std::endl;
  635. if (have_lastextract) {
  636. assert(minval_reconstruction >= lastextract);
  637. }
  638. lastextract = minval_reconstruction;
  639. have_lastextract = true;
  640. } else {
  641. tree.extract_min(tio, yield, is_optimized);
  642. }
  643. if (run_sanity == 1) {
  644. tree.verify_heap_property(tio, yield);
  645. }
  646. #ifdef HEAP_VERBOSE
  647. tree.print_heap(tio, yield);
  648. #endif
  649. }
  650. std::cout << "\n===== Extract Min Stats =====\n";
  651. tio.sync_lamport();
  652. mpcio.dump_stats(std::cout);
  653. #ifdef HEAP_VERBOSE
  654. tree.print_heap(tio, yield);
  655. #endif
  656. if(run_sanity == 1 && n_extracts != 0) tree.verify_heap_property(tio, yield);
  657. }
  658. );
  659. }