heap.cpp 39 KB

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