From 8927c9d7928c8bda596b3b45aec87ff77a0bef77 Mon Sep 17 00:00:00 2001 From: Quentin Bourgerie Date: Mon, 19 Dec 2022 15:28:12 +0100 Subject: [PATCH] chore: remove old files --- estimator_new/__init__.py | 19 - .../__pycache__/__init__.cpython-38.pyc | Bin 598 -> 0 bytes estimator_new/__pycache__/conf.cpython-38.pyc | Bin 424 -> 0 bytes estimator_new/__pycache__/cost.cpython-38.pyc | Bin 7417 -> 0 bytes .../__pycache__/errors.cpython-38.pyc | Bin 626 -> 0 bytes estimator_new/__pycache__/gb.cpython-38.pyc | Bin 7581 -> 0 bytes estimator_new/__pycache__/io.cpython-38.pyc | Bin 1699 -> 0 bytes estimator_new/__pycache__/lwe.cpython-38.pyc | Bin 4938 -> 0 bytes .../__pycache__/lwe_bkw.cpython-38.pyc | Bin 8924 -> 0 bytes .../__pycache__/lwe_dual.cpython-38.pyc | Bin 15838 -> 0 bytes .../__pycache__/lwe_guess.cpython-38.pyc | Bin 13838 -> 0 bytes .../__pycache__/lwe_parameters.cpython-38.pyc | Bin 5179 -> 0 bytes .../__pycache__/lwe_primal.cpython-38.pyc | Bin 14230 -> 0 bytes estimator_new/__pycache__/nd.cpython-38.pyc | Bin 10509 -> 0 bytes estimator_new/__pycache__/prob.cpython-38.pyc | Bin 4528 -> 0 bytes .../__pycache__/reduction.cpython-38.pyc | Bin 15254 -> 0 bytes .../__pycache__/schemes.cpython-38.pyc | Bin 3784 -> 0 bytes .../__pycache__/simulator.cpython-38.pyc | Bin 2375 -> 0 bytes estimator_new/__pycache__/util.cpython-38.pyc | Bin 8368 -> 0 bytes estimator_new/conf.py | 13 - estimator_new/cost.py | 258 -------- estimator_new/errors.py | 15 - estimator_new/gb.py | 252 -------- estimator_new/io.py | 57 -- estimator_new/lwe.py | 169 ----- estimator_new/lwe_bkw.py | 304 --------- estimator_new/lwe_dual.py | 526 --------------- estimator_new/lwe_guess.py | 417 ------------ estimator_new/lwe_parameters.py | 161 ----- estimator_new/lwe_primal.py | 596 ----------------- estimator_new/nd.py | 374 ----------- estimator_new/prob.py | 131 ---- estimator_new/reduction.py | 571 ----------------- estimator_new/schemes.py | 402 ------------ estimator_new/simulator.py | 72 --- estimator_new/util.py | 295 --------- old_files/concrete_params.py | 272 -------- old_files/estimate_oldparams.py | 129 ---- old_files/figs/iso.png | Bin 75623 -> 0 bytes old_files/figs/plot.png | Bin 34798 -> 0 bytes old_files/figs/plot2.png | Bin 32346 -> 0 bytes old_files/figs/sieve.png | Bin 26398 -> 0 bytes old_files/figs/uSVP.png | Bin 20500 -> 0 bytes old_files/hybrid_decoding.py | 329 ---------- old_files/memory_tests/test.py | 17 - old_files/memory_tests/test2.py | 31 - old_files/new_scripts.py | 471 -------------- old_files/scripts.py | 599 ------------------ 48 files changed, 6480 deletions(-) delete mode 100644 estimator_new/__init__.py delete mode 100644 estimator_new/__pycache__/__init__.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/conf.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/cost.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/errors.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/gb.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/io.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/lwe.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/lwe_bkw.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/lwe_dual.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/lwe_guess.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/lwe_parameters.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/lwe_primal.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/nd.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/prob.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/reduction.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/schemes.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/simulator.cpython-38.pyc delete mode 100644 estimator_new/__pycache__/util.cpython-38.pyc delete mode 100644 estimator_new/conf.py delete mode 100644 estimator_new/cost.py delete mode 100644 estimator_new/errors.py delete mode 100644 estimator_new/gb.py delete mode 100644 estimator_new/io.py delete mode 100644 estimator_new/lwe.py delete mode 100644 estimator_new/lwe_bkw.py delete mode 100644 estimator_new/lwe_dual.py delete mode 100644 estimator_new/lwe_guess.py delete mode 100644 estimator_new/lwe_parameters.py delete mode 100644 estimator_new/lwe_primal.py delete mode 100644 estimator_new/nd.py delete mode 100644 estimator_new/prob.py delete mode 100644 estimator_new/reduction.py delete mode 100644 estimator_new/schemes.py delete mode 100644 estimator_new/simulator.py delete mode 100644 estimator_new/util.py delete mode 100644 old_files/concrete_params.py delete mode 100644 old_files/estimate_oldparams.py delete mode 100644 old_files/figs/iso.png delete mode 100644 old_files/figs/plot.png delete mode 100644 old_files/figs/plot2.png delete mode 100644 old_files/figs/sieve.png delete mode 100644 old_files/figs/uSVP.png delete mode 100644 old_files/hybrid_decoding.py delete mode 100644 old_files/memory_tests/test.py delete mode 100644 old_files/memory_tests/test2.py delete mode 100644 old_files/new_scripts.py delete mode 100644 old_files/scripts.py diff --git a/estimator_new/__init__.py b/estimator_new/__init__.py deleted file mode 100644 index 31e6b389a..000000000 --- a/estimator_new/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from .nd import NoiseDistribution as ND # noqa -from .io import Logging # noqa -from . import reduction as RC # noqa -from . import simulator as Simulator # noqa -from . import lwe as LWE # noqa - -from .schemes import ( # noqa - Kyber512, - Kyber768, - Kyber1024, - LightSaber, - Saber, - FireSaber, - NTRUHPS2048509Enc, - NTRUHPS2048677Enc, - NTRUHPS4096821Enc, - NTRUHRSS701Enc, -) diff --git a/estimator_new/__pycache__/__init__.cpython-38.pyc b/estimator_new/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 3af4eaafc072d7a136cc9cad573e9b25d086f116..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598 zcmYk3%Wm306o!2PY!l2S*JohS+8_c%ilRy(ZKWU-;i5{ju`nLNC_Xlt87ImkMd}mH zwyVBER-NNeHDmencm6qxGvk%-Hx0dlhx_E0X&Aq;__!=xe9^DiYotbMW(JtVGSRKf zh8n3=ZfA995U28*c9(dS*R?mvQRR)S1wQeiP1?{Q9q5uS^hghm$uXRe6F4QOa7NDH zoSf@^Co=l(A3c-NH&t&@FhOTbNY0W{vLe@^GcUGVmT$FsoTlaGkb8nbnXP5P^^ToA z(|%LA_b&AUt8KbEWS5796jO)tZ% zyL_Vu`X7xalaJ9Tyc$o31B}{L*D{JGA=Zojg|hOL~!NUj~ Jp=BN(>n{zwnRNgF diff --git a/estimator_new/__pycache__/conf.cpython-38.pyc b/estimator_new/__pycache__/conf.cpython-38.pyc deleted file mode 100644 index 8f9b209a179d7e8e07d3a29ed509dcfbc102a25a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 424 zcmY+9yH3L}6o%tmdI>F;H$Y;_KqHtK5Nc7S4n+u2b@5_3@hOe4 zPS{jPa4i4UXPxu=_j)`|16TReve>)x)5G5i9ZcQUH%?f91BO`0kqB`pA{-&l5syWJ z6BIy*dl2D1i}3&wJY)$@S#K3&{r9tU4jbBXbH1az1wBc>kaNsJn=HEmls(V$~#k)ls{%iwoeF?B_VdGfVHKmWUYKY6*;j(U5S=cBbAChAG;<} zN^OXc3^=~{&!ps3Yi~^&$vdJOT7o_CO?pckI852`&&97Z9*D{aA{+A&sF8o#%YK@# zH7K1IQ0H^duSPccqB7gI$Q!Dt00Zi>Y}F2Q4%$@0rzADJ<)y4QlcuwSTNkqBaOeM{ OkDbKvDH??_I;?MuNO`jW diff --git a/estimator_new/__pycache__/cost.cpython-38.pyc b/estimator_new/__pycache__/cost.cpython-38.pyc deleted file mode 100644 index 37c5b03f93181b9df9355fd65e43f5349f2cf915..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7417 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 Qn=vc4d)arR5+MwW4^3B+BLDyZ diff --git a/estimator_new/__pycache__/gb.cpython-38.pyc b/estimator_new/__pycache__/gb.cpython-38.pyc deleted file mode 100644 index fd3556a60dc128b7d6cf75986337de42c2dd4703..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7581 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 diff --git a/estimator_new/__pycache__/lwe_bkw.cpython-38.pyc b/estimator_new/__pycache__/lwe_bkw.cpython-38.pyc deleted file mode 100644 index 007db8d57c16e37bc691e10ec2e9d334e72f61ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8924 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 W9THPa<1~3bgFpB_*#3{S=l%y@%pW`e diff --git a/estimator_new/__pycache__/lwe_dual.cpython-38.pyc b/estimator_new/__pycache__/lwe_dual.cpython-38.pyc deleted file mode 100644 index 3062c40f29925cc16527dff59f8e3a25a3b64a8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15838 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 Kr3(CwU;YmSJS1!Y diff --git a/estimator_new/__pycache__/lwe_guess.cpython-38.pyc b/estimator_new/__pycache__/lwe_guess.cpython-38.pyc deleted file mode 100644 index 6eb2e6ebc9f4335b139934fe296cb20316b822f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13838 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 diff --git a/estimator_new/__pycache__/lwe_parameters.cpython-38.pyc b/estimator_new/__pycache__/lwe_parameters.cpython-38.pyc deleted file mode 100644 index 45063c8f62932d246876498c70b7e3c5449f05e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5179 zcmdT|%WoUU8Q;ehtzbl88?RQJp}f*Q z=$WAfsnjJ3(B{w{duY(YIr^A;Pd)V506i|yOM3FZkVDh&nFH<)u^9+3JN8(B5n;wxCk zaC51dB&o9z#7PwRVQPiRW@^i4LZx!EPk2-B$#MlJvw%5=eeK zkV*s_Z51RjRGz&T%XYIFGy)#0RX=KlT;35P5e-~$@Ux(B1A=@G5jX0D8Fllq!7Xkx zaxF_uNOMrOx~UCmzFyFMa@P!&uRDAM+6$0(*vRKby~v7e6w*;vVq=h&*f^Vjbc{{1 z6OfLxDRvUl36=*jOs8WHKDqPA7kKsts8Vavplhq0yXZ9`nq?K$0^!&(SA&t%4`@sg~#dEvkuVgjQvj80&L z3y4XGTodktht1U9-eK~kAx=Z(cOTC^mayHq4Iamk{stolX z5)Fz8Pjd?=`7=atRM2G*!i*veMPhtuFzZj&0r{*aDhpbl=zccD_O!8`6HgQh3d?y2 zI8Kkny0bWEf#rS&*VnDb8U6@)umS7=GeG&Z8Bpxl-X9CspKt#g1YM-v@6OuIhmRiI zSzRjjt3Khh#YHm_5miq)jUnTQG%E`~mYDNV^=Z(g-~7ho^cp6T`Z1$BoNgvc50sfD zEDcPJi(bEeoi;@h(f%dTAZjH-(F^@8^!lyI<(1nNFt>&PUA^nKr40PIT&v8xulME0 z_N`VfFTUOxZntu10_Ax5a&QdV8<&`An?%@^OYto1=(|8u;DR=xa>~znJmiKJX-Rdhu9snf`MdOgqkO`qELnZ_W*U9pu zSgV^3;j4sodLZ_ENyIc(AZlgN8OWt0eLZ>)igE@b<5ZDMSu?~WwvqjchW&j$H$$kk zcV7~bSr7nL!_kb7C1uAsNZNLf(7v=RS8{RM^s*4uNv{nP{0plMm zYBkN2>Aaqs!sO?N0RfS%w{E2?yK=N#U0Ap}-0_|Efzutck^VtlJGEKRP^sg2n0j8C z&o(1aPhBh1PGDq4O4v~?Ep+#N9hzkxB4YwQ;@+vDJ@-!6K+EZUImqA$?Fs(|nGX6p zW1EQa4mf_yE3$2h87wLEZr~F^Pr*lR<-lWFVoBv0@U_o~Dkw*dbO^JMza5a<#;3p9 zw^R}5=s+EKJ?o?~ZLl2pMgrdRff{{LQe$cyT5(l-Yhv0s0PWwmJ9fup`D=!9I!IVd zBu;8Fvk225u$N~KVBKsz8|EJQ(q_fXU$arLnNs^7gC4VQGawOAXsP_T0UonQK_uM) zQUdB9fxdTLAZ>|+wHshb+VLTH_gb_?1hPNTfE9GDDq5l|U@vhy0*yuk?G7F2`8Ioz z{wA;ZVc2uDJ?=Y8f&2Cqr{{EksB`YA`p^VJYVtoeG2|CPbJvT?wF?)jfcfvdlS@bz z&~sEOl{eWKU|hqGd)G>9Phj=lz0i$a3N(oU!};siGz?4cZ=;X9<;OUvdoKw6-+uAi+LM%2_uivziSz&ZVs-Yz>OS=u3y?#~%8Gkpfd8Xg z6hFcUeQat++>g_fD~Y;yxD)-hN^>COO)d~|1U@AN`ayv-KAk+u7}J~$=QRuHW zlIrD8J*|L@D?bS3KpWoLmQ4Jt(wgu4rr6eXfWE5_y8kq8kzxjZ3Ss^ePm5| znH{C1mFDzLj&h0dVM*=7xQzCWO;6~BsQ3ejRcsSg)sHq-pr5(IU?*K%R%@m zK+t@HLMG$BptG7=K#Ff)d{yV0`2%3T;v{r(3#t9omrvi!_Qm_q_2_wkP~k|=djXA7 zQy;Q-dU(IRF771quNeMg0sd9=GHt<{msxgIM-=NMHD= z1%Ek-v$(5s7*UK|jOH+^Vsr>uI){j?ZxmHV;?Sb$06yWbL`tSpa6WKOIW7CDXlT-X#c-4E(MBT|Cf`RG0U(w&+(rxDfBx@YwR)GN}X%keE4;g OqQq`9xF`HsCixF9ljTwX diff --git a/estimator_new/__pycache__/lwe_primal.cpython-38.pyc b/estimator_new/__pycache__/lwe_primal.cpython-38.pyc deleted file mode 100644 index 0326ade84030fb3fbf9fc476583b14cae6f743db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14230 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�KJ#jdTuo-|v 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 Om>2o?jBAr;$A1GdVt=6k diff --git a/estimator_new/__pycache__/schemes.cpython-38.pyc b/estimator_new/__pycache__/schemes.cpython-38.pyc deleted file mode 100644 index 43e2f754d50cd52f11de425d0d90db9c24dfda37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3784 zcmb7FOK%fb6u#p)emikqqpCps)d{ zqPzTnc2%TG73*%gsH&=){)bu7c4c+Z4XRYAl%8{DY>yM0l(Bqf&i%gcJnmy=#p`u3 zxB`ne(=SIE`vV>Gg=iH1MVpN=&bX9iIVmCKED4K*z9rj|vnH$(6IR~BtwQ>ymA5{V zDt8#Oar>?3TQsBHRObH}8S?Ubr85W`QRsb^=~6?}HB7fYDD38YgByGGTOpVzdKe zkPlUq#2Dm9ficLB5yJOJfcms3!ms%cO^PO=jO*j z*TqjzcXWrbQPOo+@k-nZev`}BH#nef2|`f<`v5I)o(zpkGo{3z+ewfTkq zrbr(l{21Up+{vfA7=Nb^_HQq1z`aD=Vc-q{_cCzbZOVO*xFf*z0{4C3Mw@c45cfE6 zuL5@lxYwF;XNh})>YZy)<8{JEjT&PR>qgU99}xGXQR7YE&Nt=WBJL@p#)UWb^C59h z19uVnxeeSUh#QCha?|)Lq&Wi`H~&aT_q}jm<#%@Byun(1Oly@O&RO8#T7A+5YXrQW z1H4a(=La6H(`N>6=m767@y=1sf&hb0;m- zk;#zkw`o#dlRnohcZ%>Anv~O8N~w&(fLI1ql{^)WD9j4JOBaWN@`S=hfYl1DvK@@( znpZL#rFaU0mx0Q(R(jT)bBZeHQTZ3jSD1sb4B(9YoQy&z%S$V<#kd@th)f2jXY*@* zn+jh%reaXli3S8r#qXi&2BUlnmDTCOQ&XT*CZKAC`6|(06l&2Yg45xM975KQpia0c z>ry;E6$~j5)1eo{9MOb3HXGj#g@mdW)Pz@eU>fr&bw}9-Z5AgHFRRf7V^?6#ZTj?U z_T{ zprpOWMoKwJDNCS$O01QrauQRPf7geZwaTVs-$NxvXgL|e&082 zYVvUzHn$0~vhoOI^;>BDG-)&Q^;dYo+<9WNStT9OXSuuztlAKD@m<{zc2J!!CY9#^ zEq@6Hcbuh+n?m!<#A0z;wWK_0co-@ zJZCEI2r{o`0xLy$R|nD}AGjup4@!m2z;&gxv6T)~*K2HTOWhX5fGC!fT&h%1lX>wl zP+Z#(IZ?d0xuZEQ=L&o)E8c_S!HQ{Ecc0|(SS0*8;0O0tHs>LJ*CBWlz3V`H*S>uF zeWM`^) iuf=1^Zj%~%#`-kGD0@|B>EPN^mL&Pn35^ zvnYIV2%aS|)gYB4{eQrO-w5M=6d1{Yn6NFmlA_->VVZFGGpis-H#AQ>^}6qKSkHg> zg+Kk{Q_hz-UjXh`EB)&`6vV8>>=D-#=&XKi}Qu&E_}X zCj8Hzc`|qp#7lfAY^{&Pu@!^Wv2u*cLn#`yCU}qtxGeZ5Jm&B61oru=?|%udM}aoz zKMx^-9tIUeRsRLVO`fov-MW=KQQCb$fp0^$C#wvn0_@AM^$rl5WfIdt5i(4L6G65I zB}%e_AZulHGh@O%M!Ga38J#DhFi&Pl=8j}^mdv51)tDm#FX8+359$uQtdx0K@sXQ3 zkLflO^f&>;ipvT;tw_tibAYf_sq4os?AiQK47JgD|IbNBM{`SWvN zP)`9Nbc#+fk1jCI)G^>+2@5uXegtGMERbLaptx?BaWK8xY<%CM~8{gP!D_Ad1qBFzMPZyCzdenF`yyAyp_fU(&iC+nVKHHQK8Q0JiYK zoz}1k>o&z))cI$lwiRJ46Acw!wcR&TNa}pKAFoIS4b1PZ-{tpY8(`3Tb=K?r^HlVs zzDA9`zhDv6`K2gruW=o2N&fWreb(=Xb>3(P5ysF7&@gm1@4zDzGBKgqO2tZ`pkd| za2?$~Hg=E>eZwXLXJ3a$&QxpY2nww?3Qc?rd*TLket9yxZ4e6kM4_8`K?O-yR?5Q& z51|~}O-&;?jN}Lq_(EXfJI#V>LHue4f3UB_1|4R>fneVTPUDYc3Y+yqb{gmjdYGzH zAlnxK3}k1F(*UgoBnI$umhB+0rYmmlXruB% zzt@v$kDTUi0wd=@lJYWiO$_z9CGm5q?WdK_P!~5-9Ax{P#Z=+`0=7O2WHMhI81;^D zgu6ow!vL_yJ5X*9S;l^(;AUYmoL%S#xz8@*fYaDw{}vo9_9wn(13G|-1Sw&H;Fv#2 zI&4y=Sp8ktxGq+x1+wDUf>3XOOdUsJ;n`mn*Fioiiz3-N)r(To>gx1WC0cRdBNB9F z6=soo6Yk|@ne-u%@E<|NwFQv7f$HkqUE2`asvLM_?u}n?b^KnK;!M2*-14~F?5Ane zdG_q(rB+L%?N&=+LSB~YOHeI7*36zaP9n{{#TciSyny9pp>>bWIR2D>+$;YFMd%Gm diff --git a/estimator_new/__pycache__/util.cpython-38.pyc b/estimator_new/__pycache__/util.cpython-38.pyc deleted file mode 100644 index 7f1a2e82ce48a0c1bf5edb35deeddb2eeadcc9b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8368 zcmd5>%WoXXdGG4(>FIfp;%a4y)G9_rRyLdsO-c^zB!on0Us`XnV3bJS$9UI0&8Z%; z$)4^}RgXjxJqchglJx?1oWStKSc61>0DH>Ce?ks9; zVtw!eo7R{ygJXM0qk=3k% zvMOqLu6HakC+0t7&6>C@7Q}O?)x|~e{D($!PFxX}#Fx-AFD^VV8jDZCi_u`YeLqPe z+{-;*rlB7-%-n7VVU(9TQIg1~tRO4Dm27W^@phZKP5&%B+{L8?ioj?xVKhy_nwBty z1*WhJutn^%rd=@JEENnl9Z?r^V6H5{6z*tXV}}vyOUAvu0*@u_op_!)F~+B?YI+mYYh68^>&4Ei6qjKdesipJ`Q>fX~5NZJk&4u4dqOY@zg zl9}M!kY7%rrWyMQ6f`qYTA!4T*mqfGXNJeJ5=zjav@%oJi&(8^<5|}`CZ0-}h0+m3 zSr!gTH#2srcSV#@R@2%M^{NX867BlyBk3gxepLn}X+G~@zjscFYayYdBO6L8^;*}P z21$Y*uNOBGY%QJoz-EY&-a6+2PQQ@e!GAcN74 zg7`sJRY444ft0mclG+P#nV!Ah>tSWe+e-SeP~HE#zDm7) zR0s}GDS4y>gJN5~{f3jrRLCGqyj2=9%)@KvDCwB6O@ohqRG*nu08<*KY@mHpPu8&Y#JQaw^n$$fm<~6u zq|$E(TYh`*G(*^&U_VSfZyh6y_&Et|I0@o zad{DM8ddpgG#sa5iHd7fe3=UBDa$Bw3tAyBQ*G`6boEVas86FqWJo;ok`L28WrW5W zqczlf1vKgg3WM3u6o=Juab}yg))vRSjGBpei~nmopP@U&NHd2>`!KeSBpKK=9OM#d zkh!V13Hqa#a*hPaI(js5jl}iRIqI|ji%tj>)JGzJ2P4XJrpwvEcRpvbgEO{&ETTWg zT2teg!FrO&!4~dXWIt2b$0M?*b|ENeFCCL0StpFqen7aE;O0W8-=fxxX**iZm?-a6 zsJy{PP&^6v!R6mQA? zrgL~RkE?#g5VaTJd7`xzuWEW1^#3kuQ}pxcg>&gY*7O+vhYU&yTR)t?b_Hw0c)O$V z@P1kQ1GV^V-B#6o9->Yj!~AFP^OB~mY1%&-($>%C1#iLeuBB=Js+dcA8FgRaa3)ss z;p-0`K3JbT!$sT+f*x=C&_JF<0uF1hpDNs7$_sPVYLV%O^?poVYZ$|+257-WNqTC1 zYAmwK8yg$&V3IB$J8!GxxWSk1K?jG57i8qJEa1-{FNb$oE&k z&(L=t6NviI_?geHwpG!wm7<51cCErow;NHJ>}bqc_#?&>gmA)lMsCI7!j1xdobkTCU^7MvoM)q$yI z^@|Bk0XKexc=s>?L?nFx!U3#)$8S^M-f973+M$B!Znai)>`rm3kH8_>r$~@62O;V> zil;yp5o%m8tfP1xu?}~nU&QN_2cS5%aIXO=+Fcw5QD0cJta;Mff5vW(y@KhOS&|SJ z!d)BJw7_dIzks%%QTqal!^<=BHHmfQb9mb(r)cz}I!&Wp$-AiN7!`4PW@=l@Qi^FU zvVjAZBC;LU&D1`I<41Iee@mHjLg^{(hxY+jVOF3W~fk?k&k1mcK#8i&RW-SIZ&+_ut}D)NVXa;bk5F(^7eEL`kx5;nMPW z2^I1iGvkB;9PK?`f`6=ap-~{VHVN{Zpd8w`!YG_#iR{MT<5Gm@1{gZa%;&l2q*!pF zw2O{A(~M96i+Qz|PCT6FJ8Opi0WU|c`h{8Tob}R)K?hfxI^0D;bH0zrY+$B^GHR!A z7YU{#Gc7^=hR;{g+yElWsOD@)ZzDk`wtF6|YmFeFi#gN%p#!a+7KkdC)a2r*GiJ2`)vkAzXv! zOn5bL4*sXQAc^Le2GIYE!8AZig6p$jOfem?_lLOD$0$+_u!13A&>$-%r2Qsn?ROB9e%<&Z zU4>jx@@F#E~5CBCHao*~*v^I_A8$#K@B&a@&{ND!2EZ2$h>*s@1AlqDOB^u}G)#xtRdzpU|WT z#f`Z|Lm0`A4yK>P zVc5m{)Ich#jb83_JROkf@M8imdKVV(`3d8C!yq^4{5QHOdSwi^;S#%qut~m$mNQ8j z7ma!cUP9%Nz7ZJjKpAZ#WqVb5g8|x+RD+@)F~@ihZ=j-6R*`TqpMN<`Ww_N(i=?(* zO@kF{IK?a$LZKJnqWDT<00Uhlgypx1Ic+dep&u=P+)#2r6$iaw_*RMUmV;8kP*yrJ z<+aS{uz_({hr5WwV_-hM3g&Gv|4P9;5;-vF?-{ZAuCZ+lkn#aWtqhRZ!TtKc&79*> z>SS&QA6I_BfM-@(CZy}w8r2>?dJ9L-kcjzNXnfDe9Q1neeS6>z%7Y3}(al`+sbtk7 z8`xO;$N)Z;JLYZU(Z}&B(n2OgsGil2iPAYSzh|ViKL^jkLO|!H^~^Z}A4lMw-W(Z& zxsQmyRajxnkj)*>Wo2qVF*DR|8yFw+bxdSO@r=^NO#hb6X%Qu8u<(2X0&)nWkknbRHlMkuT43tt|?WsaPNmG&aeH`fx&BA}PK(Y3UHh17l>8;6KHX(K9&#Ny~t8V%K E0i?zLFaQ7m diff --git a/estimator_new/conf.py b/estimator_new/conf.py deleted file mode 100644 index fc1beca9e..000000000 --- a/estimator_new/conf.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Default values. -""" - -from .reduction import Kyber, ABLR21 -from .simulator import GSA - -red_cost_model = Kyber -red_cost_model_classical_poly_space = ABLR21 -red_shape_model = "gsa" -red_simulator = GSA -mitm_opt = "analytical" diff --git a/estimator_new/cost.py b/estimator_new/cost.py deleted file mode 100644 index 3c7643635..000000000 --- a/estimator_new/cost.py +++ /dev/null @@ -1,258 +0,0 @@ -# -*- coding: utf-8 -*- -from sage.all import round, log, oo -from dataclasses import dataclass - - -@dataclass -class Cost: - """ - Algorithms costs. - """ - - rop: float = oo - tag: str = None - - # An entry is "impermanent" if it grows when we run the algorithm again. For example, `δ` - # would not scale with the number of operations but `rop` would. This check is strict such that - # unknown entries raise an error. This is to enforce a decision on whether an entry should be - # scaled. - - impermanents = { - "rop": True, - "repetitions": False, - "tag": False, - "problem": False, - } - - @classmethod - def register_impermanent(cls, data=None, **kwds): - if data is not None: - for k, v in data.items(): - if cls.impermanents.get(k, v) != v: - raise ValueError(f"Attempting to overwrite {k}:{cls.impermanents[k]} with {v}") - cls.impermanents[k] = v - - for k, v in kwds.items(): - if cls.impermanents.get(k, v) != v: - raise ValueError(f"Attempting to overwrite {k}:{cls.impermanents[k]} with {v}") - cls.impermanents[k] = v - - key_map = { - "delta": "δ", - "beta": "β", - "eta": "η", - "epsilon": "ε", - "zeta": "ζ", - "ell": "ℓ", - "repetitions": "↻", - } - val_map = {"beta": "%8d", "d": "%8d", "delta": "%8.6f"} - - def __init__(self, **kwds): - for k, v in kwds.items(): - setattr(self, k, v) - - def str(self, keyword_width=None, newline=None, round_bound=2048, compact=False): # noqa C901 - """ - - :param keyword_width: keys are printed with this width - :param newline: insert a newline - :param round_bound: values beyond this bound are represented as powers of two - :param compact: do not add extra whitespace to align entries - - EXAMPLE:: - - >>> from estimator.cost import Cost - >>> s = Cost(delta=5, bar=2) - >>> s - δ: 5.000000, bar: 2 - - """ - - def wfmtf(k): - if keyword_width: - fmt = "%%%ss" % keyword_width - else: - fmt = "%s" - return fmt % k - - d = self.__dict__ - s = [] - for k, v in d.items(): - if k == "problem": # we store the problem instance in a cost object for reference - continue - kk = wfmtf(self.key_map.get(k, k)) - try: - if (1 / round_bound < abs(v) < round_bound) or (not v) or (k in self.val_map): - if abs(v % 1) < 0.0000001: - vv = self.val_map.get(k, "%8d") % round(v) - else: - vv = self.val_map.get(k, "%8.3f") % v - else: - vv = "%7s" % ("≈2^%.1f" % log(v, 2)) - except TypeError: # strings and such - vv = "%8s" % v - if compact: - kk = kk.strip() - vv = vv.strip() - s.append(f"{kk}: {vv}") - - if not newline: - return ", ".join(s) - else: - return "\n".join(s) - - def reorder(self, *args): - """ - Return a new ordered dict from the key:value pairs in dictinonary but reordered such that the - keys given to this function come first. - - :param args: keys which should come first (in order) - - EXAMPLE:: - - >>> from estimator.cost import Cost - >>> d = Cost(a=1,b=2,c=3); d - a: 1, b: 2, c: 3 - - >>> d.reorder("b","c","a") - b: 2, c: 3, a: 1 - - """ - keys = list(self.__dict__.keys()) - for key in args: - keys.pop(keys.index(key)) - keys = list(args) + keys - r = dict() - for key in keys: - r[key] = self.__dict__[key] - return Cost(**r) - - def filter(self, **keys): - """ - Return new ordered dictinonary from dictionary restricted to the keys. - - :param dictionary: input dictionary - :param keys: keys which should be copied (ordered) - """ - r = dict() - for key in keys: - r[key] = self.__dict__[key] - return Cost(**r) - - def repeat(self, times, select=None): - """ - Return a report with all costs multiplied by ``times``. - - :param times: the number of times it should be run - :param select: toggle which fields ought to be repeated and which should not - :returns: a new cost estimate - - EXAMPLE:: - - >>> from estimator.cost import Cost - >>> c0 = Cost(a=1, b=2) - >>> c0.register_impermanent(a=True, b=False) - >>> c0.repeat(1000) - a: 1000, b: 2, ↻: 1000 - - TESTS:: - - >>> from estimator.cost import Cost - >>> Cost(rop=1).repeat(1000).repeat(1000) - rop: ≈2^19.9, ↻: ≈2^19.9 - - """ - impermanents = dict(self.impermanents) - - if select is not None: - for key in select: - impermanents[key] = select[key] - - ret = dict() - for key in self.__dict__: - try: - if impermanents[key]: - ret[key] = times * self.__dict__[key] - else: - ret[key] = self.__dict__[key] - except KeyError: - raise NotImplementedError( - f"You found a bug, this function does not know about '{key}' but should." - ) - ret["repetitions"] = times * ret.get("repetitions", 1) - return Cost(**ret) - - def __rmul__(self, times): - return self.repeat(times) - - def combine(self, right, base=None): - """Combine ``left`` and ``right``. - - :param left: cost dictionary - :param right: cost dictionary - :param base: add entries to ``base`` - - EXAMPLE:: - - >>> from estimator.cost import Cost - >>> c0 = Cost(a=1) - >>> c1 = Cost(b=2) - >>> c2 = Cost(c=3) - >>> c0.combine(c1) - a: 1, b: 2 - >>> c0.combine(c1, base=c2) - c: 3, a: 1, b: 2 - - """ - if base is None: - cost = dict() - else: - cost = base.__dict__ - for key in self.__dict__: - cost[key] = self.__dict__[key] - for key in right: - cost[key] = right.__dict__[key] - return Cost(**cost) - - def __bool__(self): - return self.__dict__.get("rop", oo) < oo - - def __add__(self, other): - return self.combine(self, other) - - def __getitem__(self, key): - return self.__dict__[key] - - def __delitem__(self, key): - del self.__dict__[key] - - def get(self, key, default): - return self.__dict__.get(key, default) - - def __setitem__(self, key, value): - self.__dict__[key] = value - - def __iter__(self): - return iter(self.__dict__) - - def values(self): - return self.__dict__.values() - - def __repr__(self): - return self.str(compact=True) - - def __str__(self): - return self.str(newline=True, keyword_width=12) - - def __lt__(self, other): - try: - return self["rop"] < other["rop"] - except AttributeError: - return self["rop"] < other - - def __le__(self, other): - try: - return self["rop"] <= other["rop"] - except AttributeError: - return self["rop"] <= other diff --git a/estimator_new/errors.py b/estimator_new/errors.py deleted file mode 100644 index 2da918637..000000000 --- a/estimator_new/errors.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -class OutOfBoundsError(ValueError): - """ - Used to indicate a wrong value, for example δ < 1. - """ - - pass - - -class InsufficientSamplesError(ValueError): - """ - Used to indicate the number of samples given is too small. - """ - - pass diff --git a/estimator_new/gb.py b/estimator_new/gb.py deleted file mode 100644 index 26b1cd7d8..000000000 --- a/estimator_new/gb.py +++ /dev/null @@ -1,252 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Estimate cost of solving LWE using Gröbner bases. - -See :ref:`Arora-GB` for an overview. - -""" -from sage.all import ( - PowerSeriesRing, - QQ, - RR, - oo, - binomial, - sqrt, - ceil, - floor, - exp, - log, - pi, - RealField, -) -from .cost import Cost -from .lwe_parameters import LWEParameters -from .io import Logging - - -def gb_cost(n, D, omega=2, prec=None): - """ - Estimate the complexity of computing a Gröbner basis. - - :param n: Number of variables n > 0. - :param D: Tuple of `(d,m)` pairs where `m` is number polynomials and `d` is a degree. - :param omega: Linear algebra exponent, i.e. matrix-multiplication costs `O(n^ω)` operations. - :param prec: Compute power series up to this precision (default: `2n`). - - EXAMPLE:: - - >>> from estimator.gb import gb_cost - >>> gb_cost(128, [(2, 256)]) - rop: ≈2^144.6, dreg: 17, mem: ≈2^144.6 - - """ - prec = 2 * n if prec is None else prec - - R = PowerSeriesRing(QQ, "z", prec) - z = R.gen() - z = z.add_bigoh(prec) - s = R(1) - s = s.add_bigoh(prec) - - for d, m in D: - s *= (1 - z ** d) ** m - s /= (1 - z) ** n - - retval = Cost(rop=oo, dreg=oo) - retval.register_impermanent({"rop": True, "dreg": False, "mem": False}) - - for dreg in range(prec): - if s[dreg] < 0: - break - else: - return retval - - retval["dreg"] = dreg - retval["rop"] = binomial(n + dreg, dreg) ** omega - retval["mem"] = binomial(n + dreg, dreg) ** 2 - - return retval - - -class AroraGB: - @staticmethod - def ps_single(C): - """ - Probability that a Gaussian is within `C` standard deviations. - """ - RR = RealField(256) - C = RR(C) - return RR(1 - (RR(2) / (C * RR(sqrt(2 * pi))) * exp(-(C ** 2) / RR(2)))) # noqa - - @classmethod - def cost_bounded(cls, params, success_probability=0.99, omega=2, log_level=1, **kwds): - """ - Estimate cost using absolute bounds for secrets and noise. - - :param params: LWE parameters. - :param success_probability: target success probability - :param omega: linear algebra constant. - - """ - d = params.Xe.bounds[1] - params.Xe.bounds[0] + 1 - dn = cls.equations_for_secret(params) - cost = gb_cost(params.n, [(d, params.m)] + dn) - cost["t"] = (d - 1) // 2 - if cost["dreg"] < oo and binomial(params.n + cost["dreg"], cost["dreg"]) < params.m: - cost["m"] = binomial(params.n + cost["dreg"], cost["dreg"]) - else: - cost["m"] = params.m - cost.register_impermanent(t=False, m=True) - return cost - - @classmethod - def cost_Gaussian_like(cls, params, success_probability=0.99, omega=2, log_level=1, **kwds): - """ - Estimate cost using absolute bounds for secrets and Gaussian tail bounds for noise. - - :param params: LWE parameters. - :param success_probability: target success probability - :param omega: linear algebra constant. - - """ - dn = cls.equations_for_secret(params) - - best, stuck = None, 0 - for t in range(ceil(params.Xe.stddev), params.n): - d = 2 * t + 1 - C = RR(t / params.Xe.stddev) - assert C >= 1 # if C is too small, we ignore it - # Pr[success]^m = Pr[overall success] - single_prob = AroraGB.ps_single(C) - m_req = log(success_probability, 2) / log(single_prob, 2) - m_req = floor(m_req) - - if m_req > params.m: - break - - current = gb_cost(params.n, [(d, m_req)] + dn, omega) - - if current["dreg"] == oo: - continue - - current["t"] = t - current["m"] = m_req - current.register_impermanent(t=False, m=True) - current = current.reorder("rop", "m", "dreg", "t") - - Logging.log("repeat", log_level + 1, f"{repr(current)}") - - if best is None: - best = current - else: - if best > current: - best = current - stuck = 0 - else: - stuck += 1 - if stuck >= 5: - break - - if best is None: - best = Cost(rop=oo, dreg=oo) - return best - - @classmethod - def equations_for_secret(cls, params): - """ - Return ``(d,n)`` tuple to encode that `n` equations of degree `d` are available from the LWE secret. - - :param params: LWE parameters. - - """ - if params.Xs <= params.Xe: - a, b = params.Xs.bounds - if b - a < oo: - d = b - a + 1 - elif params.Xs.is_Gaussian_like: - d = 2 * ceil(3 * params.Xs.stddev) + 1 - else: - raise NotImplementedError(f"Do not know how to handle {params.Xs}.") - dn = [(d, params.n)] - else: - dn = [] - return dn - - def __call__( - self, params: LWEParameters, success_probability=0.99, omega=2, log_level=1, **kwds - ): - """ - Arora-GB as described in [ICALP:AroGe11]_, [EPRINT:ACFP14]_. - - :param params: LWE parameters. - :param success_probability: targeted success probability < 1. - :param omega: linear algebra constant. - :return: A cost dictionary - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``m``: Number of samples consumed. - - ``dreg``: The degree of regularity or "solving degree". - - ``t``: Polynomials of degree 2t + 1 are considered. - - ``mem``: Total memory usage. - - EXAMPLE:: - - >>> from estimator import * - >>> params = LWE.Parameters(n=64, q=7681, Xs=ND.DiscreteGaussian(3.0), Xe=ND.DiscreteGaussian(3.0), m=2**50) - >>> LWE.arora_gb(params) - rop: ≈2^307.1, m: ≈2^46.8, dreg: 99, t: 25, mem: ≈2^307.1, tag: arora-gb - - TESTS:: - - >>> LWE.arora_gb(params.updated(m=2**120)) - rop: ≈2^282.6, m: ≈2^101.1, dreg: 83, t: 36, mem: ≈2^282.6, tag: arora-gb - >>> LWE.arora_gb(params.updated(Xe=ND.UniformMod(7))) - rop: ≈2^60.6, dreg: 7, mem: ≈2^60.6, t: 3, m: ≈2^30.3, tag: arora-gb - >>> LWE.arora_gb(params.updated(Xe=ND.CenteredBinomial(8))) - rop: ≈2^122.3, dreg: 19, mem: ≈2^122.3, t: 8, m: ≈2^50.0, tag: arora-gb - >>> LWE.arora_gb(params.updated(Xs=ND.UniformMod(5), Xe=ND.CenteredBinomial(4), m=1024)) - rop: ≈2^227.2, dreg: 54, mem: ≈2^227.2, t: 4, m: 1024, tag: arora-gb - >>> LWE.arora_gb(params.updated(Xs=ND.UniformMod(3), Xe=ND.CenteredBinomial(4), m=1024)) - rop: ≈2^189.9, dreg: 39, mem: ≈2^189.9, t: 4, m: 1024, tag: arora-gb - - .. [EPRINT:ACFP14] Martin R. Albrecht, Carlos Cid, Jean-Charles Faugère & Ludovic Perret. (2014). - Algebraic algorithms for LWE. https://eprint.iacr.org/2014/1018 - - .. [ICALP:AroGe11] Sanjeev Aror & Rong Ge. (2011). New algorithms for learning in presence of - errors. In L. Aceto, M. Henzinger, & J. Sgall, ICALP 2011, Part I (pp. 403–415).: - Springer, Heidelberg. - """ - params = params.normalize() - - best = Cost(rop=oo, dreg=oo) - - if params.Xe.is_bounded: - cost = self.cost_bounded( - params, - success_probability=success_probability, - omega=omega, - log_level=log_level, - ) - Logging.log("gb", log_level, f"b: {repr(cost)}") - best = min(best, cost, key=lambda x: x["dreg"]) - - if params.Xe.is_Gaussian_like: - cost = self.cost_Gaussian_like( - params, - success_probability=success_probability, - omega=omega, - log_level=log_level, - ) - Logging.log("gb", log_level, f"G: {repr(cost)}") - best = min(best, cost, key=lambda x: x["dreg"]) - - best["tag"] = "arora-gb" - best["problem"] = params - return best - - __name__ = "arora_gb" - - -arora_gb = AroraGB() diff --git a/estimator_new/io.py b/estimator_new/io.py deleted file mode 100644 index 1fae9e6fa..000000000 --- a/estimator_new/io.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - - -class Logging: - """ - Control level of detail being printed. - """ - - plain_logger = logging.StreamHandler() - plain_logger.setFormatter(logging.Formatter("%(message)s")) - - detail_logger = logging.StreamHandler() - detail_logger.setFormatter(logging.Formatter("[%(name)8s] %(message)s")) - - logging.getLogger("estimator").handlers = [plain_logger] - logging.getLogger("estimator").setLevel(logging.INFO) - - loggers = ("batch", "bdd", "usvp", "bkw", "gb", "repeat", "guess", "bins", "dual") - - CRITICAL = logging.CRITICAL - ERROR = logging.ERROR - WARNING = logging.WARNING - INFO = logging.INFO - LEVEL0 = logging.INFO - LEVEL1 = logging.INFO - 2 - LEVEL2 = logging.INFO - 4 - LEVEL3 = logging.INFO - 6 - LEVEL4 = logging.INFO - 8 - LEVEL5 = logging.DEBUG - DEBUG = logging.DEBUG - NOTSET = logging.NOTSET - - for logger in loggers: - logging.getLogger(logger).handlers = [detail_logger] - logging.getLogger(logger).setLevel(logging.INFO) - - @staticmethod - def set_level(lvl, loggers=None): - """Set logging level - - :param lvl: one of `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `LEVELX`, `DEBUG`, `NOTSET` with `X` ∈ [0,5] - :param loggers: one of `Logging.loggers`, if `None` all loggers are used. - - """ - if loggers is None: - loggers = Logging.loggers - - for logger in loggers: - logging.getLogger(logger).setLevel(lvl) - - @classmethod - def log(cls, logger, level, msg, *args, **kwds): - level = int(level) - return logging.getLogger(logger).log( - cls.INFO - 2 * level, f"{{{level}}} " + msg, *args, **kwds - ) diff --git a/estimator_new/lwe.py b/estimator_new/lwe.py deleted file mode 100644 index 7fb440ee6..000000000 --- a/estimator_new/lwe.py +++ /dev/null @@ -1,169 +0,0 @@ -# -*- coding: utf-8 -*- -""" -High-level LWE interface -""" - -from .lwe_primal import primal_usvp, primal_bdd, primal_hybrid -from .lwe_bkw import coded_bkw -from .lwe_guess import exhaustive_search, mitm, distinguish # noqa -from .lwe_dual import dual, dual_hybrid -from .lwe_guess import guess_composition -from .gb import arora_gb # noqa -from .lwe_parameters import LWEParameters as Parameters # noqa - - -class Estimate: - @classmethod - def rough(cls, params, jobs=1): - """ - This function makes the following somewhat routine assumptions: - - - The GSA holds. - - The Core-SVP model holds. - - This function furthermore assumes the following heuristics: - - - The primal hybrid attack only applies to sparse secrets. - - The dual hybrid MITM attack only applies to sparse secrets. - - Arora-GB only applies to bounded noise with at least `n^2` samples. - - BKW is not competitive. - - :param params: LWE parameters. - :param jobs: Use multiple threads in parallel. - - EXAMPLE :: - - >>> from estimator import * - >>> _ = lwe.estimate.rough(Kyber512) - usvp :: rop: ≈2^118.6, red: ≈2^118.6, δ: 1.003941, β: 406, d: 998, tag: usvp - dual_hybrid :: rop: ≈2^127.2, mem: ≈2^123.3, m: 512, red: ≈2^127.0, δ: 1.003756, β: 435, ... - - - """ - # NOTE: Don't import these at the top-level to avoid circular imports - from functools import partial - from .reduction import ADPS16 - from .util import batch_estimate, f_name - - algorithms = {} - - algorithms["usvp"] = partial(primal_usvp, red_cost_model=ADPS16, red_shape_model="gsa") - - if params.Xs.is_sparse: - algorithms["hybrid"] = partial( - primal_hybrid, red_cost_model=ADPS16, red_shape_model="gsa" - ) - - if params.Xs.is_sparse: - algorithms["dual_mitm_hybrid"] = partial( - dual_hybrid, red_cost_model=ADPS16, mitm_optimization=True - ) - else: - algorithms["dual_hybrid"] = partial( - dual_hybrid, red_cost_model=ADPS16, mitm_optimization=False - ) - - if params.m > params.n ** 2 and params.Xe.is_bounded: - if params.Xs.is_sparse: - algorithms["arora-gb"] = guess_composition(arora_gb.cost_bounded) - else: - algorithms["arora-gb"] = arora_gb.cost_bounded - - res = batch_estimate(params, algorithms.values(), log_level=1, jobs=jobs) - res = res[params] - for algorithm in algorithms: - for k, v in res.items(): - if f_name(algorithms[algorithm]) == k: - print(f"{algorithm:20s} :: {repr(v)}") - return res - - def __call__( - self, - params, - red_cost_model=None, - red_shape_model=None, - deny_list=("arora-gb",), - add_list=tuple(), - jobs=1, - ): - """ - Run all estimates. - - :param params: LWE parameters. - :param red_cost_model: How to cost lattice reduction. - :param red_shape_model: How to model the shape of a reduced basis (applies to primal attacks) - :param deny_list: skip these algorithms - :param add_list: add these ``(name, function)`` pairs to the list of algorithms to estimate.a - :param jobs: Use multiple threads in parallel. - - EXAMPLE :: - - >>> from estimator import * - >>> _ = lwe.estimate(Kyber512) - bkw :: rop: ≈2^178.8, m: ≈2^166.8, mem: ≈2^167.8, b: 14, t1: 0, t2: 16, ℓ: 13, #cod: 448... - usvp :: rop: ≈2^148.0, red: ≈2^148.0, δ: 1.003941, β: 406, d: 998, tag: usvp - bdd :: rop: ≈2^144.5, red: ≈2^143.8, svp: ≈2^143.0, β: 391, η: 421, d: 1013, tag: bdd - dual :: rop: ≈2^169.9, mem: ≈2^130.0, m: 512, red: ≈2^169.7, δ: 1.003484, β: 484, d: 1024... - dual_hybrid :: rop: ≈2^166.8, mem: ≈2^161.9, m: 512, red: ≈2^166.6, δ: 1.003541, β: 473, d: 1011... - - """ - from sage.all import oo - from functools import partial - from .conf import red_cost_model as red_cost_model_default - from .conf import red_shape_model as red_shape_model_default - from .util import batch_estimate, f_name - - if red_cost_model is None: - red_cost_model = red_cost_model_default - if red_shape_model is None: - red_shape_model = red_shape_model_default - - algorithms = {} - - algorithms["arora-gb"] = guess_composition(arora_gb) - algorithms["bkw"] = coded_bkw - - algorithms["usvp"] = partial( - primal_usvp, red_cost_model=red_cost_model, red_shape_model=red_shape_model - ) - algorithms["bdd"] = partial( - primal_bdd, red_cost_model=red_cost_model, red_shape_model=red_shape_model - ) - algorithms["hybrid"] = partial( - primal_hybrid, red_cost_model=red_cost_model, red_shape_model=red_shape_model - ) - algorithms["dual"] = partial(dual, red_cost_model=red_cost_model) - algorithms["dual_hybrid"] = partial( - dual_hybrid, red_cost_model=red_cost_model, mitm_optimization=False - ) - algorithms["dual_mitm_hybrid"] = partial( - dual_hybrid, red_cost_model=red_cost_model, mitm_optimization=True - ) - - for k in deny_list: - del algorithms[k] - for k, v in add_list: - algorithms[k] = v - - res_raw = batch_estimate(params, algorithms.values(), log_level=1, jobs=jobs) - res_raw = res_raw[params] - res = {} - for algorithm in algorithms: - for k, v in res_raw.items(): - if f_name(algorithms[algorithm]) == k: - res[algorithm] = v - - for algorithm in algorithms: - for k, v in res.items(): - if algorithm == k: - if v["rop"] == oo: - continue - if k == "hybrid" and res["bdd"]["rop"] < v["rop"]: - continue - if k == "dual_mitm_hybrid" and res["dual_hybrid"]["rop"] < v["rop"]: - continue - print(f"{algorithm:20s} :: {repr(v)}") - return res - - -estimate = Estimate() diff --git a/estimator_new/lwe_bkw.py b/estimator_new/lwe_bkw.py deleted file mode 100644 index c6781741c..000000000 --- a/estimator_new/lwe_bkw.py +++ /dev/null @@ -1,304 +0,0 @@ -# -*- coding: utf-8 -*- -""" -See :ref:`Coded-BKW for LWE` for what is available. -""" -from sage.all import ceil, log, floor, sqrt, var, find_root, erf, oo -from .lwe_parameters import LWEParameters -from .util import local_minimum -from .cost import Cost -from .errors import InsufficientSamplesError -from .prob import amplify_sigma -from .nd import sigmaf -from .io import Logging - -cfft = 1 # convolutions mod q - - -class CodedBKW: - @staticmethod - def N(i, sigma_set, b, q): - """ - Return `N_i` for the `i`-th `[N_i, b]` linear code. - - :param i: index - :param sigma_set: target noise level - """ - return floor(b / (1 - log(12 * sigma_set ** 2 / 2 ** i, q) / 2)) - - @staticmethod - def ntest(n, ell, t1, t2, b, q): # noqa - """ - If the parameter ``ntest`` is not provided, we use this function to estimate it. - - :param n: LWE dimension > 0. - :param ell: Table size for hypothesis testing. - :param t1: Number of normal BKW steps. - :param t2: Number of coded BKW steps. - :param b: Table size for BKW steps. - - """ - - # there is no hypothesis testing because we have enough normal BKW - # tables to cover all of of n - if t1 * b >= n: - return 0 - - # solve for nest by aiming for ntop == 0 - ntest = var("ntest") - sigma_set = sqrt(q ** (2 * (1 - ell / ntest)) / 12) - ncod = sum([CodedBKW.N(i, sigma_set, b, q) for i in range(1, t2 + 1)]) - ntop = n - ncod - ntest - t1 * b - - try: - start = max(int(round(find_root(ntop, 2, n - t1 * b + 1, rtol=0.1))) - 1, 2) - except RuntimeError: - start = 2 - ntest_min = 1 - for ntest in range(start, n - t1 * b + 1): - if abs(ntop(ntest=ntest).n()) < abs(ntop(ntest=ntest_min).n()): - ntest_min = ntest - else: - break - return int(ntest_min) - - def t1(params: LWEParameters, ell, t2, b, ntest=None): - """ - We compute t1 from N_i by observing that any N_i ≤ b gives no advantage over vanilla - BKW, but the estimates for coded BKW always assume quantisation noise, which is too - pessimistic for N_i ≤ b. - """ - - t1 = 0 - if ntest is None: - ntest = CodedBKW.ntest(params.n, ell, t1, t2, b, params.q) - sigma_set = sqrt(params.q ** (2 * (1 - ell / ntest)) / 12) - Ni = [CodedBKW.N(i, sigma_set, b, params.q) for i in range(1, t2 + 1)] - t1 = len([e for e in Ni if e <= b]) - # there is no point in having more tables than needed to cover n - if b * t1 > params.n: - t1 = params.n // b - return t1 - - @staticmethod - def cost( - t2: int, - b: int, - ntest: int, - params: LWEParameters, - success_probability=0.99, - log_level=1, - ): - """ - Coded-BKW cost. - - :param t2: Number of coded BKW steps (≥ 0). - :param b: Table size (≥ 1). - :param success_probability: Targeted success probability < 1. - :param ntest: Number of coordinates to hypothesis test. - - """ - cost = Cost() - - # Our cost is mainly determined by q^b, on the other hand there are expressions in q^(ℓ+1) - # below, hence, we set ℓ = b - 1. This allows to achieve the performance reported in - # [C:GuoJohSta15]. - - cost["b"] = b - ell = b - 1 # noqa - cost["ell"] = ell - - secret_bounds = params.Xs.bounds - if params.Xs.is_Gaussian_like and params.Xs.mean == 0: - secret_bounds = ( - max(secret_bounds[0], -3 * params.Xs.stddev), - min(secret_bounds[1], 3 * params.Xs.stddev), - ) - - # base of the table size. - zeta = secret_bounds[1] - secret_bounds[0] + 1 - - t1 = CodedBKW.t1(params, ell, t2, b, ntest) - t2 -= t1 - - cost["t1"] = t1 - cost["t2"] = t2 - - cost.register_impermanent(t1=False, t2=False) - - # compute ntest with the t1 just computed - if ntest is None: - ntest = CodedBKW.ntest(params.n, ell, t1, t2, b, params.q) - - # if there's no ntest then there's no `σ_{set}` and hence no ncod - if ntest: - sigma_set = sqrt(params.q ** (2 * (1 - ell / ntest)) / 12) - ncod = sum([CodedBKW.N(i, sigma_set, b, params.q) for i in range(1, t2 + 1)]) - else: - ncod = 0 - - ntot = ncod + ntest - ntop = max(params.n - ncod - ntest - t1 * b, 0) - cost["#cod"] = ncod # coding step - cost["#top"] = ntop # guessing step, typically zero - cost["#test"] = ntest # hypothesis testing - - cost.register_impermanent({"#cod": False, "#top": False, "#test": False}) - - # Theorem 1: quantization noise + addition noise - coding_variance = params.Xs.stddev ** 2 * sigma_set ** 2 * ntot - sigma_final = float(sqrt(2 ** (t1 + t2) * params.Xe.stddev ** 2 + coding_variance)) - - M = amplify_sigma(success_probability, sigmaf(sigma_final), params.q) - if M is oo: - cost["rop"] = oo - cost["m"] = oo - return cost - m = (t1 + t2) * (params.q ** b - 1) / 2 + M - cost["m"] = float(m) - cost.register_impermanent(m=True) - - if not params.Xs <= params.Xe: - # Equation (7) - n = params.n - t1 * b - C0 = (m - n) * (params.n + 1) * ceil(n / (b - 1)) - assert C0 >= 0 - else: - C0 = 0 - - # Equation (8) - C1 = sum( - [(params.n + 1 - i * b) * (m - i * (params.q ** b - 1) / 2) for i in range(1, t1 + 1)] - ) - assert C1 >= 0 - - # Equation (9) - C2_ = sum( - [ - 4 * (M + i * (params.q ** b - 1) / 2) * CodedBKW.N(i, sigma_set, b, params.q) - for i in range(1, t2 + 1) - ] - ) - C2 = float(C2_) - for i in range(1, t2 + 1): - C2 += float( - ntop + ntest + sum([CodedBKW.N(j, sigma_set, b, params.q) for j in range(1, i + 1)]) - ) * (M + (i - 1) * (params.q ** b - 1) / 2) - assert C2 >= 0 - - # Equation (10) - C3 = M * ntop * (2 * zeta + 1) ** ntop - assert C3 >= 0 - - # Equation (11) - C4_ = 4 * M * ntest - C4 = C4_ + (2 * zeta + 1) ** ntop * ( - cfft * params.q ** (ell + 1) * (ell + 1) * log(params.q, 2) + params.q ** (ell + 1) - ) - assert C4 >= 0 - - C = (C0 + C1 + C2 + C3 + C4) / ( - erf(zeta / sqrt(2 * params.Xe.stddev)) ** ntop - ) # TODO don't ignore success probability - cost["rop"] = float(C) - cost["mem"] = (t1 + t2) * params.q ** b - - cost = cost.reorder("rop", "m", "mem", "b", "t1", "t2") - cost["tag"] = "coded-bkw" - cost["problem"] = params - Logging.log("bkw", log_level + 1, f"{repr(cost)}") - - return cost - - @classmethod - def b( - cls, - params: LWEParameters, - ntest=None, - log_level=1, - ): - def sf(x, best): - return (x["rop"] <= best["rop"]) and (best["m"] > params.m or x["m"] <= params.m) - - # the outer search is over b, which determines the size of the tables: q^b - b_max = 3 * ceil(log(params.q, 2)) - with local_minimum(2, b_max, smallerf=sf) as it_b: - for b in it_b: - # the inner search is over t2, the number of coded steps - t2_max = min(params.n // b, ceil(3 * log(params.q, 2))) - with local_minimum(2, t2_max, smallerf=sf) as it_t2: - for t2 in it_t2: - y = cls.cost(b=b, t2=t2, ntest=ntest, params=params) - it_t2.update(y) - it_b.update(it_t2.y) - best = it_b.y - - # the search cannot fail. It just outputs some X with X["oracle"]>m. - if best["m"] > params.m: - raise InsufficientSamplesError( - f"Got m≈2^{float(log(params.m, 2.0)):.1f} samples, but require ≈2^{float(log(best['m'],2.0)):.1f}.", - best["m"], - ) - return best - - def __call__( - self, - params: LWEParameters, - ntest=None, - log_level=1, - ): - """ - Coded-BKW as described in [C:GuoJohSta15]_. - - :param params: LWE parameters - :param ntest: Number of coordinates to hypothesis test. - :return: A cost dictionary - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``b``: BKW tables have size `q^b`. - - ``t1``: Number of plain BKW tables. - - ``t2``: Number of Coded-BKW tables. - - ``ℓ``: Hypothesis testing has tables of size `q^{ℓ+1}` - - ``#cod``: Number of coding steps. - - ``#top``: Number of guessing steps (typically zero) - - ``#test``: Number of coordinates to do hypothesis testing on. - - EXAMPLE:: - - >>> from sage.all import oo - >>> from estimator import * - >>> LWE.coded_bkw(LightSaber.updated(m=oo)) - rop: ≈2^171.7, m: ≈2^159.4, mem: ≈2^160.4, b: 12, t1: 3, t2: 18, ℓ: 11, #cod: 423, #top: 1... - - We may need to amplify the number of samples, which modifies the noise distribution:: - - >>> from sage.all import oo - >>> from estimator import * - >>> LightSaber - LWEParameters(n=512, q=8192, Xs=D(σ=1.58), Xe=D(σ=2.00), m=512, tag='LightSaber') - >>> cost = LWE.coded_bkw(LightSaber); cost - rop: ≈2^184.3, m: ≈2^172.2, mem: ≈2^173.2, b: 13, t1: 0, t2: 18, ℓ: 12, #cod: 456, #top: 0... - >>> cost["problem"] - LWEParameters(n=512, q=8192, Xs=D(σ=1.58), Xe=D(σ=10.39), m=..., tag='LightSaber') - - .. note :: See also [C:KirFou15]_. - - """ - params = LWEParameters.normalize(params) - try: - cost = self.b(params, ntest=ntest, log_level=log_level) - except InsufficientSamplesError as e: - m = e.args[1] - while True: - params_ = params.amplify_m(m) - try: - cost = self.b(params_, ntest=ntest, log_level=log_level) - break - except InsufficientSamplesError as e: - m = e.args[1] - - return cost - - -coded_bkw = CodedBKW() diff --git a/estimator_new/lwe_dual.py b/estimator_new/lwe_dual.py deleted file mode 100644 index 01f8ba054..000000000 --- a/estimator_new/lwe_dual.py +++ /dev/null @@ -1,526 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Estimate cost of solving LWE using dial attacks. - -See :ref:`LWE Dual Attacks` for an introduction what is available. - -""" - -from functools import partial -from dataclasses import replace - -from sage.all import oo, ceil, sqrt, log, cached_function, exp -from .reduction import delta as deltaf -from .reduction import cost as costf -from .reduction import ADPS16, BDGL16 -from .reduction import LLL -from .util import local_minimum -from .cost import Cost -from .lwe_parameters import LWEParameters -from .prob import drop as prob_drop -from .prob import amplify as prob_amplify -from .io import Logging -from .conf import red_cost_model as red_cost_model_default -from .conf import mitm_opt as mitm_opt_default -from .errors import OutOfBoundsError -from .nd import NoiseDistribution -from .lwe_guess import exhaustive_search, mitm, distinguish - - -class DualHybrid: - """ - Estimate cost of solving LWE using dual attacks. - """ - - full_sieves = [ADPS16.__name__, BDGL16.__name__] - - @staticmethod - @cached_function - def dual_reduce( - delta: float, - params: LWEParameters, - zeta: int = 0, - h1: int = 0, - rho: float = 1.0, - log_level=None, - ): - """ - Produce new LWE sample using a dual vector on first `n-ζ` coordinates of the secret. The - length of the dual vector is given by `δ` in root Hermite form and using a possible - scaling factor, i.e. `|v| = ρ ⋅ δ^d * q^((n-ζ)/d)`. - - :param delta: Length of the vector in root Hermite form - :param params: LWE parameters - :param zeta: Dimension ζ ≥ 0 of new LWE instance - :param h1: Number of non-zero components of the secret of the new LWE instance - :param rho: Factor introduced by obtaining multiple dual vectors - :returns: new ``LWEParameters`` and ``m`` - - .. note :: This function assumes that the instance is normalized. - - """ - if not 0 <= zeta <= params.n: - raise OutOfBoundsError( - f"Splitting dimension {zeta} must be between 0 and n={params.n}." - ) - - # Compute new secret distribution - - if params.Xs.is_sparse: - h = params.Xs.get_hamming_weight(params.n) - if not 0 <= h1 <= h: - raise OutOfBoundsError(f"Splitting weight {h1} must be between 0 and h={h}.") - # assuming the non-zero entries are uniform - p = h1 / 2 - red_Xs = NoiseDistribution.SparseTernary(params.n - zeta, h / 2 - p) - slv_Xs = NoiseDistribution.SparseTernary(zeta, p) - - if h1 == h: - # no reason to do lattice reduction if we assume - # that the hw on the reduction part is 0 - return replace(params, Xs=slv_Xs, m=oo), 1 - else: - # distribution is i.i.d. for each coordinate - red_Xs = replace(params.Xs, n=params.n - zeta) - slv_Xs = replace(params.Xs, n=zeta) - - c = red_Xs.stddev * params.q / params.Xe.stddev - - # see if we have optimally many samples (as in [INDOCRYPT:EspJouKha20]) available - m_ = max(1, ceil(sqrt(red_Xs.n * log(c) / log(delta))) - red_Xs.n) - m_ = min(params.m, m_) - - # Compute new noise as in [INDOCRYPT:EspJouKha20] - sigma_ = rho * red_Xs.stddev * delta ** (m_ + red_Xs.n) / c ** (m_ / (m_ + red_Xs.n)) - slv_Xe = NoiseDistribution.DiscreteGaussian(params.q * sigma_) - - slv_params = LWEParameters( - n=zeta, - q=params.q, - Xs=slv_Xs, - Xe=slv_Xe, - ) - - # The m_ we compute there is the optimal number of samples that we pick from the input LWE - # instance. We then need to return it because it determines the lattice dimension for the - # reduction. - - return slv_params, m_ - - @staticmethod - @cached_function - def cost( - solver, - params: LWEParameters, - beta: int, - zeta: int = 0, - h1: int = 0, - success_probability: float = 0.99, - red_cost_model=red_cost_model_default, - use_lll=True, - log_level=None, - ): - """ - Computes the cost of the dual hybrid attack that dual reduces the LWE instance and then - uses the given solver to solve the reduced instance. - - :param solver: Algorithm for solving the reduced instance - :param params: LWE parameters - :param beta: Block size used to produce short dual vectors for dual reduction - :param zeta: Dimension ζ ≥ 0 of new LWE instance - :param h1: Number of non-zero components of the secret of the new LWE instance - :param success_probability: The success probability to target - :param red_cost_model: How to cost lattice reduction - :param use_lll: Use LLL calls to produce more small vectors - - .. note :: This function assumes that the instance is normalized. It runs no optimization, - it merely reports costs. - - """ - Logging.log("dual", log_level, f"β={beta}, ζ={zeta}, h1={h1}") - - delta = deltaf(beta) - - if red_cost_model.__name__ in DualHybrid.full_sieves: - rho = 4.0 / 3 - elif use_lll: - rho = 2.0 - else: - rho = 1.0 - - params_slv, m_ = DualHybrid.dual_reduce( - delta, params, zeta, h1, rho, log_level=log_level + 1 - ) - Logging.log("dual", log_level + 1, f"red LWE instance: {repr(params_slv)}") - - cost_slv = solver(params_slv, success_probability) - Logging.log("dual", log_level + 2, f"solve: {repr(cost_slv)}") - - d = m_ + params.n - zeta - cost_red = costf(red_cost_model, beta, d) - if red_cost_model.__name__ in DualHybrid.full_sieves: - # if we use full sieving, we get many short vectors - # we compute in logs to avoid overflows in case m - # or beta happen to be large - try: - log_rep = max(0, log(cost_slv["m"]) - (beta / 2) * log(4 / 3)) - if log_rep > 10 ** 10: - # sage's ceil function seems to completely freak out for large - # inputs, but then m is huge, so unlikely to be relevant - raise OverflowError() - cost_red = cost_red.repeat(ceil(exp(log_rep))) - except OverflowError: - # if we get an overflow, m must be huge - # so we can probably approximate the cost with - # oo for our purposes - return Cost(rop=oo) - elif use_lll: - cost_red["rop"] += cost_slv["m"] * LLL(d, log(params.q, 2)) - cost_red["repetitions"] = cost_slv["m"] - else: - cost_red = cost_red.repeat(cost_slv["m"]) - - Logging.log("dual", log_level + 2, f"red: {repr(cost_red)}") - - total_cost = cost_slv.combine(cost_red) - total_cost["m"] = m_ - total_cost["rop"] = cost_red["rop"] + cost_slv["rop"] - total_cost["mem"] = cost_slv["mem"] - - if d < params.n - zeta: - raise RuntimeError(f"{d} < {params.n - zeta}, {params.n}, {zeta}, {m_}") - total_cost["d"] = d - - Logging.log("dual", log_level, f"{repr(total_cost)}") - - rep = 1 - if params.Xs.is_sparse: - h = params.Xs.get_hamming_weight(params.n) - probability = prob_drop(params.n, h, zeta, h1) - rep = prob_amplify(success_probability, probability) - # don't need more samples to re-run attack, since we may - # just guess different components of the secret - return total_cost.repeat(times=rep, select={"m": False}) - - @staticmethod - def optimize_blocksize( - solver, - params: LWEParameters, - zeta: int = 0, - h1: int = 0, - success_probability: float = 0.99, - red_cost_model=red_cost_model_default, - use_lll=True, - log_level=5, - opt_step=2, - ): - """ - Optimizes the cost of the dual hybrid attack over the block size β. - - :param solver: Algorithm for solving the reduced instance - :param params: LWE parameters - :param zeta: Dimension ζ ≥ 0 of new LWE instance - :param h1: Number of non-zero components of the secret of the new LWE instance - :param success_probability: The success probability to target - :param red_cost_model: How to cost lattice reduction - :param use_lll: Use LLL calls to produce more small vectors - :param opt_step: control robustness of optimizer - - .. note :: This function assumes that the instance is normalized. ζ and h1 are fixed. - - """ - - f = partial( - DualHybrid.cost, - solver=solver, - params=params, - zeta=zeta, - h1=h1, - success_probability=success_probability, - red_cost_model=red_cost_model, - use_lll=use_lll, - log_level=log_level, - ) - # don't have a reliable upper bound for beta - # we choose n - k arbitrarily and adjust later if - # necessary - beta_upper = max(params.n - zeta, 40) - beta = beta_upper - while beta == beta_upper: - beta_upper *= 2 - with local_minimum(2, beta_upper, opt_step) as it: - for beta in it: - it.update(f(beta=beta)) - for beta in it.neighborhood: - it.update(f(beta=beta)) - cost = it.y - beta = cost["beta"] - - cost["zeta"] = zeta - if params.Xs.is_sparse: - cost["h1"] = h1 - return cost - - def __call__( - self, - solver, - params: LWEParameters, - success_probability: float = 0.99, - red_cost_model=red_cost_model_default, - use_lll=True, - opt_step=2, - log_level=1, - ): - """ - Optimizes the cost of the dual hybrid attack (using the given solver) over - all attack parameters: block size β, splitting dimension ζ, and - splitting weight h1 (in case the secret distribution is sparse). Since - the cost function for the dual hybrid might only be convex in an approximate - sense, the parameter ``opt_step`` allows to make the optimization procedure more - robust against local irregularities (higher value) at the cost of a longer - running time. In a nutshell, if the cost of the dual hybrid seems suspiciosly - high, try a larger ``opt_step`` (e.g. 4 or 8). - - :param solver: Algorithm for solving the reduced instance - :param params: LWE parameters - :param success_probability: The success probability to target - :param red_cost_model: How to cost lattice reduction - :param use_lll: use LLL calls to produce more small vectors - :param opt_step: control robustness of optimizer - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``mem``: Total amount of memory used by solver (in elements mod q). - - ``red``: Number of word operations in lattice reduction. - - ``δ``: Root-Hermite factor targeted by lattice reduction. - - ``β``: BKZ block size. - - ``ζ``: Number of guessed coordinates. - - ``h1``: Number of non-zero components among guessed coordinates (if secret distribution is sparse) - - ``prob``: Probability of success in guessing. - - ``repetitions``: How often we are required to repeat the attack. - - ``d``: Lattice dimension. - - - When ζ = 1 this function essentially estimates the dual attack. - - When ζ > 1 and ``solver`` is ``exhaustive_search`` this function estimates - the hybrid attack as given in [INDOCRYPT:EspJouKha20]_ - - When ζ > 1 and ``solver`` is ``mitm`` this function estimates the dual MITM - hybrid attack roughly following [EPRINT:CHHS19]_ - - EXAMPLES:: - - >>> from estimator import * - >>> params = LWE.Parameters(n=1024, q = 2**32, Xs=ND.Uniform(0,1), Xe=ND.DiscreteGaussian(3.0)) - >>> LWE.dual(params) - rop: ≈2^115.5, mem: ≈2^68.0, m: 1018, red: ≈2^115.4, δ: 1.005021, β: 284, d: 2042, ↻: ≈2^68.0, tag: dual - >>> LWE.dual_hybrid(params) - rop: ≈2^111.3, mem: ≈2^106.4, m: 983, red: ≈2^111.2, δ: 1.005204, β: 269, d: 1957, ↻: ≈2^56.4, ζ: 50... - >>> LWE.dual_hybrid(params, mitm_optimization=True) - rop: ≈2^141.1, mem: ≈2^139.1, m: 1189, k: 132, ↻: 139, red: ≈2^140.8, δ: 1.004164, β: 375, d: 2021... - >>> LWE.dual_hybrid(params, mitm_optimization="numerical") - rop: ≈2^140.6, m: 1191, k: 128, mem: ≈2^136.0, ↻: 133, red: ≈2^140.2, δ: 1.004179, β: 373, d: 2052... - - >>> params = params.updated(Xs=ND.SparseTernary(params.n, 32)) - >>> LWE.dual(params) - rop: ≈2^111.7, mem: ≈2^66.0, m: 950, red: ≈2^111.5, δ: 1.005191, β: 270, d: 1974, ↻: ≈2^66.0, tag: dual - >>> LWE.dual_hybrid(params) - rop: ≈2^97.8, mem: ≈2^81.9, m: 730, red: ≈2^97.4, δ: 1.006813, β: 175, d: 1453, ↻: ≈2^36.3, ζ: 301... - >>> LWE.dual_hybrid(params, mitm_optimization=True) - rop: ≈2^103.4, mem: ≈2^81.5, m: 724, k: 310, ↻: ≈2^27.3, red: ≈2^102.7, δ: 1.006655, β: 182... - - >>> params = params.updated(Xs=ND.CenteredBinomial(8)) - >>> LWE.dual(params) - rop: ≈2^123.1, mem: ≈2^75.4, m: 1151, red: ≈2^123.0, δ: 1.004727, β: 311, d: 2175, ↻: ≈2^75.4, tag: dual - >>> LWE.dual_hybrid(params) - rop: ≈2^122.4, mem: ≈2^116.6, m: 1143, red: ≈2^122.2, δ: 1.004758, β: 308, d: 2157, ↻: ≈2^75.8, ζ: 10... - >>> LWE.dual_hybrid(params, mitm_optimization=True) - rop: ≈2^181.7, mem: ≈2^179.2, m: 1554, k: 42, ↻: 179, red: ≈2^181.4, δ: 1.003315, β: 519, d: 2513... - - >>> params = params.updated(Xs=ND.DiscreteGaussian(3.0)) - >>> LWE.dual(params) - rop: ≈2^125.4, mem: ≈2^78.0, m: 1190, red: ≈2^125.3, δ: 1.004648, β: 319, d: 2214, ↻: ≈2^78.0, tag: dual - >>> LWE.dual_hybrid(params) - rop: ≈2^125.1, mem: ≈2^117.7, m: 1187, red: ≈2^125.0, δ: 1.004657, β: 318, d: 2204, ↻: ≈2^75.9, ζ: 7... - >>> LWE.dual_hybrid(params, mitm_optimization=True) - rop: ≈2^175.0, mem: ≈2^168.9, m: 1547, k: 27, ↻: 169, red: ≈2^175.0, δ: 1.003424, β: 496, d: 2544, ζ: 27... - """ - - Cost.register_impermanent( - rop=True, - mem=False, - red=True, - beta=False, - delta=False, - m=True, - d=False, - zeta=False, - ) - Logging.log("dual", log_level, f"costing LWE instance: {repr(params)}") - - params = params.normalize() - - if params.Xs.is_sparse: - Cost.register_impermanent(h1=False) - - def _optimize_blocksize( - solver, - params: LWEParameters, - zeta: int = 0, - success_probability: float = 0.99, - red_cost_model=red_cost_model_default, - use_lll=True, - log_level=None, - ): - h = params.Xs.get_hamming_weight(params.n) - h1_min = max(0, h - (params.n - zeta)) - h1_max = min(zeta, h) - Logging.log("dual", log_level, f"h1 ∈ [{h1_min},{h1_max}] (zeta={zeta})") - with local_minimum(h1_min, h1_max, log_level=log_level + 1) as it: - for h1 in it: - cost = self.optimize_blocksize( - h1=h1, - solver=solver, - params=params, - zeta=zeta, - success_probability=success_probability, - red_cost_model=red_cost_model, - use_lll=use_lll, - log_level=log_level + 2, - ) - it.update(cost) - return it.y - - else: - _optimize_blocksize = self.optimize_blocksize - - f = partial( - _optimize_blocksize, - solver=solver, - params=params, - success_probability=success_probability, - red_cost_model=red_cost_model, - use_lll=use_lll, - log_level=log_level + 1, - ) - - with local_minimum(1, params.n - 1, opt_step) as it: - for zeta in it: - it.update(f(zeta=zeta)) - for zeta in it.neighborhood: - it.update(f(zeta=zeta)) - cost = it.y - - cost["problem"] = params - return cost - - -DH = DualHybrid() - - -def dual( - params: LWEParameters, - success_probability: float = 0.99, - red_cost_model=red_cost_model_default, - use_lll=True, -): - """ - Dual hybrid attack as in [PQCBook:MicReg09]_. - - :param params: LWE parameters. - :param success_probability: The success probability to target. - :param red_cost_model: How to cost lattice reduction. - :param use_lll: use LLL calls to produce more small vectors. - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``mem``: Total amount of memory used by solver (in elements mod q). - - ``red``: Number of word operations in lattice reduction. - - ``δ``: Root-Hermite factor targeted by lattice reduction. - - ``β``: BKZ block size. - - ``prob``: Probability of success in guessing. - - ``repetitions``: How often we are required to repeat the attack. - - ``d``: Lattice dimension. - - """ - Cost.register_impermanent( - rop=True, - mem=False, - red=True, - beta=False, - delta=False, - m=True, - d=False, - ) - - ret = DH.optimize_blocksize( - solver=distinguish, - params=params, - zeta=0, - h1=0, - success_probability=success_probability, - red_cost_model=red_cost_model, - use_lll=use_lll, - log_level=1, - ) - del ret["zeta"] - if hasattr(ret, "h1"): - del ret["h1"] - ret["tag"] = "dual" - return ret - - -def dual_hybrid( - params: LWEParameters, - success_probability: float = 0.99, - red_cost_model=red_cost_model_default, - use_lll=True, - mitm_optimization=False, - opt_step=2, -): - """ - Dual hybrid attack from [INDOCRYPT:EspJouKha20]_. - - :param params: LWE parameters. - :param success_probability: The success probability to target. - :param red_cost_model: How to cost lattice reduction. - :param use_lll: Use LLL calls to produce more small vectors. - :param mitm_optimization: One of "analytical" or "numerical". If ``True`` a default from the - ``conf`` module is picked, ``False`` disables MITM. - :param opt_step: Control robustness of optimizer. - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``mem``: Total amount of memory used by solver (in elements mod q). - - ``red``: Number of word operations in lattice reduction. - - ``δ``: Root-Hermite factor targeted by lattice reduction. - - ``β``: BKZ block size. - - ``ζ``: Number of guessed coordinates. - - ``h1``: Number of non-zero components among guessed coordinates (if secret distribution is sparse) - - ``prob``: Probability of success in guessing. - - ``repetitions``: How often we are required to repeat the attack. - - ``d``: Lattice dimension. - """ - - if mitm_optimization is True: - mitm_optimization = mitm_opt_default - - if mitm_optimization: - solver = partial(mitm, optimization=mitm_optimization) - else: - solver = exhaustive_search - - ret = DH( - solver=solver, - params=params, - success_probability=success_probability, - red_cost_model=red_cost_model, - use_lll=use_lll, - opt_step=opt_step, - ) - if mitm_optimization: - ret["tag"] = "dual_mitm_hybrid" - else: - ret["tag"] = "dual_hybrid" - return ret diff --git a/estimator_new/lwe_guess.py b/estimator_new/lwe_guess.py deleted file mode 100644 index 5ed81061c..000000000 --- a/estimator_new/lwe_guess.py +++ /dev/null @@ -1,417 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Generic multiplicative composition of guessing some components of the LWE secret and some LWE solving algorithm. - -By "multiplicative" we mean that costs multiply rather than add. It is often possible to achieve -some form of additive composition, i.e. this strategy is rarely the most efficient. - -""" - -from sage.all import log, floor, ceil, binomial -from sage.all import sqrt, pi, exp, RR, ZZ, oo, round, e - -from .conf import mitm_opt -from .cost import Cost -from .errors import InsufficientSamplesError, OutOfBoundsError -from .lwe_parameters import LWEParameters -from .prob import amplify as prob_amplify -from .prob import drop as prob_drop -from .prob import amplify_sigma -from .util import local_minimum -from .nd import sigmaf - - -def log2(x): - return log(x, 2) - - -class guess_composition: - def __init__(self, f): - """ - Create a generic composition of guessing and `f`. - """ - self.f = f - self.__name__ = f"{f.__name__}+guessing" - - @classmethod - def dense_solve(cls, f, params, log_level=5, **kwds): - """ - Guess components of a dense secret then call `f`. - - :param f: Some object consuming `params` and outputting some `cost` - :param params: LWE parameters. - - """ - base = params.Xs.bounds[1] - params.Xs.bounds[0] + 1 - - baseline_cost = f(params, **kwds) - - max_zeta = min(floor(log(baseline_cost["rop"], base)), params.n) - - with local_minimum(0, max_zeta, log_level=log_level) as it: - for zeta in it: - search_space = base ** zeta - cost = f(params.updated(n=params.n - zeta), log_level=log_level + 1, **kwds) - repeated_cost = cost.repeat(search_space) - repeated_cost["zeta"] = zeta - it.update(repeated_cost) - return it.y - - @classmethod - def gammaf(cls, n, h, zeta, base, g=lambda x: x): - """ - Find optimal hamming weight for sparse guessing. - - Let `s` be a vector of dimension `n` where we expect `h` non-zero entries. We are ignoring `η-γ` - components and are guessing `γ`. This succeeds with some probability given by ``prob_drop(n, h, - ζ, γ)``. Exhaustively searching the guesses takes `binomial(n, γ) ⋅ b^γ` steps where `b` is the - number of non-zero values in a component of `s`. We call a `γ` optimal if it minimizes the - overall number of repetitions that need to be performed to succeed with probability 99%. - - :param n: vector dimension - :param h: hamming weight of the vector - :param zeta: number of ignored + guesses components - :param base: number of possible non-zero scalars - :param g: We do not consider search space directly by `g()` applied to it (think time-memory - trade-offs). - :returns: (number of repetitions, γ, size of the search space, probability of success) - - """ - if not zeta: - return 1, 0, 0, 1.0 - - search_space = 0 - gamma = 0 - probability = 0 - best = None, None, None, None - while gamma < min(h, zeta): - probability += prob_drop(n, h, zeta, fail=gamma) - search_space += binomial(zeta, gamma) * base ** gamma - repeat = prob_amplify(0.99, probability) * g(search_space) - if best[0] is None or repeat < best[0]: - best = repeat, gamma, search_space, probability - gamma += 1 - else: - break - return best - - @classmethod - def sparse_solve(cls, f, params, log_level=5, **kwds): - """ - Guess components of a sparse secret then call `f`. - - :param f: Some object consuming `params` and outputting some `cost` - :param params: LWE parameters. - """ - base = params.Xs.bounds[1] - params.Xs.bounds[0] # we exclude zero - h = ceil(len(params.Xs) * params.Xs.density) # nr of non-zero entries - - with local_minimum(0, params.n - 40, log_level=log_level) as it: - for zeta in it: - single_cost = f(params.updated(n=params.n - zeta), log_level=log_level + 1, **kwds) - repeat, gamma, search_space, probability = cls.gammaf(params.n, h, zeta, base) - cost = single_cost.repeat(repeat) - cost["zeta"] = zeta - cost["|S|"] = search_space - cost["prop"] = probability - it.update(cost) - return it.y - - def __call__(self, params, log_level=5, **kwds): - """ - Guess components of a secret then call `f`. - - :param params: LWE parameters. - - EXAMPLE:: - - >>> from estimator import * - >>> from estimator.lwe_guess import guess_composition - >>> guess_composition(LWE.primal_usvp)(Kyber512.updated(Xs=ND.SparseTernary(512, 16))) - rop: ≈2^102.8, red: ≈2^102.8, δ: 1.008705, β: 113, d: 421, tag: usvp, ↻: ≈2^37.5, ζ: 265, |S|: 1, ... - - Compare:: - - >>> LWE.primal_hybrid(Kyber512.updated(Xs=ND.SparseTernary(512, 16))) - rop: ≈2^86.6, red: ≈2^85.7, svp: ≈2^85.6, β: 104, η: 2, ζ: 371, |S|: ≈2^91.1, d: 308, prob: ≈2^-21.3, ... - - """ - if params.Xs.is_sparse: - return self.sparse_solve(self.f, params, log_level, **kwds) - else: - return self.dense_solve(self.f, params, log_level, **kwds) - - -class ExhaustiveSearch: - def __call__(self, params: LWEParameters, success_probability=0.99, quantum: bool = False): - """ - Estimate cost of solving LWE via exhaustive search. - - :param params: LWE parameters - :param success_probability: the targeted success probability - :param quantum: use estimate for quantum computer (we simply take the square root of the search space) - :return: A cost dictionary - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``mem``: memory requirement in integers mod q. - - ``m``: Required number of samples to distinguish the correct solution with high probability. - - EXAMPLE:: - - >>> from estimator import * - >>> params = LWE.Parameters(n=64, q=2**40, Xs=ND.UniformMod(2), Xe=ND.DiscreteGaussian(3.2)) - >>> exhaustive_search(params) - rop: ≈2^73.6, mem: ≈2^72.6, m: 397.198 - >>> params = LWE.Parameters(n=1024, q=2**40, Xs=ND.SparseTernary(n=1024, p=32), Xe=ND.DiscreteGaussian(3.2)) - >>> exhaustive_search(params) - rop: ≈2^417.3, mem: ≈2^416.3, m: ≈2^11.2 - - """ - params = LWEParameters.normalize(params) - - # there are two stages: enumeration and distinguishing, so we split up the success_probability - probability = sqrt(success_probability) - - try: - size = params.Xs.support_size(n=params.n, fraction=probability) - except NotImplementedError: - # not achieving required probability with search space - # given our settings that means the search space is huge - # so we approximate the cost with oo - return Cost(rop=oo, mem=oo, m=1) - - if quantum: - size = size.sqrt() - - # set m according to [ia.cr/2020/515] - sigma = params.Xe.stddev / params.q - m_required = RR( - 8 * exp(4 * pi * pi * sigma * sigma) * (log(size) - log(log(1 / probability))) - ) - - if params.m < m_required: - raise InsufficientSamplesError( - f"Exhaustive search: Need {m_required} samples but only {params.m} available." - ) - else: - m = m_required - - # we can compute A*s for all candidate s in time 2*size*m using - # (the generalization [ia.cr/2021/152] of) the recursive algorithm - # from [ia.cr/2020/515] - cost = 2 * size * m - - ret = Cost(rop=cost, mem=cost / 2, m=m) - return ret - - __name__ = "exhaustive_search" - - -exhaustive_search = ExhaustiveSearch() - - -class MITM: - - locality = 0.05 - - def X_range(self, nd): - if nd.is_bounded: - a, b = nd.bounds - return b - a + 1, 1.0 - else: - # setting fraction=0 to ensure that support size does not - # throw error. we'll take the probability into account later - rng = nd.support_size(n=1, fraction=0.0) - return rng, nd.gaussian_tail_prob - - def local_range(self, center): - return ZZ(floor((1 - self.locality) * center)), ZZ(ceil((1 + self.locality) * center)) - - def mitm_analytical(self, params: LWEParameters, success_probability=0.99): - nd_rng, nd_p = self.X_range(params.Xe) - delta = nd_rng / params.q # possible error range scaled - - sd_rng, sd_p = self.X_range(params.Xs) - - # determine the number of elements in the tables depending on splitting dim - n = params.n - k = round(n / (2 - delta)) - # we could now call self.cost with this k, but using our model below seems - # about 3x faster and reasonably accurate - - if params.Xs.is_sparse: - h = params.Xs.get_hamming_weight(n=params.n) - split_h = round(h * k / n) - success_probability_ = ( - binomial(k, split_h) * binomial(n - k, h - split_h) / binomial(n, h) - ) - - logT = RR(h * (log2(n) - log2(h) + log2(sd_rng - 1) + log2(e))) / (2 - delta) - logT -= RR(log2(h) / 2) - logT -= RR(h * h * log2(e) / (2 * n * (2 - delta) ** 2)) - else: - success_probability_ = 1.0 - logT = k * log(sd_rng, 2) - - m_ = max(1, round(logT + log(logT, 2))) - if params.m < m_: - raise InsufficientSamplesError( - f"MITM: Need {m_} samples but only {params.m} available." - ) - - # since m = logT + loglogT and rop = T*m, we have rop=2^m - ret = Cost(rop=RR(2 ** m_), mem=2 ** logT * m_, m=m_, k=ZZ(k)) - repeat = prob_amplify(success_probability, sd_p ** n * nd_p ** m_ * success_probability_) - return ret.repeat(times=repeat) - - def cost( - self, - params: LWEParameters, - k: int, - success_probability=0.99, - ): - nd_rng, nd_p = self.X_range(params.Xe) - delta = nd_rng / params.q # possible error range scaled - - sd_rng, sd_p = self.X_range(params.Xs) - n = params.n - - if params.Xs.is_sparse: - h = params.Xs.get_hamming_weight(n=n) - - # we assume the hamming weight to be distributed evenly across the two parts - # if not we can rerandomize on the coordinates and try again -> repeat - split_h = round(h * k / n) - size_tab = RR((sd_rng - 1) ** split_h * binomial(k, split_h)) - size_sea = RR((sd_rng - 1) ** (h - split_h) * binomial(n - k, h - split_h)) - success_probability_ = ( - binomial(k, split_h) * binomial(n - k, h - split_h) / binomial(n, h) - ) - else: - size_tab = sd_rng ** k - size_sea = sd_rng ** (n - k) - success_probability_ = 1 - - # we set m such that it approximately minimizes the search cost per query as - # a reasonable starting point and then optimize around it - m_ = ceil(max(log2(size_tab) + log2(log2(size_tab)), 1)) - a, b = self.local_range(m_) - with local_minimum(a, b, smallerf=lambda x, best: x[1] <= best[1]) as it: - for m in it: - # for search we effectively build a second table and for each entry, we expect - # 2^( m * 4 * B / q) = 2^(delta * m) table look ups + a l_oo computation (costing m) - # for every hit in the table (which has probability T/2^m) - cost = (m, size_sea * (2 * m + 2 ** (delta * m) * (1 + size_tab * m / 2 ** m))) - it.update(cost) - m, cost_search = it.y - m = min(m, params.m) - - # building the table costs 2*T*m using the generalization [ia.cr/2021/152] of - # the recursive algorithm from [ia.cr/2020/515] - cost_table = size_tab * 2 * m - - ret = Cost(rop=(cost_table + cost_search), m=m, k=k) - ret["mem"] = size_tab * (k + m) + size_sea * (n - k + m) - repeat = prob_amplify(success_probability, sd_p ** n * nd_p ** m * success_probability_) - return ret.repeat(times=repeat) - - def __call__(self, params: LWEParameters, success_probability=0.99, optimization=mitm_opt): - """ - Estimate cost of solving LWE via Meet-In-The-Middle attack. - - :param params: LWE parameters - :param success_probability: the targeted success probability - :param model: Either "analytical" (faster, default) or "numerical" (more accurate) - :return: A cost dictionary - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``mem``: memory requirement in integers mod q. - - ``m``: Required number of samples to distinguish the correct solution with high probability. - - ``k``: Splitting dimension. - - ``↻``: Repetitions required to achieve targeted success probability - - EXAMPLE:: - - >>> from estimator import * - >>> params = LWE.Parameters(n=64, q=2**40, Xs=ND.UniformMod(2), Xe=ND.DiscreteGaussian(3.2)) - >>> mitm(params) - rop: ≈2^37.0, mem: ≈2^37.2, m: 37, k: 32, ↻: 1 - >>> mitm(params, optimization="numerical") - rop: ≈2^39.2, m: 36, k: 32, mem: ≈2^39.1, ↻: 1 - >>> params = LWE.Parameters(n=1024, q=2**40, Xs=ND.SparseTernary(n=1024, p=32), Xe=ND.DiscreteGaussian(3.2)) - >>> mitm(params) - rop: ≈2^215.4, mem: ≈2^210.2, m: ≈2^13.1, k: 512, ↻: 43 - >>> mitm(params, optimization="numerical") - rop: ≈2^216.0, m: ≈2^13.1, k: 512, mem: ≈2^211.4, ↻: 43 - - """ - Cost.register_impermanent(rop=True, mem=False, m=True, k=False) - - params = LWEParameters.normalize(params) - - nd_rng, _ = self.X_range(params.Xe) - if nd_rng >= params.q: - # MITM attacks cannot handle an error this large. - return Cost(rop=oo, mem=oo, m=0, k=0) - - if "analytical" in optimization: - return self.mitm_analytical(params=params, success_probability=success_probability) - elif "numerical" in optimization: - with local_minimum(1, params.n - 1) as it: - for k in it: - cost = self.cost(k=k, params=params, success_probability=success_probability) - it.update(cost) - ret = it.y - # if the noise is large, the curve might not be convex, so the above minimum - # is not correct. Interestingly, in these cases, it seems that k=1 might be smallest - ret1 = self.cost(k=1, params=params, success_probability=success_probability) - return min(ret, ret1) - else: - raise ValueError("Unknown optimization method for MITM.") - - __name__ = "mitm" - - -mitm = MITM() - - -class Distinguisher: - def __call__(self, params: LWEParameters, success_probability=0.99): - """ - Estimate cost of distinguishing a 0-dimensional LWE instance from uniformly random, - which is essentially the number of samples required. - - :param params: LWE parameters - :param success_probability: the targeted success probability - :return: A cost dictionary - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``mem``: memory requirement in integers mod q. - - ``m``: Required number of samples to distinguish. - - EXAMPLE:: - - >>> from estimator import * - >>> params = LWE.Parameters(n=0, q=2 ** 32, Xs=ND.UniformMod(2), Xe=ND.DiscreteGaussian(2 ** 32)) - >>> distinguish(params) - rop: ≈2^60.0, mem: ≈2^60.0, m: ≈2^60.0 - - """ - - if params.n > 0: - raise OutOfBoundsError("Secret dimension should be 0 for distinguishing. Try exhaustive search for n > 0.") - m = amplify_sigma(success_probability, sigmaf(params.Xe.stddev), params.q) - if (m > params.m): - raise InsufficientSamplesError("Not enough samples to distinguish with target advantage.") - return Cost(rop=m, mem=m, m=m) - - __name__ = "distinguish" - - -distinguish = Distinguisher() diff --git a/estimator_new/lwe_parameters.py b/estimator_new/lwe_parameters.py deleted file mode 100644 index c4f16d553..000000000 --- a/estimator_new/lwe_parameters.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- coding: utf-8 -*- -from sage.all import oo, binomial, log, sqrt, ceil -from dataclasses import dataclass -from copy import copy -from .nd import NoiseDistribution -from .errors import InsufficientSamplesError - - -@dataclass -class LWEParameters: - n: int - q: int - Xs: NoiseDistribution - Xe: NoiseDistribution - m: int = oo - tag: str = None - - def __post_init__(self, **kwds): - self.Xs = copy(self.Xs) - self.Xs.n = self.n - if self.m < oo: - self.Xe = copy(self.Xe) - self.Xe.n = self.m - - def normalize(self): - """ - EXAMPLES: - - We perform the normal form transformation if χ_e < χ_s and we got the samples:: - - >>> from estimator import * - >>> Xs=ND.DiscreteGaussian(2.0) - >>> Xe=ND.DiscreteGaussian(1.58) - >>> LWE.Parameters(n=512, q=8192, Xs=Xs, Xe=Xe).normalize() - LWEParameters(n=512, q=8192, Xs=D(σ=1.58), Xe=D(σ=1.58), m=+Infinity, tag=None) - - If m = n, we swap the secret and the noise:: - - >>> from estimator import * - >>> Xs=ND.DiscreteGaussian(2.0) - >>> Xe=ND.DiscreteGaussian(1.58) - >>> LWE.Parameters(n=512, q=8192, Xs=Xs, Xe=Xe, m=512).normalize() - LWEParameters(n=512, q=8192, Xs=D(σ=1.58), Xe=D(σ=2.00), m=512, tag=None) - - """ - if self.m < 1: - raise InsufficientSamplesError(f"m={self.m} < 1") - - # Normal form transformation - if self.Xe < self.Xs and self.m >= 2 * self.n: - return LWEParameters( - n=self.n, q=self.q, Xs=self.Xe, Xe=self.Xe, m=self.m - self.n, tag=self.tag - ) - # swap secret and noise - # TODO: this is somewhat arbitrary - if self.Xe < self.Xs and self.m < 2 * self.n: - return LWEParameters(n=self.n, q=self.q, Xs=self.Xe, Xe=self.Xs, m=self.n, tag=self.tag) - - # nothing to do - return self - - def updated(self, **kwds): - """ - Return a new set of parameters updated according to ``kwds``. - - :param kwds: We set ``key`` to ``value`` in the new set of parameters. - - EXAMPLE:: - - >>> from estimator import * - >>> Kyber512 - LWEParameters(n=512, q=3329, Xs=D(σ=1.22), Xe=D(σ=1.22), m=512, tag='Kyber 512') - >>> Kyber512.updated(m=1337) - LWEParameters(n=512, q=3329, Xs=D(σ=1.22), Xe=D(σ=1.22), m=1337, tag='Kyber 512') - - """ - d = dict(self.__dict__) - d.update(kwds) - return LWEParameters(**d) - - def amplify_m(self, m): - """ - Return a LWE instance parameters with ``m`` samples produced from the samples in this instance. - - :param m: New number of samples. - - EXAMPLE:: - - >>> from sage.all import binomial, log - >>> from estimator import * - >>> Kyber512 - LWEParameters(n=512, q=3329, Xs=D(σ=1.22), Xe=D(σ=1.22), m=512, tag='Kyber 512') - >>> Kyber512.amplify_m(2**100) - LWEParameters(n=512, q=3329, Xs=D(σ=1.22), Xe=D(σ=4.58), m=..., tag='Kyber 512') - - We can produce 2^100 samples by random ± linear combinations of 12 vectors:: - - >>> float(sqrt(12.)), float(log(binomial(1024, 12) , 2.0)) + 12 - (3.46..., 103.07...) - - """ - if m <= self.m: - return self - if self.m == oo: - return self - d = dict(self.__dict__) - - if self.Xe.mean != 0: - raise NotImplementedError("Amplifying for μ≠0 not implemented.") - - for k in range(ceil(log(m, 2.0))): - # - binom(n,k) positions - # -two signs per position (+1,-1) - # - all "-" and all "+" are the same - if binomial(self.m, k) * 2 ** k - 1 >= m: - Xe = NoiseDistribution.DiscreteGaussian(float(sqrt(k) * self.Xe.stddev)) - d["Xe"] = Xe - d["m"] = ceil(m) - return LWEParameters(**d) - else: - raise NotImplementedError( - f"Cannot amplify to ≈2^{log(m,2):1} using {{+1,-1}} additions." - ) - - def switch_modulus(self): - """ - Apply modulus switching and return new instance. - - See [JMC:AlbPlaSco15]_ for details. - - EXAMPLE:: - - >>> from estimator import * - >>> LWE.Parameters(n=128, q=7681, Xs=ND.UniformMod(3), Xe=ND.UniformMod(11)).switch_modulus() - LWEParameters(n=128, q=5289, Xs=D(σ=0.82), Xe=D(σ=3.08), m=+Infinity, tag=None) - - """ - n = self.Xs.density * len(self.Xs) - - # n uniform in -(0.5,0.5) ± stddev(χ_s) - Xr_stddev = sqrt(n / 12) * self.Xs.stddev # rounding noise - # χ_r == p/q ⋅ χ_e # we want the rounding noise match the scaled noise - p = ceil(Xr_stddev * self.q / self.Xe.stddev) - - scale = float(p) / self.q - - # there is no point in scaling if the improvement is eaten up by rounding noise - if scale > 1 / sqrt(2): - return self - - return LWEParameters( - self.n, - p, - Xs=self.Xs, - Xe=NoiseDistribution.DiscreteGaussian(sqrt(2) * self.Xe.stddev * scale), - m=self.m, - tag=self.tag + ",scaled" if self.tag else None, - ) - - def __hash__(self): - return hash((self.n, self.q, self.Xs, self.Xe, self.m, self.tag)) diff --git a/estimator_new/lwe_primal.py b/estimator_new/lwe_primal.py deleted file mode 100644 index 10afd6cf1..000000000 --- a/estimator_new/lwe_primal.py +++ /dev/null @@ -1,596 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Estimate cost of solving LWE using primal attacks. - -See :ref:`LWE Primal Attacks` for an introduction what is available. - -""" -from functools import partial - -from sage.all import oo, ceil, sqrt, log, RR, ZZ, binomial, cached_function -from .reduction import delta as deltaf -from .reduction import cost as costf -from .util import local_minimum -from .cost import Cost -from .lwe_parameters import LWEParameters -from .simulator import normalize as simulator_normalize -from .prob import drop as prob_drop -from .prob import amplify as prob_amplify -from .prob import babai as prob_babai -from .prob import mitm_babai_probability -from .io import Logging -from .conf import red_cost_model as red_cost_model_default -from .conf import red_shape_model as red_shape_model_default -from .conf import red_simulator as red_simulator_default - - -class PrimalUSVP: - """ - Estimate cost of solving LWE via uSVP reduction. - """ - - @staticmethod - def _xi_factor(Xs, Xe): - xi = RR(1) - if Xs < Xe: - xi = Xe.stddev / Xs.stddev - return xi - - @staticmethod - def _solve_for_d(params, m, beta, tau, xi): - """ - Find smallest d ∈ [n,m] to satisfy uSVP condition. - - If no such d exists, return the upper bound m. - """ - # Find the smallest d ∈ [n,m] s.t. a*d^2 + b*d + c >= 0 - delta = deltaf(beta) - a = -log(delta) - C = log(params.Xe.stddev ** 2 * (beta - 1) + tau ** 2) / 2.0 - b = log(delta) * (2 * beta - 1) + log(params.q) - C - c = log(tau) + params.n * log(xi) - (params.n + 1) * log(params.q) - n = params.n - if a * n * n + b * n + c >= 0: # trivial case - return n - - # solve for ad^2 + bd + c == 0 - disc = b * b - 4 * a * c # the discriminant - if disc < 0: # no solution, return m - return m - - # compute the two solutions - d1 = (-b + sqrt(disc)) / (2 * a) - d2 = (-b - sqrt(disc)) / (2 * a) - if a > 0: # the only possible solution is ceiling(d2) - return min(m, ceil(d2)) - - # the case a<=0: - # if n is to the left of d1 then the first solution is ceil(d1) - if n <= d1: - return min(m, ceil(d1)) - - # otherwise, n must be larger than d2 (since an^2+bn+c<0) so no solution - return m - - @staticmethod - @cached_function - def cost_gsa( - beta: int, - params: LWEParameters, - m: int = oo, - tau=None, - d=None, - red_cost_model=red_cost_model_default, - log_level=None, - ): - - delta = deltaf(beta) - xi = PrimalUSVP._xi_factor(params.Xs, params.Xe) - m = min(2 * ceil(sqrt(params.n * log(params.q) / log(delta))), m) - tau = params.Xe.stddev if tau is None else tau - d = PrimalUSVP._solve_for_d(params, m, beta, tau, xi) if d is None else d - assert d <= m + 1 - - lhs = log(sqrt(params.Xe.stddev ** 2 * (beta - 1) + tau ** 2)) - rhs = RR( - log(delta) * (2 * beta - d - 1) - + (log(tau) + log(xi) * params.n + log(params.q) * (d - params.n - 1)) / d - ) - - return costf(red_cost_model, beta, d, predicate=lhs <= rhs) - - @staticmethod - @cached_function - def cost_simulator( - beta: int, - params: LWEParameters, - simulator, - m: int = oo, - tau=None, - d=None, - red_cost_model=red_cost_model_default, - log_level=None, - ): - delta = deltaf(beta) - if d is None: - d = min(ceil(sqrt(params.n * log(params.q) / log(delta))), m) + 1 - xi = PrimalUSVP._xi_factor(params.Xs, params.Xe) - tau = params.Xe.stddev if tau is None else tau - - r = simulator(d=d, n=params.n, q=params.q, beta=beta, xi=xi, tau=tau) - lhs = params.Xe.stddev ** 2 * (beta - 1) + tau ** 2 - if r[d - beta] > lhs: - cost = costf(red_cost_model, beta, d) - else: - cost = costf(red_cost_model, beta, d, predicate=False) - return cost - - def __call__( - self, - params: LWEParameters, - red_cost_model=red_cost_model_default, - red_shape_model=red_shape_model_default, - optimize_d=True, - log_level=1, - **kwds, - ): - """ - Estimate cost of solving LWE via uSVP reduction. - - :param params: LWE parameters. - :param red_cost_model: How to cost lattice reduction. - :param red_shape_model: How to model the shape of a reduced basis. - :param optimize_d: Attempt to find minimal d, too. - :return: A cost dictionary. - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``red``: Number of word operations in lattice reduction. - - ``δ``: Root-Hermite factor targeted by lattice reduction. - - ``β``: BKZ block size. - - ``d``: Lattice dimension. - - EXAMPLE:: - - >>> from estimator import * - >>> LWE.primal_usvp(Kyber512) - rop: ≈2^148.0, red: ≈2^148.0, δ: 1.003941, β: 406, d: 998, tag: usvp - - >>> params = LWE.Parameters(n=200, q=127, Xs=ND.UniformMod(3), Xe=ND.UniformMod(3)) - >>> LWE.primal_usvp(params, red_shape_model="cn11") - rop: ≈2^91.2, red: ≈2^91.2, δ: 1.006114, β: 209, d: 388, tag: usvp - - >>> LWE.primal_usvp(params, red_shape_model=Simulator.CN11) - rop: ≈2^91.2, red: ≈2^91.2, δ: 1.006114, β: 209, d: 388, tag: usvp - - >>> LWE.primal_usvp(params, red_shape_model=Simulator.CN11, optimize_d=False) - rop: ≈2^91.3, red: ≈2^91.3, δ: 1.006114, β: 209, d: 400, tag: usvp - - The success condition was formulated in [USENIX:ADPS16]_ and studied/verified in - [AC:AGVW17]_, [C:DDGR20]_, [PKC:PosVir21]_. The treatment of small secrets is from - [ACISP:BaiGal14]_. - - """ - params = LWEParameters.normalize(params) - - # allow for a larger embedding lattice dimension: Bai and Galbraith - m = params.m + params.n if params.Xs <= params.Xe else params.m - - if red_shape_model == "gsa": - with local_minimum(40, 2 * params.n) as it: - for beta in it: - cost = self.cost_gsa( - beta=beta, params=params, m=m, red_cost_model=red_cost_model, **kwds - ) - it.update(cost) - cost = it.y - cost["tag"] = "usvp" - cost["problem"] = params - return cost - - try: - red_shape_model = simulator_normalize(red_shape_model) - except ValueError: - pass - - # step 0. establish baseline - cost_gsa = self( - params, - red_cost_model=red_cost_model, - red_shape_model="gsa", - ) - - Logging.log("usvp", log_level + 1, f"GSA: {repr(cost_gsa)}") - - f = partial( - self.cost_simulator, - simulator=red_shape_model, - red_cost_model=red_cost_model, - m=m, - params=params, - ) - - # step 1. find β - - with local_minimum( - cost_gsa["beta"] - ceil(0.10 * cost_gsa["beta"]), - cost_gsa["beta"] + ceil(0.20 * cost_gsa["beta"]), - ) as it: - for beta in it: - it.update(f(beta=beta, **kwds)) - cost = it.y - - Logging.log("usvp", log_level, f"Opt-β: {repr(cost)}") - - if cost and optimize_d: - # step 2. find d - with local_minimum(params.n, stop=cost["d"] + 1) as it: - for d in it: - it.update(f(d=d, beta=cost["beta"], **kwds)) - cost = it.y - Logging.log("usvp", log_level + 1, f"Opt-d: {repr(cost)}") - - cost["tag"] = "usvp" - cost["problem"] = params - return cost - - __name__ = "primal_usvp" - - -primal_usvp = PrimalUSVP() - - -class PrimalHybrid: - @classmethod - def babai_cost(cls, d): - return Cost(rop=max(d, 1) ** 2) - - @classmethod - def svp_dimension(cls, r, D): - """ - Return η for a given lattice shape and distance. - - :param r: squared Gram-Schmidt norms - - """ - from fpylll.util import gaussian_heuristic - - d = len(r) - for i, _ in enumerate(r): - if gaussian_heuristic(r[i:]) < D.stddev ** 2 * (d - i): - return ZZ(d - (i - 1)) - return ZZ(2) - - @staticmethod - @cached_function - def cost( - beta: int, - params: LWEParameters, - zeta: int = 0, - babai=False, - mitm=False, - m: int = oo, - d: int = None, - simulator=red_simulator_default, - red_cost_model=red_cost_model_default, - log_level=5, - ): - """ - Cost of the hybrid attack. - - :param beta: Block size. - :param params: LWE parameters. - :param zeta: Guessing dimension ζ ≥ 0. - :param babai: Insist on Babai's algorithm for finding close vectors. - :param mitm: Simulate MITM approach (√ of search space). - :param m: We accept the number of samples to consider from the calling function. - :param d: We optionally accept the dimension to pick. - - .. note :: This is the lowest level function that runs no optimization, it merely reports - costs. - - """ - if d is None: - delta = deltaf(beta) - d = min(ceil(sqrt(params.n * log(params.q) / log(delta))), m) + 1 - d -= zeta - xi = PrimalUSVP._xi_factor(params.Xs, params.Xe) - - # 1. Simulate BKZ-β - # TODO: pick τ - r = simulator(d, params.n - zeta, params.q, beta, xi=xi) - bkz_cost = costf(red_cost_model, beta, d) - - # 2. Required SVP dimension η - if babai: - eta = 2 - svp_cost = PrimalHybrid.babai_cost(d) - else: - # we scaled the lattice so that χ_e is what we want - eta = PrimalHybrid.svp_dimension(r, params.Xe) - svp_cost = costf(red_cost_model, eta, eta) - # when η ≪ β, lifting may be a bigger cost - svp_cost["rop"] += PrimalHybrid.babai_cost(d - eta)["rop"] - - # 3. Search - # We need to do one BDD call at least - search_space, probability, hw = 1, 1.0, 0 - - # MITM or no MITM - # TODO: this is rather clumsy as a model - def ssf(x): - if mitm: - return RR(sqrt(x)) - else: - return x - - # e.g. (-1, 1) -> two non-zero per entry - base = params.Xs.bounds[1] - params.Xs.bounds[0] - - if zeta: - # the number of non-zero entries - h = ceil(len(params.Xs) * params.Xs.density) - probability = RR(prob_drop(params.n, h, zeta)) - hw = 1 - while hw < min(h, zeta): - new_search_space = binomial(zeta, hw) * base ** hw - if svp_cost.repeat(ssf(search_space + new_search_space))["rop"] < bkz_cost["rop"]: - search_space += new_search_space - probability += prob_drop(params.n, h, zeta, fail=hw) - hw += 1 - else: - break - - svp_cost = svp_cost.repeat(ssf(search_space)) - - if mitm and zeta > 0: - if babai: - probability *= mitm_babai_probability(r, params.Xe.stddev, params.q) - else: - # TODO: the probability in this case needs to be analysed - probability *= 1 - - if eta <= 20 and d >= 0: # NOTE: η: somewhat arbitrary bound, d: we may guess it all - probability *= RR(prob_babai(r, sqrt(d) * params.Xe.stddev)) - - ret = Cost() - ret["rop"] = bkz_cost["rop"] + svp_cost["rop"] - ret["red"] = bkz_cost["rop"] - ret["svp"] = svp_cost["rop"] - ret["beta"] = beta - ret["eta"] = eta - ret["zeta"] = zeta - ret["|S|"] = search_space - ret["d"] = d - ret["prob"] = probability - - ret.register_impermanent( - {"|S|": False}, - rop=True, - red=True, - svp=True, - eta=False, - zeta=False, - prob=False, - ) - - # 4. Repeat whole experiment ~1/prob times - if probability and not RR(probability).is_NaN(): - ret = ret.repeat( - prob_amplify(0.99, probability), - ) - else: - return Cost(rop=oo) - - return ret - - @classmethod - def cost_zeta( - cls, - zeta: int, - params: LWEParameters, - red_shape_model=red_simulator_default, - red_cost_model=red_cost_model_default, - m: int = oo, - babai: bool = True, - mitm: bool = True, - optimize_d=True, - log_level=5, - **kwds, - ): - """ - This function optimizes costs for a fixed guessing dimension ζ. - """ - - # step 0. establish baseline - baseline_cost = primal_usvp( - params, - red_shape_model=red_shape_model, - red_cost_model=red_cost_model, - optimize_d=False, - log_level=log_level + 1, - **kwds, - ) - Logging.log("bdd", log_level, f"H0: {repr(baseline_cost)}") - - f = partial( - cls.cost, - params=params, - zeta=zeta, - babai=babai, - mitm=mitm, - simulator=red_shape_model, - red_cost_model=red_cost_model, - m=m, - **kwds, - ) - - # step 1. optimize β - with local_minimum( - 40, baseline_cost["beta"] + 1, precision=2, log_level=log_level + 1 - ) as it: - for beta in it: - it.update(f(beta)) - for beta in it.neighborhood: - it.update(f(beta)) - cost = it.y - - Logging.log("bdd", log_level, f"H1: {repr(cost)}") - - # step 2. optimize d - if cost and cost.get("tag", "XXX") != "usvp" and optimize_d: - with local_minimum( - params.n, cost["d"] + cost["zeta"] + 1, log_level=log_level + 1 - ) as it: - for d in it: - it.update(f(beta=cost["beta"], d=d)) - cost = it.y - Logging.log("bdd", log_level, f"H2: {repr(cost)}") - - if cost is None: - return Cost(rop=oo) - return cost - - def __call__( - self, - params: LWEParameters, - babai: bool = True, - zeta: int = None, - mitm: bool = True, - red_shape_model=red_shape_model_default, - red_cost_model=red_cost_model_default, - log_level=1, - **kwds, - ): - """ - Estimate the cost of the hybrid attack and its variants. - - :param params: LWE parameters. - :param zeta: Guessing dimension ζ ≥ 0. - :param babai: Insist on Babai's algorithm for finding close vectors. - :param mitm: Simulate MITM approach (√ of search space). - :return: A cost dictionary - - The returned cost dictionary has the following entries: - - - ``rop``: Total number of word operations (≈ CPU cycles). - - ``red``: Number of word operations in lattice reduction. - - ``δ``: Root-Hermite factor targeted by lattice reduction. - - ``β``: BKZ block size. - - ``η``: Dimension of the final BDD call. - - ``ζ``: Number of guessed coordinates. - - ``|S|``: Guessing search space. - - ``prob``: Probability of success in guessing. - - ``repeat``: How often to repeat the attack. - - ``d``: Lattice dimension. - - - When ζ = 0 this function essentially estimates the BDD strategy as given in [RSA:LiuNgu13]_. - - When ζ ≠ 0 and ``babai=True`` this function estimates the hybrid attack as given in - [C:HowgraveGraham07]_ - - When ζ ≠ 0 and ``babai=False`` this function estimates the hybrid attack as given in - [SAC:AlbCurWun19]_ - - EXAMPLE:: - - >>> from estimator import * - >>> LWE.primal_hybrid(Kyber512.updated(Xs=ND.SparseTernary(512, 16)), mitm = False, babai = False) - rop: ≈2^94.9, red: ≈2^94.3, svp: ≈2^93.3, β: 178, η: 21, ζ: 256, |S|: ≈2^56.6, d: 531, prob: 0.003, ... - - >>> LWE.primal_hybrid(Kyber512.updated(Xs=ND.SparseTernary(512, 16)), mitm = False, babai = True) - rop: ≈2^94.7, red: ≈2^94.0, svp: ≈2^93.3, β: 169, η: 2, ζ: 256, |S|: ≈2^62.4, d: 519, prob: 0.001, ... - - >>> LWE.primal_hybrid(Kyber512.updated(Xs=ND.SparseTernary(512, 16)), mitm = True, babai = False) - rop: ≈2^75.4, red: ≈2^75.0, svp: ≈2^73.3, β: 102, η: 15, ζ: 322, |S|: ≈2^82.9, d: 354, prob: 0.001, ... - - >>> LWE.primal_hybrid(Kyber512.updated(Xs=ND.SparseTernary(512, 16)), mitm = True, babai = True) - rop: ≈2^86.6, red: ≈2^85.7, svp: ≈2^85.6, β: 104, η: 2, ζ: 371, |S|: ≈2^91.1, d: 308, prob: ≈2^-21.3, ... - - """ - - if zeta == 0: - tag = "bdd" - else: - tag = "hybrid" - - params = LWEParameters.normalize(params) - - # allow for a larger embedding lattice dimension: Bai and Galbraith - m = params.m + params.n if params.Xs <= params.Xe else params.m - - red_shape_model = simulator_normalize(red_shape_model) - - f = partial( - self.cost_zeta, - params=params, - red_shape_model=red_shape_model, - red_cost_model=red_cost_model, - babai=babai, - mitm=mitm, - m=m, - log_level=log_level + 1, - ) - - if zeta is None: - with local_minimum(0, params.n, log_level=log_level) as it: - for zeta in it: - it.update( - f( - zeta=zeta, - optimize_d=False, - **kwds, - ) - ) - # TODO: this should not be required - cost = min(it.y, f(0, optimize_d=False, **kwds)) - else: - cost = f(zeta=zeta) - - cost["tag"] = tag - cost["problem"] = params - - if tag == "bdd": - cost["tag"] = tag - cost["problem"] = params - try: - del cost["|S|"] - del cost["prob"] - del cost["repetitions"] - del cost["zeta"] - except KeyError: - pass - - return cost - - __name__ = "primal_hybrid" - - -primal_hybrid = PrimalHybrid() - - -def primal_bdd( - params: LWEParameters, - red_shape_model=red_shape_model_default, - red_cost_model=red_cost_model_default, - log_level=1, - **kwds, -): - """ - Estimate the cost of the BDD approach as given in [RSA:LiuNgu13]_. - - :param params: LWE parameters. - :param red_cost_model: How to cost lattice reduction - :param red_shape_model: How to model the shape of a reduced basis - - """ - - return primal_hybrid( - params, - zeta=0, - mitm=False, - babai=False, - red_shape_model=red_shape_model, - red_cost_model=red_cost_model, - log_level=log_level, - **kwds, - ) diff --git a/estimator_new/nd.py b/estimator_new/nd.py deleted file mode 100644 index 4de7d3291..000000000 --- a/estimator_new/nd.py +++ /dev/null @@ -1,374 +0,0 @@ -# -*- coding: utf-8 -*- - -from dataclasses import dataclass - -from sage.all import parent, RR, RealField, sqrt, pi, oo, ceil, binomial, exp - - -def stddevf(sigma): - """ - Gaussian width parameter σ → standard deviation. - - :param sigma: Gaussian width parameter σ - - EXAMPLE:: - - >>> from estimator.nd import stddevf - >>> stddevf(64.0) - 25.532... - - >>> stddevf(64) - 25.532... - - >>> stddevf(RealField(256)(64)).prec() - 256 - - """ - - try: - prec = parent(sigma).prec() - except AttributeError: - prec = 0 - if prec > 0: - FF = parent(sigma) - else: - FF = RR - return FF(sigma) / FF(sqrt(2 * pi)) - - -def sigmaf(stddev): - """ - Standard deviation → Gaussian width parameter σ. - - :param stddev: standard deviation - - EXAMPLE:: - - >>> from estimator.nd import stddevf, sigmaf - >>> n = 64.0 - >>> sigmaf(stddevf(n)) - 64.000... - - >>> sigmaf(RealField(128)(1.0)) - 2.5066282746310005024157652848110452530 - >>> sigmaf(1.0) - 2.506628274631... - >>> sigmaf(1) - 2.506628274631... - - """ - RR = parent(stddev) - # check that we got ourselves a real number type - try: - if abs(RR(0.5) - 0.5) > 0.001: - RR = RealField(53) # hardcode something - except TypeError: - RR = RealField(53) # hardcode something - return RR(sqrt(2 * pi)) * stddev - - -@dataclass -class NoiseDistribution: - """ - All noise distributions are instances of this class. - - """ - - # cut-off for Gaussian distributions - gaussian_tail_bound = 2 - - # probability that a coefficient falls within the cut-off - gaussian_tail_prob = 1 - 2 * exp(-4 * pi) - - stddev: float - mean: float = 0 - n: int = None - bounds: tuple = None - density: float = 1.0 # Hamming weight / dimension. - tag: str = "" - - def __lt__(self, other): - """ - We compare distributions by comparing their standard deviation. - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.DiscreteGaussian(2.0) < ND.CenteredBinomial(18) - True - >>> ND.DiscreteGaussian(3.0) < ND.CenteredBinomial(18) - False - >>> ND.DiscreteGaussian(4.0) < ND.CenteredBinomial(18) - False - - """ - try: - return self.stddev < other.stddev - except AttributeError: - return self.stddev < other - - def __le__(self, other): - """ - We compare distributions by comparing their standard deviation. - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.DiscreteGaussian(2.0) <= ND.CenteredBinomial(18) - True - >>> ND.DiscreteGaussian(3.0) <= ND.CenteredBinomial(18) - True - >>> ND.DiscreteGaussian(4.0) <= ND.CenteredBinomial(18) - False - - """ - try: - return self.stddev <= other.stddev - except AttributeError: - return self.stddev <= other - - def __str__(self): - """ - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.DiscreteGaussianAlpha(0.01, 7681) - D(σ=30.64) - - """ - if self.n: - return f"D(σ={float(self.stddev):.2f}, μ={float(self.mean):.2f}, n={int(self.n)})" - else: - return f"D(σ={float(self.stddev):.2f}, μ={float(self.mean):.2f})" - - def __repr__(self): - if self.mean == 0.0: - return f"D(σ={float(self.stddev):.2f})" - else: - return f"D(σ={float(self.stddev):.2f}, μ={float(self.mean):.2f})" - - def __hash__(self): - """ - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> hash(ND(3.0, 1.0)) == hash((3.0, 1.0, None)) - True - - """ - return hash((self.stddev, self.mean, self.n)) - - def __len__(self): - """ - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> D = ND.SparseTernary(1024, p=128, m=128) - >>> len(D) - 1024 - >>> int(round(len(D) * float(D.density))) - 256 - - """ - if self.n is not None: - return self.n - else: - raise ValueError("Distribution has no length.") - - @property - def is_Gaussian_like(self): - return ("Gaussian" in self.tag) or ("CenteredBinomial" in self.tag) - - @property - def is_bounded(self): - return (self.bounds[1] - self.bounds[0]) < oo - - @property - def is_sparse(self): - """ - We consider a distribution "sparse" if its density is < 1/2. - """ - # NOTE: somewhat arbitrary - return self.density < 0.5 - - def support_size(self, n=None, fraction=1.0): - """ - Compute the size of the support covering the probability given as fraction. - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> D = ND.Uniform(-3,3, 64) - >>> D.support_size(fraction=.99) - 1207562882759477428726191443614714994252339953407098880 - >>> D = ND.SparseTernary(64, 8) - >>> D.support_size() - 32016101348447354880 - """ - if not n: - if not self.n: - raise ValueError(f"Length required to determine support size, but n was {n}.") - n = self.n - - if "SparseTernary" in self.tag: - h = self.h - # TODO: this is assuming that the non-zero entries are uniform over {-1,1} - # need p and m for more accurate calculation - size = 2 ** h * binomial(n, h) * RR(fraction) - elif self.is_bounded: - # TODO: this might be suboptimal/inaccurate for binomial distribution - a, b = self.bounds - size = RR(fraction) * (b - a + 1) ** n - else: - # Looks like nd is Gaussian - # -> we'll treat it as bounded (with failure probability) - t = self.gaussian_tail_bound - p = self.gaussian_tail_prob - - if p ** n < fraction: - raise NotImplementedError( - f"TODO(nd.support-size): raise t. {RR(p ** n)}, {n}, {fraction}" - ) - - b = 2 * t * sigmaf(self.stddev) + 1 - return (2 * b + 1) ** n - return ceil(size) - - def get_hamming_weight(self, n=None): - if hasattr(self, "h"): - return self.h - - if not n: - if not self.n: - raise ValueError("Length required to determine hamming weight.") - n = self.n - return round(n * self.density) - - @staticmethod - def DiscreteGaussian(stddev, mean=0, n=None): - """ - A discrete Gaussian distribution with standard deviation ``stddev`` per component. - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.DiscreteGaussian(3.0, 1.0) - D(σ=3.00, μ=1.00) - - """ - return NoiseDistribution( - stddev=RR(stddev), mean=RR(mean), n=n, bounds=(-oo, oo), tag="DiscreteGaussian" - ) - - @staticmethod - def DiscreteGaussianAlpha(alpha, q, mean=0, n=None): - """ - A discrete Gaussian distribution with standard deviation α⋅q/√(2π) per component. - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.DiscreteGaussianAlpha(0.001, 2048) - D(σ=0.82) - - """ - stddev = stddevf(alpha * q) - return NoiseDistribution.DiscreteGaussian(stddev=RR(stddev), mean=RR(mean), n=n) - - @staticmethod - def CenteredBinomial(eta, n=None): - """ - Sample a_1, …, a_η, b_1, …, b_η and return Σ(a_i - b_i). - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.CenteredBinomial(8) - D(σ=2.00) - - """ - stddev = sqrt(eta / 2.0) - # TODO: density - return NoiseDistribution( - stddev=RR(stddev), mean=RR(0), n=n, bounds=(-eta, eta), tag="CenteredBinomial" - ) - - @staticmethod - def Uniform(a, b, n=None): - """ - Uniform distribution ∈ ``[a,b]``, endpoints inclusive. - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.Uniform(-3, 3) - D(σ=2.00) - >>> ND.Uniform(-4, 3) - D(σ=2.29, μ=-0.50) - - """ - if b < a: - raise ValueError(f"upper limit must be larger than lower limit but got: {b} < {a}") - m = b - a + 1 - mean = (a + b) / RR(2) - stddev = sqrt((m ** 2 - 1) / RR(12)) - - if a <= 0 and 0 <= b: - density = 1.0 - 1.0 / m - else: - density = 0.0 - - return NoiseDistribution( - n=n, stddev=stddev, mean=mean, bounds=(a, b), density=density, tag="Uniform" - ) - - @staticmethod - def UniformMod(q, n=None): - """ - Uniform mod ``q``, with balanced representation. - - EXAMPLE:: - - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.UniformMod(7) - D(σ=2.00) - >>> ND.UniformMod(8) - D(σ=2.29, μ=-0.50) - - - """ - a = -(q // 2) - b = q // 2 - if q % 2 == 0: - b -= 1 - return NoiseDistribution.Uniform(a, b, n=n) - - @staticmethod - def SparseTernary(n, p, m=None): - """ - Distribution of vectors of length ``n`` with ``p`` entries of 1 and ``m`` entries of -1, rest 0. - - EXAMPLE:: - >>> from estimator.nd import NoiseDistribution as ND - >>> ND.SparseTernary(100, p=10) - D(σ=0.45) - >>> ND.SparseTernary(100, p=10, m=10) - D(σ=0.45) - >>> ND.SparseTernary(100, p=10, m=8) - D(σ=0.42, μ=0.02) - - """ - if m is None: - m = p - - if n == 0: - # this might happen in the dual attack - return NoiseDistribution(stddev=0, mean=0, density=0, bounds=(-1, 1), tag="SparseTernary", n=0) - mean = RR(p / n - m / n) - stddev = RR(sqrt((p + m) / n)) - density = RR((p + m) / n) - D = NoiseDistribution( - stddev=stddev, mean=mean, density=density, bounds=(-1, 1), tag="SparseTernary", n=n - ) - D.h = p + m - return D diff --git a/estimator_new/prob.py b/estimator_new/prob.py deleted file mode 100644 index 9e2a26794..000000000 --- a/estimator_new/prob.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -from sage.all import binomial, ZZ, log, ceil, RealField, oo, exp, pi -from sage.all import RealDistribution, RR, sqrt, prod, erf -from .nd import sigmaf - - -def mitm_babai_probability(r, stddev, q, fast=False): - """ - Compute the "e-admissibility" probability associated to the mitm step, according to - [EPRINT:SonChe19]_ - - :params r: the squared GSO lengths - :params stddev: the std.dev of the error distribution - :params q: the LWE modulus - :param fast: toggle for setting p = 1 (faster, but underestimates security) - :return: probability for the mitm process - - # NOTE: the model sometimes outputs negative probabilities, we set p = 0 in this case - """ - - if fast: - # overestimate the probability -> underestimate security - p = 1 - else: - # get non-squared norms - R = [sqrt(s) for s in r] - alphaq = sigmaf(stddev) - probs = [ - RR( - erf(s * sqrt(RR(pi)) / alphaq) - + (alphaq / s) * ((exp(-s * sqrt(RR(pi)) / alphaq) - 1) / RR(pi)) - ) - for s in R - ] - p = RR(prod(probs)) - if p < 0 or p > 1: - p = 0.0 - return p - - -def babai(r, norm): - """ - Babai probability following [EPRINT:Wun16]_. - - """ - R = [RR(sqrt(t) / (2 * norm)) for t in r] - T = RealDistribution("beta", ((len(r) - 1) / 2, 1.0 / 2)) - probs = [1 - T.cum_distribution_function(1 - s ** 2) for s in R] - return prod(probs) - - -def drop(n, h, k, fail=0, rotations=False): - """ - Probability that ``k`` randomly sampled components have ``fail`` non-zero components amongst - them. - - :param n: LWE dimension `n > 0` - :param h: number of non-zero components - :param k: number of components to ignore - :param fail: we tolerate ``fail`` number of non-zero components amongst the `k` ignored - components - :param rotations: consider rotations of the basis to exploit ring structure (NTRU only) - """ - - N = n # population size - K = n - h # number of success states in the population - n = k # number of draws - k = n - fail # number of observed successes - prob_drop = binomial(K, k) * binomial(N - K, n - k) / binomial(N, n) - if rotations: - return 1 - (1 - prob_drop) ** N - else: - return prob_drop - - -def amplify(target_success_probability, success_probability, majority=False): - """ - Return the number of trials needed to amplify current `success_probability` to - `target_success_probability` - - :param target_success_probability: targeted success probability < 1 - :param success_probability: targeted success probability < 1 - :param majority: if `True` amplify a deicsional problem, not a computational one - if `False` then we assume that we can check solutions, so one success suffices - - :returns: number of required trials to amplify - """ - if target_success_probability < success_probability: - return ZZ(1) - if success_probability == 0.0: - return oo - - prec = max( - 53, - 2 * ceil(abs(log(success_probability, 2))), - 2 * ceil(abs(log(1 - success_probability, 2))), - 2 * ceil(abs(log(target_success_probability, 2))), - 2 * ceil(abs(log(1 - target_success_probability, 2))), - ) - prec = min(prec, 2048) - RR = RealField(prec) - - success_probability = RR(success_probability) - target_success_probability = RR(target_success_probability) - - try: - if majority: - eps = success_probability / 2 - return ceil(2 * log(2 - 2 * target_success_probability) / log(1 - 4 * eps ** 2)) - else: - # target_success_probability = 1 - (1-success_probability)^trials - return ceil(log(1 - target_success_probability) / log(1 - success_probability)) - except ValueError: - return oo - - -def amplify_sigma(target_advantage, sigma, q): - """ - Amplify distinguishing advantage for a given σ and q - - :param target_advantage: - :param sigma: (Lists of) Gaussian width parameters - :param q: Modulus q > 0 - - """ - try: - sigma = sum(sigma_ ** 2 for sigma_ in sigma).sqrt() - except TypeError: - pass - advantage = float(exp(-float(pi) * (float(sigma / q) ** 2))) - return amplify(target_advantage, advantage, majority=True) diff --git a/estimator_new/reduction.py b/estimator_new/reduction.py deleted file mode 100644 index 290349f00..000000000 --- a/estimator_new/reduction.py +++ /dev/null @@ -1,571 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Cost estimates for lattice redution. -""" - -from sage.all import ZZ, RR, pi, e, find_root, ceil, log, oo, round -from scipy.optimize import newton - - -def _delta(beta): - """ - Compute δ from block size β without enforcing β ∈ ZZ. - - δ for β ≤ 40 were computed as follows: - - ``` - # -*- coding: utf-8 -*- - from fpylll import BKZ, IntegerMatrix - - from multiprocessing import Pool - from sage.all import mean, sqrt, exp, log, cputime - - d, trials = 320, 32 - - def f((A, beta)): - - par = BKZ.Param(block_size=beta, strategies=BKZ.DEFAULT_STRATEGY, flags=BKZ.AUTO_ABORT) - q = A[-1, -1] - d = A.nrows - t = cputime() - A = BKZ.reduction(A, par, float_type="dd") - t = cputime(t) - return t, exp(log(A[0].norm()/sqrt(q).n())/d) - - if __name__ == '__main__': - for beta in (5, 10, 15, 20, 25, 28, 30, 35, 40): - delta = [] - t = [] - i = 0 - while i < trials: - threads = int(open("delta.nthreads").read()) # make sure this file exists - pool = Pool(threads) - A = [(IntegerMatrix.random(d, "qary", beta=d//2, bits=50), beta) for j in range(threads)] - for (t_, delta_) in pool.imap_unordered(f, A): - t.append(t_) - delta.append(delta_) - i += threads - print u"β: %2d, δ_0: %.5f, time: %5.1fs, (%2d,%2d)"%(beta, mean(delta), mean(t), i, threads) - print - ``` - - """ - small = ( - (2, 1.02190), # noqa - (5, 1.01862), # noqa - (10, 1.01616), - (15, 1.01485), - (20, 1.01420), - (25, 1.01342), - (28, 1.01331), - (40, 1.01295), - ) - - if beta <= 2: - return RR(1.0219) - elif beta < 40: - for i in range(1, len(small)): - if small[i][0] > beta: - return RR(small[i - 1][1]) - elif beta == 40: - return RR(small[-1][1]) - else: - return RR(beta / (2 * pi * e) * (pi * beta) ** (1 / beta)) ** (1 / (2 * (beta - 1))) - - -def delta(beta): - """ - Compute root-Hermite factor δ from block size β. - - :param beta: Block size. - """ - beta = ZZ(round(beta)) - return _delta(beta) - - -def _beta_secant(delta): - """ - Estimate required block size β for a given root-Hermite factor δ based on [PhD:Chen13]_. - - :param delta: Root-Hermite factor. - - EXAMPLE:: - - >>> import estimator.reduction as RC - >>> 50 == RC._beta_secant(1.0121) - True - >>> 100 == RC._beta_secant(1.0093) - True - >>> RC._beta_secant(1.0024) # Chen reports 800 - 808 - - """ - # newton() will produce a "warning", if two subsequent function values are - # indistinguishable (i.e. equal in terms of machine precision). In this case - # newton() will return the value beta in the middle between the two values - # k1,k2 for which the function values were indistinguishable. - # Since f approaches zero for beta->+Infinity, this may be the case for very - # large inputs, like beta=1e16. - # For now, these warnings just get printed and the value beta is used anyways. - # This seems reasonable, since for such large inputs the exact value of beta - # doesn't make such a big difference. - try: - beta = newton( - lambda beta: RR(_delta(beta) - delta), - 100, - fprime=None, - args=(), - tol=1.48e-08, - maxiter=500, - ) - beta = ceil(beta) - if beta < 40: - # newton may output beta < 40. The old beta method wouldn't do this. For - # consistency, call the old beta method, i.e. consider this try as "failed". - raise RuntimeError("β < 40") - return beta - except (RuntimeError, TypeError): - # if something fails, use old beta method - beta = _beta_simple(delta) - return beta - - -def _beta_find_root(delta): - """ - Estimate required block size β for a given root-Hermite factor δ based on [PhD:Chen13]_. - - :param delta: Root-Hermite factor. - - TESTS:: - - >>> import estimator.reduction as RC - >>> RC._beta_find_root(RC.delta(500)) - 500 - - """ - # handle beta < 40 separately - beta = ZZ(40) - if _delta(beta) < delta: - return beta - - try: - beta = find_root(lambda beta: RR(_delta(beta) - delta), 40, 2 ** 16, maxiter=500) - beta = ceil(beta - 1e-8) - except RuntimeError: - # finding root failed; reasons: - # 1. maxiter not sufficient - # 2. no root in given interval - beta = _beta_simple(delta) - return beta - - -def _beta_simple(delta): - """ - Estimate required block size β for a given root-Hermite factor δ based on [PhD:Chen13]_. - - :param delta: Root-Hermite factor. - - TESTS:: - - >>> import estimator.reduction as RC - >>> RC._beta_simple(RC.delta(500)) - 501 - - """ - beta = ZZ(40) - - while _delta(2 * beta) > delta: - beta *= 2 - while _delta(beta + 10) > delta: - beta += 10 - while True: - if _delta(beta) < delta: - break - beta += 1 - - return beta - - -def beta(delta): - """ - Estimate required block size β for a given root-hermite factor δ based on [PhD:Chen13]_. - - :param delta: Root-hermite factor. - - EXAMPLE:: - - >>> import estimator.reduction as RC - >>> 50 == RC.beta(1.0121) - True - >>> 100 == RC.beta(1.0093) - True - >>> RC.beta(1.0024) # Chen reports 800 - 808 - - """ - # TODO: decide for one strategy (secant, find_root, old) and its error handling - beta = _beta_find_root(delta) - return beta - - -# BKZ Estimates - - -def svp_repeat(beta, d): - """ - Return number of SVP calls in BKZ-β. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - - .. note :: Loosely based on experiments in [PhD:Chen13]. - - .. note :: When d ≤ β we return 1. - - """ - if beta < d: - return 8 * d - else: - return 1 - - -def LLL(d, B=None): - """ - Runtime estimation for LLL algorithm based on [AC:CheNgu11]_. - - :param d: Lattice dimension. - :param B: Bit-size of entries. - - """ - if B: - return d ** 3 * B ** 2 - else: - return d ** 3 # ignoring B for backward compatibility - - -def _BDGL16_small(beta, d, B=None): - """ - Runtime estimation given β and assuming sieving is used to realise the SVP oracle for small - dimensions following [SODA:BDGL16]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - TESTS:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC._BDGL16_small(500, 1024), 2.0) - 222.9 - - """ - return LLL(d, B) + ZZ(2) ** RR(0.387 * beta + 16.4 + log(svp_repeat(beta, d), 2)) - - -def _BDGL16_asymptotic(beta, d, B=None): - """ - Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle following [SODA:BDGL16]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - TESTS:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC._BDGL16_asymptotic(500, 1024), 2.0) - 175.4 - """ - # TODO we simply pick the same additive constant 16.4 as for the experimental result in [SODA:BDGL16]_ - return LLL(d, B) + ZZ(2) ** RR(0.292 * beta + 16.4 + log(svp_repeat(beta, d), 2)) - - -def BDGL16(beta, d, B=None): - """ - Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle following [SODA:BDGL16]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - EXAMPLE:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC.BDGL16(500, 1024), 2.0) - 175.4 - - """ - # TODO this is somewhat arbitrary - if beta <= 90: - return _BDGL16_small(beta, d, B) - else: - return _BDGL16_asymptotic(beta, d, B) - - -def LaaMosPol14(beta, d, B=None): - """ - Runtime estimation for quantum sieving following [EPRINT:LaaMosPol14]_ and [PhD:Laarhoven15]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - EXAMPLE:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC.LaaMosPol14(500, 1024), 2.0) - 161.9 - - """ - return LLL(d, B) + ZZ(2) ** RR((0.265 * beta + 16.4 + log(svp_repeat(beta, d), 2))) - - -def CheNgu12(beta, d, B=None): - """ - Runtime estimation given β and assuming [CheNgu12]_ estimates are correct. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - The constants in this function were derived as follows based on Table 4 in - [CheNgu12]_:: - - >>> from sage.all import var, find_fit - >>> dim = [100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250] - >>> nodes = [39.0, 44.0, 49.0, 54.0, 60.0, 66.0, 72.0, 78.0, 84.0, 96.0, 99.0, 105.0, 111.0, 120.0, 127.0, 134.0] # noqa - >>> times = [c + log(200,2).n() for c in nodes] - >>> T = list(zip(dim, nodes)) - >>> var("a,b,c,beta") - (a, b, c, beta) - >>> f = a*beta*log(beta, 2.0) + b*beta + c - >>> f = f.function(beta) - >>> f.subs(find_fit(T, f, solution_dict=True)) - beta |--> 0.2701...*beta*log(beta) - 1.0192...*beta + 16.10... - - The estimation 2^(0.18728 β⋅log_2(β) - 1.019⋅β + 16.10) is of the number of enumeration - nodes, hence we need to multiply by the number of cycles to process one node. This cost per - node is typically estimated as 64. - - EXAMPLE:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC.CheNgu12(500, 1024), 2.0) - 365.70... - - """ - repeat = svp_repeat(beta, d) - cost = RR( - 0.270188776350190 * beta * log(beta) - - 1.0192050451318417 * beta - + 16.10253135200765 - + log(100, 2) - ) - return LLL(d, B) + repeat * ZZ(2) ** cost - - -def ABFKSW20(beta, d, B=None): - """ - Enumeration cost according to [C:ABFKSW20]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - EXAMPLE:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC.ABFKSW20(500, 1024), 2.0) - 316.26... - - """ - if 1.5 * beta >= d or beta <= 92: # 1.5β is a bit arbitrary, β≤92 is the crossover point - cost = RR(0.1839 * beta * log(beta, 2) - 0.995 * beta + 16.25 + log(64, 2)) - else: - cost = RR(0.125 * beta * log(beta, 2) - 0.547 * beta + 10.4 + log(64, 2)) - - repeat = svp_repeat(beta, d) - - return LLL(d, B) + repeat * ZZ(2) ** cost - - -def ABLR21(beta, d, B=None): - """ - Enumeration cost according to [C:ABLR21]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - EXAMPLE:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC.ABLR21(500, 1024), 2.0) - 278.20... - - """ - if 1.5 * beta >= d or beta <= 97: # 1.5β is a bit arbitrary, 97 is the crossover - cost = RR(0.1839 * beta * log(beta, 2) - 1.077 * beta + 29.12 + log(64, 2)) - else: - cost = RR(0.1250 * beta * log(beta, 2) - 0.654 * beta + 25.84 + log(64, 2)) - - repeat = svp_repeat(beta, d) - - return LLL(d, B) + repeat * ZZ(2) ** cost - - -def ADPS16(beta, d, B=None, mode="classical"): - """ - Runtime estimation from [USENIX:ADPS16]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - - EXAMPLE:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC.ADPS16(500, 1024), 2.0) - 146.0 - >>> log(RC.ADPS16(500, 1024, mode="quantum"), 2.0) - 132.5 - >>> log(RC.ADPS16(500, 1024, mode="paranoid"), 2.0) - 103.75 - - """ - - if mode not in ("classical", "quantum", "paranoid"): - raise ValueError(f"Mode {mode} not understood.") - - c = { - "classical": 0.2920, - "quantum": 0.2650, # paper writes 0.262 but this isn't right, see above - "paranoid": 0.2075, - } - - c = c[mode] - - return ZZ(2) ** RR(c * beta) - - -def d4f(beta): - """ - Dimensions "for free" following [EC:Ducas18]_. - - :param beta: Block size ≥ 2. - - If β' is output by this function then sieving is expected to be required up to dimension β-β'. - - EXAMPLE:: - - >>> import estimator.reduction as RC - >>> RC.d4f(500) - 42.597... - - """ - return max(float(beta * log(4 / 3.0) / log(beta / (2 * pi * e))), 0.0) - - -# These are not asymptotic expressions but compress the data in [AC:AGPS20]_ which covers up to -# β = 1024 -NN_AGPS = { - "all_pairs-classical": {"a": 0.4215069316613415, "b": 20.1669683097337}, - "all_pairs-dw": {"a": 0.3171724396445732, "b": 25.29828951733785}, - "all_pairs-g": {"a": 0.3155285835002801, "b": 22.478746811528048}, - "all_pairs-ge19": {"a": 0.3222895263943544, "b": 36.11746438609666}, - "all_pairs-naive_classical": {"a": 0.4186251294633655, "b": 9.899382654377058}, - "all_pairs-naive_quantum": {"a": 0.31401512556555794, "b": 7.694659515948326}, - "all_pairs-t_count": {"a": 0.31553282515234704, "b": 20.878594142502994}, - "list_decoding-classical": {"a": 0.2988026130564745, "b": 26.011121212891872}, - "list_decoding-dw": {"a": 0.26944796385592995, "b": 28.97237346443934}, - "list_decoding-g": {"a": 0.26937450988892553, "b": 26.925140365395972}, - "list_decoding-ge19": {"a": 0.2695210400018704, "b": 35.47132142280775}, - "list_decoding-naive_classical": {"a": 0.2973130399197453, "b": 21.142124058689426}, - "list_decoding-naive_quantum": {"a": 0.2674316807758961, "b": 18.720680589028465}, - "list_decoding-t_count": {"a": 0.26945736714156543, "b": 25.913746774011887}, - "random_buckets-classical": {"a": 0.35586144233444716, "b": 23.082527816636638}, - "random_buckets-dw": {"a": 0.30704199612690264, "b": 25.581968903639485}, - "random_buckets-g": {"a": 0.30610964725102385, "b": 22.928235564044563}, - "random_buckets-ge19": {"a": 0.31089687599538407, "b": 36.02129978813208}, - "random_buckets-naive_classical": {"a": 0.35448283789554513, "b": 15.28878540793908}, - "random_buckets-naive_quantum": {"a": 0.30211421791887644, "b": 11.151745013027089}, - "random_buckets-t_count": {"a": 0.30614770082829745, "b": 21.41830142853265}, -} - - -def Kyber(beta, d, B=None, nn="classical", C=5.46): - """ - Runtime estimation from [Kyber20]_ and [AC:AGPS20]_. - - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - :param nn: Nearest neighbor cost model. We default to "ListDecoding" (i.e. BDGL16) and to - the "depth × width" metric. Kyber uses "AllPairs". - :param C: Progressive overhead lim_{β → ∞} ∑_{i ≤ β} 2^{0.292 i + o(i)}/2^{0.292 β + o(β)}. - - EXAMPLE:: - - >>> from math import log - >>> import estimator.reduction as RC - >>> log(RC.Kyber(500, 1024), 2.0) - 174.16... - >>> log(RC.Kyber(500, 1024, nn="list_decoding-ge19"), 2.0) - 170.23... - - """ - - if beta < 20: # goes haywire - return CheNgu12(beta, d, B) - - if nn == "classical": - nn = "list_decoding-classical" - elif nn == "quantum": - nn = "list_decoding-dw" - - svp_calls = C * max(d - beta, 1) - # we do not round to the nearest integer to ensure cost is continuously increasing with β which - # rounding can violate. - beta_ = beta - d4f(beta) - gate_count = 2 ** (NN_AGPS[nn]["a"] * beta_ + NN_AGPS[nn]["b"]) - return LLL(d, B=B) + svp_calls * gate_count - - -def cost(cost_model, beta, d, B=None, predicate=None, **kwds): - """ - Return cost dictionary for computing vector of norm` δ_0^{d-1} Vol(Λ)^{1/d}` using provided lattice - reduction algorithm. - - :param cost_model: - :param beta: Block size ≥ 2. - :param d: Lattice dimension. - :param B: Bit-size of entries. - :param predicate: if ``False`` cost will be infinity. - - EXAMPLE:: - - >>> import estimator.reduction as RC - >>> RC.cost(RC.ABLR21, 120, 500) - rop: ≈2^68.9, red: ≈2^68.9, δ: 1.008435, β: 120, d: 500 - >>> RC.cost(RC.ABLR21, 120, 500, predicate=False) - rop: ≈2^inf, red: ≈2^inf, δ: 1.008435, β: 120, d: 500 - - """ - from .cost import Cost - - cost = cost_model(beta, d, B) - delta_ = delta(beta) - cost = Cost(rop=cost, red=cost, delta=delta_, beta=beta, d=d, **kwds) - cost.register_impermanent(rop=True, red=True, delta=False, beta=False, d=False) - if predicate is not None and not predicate: - cost["red"] = oo - cost["rop"] = oo - return cost diff --git a/estimator_new/schemes.py b/estimator_new/schemes.py deleted file mode 100644 index db7b36c12..000000000 --- a/estimator_new/schemes.py +++ /dev/null @@ -1,402 +0,0 @@ -from .nd import NoiseDistribution, stddevf -from .lwe_parameters import LWEParameters - -# -# Kyber -# -# -# https://pq-crystals.org/kyber/data/kyber-specification-round3-20210804.pdf -# Table 1, Page 11, we are ignoring the compression -# -# https://eprint.iacr.org/2020/1308.pdf -# Table 2, page 27, disagrees on Kyber 512 - -Kyber512 = LWEParameters( - n=2 * 256, - q=3329, - Xs=NoiseDistribution.CenteredBinomial(3), - Xe=NoiseDistribution.CenteredBinomial(3), - m=2 * 256, - tag="Kyber 512", -) - -Kyber768 = LWEParameters( - n=3 * 256, - q=3329, - Xs=NoiseDistribution.CenteredBinomial(2), - Xe=NoiseDistribution.CenteredBinomial(2), - m=3 * 256, - tag="Kyber 768", -) - -Kyber1024 = LWEParameters( - n=4 * 256, - q=3329, - Xs=NoiseDistribution.CenteredBinomial(2), - Xe=NoiseDistribution.CenteredBinomial(2), - m=4 * 256, - tag="Kyber 1024", -) - -# -# Saber -# -# -# https://www.esat.kuleuven.be/cosic/pqcrypto/saber/files/saberspecround3.pdf -# Table 1, page 11 -# -# https://eprint.iacr.org/2020/1308.pdf -# Table 2, page 27, agrees - -LightSaber = LWEParameters( - n=2 * 256, - q=8192, - Xs=NoiseDistribution.CenteredBinomial(5), - Xe=NoiseDistribution.UniformMod(7), - m=2 * 256, - tag="LightSaber", -) - -Saber = LWEParameters( - n=3 * 256, - q=8192, - Xs=NoiseDistribution.CenteredBinomial(4), - Xe=NoiseDistribution.UniformMod(7), - m=3 * 256, - tag="Saber", -) - -FireSaber = LWEParameters( - n=4 * 256, - q=8192, - Xs=NoiseDistribution.CenteredBinomial(3), - Xe=NoiseDistribution.UniformMod(7), - m=4 * 256, - tag="FireSaber", -) - -NTRUHPS2048509Enc = LWEParameters( - n=508, - q=2048, - Xe=NoiseDistribution.SparseTernary(508, 2048 / 16 - 1), - Xs=NoiseDistribution.UniformMod(3), - m=508, - tag="NTRUHPS2048509Enc", -) - -NTRUHPS2048677Enc = LWEParameters( - n=676, - q=2048, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.SparseTernary(676, 2048 / 16 - 1), - m=676, - tag="NTRUHPS2048677Enc", -) - -NTRUHPS4096821Enc = LWEParameters( - n=820, - q=4096, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.SparseTernary(820, 4096 / 16 - 1), - m=820, - tag="NTRUHPS4096821Enc", -) - -NTRUHRSS701Enc = LWEParameters( - n=700, - q=8192, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.UniformMod(3), - m=700, - tag="NTRUHRSS701", -) - -NISTPQC_R3 = ( - Kyber512, - Kyber768, - Kyber1024, - LightSaber, - Saber, - FireSaber, - NTRUHPS2048509Enc, - NTRUHPS2048677Enc, - NTRUHPS4096821Enc, - NTRUHRSS701Enc, -) - -HESv111024128error = LWEParameters( - n=1024, - q=2 ** 27, - Xs=NoiseDistribution.DiscreteGaussian(3.0), - Xe=NoiseDistribution.DiscreteGaussian(3.0), - m=1024, - tag="HESv11error", -) - -HESv111024128ternary = LWEParameters( - n=1024, - q=2 ** 27, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(3.0), - m=1024, - tag="HESv11ternary", -) - -HESv11 = (HESv111024128error, HESv111024128ternary) - - -# FHE schemes - -# TFHE -# https://tfhe.github.io/tfhe/security_and_params.html -TFHE630 = LWEParameters( - n=630, - q=2 ** 32, - Xs=NoiseDistribution.UniformMod(2), - Xe=NoiseDistribution.DiscreteGaussian(stddev=2 ** (-15) * 2 ** 32), - tag="TFHE630", -) - -TFHE1024 = LWEParameters( - n=1024, - q=2 ** 32, - Xs=NoiseDistribution.UniformMod(2), - Xe=NoiseDistribution.DiscreteGaussian(stddev=2 ** (-25) * 2 ** 32), - tag="TFHE630", -) - -# https://eprint.iacr.org/2018/421.pdf -# Table 3, page 55 -TFHE16_500 = LWEParameters( - n=500, - q=2 ** 32, - Xs=NoiseDistribution.UniformMod(2), - Xe=NoiseDistribution.DiscreteGaussian(stddev=2.43 * 10 ** (-5) * 2 ** 32), - tag="TFHE16_500", -) - -TFHE16_1024 = LWEParameters( - n=1024, - q=2 ** 32, - Xs=NoiseDistribution.UniformMod(2), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.73 * 10 ** (-9) * 2 ** 32), - tag="TFHE16_1024", -) - -# https://eprint.iacr.org/2018/421.pdf -# Table 4, page 55 -TFHE20_612 = LWEParameters( - n=612, - q=2 ** 32, - Xs=NoiseDistribution.UniformMod(2), - Xe=NoiseDistribution.DiscreteGaussian(stddev=2 ** (-15) * 2 ** 32), - tag="TFHE20_612", -) - -TFHE20_1024 = LWEParameters( - n=1024, - q=2 ** 32, - Xs=NoiseDistribution.UniformMod(2), - Xe=NoiseDistribution.DiscreteGaussian(stddev=2 ** (-26) * 2 ** 32), - tag="TFHE20_1024", -) - -# FHEW -# https://eprint.iacr.org/2014/816.pdf -# page 14 -FHEW = LWEParameters( - n=500, - q=2 ** 32, - Xs=NoiseDistribution.UniformMod(2), - Xe=NoiseDistribution.DiscreteGaussian(stddev=2 ** (-15) * 2 ** 32), - tag="FHEW", -) - -# SEAL - -# v2.0 -# https://www.microsoft.com/en-us/research/wp-content/uploads/2016/09/sealmanual.pdf -# Table 3, page 19 - -SEAL20_1024 = LWEParameters( - n=1024, - q=2 ** 48 - 2 ** 20 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL20_1024", -) - -SEAL20_2048 = LWEParameters( - n=2048, - q=2 ** 94 - 2 ** 20 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL20_2048", -) - -SEAL20_4096 = LWEParameters( - n=4096, - q=2 ** 190 - 2 ** 30 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL20_4096", -) - -SEAL20_8192 = LWEParameters( - n=8192, - q=2 ** 383 - 2 ** 33 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL20_8192", -) - -SEAL20_16384 = LWEParameters( - n=16384, - q=2 ** 767 - 2 ** 56 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL20_16384", -) - -# v2.2 -# https://www.microsoft.com/en-us/research/wp-content/uploads/2017/06/sealmanual_v2.2.pdf -# Table 3, page 20 -SEAL22_2048 = LWEParameters( - n=2048, - q=2 ** 60 - 2 ** 14 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL22_2048", -) - -SEAL22_4096 = LWEParameters( - n=4096, - q=2 ** 116 - 2 ** 18 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL22_4096", -) - -SEAL22_8192 = LWEParameters( - n=8192, - q=2 ** 226 - 2 ** 26 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL22_8192", -) - -SEAL22_16384 = LWEParameters( - n=16384, - q=2 ** 435 - 2 ** 33 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL22_16384", -) - -SEAL22_32768 = LWEParameters( - n=32768, - q=2 ** 889 - 2 ** 54 - 2 ** 53 - 2 ** 52 + 1, - Xs=NoiseDistribution.UniformMod(3), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.19), - tag="SEAL22_32768", -) - -# The following are not parameters of actual schemes -# but useful for benchmarking - -# HElib -# https://eprint.iacr.org/2017/047.pdf -# Table 1, page 6 -# 80-bit security -HElib80_1024 = LWEParameters( - n=1024, - q=2 ** 47, - Xs=NoiseDistribution.SparseTernary(n=1024, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.2), - tag="HElib80_1024", -) - -HElib80_2048 = LWEParameters( - n=2048, - q=2 ** 87, - Xs=NoiseDistribution.SparseTernary(n=2048, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.2), - tag="HElib80_2048", -) - -HElib80_4096 = LWEParameters( - n=4096, - q=2 ** 167, - Xs=NoiseDistribution.SparseTernary(n=4096, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.2), - tag="HElib80_4096", -) - -# 120-bit security -HElib120_1024 = LWEParameters( - n=1024, - q=2 ** 38, - Xs=NoiseDistribution.SparseTernary(n=1024, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.2), - tag="HElib80_1024", -) - -HElib120_2048 = LWEParameters( - n=2048, - q=2 ** 70, - Xs=NoiseDistribution.SparseTernary(n=2048, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.2), - tag="HElib80_2048", -) - -HElib120_4096 = LWEParameters( - n=4096, - q=2 ** 134, - Xs=NoiseDistribution.SparseTernary(n=4096, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=3.2), - tag="HElib80_4096", -) - - -# Test parameters from CHHS -# https://eprint.iacr.org/2019/1114.pdf -# Table 4, page 18 -CHHS_1024_25 = LWEParameters( - n=1024, - q=2 ** 25, - Xs=NoiseDistribution.SparseTernary(n=1024, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=stddevf(8)), - tag="CHHS_1024_25", -) - -CHHS_2048_38 = LWEParameters( - n=2048, - q=2 ** 38, - Xs=NoiseDistribution.SparseTernary(n=2048, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=stddevf(8)), - tag="CHHS_2048_38", -) - -CHHS_2048_45 = LWEParameters( - n=2048, - q=2 ** 45, - Xs=NoiseDistribution.SparseTernary(n=2048, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=stddevf(8)), - tag="CHHS_2048_45", -) - -CHHS_4096_67 = LWEParameters( - n=4096, - q=2 ** 67, - Xs=NoiseDistribution.SparseTernary(n=4096, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=stddevf(8)), - tag="CHHS_4096_67", -) - -CHHS_4096_82 = LWEParameters( - n=4096, - q=2 ** 82, - Xs=NoiseDistribution.SparseTernary(n=4096, p=32), - Xe=NoiseDistribution.DiscreteGaussian(stddev=stddevf(8)), - tag="CHHS_4096_82", -) diff --git a/estimator_new/simulator.py b/estimator_new/simulator.py deleted file mode 100644 index 486008452..000000000 --- a/estimator_new/simulator.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Simulate lattice reduction on the rows of:: - - ⌜ ξI A 0 ⌝ - ǀ 0 qI 0 | - ⌞ 0 c τ ⌟ - -where - -- ξI ∈ ZZ^{n × n}, -- A ∈ ZZ_q^{n × m}, -- qI ∈ ZZ^{m × m}, -- τ ∈ ZZ and -- d = m + n + 1. - -The last row is optional. -""" - -from sage.all import RR, log - - -def CN11(d, n, q, beta, xi=1, tau=1): - from fpylll import BKZ - from fpylll.tools.bkz_simulator import simulate - - if tau is not None: - r = [q ** 2] * (d - n - 1) + [xi ** 2] * n + [tau ** 2] - else: - r = [q ** 2] * (d - n) + [xi ** 2] * n - - return simulate(r, BKZ.EasyParam(beta))[0] - - -def GSA(d, n, q, beta, xi=1, tau=1): - - """Reduced lattice shape fallowing the Geometric Series Assumption [Schnorr03]_ - - :param d: Lattice dimension. - :param n: Number of `q` vectors - :param q: Modulus `q` - :param beta: Block size β. - :param xi: Scaling factor ξ for identity part. - :param tau: Kannan factor τ. - - """ - from .reduction import delta as deltaf - - if tau is not None: - log_vol = RR(log(q, 2) * (d - n - 1) + log(xi, 2) * n + log(tau, 2)) - else: - log_vol = RR(log(q, 2) * (d - n) + log(xi, 2) * n) - - delta = deltaf(beta) - r_log = [(d - 1 - 2 * i) * RR(log(delta, 2)) + log_vol / d for i in range(d)] - r = [2 ** (2 * r_) for r_ in r_log] - return r - - -def normalize(name): - if str(name).upper() == "CN11": - return CN11 - elif str(name).upper() == "GSA": - return GSA - else: - return name - - -def plot_gso(r, *args, **kwds): - from sage.all import line - - return line([(i, log(r_, 2) / 2.0) for i, r_ in enumerate(r)], *args, **kwds) diff --git a/estimator_new/util.py b/estimator_new/util.py deleted file mode 100644 index 723e13acd..000000000 --- a/estimator_new/util.py +++ /dev/null @@ -1,295 +0,0 @@ -from multiprocessing import Pool -from functools import partial - -from sage.all import ceil, floor - -from .io import Logging - - -class local_minimum_base: - """ - An iterator context for finding a local minimum using binary search. - - We use the immediate neighborhood of a point to decide the next direction to go into (gradient - descent style), so the algorithm is not plain binary search (see ``update()`` function.) - - .. note :: We combine an iterator and a context to give the caller access to the result. - """ - - def __init__( - self, - start, - stop, - smallerf=lambda x, best: x <= best, - suppress_bounds_warning=False, - log_level=5, - ): - """ - Create a fresh local minimum search context. - - :param start: starting point - :param stop: end point (exclusive) - :param smallerf: a function to decide if ``lhs`` is smaller than ``rhs``. - :param suppress_bounds_warning: do not warn if a boundary is picked as optimal - - """ - - if stop < start: - raise ValueError(f"Incorrect bounds {start} > {stop}.") - - self._suppress_bounds_warning = suppress_bounds_warning - self._log_level = log_level - self._start = start - self._stop = stop - 1 - self._initial_bounds = (start, stop - 1) - self._smallerf = smallerf - # abs(self._direction) == 2: binary search step - # abs(self._direction) == 1: gradient descent direction - self._direction = -1 # going down - self._last_x = None - self._next_x = self._stop - self._best = (None, None) - self._all_x = set() - - def __enter__(self): - """ """ - return self - - def __exit__(self, type, value, traceback): - """ """ - pass - - def __iter__(self): - """ """ - return self - - def __next__(self): - abort = False - if self._next_x is None: - abort = True # we're told to abort - elif self._next_x in self._all_x: - abort = True # we're looping - elif self._next_x < self._initial_bounds[0] or self._initial_bounds[1] < self._next_x: - abort = True # we're stepping out of bounds - - if not abort: - self._last_x = self._next_x - self._next_x = None - return self._last_x - - if self._best[0] in self._initial_bounds and not self._suppress_bounds_warning: - # We warn the user if the optimal solution is at the edge and thus possibly not optimal. - Logging.log( - "bins", - self._log_level, - f'warning: "optimal" solution {self._best[0]} matches a bound ∈ {self._initial_bounds}.', - ) - - raise StopIteration - - @property - def x(self): - return self._best[0] - - @property - def y(self): - return self._best[1] - - def update(self, res): - """ - - TESTS: - - We keep cache old inputs in ``_all_x`` to prevent infinite loops:: - - >>> from estimator.util import binary_search - >>> from estimator.cost import Cost - >>> f = lambda x, log_level=1: Cost(rop=1) if x >= 19 else Cost(rop=2) - >>> binary_search(f, 10, 30, "x") - rop: 1 - - """ - - Logging.log("bins", self._log_level, f"({self._last_x}, {repr(res)})") - - self._all_x.add(self._last_x) - - # We got nothing yet - if self._best[0] is None: - self._best = self._last_x, res - - # We found something better - if res is not False and self._smallerf(res, self._best[1]): - # store it - self._best = self._last_x, res - - # if it's a result of a long jump figure out the next direction - if abs(self._direction) != 1: - self._direction = -1 - self._next_x = self._last_x - 1 - # going down worked, so let's keep on doing that. - elif self._direction == -1: - self._direction = -2 - self._stop = self._last_x - self._next_x = ceil((self._start + self._stop) / 2) - # going up worked, so let's keep on doing that. - elif self._direction == 1: - self._direction = 2 - self._start = self._last_x - self._next_x = floor((self._start + self._stop) / 2) - else: - # going downwards didn't help, let's try up - if self._direction == -1: - self._direction = 1 - self._next_x = self._last_x + 2 - # going up didn't help either, so we stop - elif self._direction == 1: - self._next_x = None - # it got no better in a long jump, half the search space and try again - elif self._direction == -2: - self._start = self._last_x - self._next_x = ceil((self._start + self._stop) / 2) - elif self._direction == 2: - self._stop = self._last_x - self._next_x = floor((self._start + self._stop) / 2) - - # We are repeating ourselves, time to stop - if self._next_x == self._last_x: - self._next_x = None - - -class local_minimum(local_minimum_base): - """ - An iterator context for finding a local minimum using binary search. - - We use the neighborhood of a point to decide the next direction to go into (gradient descent - style), so the algorithm is not plain binary search (see ``update()`` function.) - - We also zoom out by a factor ``precision``, find an approximate local minimum and then - search the neighbourhood for the smallest value. - - .. note :: We combine an iterator and a context to give the caller access to the result. - - """ - - def __init__( - self, - start, - stop, - precision=1, - smallerf=lambda x, best: x <= best, - suppress_bounds_warning=False, - log_level=5, - ): - """ - Create a fresh local minimum search context. - - :param start: starting point - :param stop: end point (exclusive) - :param precision: only consider every ``precision``-th value in the main loop - :param smallerf: a function to decide if ``lhs`` is smaller than ``rhs``. - :param suppress_bounds_warning: do not warn if a boundary is picked as optimal - - """ - self._precision = precision - self._orig_bounds = (start, stop) - start = ceil(start / precision) - stop = floor(stop / precision) - local_minimum_base.__init__(self, start, stop, smallerf, suppress_bounds_warning, log_level) - - def __next__(self): - x = local_minimum_base.__next__(self) - return x * self._precision - - @property - def x(self): - return self._best[0] * self._precision - - @property - def neighborhood(self): - """ - An iterator over the neighborhood of the currently best value. - """ - - start, stop = self._orig_bounds - - for x in range(max(start, self.x - self._precision), min(stop, self.x + self._precision)): - yield x - - -def binary_search( - f, start, stop, param, step=1, smallerf=lambda x, best: x <= best, log_level=5, *args, **kwds -): - """ - Searches for the best value in the interval [start,stop] depending on the given comparison function. - - :param start: start of range to search - :param stop: stop of range to search (exclusive) - :param param: the parameter to modify when calling `f` - :param smallerf: comparison is performed by evaluating ``smallerf(current, best)`` - :param step: initially only consider every `steps`-th value - """ - - with local_minimum(start, stop + 1, step, smallerf=smallerf, log_level=log_level) as it: - for x in it: - kwds_ = dict(kwds) - kwds_[param] = x - it.update(f(*args, **kwds_)) - - for x in it.neighborhood: - kwds_ = dict(kwds) - kwds_[param] = x - it.update(f(*args, **kwds_)) - - return it.y - - -def _batch_estimatef(f, x, log_level=0, f_repr=None): - y = f(x) - if f_repr is None: - f_repr = repr(f) - Logging.log("batch", log_level, f"f: {f_repr}") - Logging.log("batch", log_level, f"x: {x}") - Logging.log("batch", log_level, f"f(x): {repr(y)}") - return y - - -def f_name(f): - try: - return f.__name__ - except AttributeError: - return repr(f) - - -def batch_estimate(params, algorithm, jobs=1, log_level=0, **kwds): - from .lwe_parameters import LWEParameters - - if isinstance(params, LWEParameters): - params = (params,) - try: - iter(algorithm) - except TypeError: - algorithm = (algorithm,) - - tasks = [] - - for x in params: - for f in algorithm: - tasks.append((partial(f, **kwds), x, log_level, f_name(f))) - - if jobs == 1: - res = {} - for f, x, lvl, f_repr in tasks: - y = _batch_estimatef(f, x, lvl, f_repr) - res[f_repr, x] = y - else: - pool = Pool(jobs) - res = pool.starmap(_batch_estimatef, tasks) - res = dict([((f_repr, x), res[i]) for i, (f, x, _, f_repr) in enumerate(tasks)]) - - ret = dict() - for f, x in res: - ret[x] = ret.get(x, dict()) - ret[x][f] = res[f, x] - - return ret diff --git a/old_files/concrete_params.py b/old_files/concrete_params.py deleted file mode 100644 index 09159b261..000000000 --- a/old_files/concrete_params.py +++ /dev/null @@ -1,272 +0,0 @@ -concrete_LWE_params = { - - # 128-bits - - "LWE128_256": - { - "k": 1, - "n": 256, - "sd": -5}, - - "LWE128_512": - { - "k": 1, - "n": 512, - "sd": -11}, - - "LWE128_638": - { - "k": 1, - "n": 630, - "sd": -14}, - - "LWE128_650": - { - "k": 1, - "n": 650, - "sd": -15}, - - "LWE128_688": - { - "k": 1, - "n": 688, - "sd": -16}, - - "LWE128_710": - { - "k": 1, - "n": 710, - "sd": -17}, - - "LWE128_750": - { - "k": 1, - "n": 750, - "sd": -17}, - - "LWE128_800": - { - "k": 1, - "n": 800, - "sd": -19}, - - "LWE128_830": - { - "k": 1, - "n": 830, - "sd": -20}, - - "LWE128_1024": - { - "k": 1, - "n": 1024, - "sd": -25}, - - "LWE128_2048": - { - "k": 1, - "n": 2048, - "sd": -52}, - - "LWE128_4096": - { - "k": 1, - "n": 4096, - "sd": -105}, - - # 80 bits - - "LWE80_256": - { - "k": 1, - "n": 256, - "sd": -9, - }, - - "LWE80_256": - { - "k": 1, - "n": 256, - "sd": -19, - }, - - "LWE80_512": - { - "k": 1, - "n": 512, - "sd": -24, - }, - - "LWE80_650": - { - "k": 1, - "n": 650, - "sd": -25, - }, - - "LWE80_688": - { - "k": 1, - "n": 688, - "sd": -26, - }, - - "LWE80_710": - { - "k": 1, - "n": 710, - "sd": -27, - }, - - "LWE80_750": - { - "k": 1, - "n": 750, - "sd": -29, - }, - - "LWE80_800": - { - "k": 1, - "n": 800, - "sd": -31, - }, - - "LWE80_830": - { - "k": 1, - "n": 830, - "sd": -32, - }, - - "LWE80_1024": - { - "k": 1, - "n": 1024, - "sd": -40, - }, - - "LWE80_2048": - { - "k": 1, - "n": 2048, - "sd": -82, - } -} - - - -concrete_RLWE_params = { - - # 128-bits - - ## dimension 1 - - "RLWE128_256_1": - { - "k": 1, - "n": 256, - "sd": -5}, - - "RLWE128_512_1": - { - "k": 1, - "n": 512, - "sd": -11}, - - "RLWE128_1024_1": - { - "k": 1, - "n": 1024, - "sd": -25}, - - "RLWE128_2048_1": - { - "k": 1, - "n": 2048, - "sd": -52}, - - "RLWE128_4096_1": - { - "k": 1, - "n": 4096, - "sd": -105}, - - ## dimension 2 - - "RLWE128_256_2": - { - "k": 2, - "n": 256, - "sd": -11}, - - "RLWE128_512_2": - { - "k": 2, - "n": 512, - "sd": -25}, - - ## dimension 4 - - "RLWE128_256_4": - { - "k": 4, - "n": 256, - "sd": -25}, - - # 80 bits - ## dimension 1 - - "RLWE80_256_1": - { - "k": 1, - "n": 256, - "sd": -9, - }, - - "RLWE80_512_1": - { - "k": 1, - "n": 512, - "sd": -19, - }, - - "RLWE80_1024_1": - { - "k": 1, - "n": 1024, - "sd": -40, - }, - - "RLWE80_2048_1": - { - "k": 1, - "n": 2048, - "sd": -82, - }, - - # dimension 2 - - "RLWE80_256_2": - { - "k": 2, - "n": 256, - "sd": -19, - }, - - "RLWE80_512_2": - { - "k": 1, - "n": 512, - "sd": -40, - }, - - # dimension 4 - - "RLWE80_256_4": - { - "k": 4, - "n": 256, - "sd": -40, - }, -} \ No newline at end of file diff --git a/old_files/estimate_oldparams.py b/old_files/estimate_oldparams.py deleted file mode 100644 index 2054f3478..000000000 --- a/old_files/estimate_oldparams.py +++ /dev/null @@ -1,129 +0,0 @@ -import estimator.estimator as est -from concrete_params import concrete_LWE_params, concrete_RLWE_params -from hybrid_decoding import parameter_search - -def get_all_security_levels(params): - """ A function which gets the security levels of a collection of TFHE parameters, - using the four cost models: classical, quantum, classical_conservative, and - quantum_conservative - :param params: a dictionary of LWE parameter sets (see concrete_params) - - EXAMPLE: - sage: X = get_all_security_levels(concrete_LWE_params) - sage: X - [['LWE128_256', - 126.692189756144, - 117.566189756144, - 98.6960000000000, - 89.5700000000000], ...] - """ - - RESULTS = [] - - for param in params: - - results = [param] - x = params["{}".format(param)] - n = x["n"] * x["k"] - q = 2 ** 32 - sd = 2 ** (x["sd"]) * q - alpha = sqrt(2 * pi) * sd / RR(q) - secret_distribution = (0, 1) - # assume access to an infinite number of samples - m = oo - - for model in cost_models: - try: - model = model[0] - except: - model = model - estimate = parameter_search(mitm = True, reduction_cost_model = est.BKZ.sieve, n = n, q = q, alpha = alpha, m = m, secret_distribution = secret_distribution) - results.append(get_security_level(estimate)) - - RESULTS.append(results) - - return RESULTS - - -def get_hybrid_security_levels(params): - """ A function which gets the security levels of a collection of TFHE parameters, - using the four cost models: classical, quantum, classical_conservative, and - quantum_conservative - :param params: a dictionary of LWE parameter sets (see concrete_params) - - EXAMPLE: - sage: X = get_all_security_levels(concrete_LWE_params) - sage: X - [['LWE128_256', - 126.692189756144, - 117.566189756144, - 98.6960000000000, - 89.5700000000000], ...] - """ - - RESULTS = [] - - for param in params: - - results = [param] - x = params["{}".format(param)] - n = x["n"] * x["k"] - q = 2 ** 32 - sd = 2 ** (x["sd"]) * q - alpha = sqrt(2 * pi) * sd / RR(q) - secret_distribution = (0, 1) - # assume access to an infinite number of papers - m = oo - - model = est.BKZ.sieve - estimate = parameter_search(mitm = True, reduction_cost_model = est.BKZ.sieve, n = n, q = q, alpha = alpha, m = m, secret_distribution = secret_distribution) - results.append(get_security_level(estimate)) - - RESULTS.append(results) - - return RESULTS - - -def latexit(results): - """ - A function which takes the output of get_all_security_levels() and - turns it into a latex table - :param results: the security levels - - sage: X = get_all_security_levels(concrete_LWE_params) - sage: latextit(X) - \begin{tabular}{llllll} - LWE128_256 & $126.69$ & $117.57$ & $98.7$ & $89.57$ & $217.55$ \\ - LWE128_512 & $135.77$ & $125.92$ & $106.58$ & $96.73$ & $218.53$ \\ - LWE128_638 & $135.27$ & $125.49$ & $105.7$ & $95.93$ & $216.81$ \\ - [...] - """ - - return latex(table(results)) - - -def markdownit(results, headings = ["Parameter Set", "Classical", "Quantum", "Classical (c)", "Quantum (c)", "Enum"]): - """ - A function which takes the output of get_all_security_levels() and - turns it into a markdown table - :param results: the security levels - - sage: X = get_all_security_levels(concrete_LWE_params) - sage: markdownit(X) - # estimates - |Parameter Set|Classical|Quantum|Classical (c)|Quantum (c)| Enum | - |-------------|---------|-------|-------------|-----------|------| - |LWE128_256 |126.69 |117.57 |98.7 |89.57 |217.55| - |LWE128_512 |135.77 |125.92 |106.58 |96.73 |218.53| - |LWE128_638 |135.27 |125.49 |105.7 |95.93 |216.81| - [...] - """ - - writer = MarkdownTableWriter(value_matrix = results, headers = headings, table_name = "estimates") - writer.write_table() - - return writer - -# dual bug example -# n = 256; q = 2**32; sd = 2**(-4); reduction_cost_model = BKZ.sieve -# _ = estimate_lwe(n, alpha, q, reduction_cost_model) \ No newline at end of file diff --git a/old_files/figs/iso.png b/old_files/figs/iso.png deleted file mode 100644 index e05b76a01ee3a3ad6de7b0705f2e33c701ecb9e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75623 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 LEy0j2<={E5Y!-G?(Px7C!T;g`o4)|4PTH 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 diff --git a/old_files/figs/plot.png b/old_files/figs/plot.png deleted file mode 100644 index e9305519f21dd1e8131454aeec3ec9908e950006..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34798 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 diff --git a/old_files/figs/sieve.png b/old_files/figs/sieve.png deleted file mode 100644 index e5a37b25adf090cec205c8910c633fade28c38b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26398 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 ADF6Tf diff --git a/old_files/hybrid_decoding.py b/old_files/hybrid_decoding.py deleted file mode 100644 index d70a3b584..000000000 --- a/old_files/hybrid_decoding.py +++ /dev/null @@ -1,329 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Seucrity estimates for the Hybrid Decoding attack - -Requires a local copy of the LWE Estimator from https://bitbucket.org/malb/lwe-estimator/src/master/estimator.py in a folder called "estimator" -""" - -from sage.all import ZZ, binomial, sqrt, log, exp, oo, pi, prod, RR -from sage.probability.probability_distribution import RealDistribution -from estimator import estimator as est -from concrete_params import concrete_LWE_params - -## Core cost models - -core_sieve = lambda beta, d, B: ZZ(2)**RR(0.292*beta + 16.4) -core_qsieve = lambda beta, d, B: ZZ(2)**RR(0.265*beta + 16.4) - -## Utility functions - -def sq_GSO(d, beta, det): - """ - Return squared GSO lengths after lattice reduction according to the GSA - - :param q: LWE modulus - :param d: lattice dimension - :param beta: blocksize used in BKZ - :param det: lattice determinant - - """ - - r = [] - for i in range(d): - r_i = est.delta_0f(beta)**(((-2*d*i) / (d-1))+d) * det**(1/d) - r.append(r_i**2) - - return r - - -def babai_probability_wun16(r, norm): - """ - Compute the probability of Babai's Nearest Plane, using techniques from the NTRULPrime submission to NIST - - :param r: squared GSO lengths - :param norm: expected norm of the target vector - - """ - R = [RR(sqrt(t)/(2*norm)) for t in r] - T = RealDistribution('beta', ((len(r)-1)/2,1./2)) - probs = [1 - T.cum_distribution_function(1 - s**2) for s in R] - return prod(probs) - - -## Estimate hybrid decoding attack complexity - -def hybrid_decoding_attack(n, alpha, q, m, secret_distribution, - beta, tau = None, mitm=True, reduction_cost_model=est.BKZ.sieve): - """ - Estimate cost of the Hybrid Attack, - - :param n: LWE dimension `n > 0` - :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/sqrt{2π}` - :param q: modulus `0 < q` - :param m: number of LWE samples `m > 0` - :param secret_distribution: distribution of secret - :param beta: BKZ block size β - :param tau: guessing dimension τ - :param mitm: simulate MITM approach (√ of search space) - :param reduction_cost_model: BKZ reduction cost model - - EXAMPLE: - - hybrid_decoding_attack(beta = 100, tau = 250, mitm = True, reduction_cost_model = est.BKZ.sieve, **example_64()) - - rop: 2^65.1 - pre: 2^64.8 - enum: 2^62.5 - beta: 100 - |S|: 2^73.1 - prob: 0.104533 - scale: 12.760 - pp: 11 - d: 1798 - repeat: 42 - - """ - - n, alpha, q = est.Param.preprocess(n, alpha, q) - - # d is the dimension of the attack lattice - d = m + n - tau - - # h is the Hamming weight of the secret - # NOTE: binary secrets are assumed to have Hamming weight ~n/2, ternary secrets ~2n/3 - # this aligns with the assumptions made in the LWE Estimator - h = est.SDis.nonzero(secret_distribution, n=n) - sd = alpha*q/sqrt(2*pi) - - # compute the scaling factor used in the primal lattice to balance the secret and error - scale = est._primal_scale_factor(secret_distribution, alpha=alpha, q=q, n=n) - - # 1. get squared-GSO lengths via the Geometric Series Assumption - # we could also consider using the BKZ simulator, using the GSA is conservative - r = sq_GSO(d, beta, q**m * scale**(n-tau)) - - # 2. Costs - bkz_cost = est.lattice_reduction_cost(reduction_cost_model, est.delta_0f(beta), d) - enm_cost = est.Cost() - enm_cost["rop"] = d**2/(2**1.06) - - # 3. Size of search space - # We need to do one BDD call at least - search_space, prob, hw = ZZ(1), 1.0, 0 - - # if mitm is True, sqrt speedup in the guessing phase. This allows us to square the size - # of the search space at no extra cost. - # NOTE: we conservatively assume that this mitm process succeeds with probability 1. - ssf = sqrt if mitm else lambda x: x - - # use the secret distribution bounds to determine the size of the search space - a, b = est.SDis.bounds(secret_distribution) - - # perform "searching". This part of the code balances the enm_cost with the cost of lattice - # reduction, where enm_cost is the total cost of calling Babai's algorithm on each vector in - # the search space. - - if tau: - prob = est.success_probability_drop(n, h, tau) - hw = 1 - while hw < h and hw < tau: - prob += est.success_probability_drop(n, h, tau, fail=hw) - search_space += binomial(tau, hw) * (b-a)**hw - - if enm_cost.repeat(ssf(search_space))["rop"] > bkz_cost["rop"]: - # we moved too far, so undo - prob -= est.success_probability_drop(n, h, tau, fail=hw) - search_space -= binomial(tau, hw) * (b-a)**hw - hw -= 1 - break - hw += 1 - - enm_cost = enm_cost.repeat(ssf(search_space)) - - # we use the expectation of the target norm. This could be longer, or shorter, for any given instance. - target_norm = sqrt(m * sd**2 + h * RR((n-tau)/n) * scale**2) - - # account for the success probability of Babai's algorithm - prob*=babai_probability_wun16(r, target_norm) - - # create a cost string, as in the LWE Estimator, to store the attack parameters and costs - ret = est.Cost() - ret["rop"] = bkz_cost["rop"] + enm_cost["rop"] - ret["pre"] = bkz_cost["rop"] - ret["enum"] = enm_cost["rop"] - ret["beta"] = beta - ret["|S|"] = search_space - ret["prob"] = prob - ret["scale"] = scale - ret["pp"] = hw - ret["d"] = d - ret["tau"] = tau - - # 5. Repeat whole experiment ~1/prob times - ret = ret.repeat(est.amplify(0.99, prob), select={"rop": True, - "pre": True, - "enum": True, - "beta": False, - "d": False, - "|S|": False, - "scale": False, - "prob": False, - "pp": False, - "tau": False}) - - return ret - - -## Optimize attack parameters - -def parameter_search(n, alpha, q, m, secret_distribution, mitm = True, reduction_cost_model=est.BKZ.sieve): - - """ - :param n: LWE dimension `n > 0` - :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/sqrt{2π}` - :param q: modulus `0 < q` - :param m: number of LWE samples `m > 0` - :param secret_distribution: distribution of secret - :param beta_search: tuple (β_min, β_max, granularity) for the search space of β, default is (60,301,20) - :param tau: tuple (τ_min, τ_max, granularity) for the search space of τ, default is (0,501,20) - :param mitm: simulate MITM approach (√ of search space) - :param reduction_cost_model: BKZ reduction cost model - - EXAMPLE: - - parameter_search(mitm = False, reduction_cost_model = est.BKZ.sieve, **example_64()) - - rop: 2^69.5 - pre: 2^68.9 - enum: 2^68.0 - beta: 110 - |S|: 2^40.9 - prob: 0.045060 - scale: 12.760 - pp: 6 - d: 1730 - repeat: 100 - tau: 170 - - parameter_search(mitm = True, reduction_cost_model = est.BKZ.sieve, **example_64()) - - rop: 2^63.4 - pre: 2^63.0 - enum: 2^61.5 - beta: 95 - |S|: 2^72.0 - prob: 0.125126 - scale: 12.760 - pp: 11 - d: 1666 - repeat: 35 - tau: 234 - - """ - - primald = est.partial(est.drop_and_solve, est.dual_scale, postprocess=True, decision=True) - bl = primald(n, alpha, q, secret_distribution=secret_distribution, m=m, reduction_cost_model=reduction_cost_model) - - # we take the number of LWE samples used to be the same as in the primal attack in the LWE Estimator - m = bl["m"] - - f = est.partial(hybrid_decoding_attack, n=n, alpha=alpha, q=q, m=m, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, - mitm=mitm) - - # NOTE: we decribe our searching strategy below. To produce more accurate estimates, - # change this part of the code to ensure a more granular search. As we are using - # homomorphic-encryption style parameters, the running time of the code can be quite high, - # justifying the below choices. - # We start at beta = 60 and go up to beta_max in steps of 50 - - beta_max = bl["beta"] + 100 - beta_search = (40, beta_max, 50) - - best = None - for beta in range(beta_search[0], beta_search[1], beta_search[2])[::-1]: - tau = 0 - best_beta = None - count = 3 - while tau < n: - if count >= 0: - cost = f(beta=beta, tau=tau) - if best_beta is not None: - # if two consecutive estimates don't decrease, stop optimising over tau - if best_beta["rop"] < cost["rop"]: - count -= 1 - cost["tau"] = tau - if best_beta is None: - best_beta = cost - if RR(log(cost["rop"],2)) < RR(log(best_beta["rop"],2)): - best_beta = cost - if best is None: - best = cost - if RR(log(cost["rop"],2)) < RR(log(best["rop"],2)): - best = cost - tau += n//100 - - # now do a second, more granular search - # we start at the beta which produced the lowest running time, and search ± 25 in steps of 10 - tau_gap = max(n//100, 1) - for beta in range(best["beta"] - 25, best["beta"] + 25, 10)[::-1]: - tau = max(best["tau"] - 25,0) - best_beta = None - count = 3 - while tau <= best["tau"] + 25: - if count >= 0: - cost = f(beta=beta, tau=tau) - if best_beta is not None: - # if two consecutive estimates don't decrease, stop optimising over tau - if best_beta["rop"] < cost["rop"]: - count -= 1 - cost["tau"] = tau - if best_beta is None: - best_beta = cost - if RR(log(cost["rop"],2)) < RR(log(best_beta["rop"],2)): - best_beta = cost - if best is None: - best = cost - if RR(log(cost["rop"],2)) < RR(log(best["rop"],2)): - best = cost - tau += tau_gap - - return best - -def get_all_security_levels(params): - """ A function which gets the security levels of a collection of TFHE parameters, - using the four cost models: classical, quantum, classical_conservative, and - quantum_conservative - :param params: a dictionary of LWE parameter sets (see concrete_params) - - EXAMPLE: - sage: X = get_all_security_levels(concrete_LWE_params) - sage: X - [['LWE128_256', - 126.692189756144, - 117.566189756144, - 98.6960000000000, - 89.5700000000000], ...] - """ - - RESULTS = [] - - for param in params: - - results = [param] - x = params["{}".format(param)] - n = x["n"] * x["k"] - q = 2 ** 32 - sd = 2 ** (x["sd"]) * q - alpha = sqrt(2 * pi) * sd / RR(q) - secret_distribution = (0, 1) - # assume access to an infinite number of papers - m = oo - - model = est.BKZ.sieve - estimate = parameter_search(mitm = True, reduction_cost_model = est.BKZ.sieve, n = n, q = q, alpha = alpha, m = m, secret_distribution = secret_distribution) - results.append(get_security_level(estimate)) - - RESULTS.append(results) - - return RESULTS \ No newline at end of file diff --git a/old_files/memory_tests/test.py b/old_files/memory_tests/test.py deleted file mode 100644 index 22a58d63e..000000000 --- a/old_files/memory_tests/test.py +++ /dev/null @@ -1,17 +0,0 @@ -from estimator_new import * -from sage.all import oo, save - -def test(): - - # code - D = ND.DiscreteGaussian - params = LWE.Parameters(n=1024, q=2 ** 64, Xs=D(0.50, -0.50), Xe=D(2**57), m=oo, tag='TFHE_DEFAULT') - - names = [params, params.updated(n=761), params.updated(q=2 ** 65), params.updated(n=762)] - - for name in names: - x = LWE.estimate(name, deny_list=("arora-gb", "bkw")) - - return 0 - -test() \ No newline at end of file diff --git a/old_files/memory_tests/test2.py b/old_files/memory_tests/test2.py deleted file mode 100644 index 4ad476806..000000000 --- a/old_files/memory_tests/test2.py +++ /dev/null @@ -1,31 +0,0 @@ - -from multiprocessing import * -from estimator_new import * -from sage.all import oo, save - - -def test_memory(x): - print("doing job...") - print(x) - y = LWE.estimate(x, deny_list=("arora-gb", "bkw")) - return y - -if __name__ == "__main__": - D = ND.DiscreteGaussian - params = LWE.Parameters(n=1024, q=2 ** 64, Xs=D(0.50, -0.50), Xe=D(2**57), m=oo, tag='TFHE_DEFAULT') - - names = [params, params.updated(n=761), params.updated(q=2**65), params.updated(n=762)] - procs = [] - proc = Process(target=print_func) - procs.append(proc) - proc.start() - p = Pool(1) - - for name in names: - proc = Process(target=test_memory, args=(name,)) - procs.append(proc) - proc.start() - proc.join() - - for proc in procs: - proc.join() \ No newline at end of file diff --git a/old_files/new_scripts.py b/old_files/new_scripts.py deleted file mode 100644 index db1543550..000000000 --- a/old_files/new_scripts.py +++ /dev/null @@ -1,471 +0,0 @@ -from estimator_new import * -from sage.all import oo, save, load, ceil -from math import log2 -import multiprocessing - - -def old_models(security_level, sd, logq=32): - """ - Use the old model as a starting point for the data gathering step - :param security_level: the security level under consideration - :param sd : the standard deviation of the LWE error distribution Xe - :param logq : the (base 2 log) value of the LWE modulus q - """ - - def evaluate_model(a, b, stddev=sd): - return (stddev - b)/a - - models = dict() - - models["80"] = (-0.04049295502947623, 1.1288318226557081 + logq) - models["96"] = (-0.03416314056943681, 1.4704806061716345 + logq) - models["112"] = (-0.02970984362676178, 1.7848907787798667 + logq) - models["128"] = (-0.026361288425133814, 2.0014671315214696 + logq) - models["144"] = (-0.023744534465622812, 2.1710601038230712 + logq) - models["160"] = (-0.021667220727651954, 2.3565507936475476 + logq) - models["176"] = (-0.019947662046189942, 2.5109588704235803 + logq) - models["192"] = (-0.018552804646747204, 2.7168913723130816 + logq) - models["208"] = (-0.017291091126923574, 2.7956961446214326 + logq) - models["224"] = (-0.016257546811508806, 2.9582401000615226 + logq) - models["240"] = (-0.015329741032015766, 3.0744579055889782 + logq) - models["256"] = (-0.014530554319171845, 3.2094375376751745 + logq) - - (a, b) = models["{}".format(security_level)] - n_est = evaluate_model(a, b, sd) - - return round(n_est) - - -def estimate(params, red_cost_model=RC.BDGL16, skip=("arora-gb", "bkw")): - """ - Retrieve an estimate using the Lattice Estimator, for a given set of input parameters - :param params: the input LWE parameters - :param red_cost_model: the lattice reduction cost model - :param skip: attacks to skip - """ - - est = LWE.estimate(params, red_cost_model=red_cost_model, deny_list=skip) - - return est - - -def get_security_level(est, dp=2): - """ - Get the security level lambda from a Lattice Estimator output - :param est: the Lattice Estimator output - :param dp: the number of decimal places to consider - """ - attack_costs = [] - # note: key does not need to be specified est vs est.keys() - for key in est: - attack_costs.append(est[key]["rop"]) - # get the security level correct to 'dp' decimal places - security_level = round(log2(min(attack_costs)), dp) - - return security_level - - -def inequality(x, y): - """ A utility function which compresses the conditions x < y and x > y into a single condition via a multiplier - :param x: the LHS of the inequality - :param y: the RHS of the inequality - """ - if x <= y: - return 1 - - if x > y: - return -1 - - -def automated_param_select_n(params, target_security=128): - """ A function used to generate the smallest value of n which allows for - target_security bits of security, for the input values of (params.Xe.stddev,params.q) - :param params: the standard deviation of the error - :param target_security: the target number of bits of security, 128 is default - - EXAMPLE: - sage: X = automated_param_select_n(Kyber512, target_security = 128) - sage: X - 456 - """ - - # get an estimate based on the prev. model - print("n = {}".format(params.n)) - n_start = old_models(target_security, log2(params.Xe.stddev), log2(params.q)) - # n_start = max(n_start, 450) - # TODO: think about throwing an error if the required n < 450 - - params = params.updated(n=n_start) - costs2 = estimate(params) - security_level = get_security_level(costs2, 2) - z = inequality(security_level, target_security) - - # we keep n > 2 * target_security as a rough baseline for mitm security (on binary key guessing) - while z * security_level < z * target_security: - # TODO: fill in this case! For n > 1024 we only need to consider every 256 (optimization) - params = params.updated(n = params.n + z * 8) - costs = estimate(params) - security_level = get_security_level(costs, 2) - - if -1 * params.Xe.stddev > 0: - print("target security level is unattainable") - break - - # final estimate (we went too far in the above loop) - if security_level < target_security: - # we make n larger - print("we make n larger") - params = params.updated(n=params.n + 8) - costs = estimate(params) - security_level = get_security_level(costs, 2) - - print("the finalised parameters are n = {}, log2(sd) = {}, log2(q) = {}, with a security level of {}-bits".format(params.n, - log2(params.Xe.stddev), - log2(params.q), - security_level)) - - if security_level < target_security: - params.updated(n=None) - - return params, security_level - - -def generate_parameter_matrix(params_in, sd_range, target_security_levels=[128], name="default_name"): - """ - :param params_in: a initial set of LWE parameters - :param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters - :param target_security_levels: a list of the target number of bits of security, 128 is default - :param name: a name to save the file - """ - - (sd_min, sd_max) = sd_range - for lam in target_security_levels: - for sd in range(sd_min, sd_max + 1): - print("run for {}".format(lam, sd)) - Xe_new = nd.NoiseDistribution.DiscreteGaussian(2**sd) - (params_out, sec) = automated_param_select_n(params_in.updated(Xe=Xe_new), target_security=lam) - - try: - results = load("{}.sobj".format(name)) - except: - results = dict() - results["{}".format(lam)] = [] - - results["{}".format(lam)].append((params_out.n, log2(params_out.q), log2(params_out.Xe.stddev), sec)) - save(results, "{}.sobj".format(name)) - - return results - - -def generate_zama_curves64(sd_range=[2, 58], target_security_levels=[128], name="default_name"): - """ - The top level function which we use to run the experiment - - :param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters - :param target_security_levels: a list of the target number of bits of security, 128 is default - :param name: a name to save the file - """ - if __name__ == '__main__': - - D = ND.DiscreteGaussian - vals = range(sd_range[0], sd_range[1]) - pool = multiprocessing.Pool(2) - init_params = LWE.Parameters(n=1024, q=2 ** 64, Xs=D(0.50, -0.50), Xe=D(2 ** 55), m=oo, tag='params') - inputs = [(init_params, (val, val), target_security_levels, name) for val in vals] - res = pool.starmap(generate_parameter_matrix, inputs) - - return "done" - - -# The script runs the following commands -import sys -# grab values of the command-line input arguments -a = int(sys.argv[1]) -b = int(sys.argv[2]) -c = int(sys.argv[3]) -# run the code -generate_zama_curves64(sd_range= (b,c), target_security_levels=[a], name="{}".format(a)) - -from estimator_new import * -from sage.all import oo, save, load -from math import log2 -import multiprocessing - - -def old_models(security_level, sd, logq=32): - """ - Use the old model as a starting point for the data gathering step - :param security_level: the security level under consideration - :param sd : the standard deviation of the LWE error distribution Xe - :param logq : the (base 2 log) value of the LWE modulus q - """ - - def evaluate_model(a, b, stddev=sd): - return (stddev - b)/a - - models = dict() - - models["80"] = (-0.04049295502947623, 1.1288318226557081 + logq) - models["96"] = (-0.03416314056943681, 1.4704806061716345 + logq) - models["112"] = (-0.02970984362676178, 1.7848907787798667 + logq) - models["128"] = (-0.026361288425133814, 2.0014671315214696 + logq) - models["144"] = (-0.023744534465622812, 2.1710601038230712 + logq) - models["160"] = (-0.021667220727651954, 2.3565507936475476 + logq) - models["176"] = (-0.019947662046189942, 2.5109588704235803 + logq) - models["192"] = (-0.018552804646747204, 2.7168913723130816 + logq) - models["208"] = (-0.017291091126923574, 2.7956961446214326 + logq) - models["224"] = (-0.016257546811508806, 2.9582401000615226 + logq) - models["240"] = (-0.015329741032015766, 3.0744579055889782 + logq) - models["256"] = (-0.014530554319171845, 3.2094375376751745 + logq) - - (a, b) = models["{}".format(security_level)] - n_est = evaluate_model(a, b, sd) - - return round(n_est) - - -def estimate(params, red_cost_model=RC.BDGL16, skip=("arora-gb", "bkw")): - """ - Retrieve an estimate using the Lattice Estimator, for a given set of input parameters - :param params: the input LWE parameters - :param red_cost_model: the lattice reduction cost model - :param skip: attacks to skip - """ - - est = LWE.estimate(params, red_cost_model=red_cost_model, deny_list=skip) - - return est - - -def get_security_level(est, dp=2): - """ - Get the security level lambda from a Lattice Estimator output - :param est: the Lattice Estimator output - :param dp: the number of decimal places to consider - """ - attack_costs = [] - # note: key does not need to be specified est vs est.keys() - for key in est: - attack_costs.append(est[key]["rop"]) - # get the security level correct to 'dp' decimal places - security_level = round(log2(min(attack_costs)), dp) - - return security_level - - -def inequality(x, y): - """ A utility function which compresses the conditions x < y and x > y into a single condition via a multiplier - :param x: the LHS of the inequality - :param y: the RHS of the inequality - """ - if x <= y: - return 1 - - if x > y: - return -1 - - -def automated_param_select_n(params, target_security=128): - """ A function used to generate the smallest value of n which allows for - target_security bits of security, for the input values of (params.Xe.stddev,params.q) - :param params: the standard deviation of the error - :param target_security: the target number of bits of security, 128 is default - - EXAMPLE: - sage: X = automated_param_select_n(Kyber512, target_security = 128) - sage: X - 456 - """ - - # get an estimate based on the prev. model - print("n = {}".format(params.n)) - n_start = old_models(target_security, log2(params.Xe.stddev), log2(params.q)) - # n_start = max(n_start, 450) - # TODO: think about throwing an error if the required n < 450 - - params = params.updated(n=n_start) - costs2 = estimate(params) - security_level = get_security_level(costs2, 2) - z = inequality(security_level, target_security) - - # we keep n > 2 * target_security as a rough baseline for mitm security (on binary key guessing) - while z * security_level < z * target_security: - # TODO: fill in this case! For n > 1024 we only need to consider every 256 (optimization) - params = params.updated(n = params.n + z * 8) - costs = estimate(params) - security_level = get_security_level(costs, 2) - - if -1 * params.Xe.stddev > 0: - print("target security level is unattainable") - break - - # final estimate (we went too far in the above loop) - if security_level < target_security: - # we make n larger - print("we make n larger") - params = params.updated(n=params.n + 8) - costs = estimate(params) - security_level = get_security_level(costs, 2) - - print("the finalised parameters are n = {}, log2(sd) = {}, log2(q) = {}, with a security level of {}-bits".format(params.n, - log2(params.Xe.stddev), - log2(params.q), - security_level)) - - if security_level < target_security: - params.updated(n=None) - - return params, security_level - - -def generate_parameter_matrix(params_in, sd_range, target_security_levels=[128], name="default_name"): - """ - :param params_in: a initial set of LWE parameters - :param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters - :param target_security_levels: a list of the target number of bits of security, 128 is default - :param name: a name to save the file - """ - - (sd_min, sd_max) = sd_range - for lam in target_security_levels: - for sd in range(sd_min, sd_max + 1): - print("run for {}".format(lam, sd)) - Xe_new = nd.NoiseDistribution.DiscreteGaussian(2**sd) - (params_out, sec) = automated_param_select_n(params_in.updated(Xe=Xe_new), target_security=lam) - - try: - results = load("{}.sobj".format(name)) - except: - results = dict() - results["{}".format(lam)] = [] - - results["{}".format(lam)].append((params_out.n, log2(params_out.q), log2(params_out.Xe.stddev), sec)) - save(results, "{}.sobj".format(name)) - - return results - - -def generate_zama_curves64(sd_range=[2, 58], target_security_levels=[128], name="default_name"): - """ - The top level function which we use to run the experiment - - :param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters - :param target_security_levels: a list of the target number of bits of security, 128 is default - :param name: a name to save the file - """ - if __name__ == '__main__': - - D = ND.DiscreteGaussian - vals = range(sd_range[0], sd_range[1]) - pool = multiprocessing.Pool(2) - init_params = LWE.Parameters(n=1024, q=2 ** 64, Xs=D(0.50, -0.50), Xe=D(2 ** 55), m=oo, tag='params') - inputs = [(init_params, (val, val), target_security_levels, name) for val in vals] - res = pool.starmap(generate_parameter_matrix, inputs) - - return "done" - - -# The script runs the following commands -import sys -# grab values of the command-line input arguments -a = int(sys.argv[1]) -b = int(sys.argv[2]) -c = int(sys.argv[3]) -# run the code -generate_zama_curves64(sd_range= (b,c), target_security_levels=[a], name="{}".format(a)) - -import numpy as np -from sage.all import save, load - -def sort_data(security_level): - from operator import itemgetter - - # step 1. load the data - X = load("{}.sobj".format(security_level)) - - # step 2. sort by SD - x = sorted(X["{}".format(security_level)], key = itemgetter(2)) - - # step3. replace the sorted value - X["{}".format(security_level)] = x - - return X - -def generate_curve(security_level): - - # step 1. get the data - X = sort_data(security_level) - - # step 2. group the n and sigma data into lists - N = [] - SD = [] - for x in X["{}".format(security_level)]: - N.append(x[0]) - SD.append(x[2] + 0.5) - - # step 3. perform interpolation and return coefficients - (a,b) = np.polyfit(N, SD, 1) - - return a, b - - -def verify_curve(security_level, a = None, b = None): - - # step 1. get the table and max values of n, sd - X = sort_data(security_level) - n_max = X["{}".format(security_level)][0][0] - sd_max = X["{}".format(security_level)][-1][2] - - # step 2. a function to get model values - def f_model(a, b, n): - return ceil(a * n + b) - - # step 3. a function to get table values - def f_table(table, n): - for i in range(len(table)): - n_val = table[i][0] - if n < n_val: - pass - else: - j = i - break - - # now j is the correct index, we return the corresponding sd - return table[j][2] - - # step 3. for each n, check whether we satisfy the table - n_min = max(2 * security_level, 450, X["{}".format(security_level)][-1][0]) - print(n_min) - print(n_max) - - for n in range(n_max, n_min, - 1): - model_sd = f_model(a, b, n) - table_sd = f_table(X["{}".format(security_level)], n) - print(n , table_sd, model_sd, model_sd >= table_sd) - - if table_sd > model_sd: - print("MODEL FAILS at n = {}".format(n)) - return "FAIL" - - return "PASS", n_min - - -def generate_and_verify(security_levels, log_q, name = "verified_curves"): - - data = [] - - for sec in security_levels: - print("WE GO FOR {}".format(sec)) - # generate the model for security level sec - (a_sec, b_sec) = generate_curve(sec) - # verify the model for security level sec - res = verify_curve(sec, a_sec, b_sec) - # append the information into a list - data.append((a_sec, b_sec - log_q, sec, res[0], res[1])) - save(data, "{}.sobj".format(name)) - - return data - -# To verify the curves we use -generate_and_verify([80, 96, 112, 128, 144, 160, 176, 192, 256], log_q = 64) - diff --git a/old_files/scripts.py b/old_files/scripts.py deleted file mode 100644 index 5ceba6743..000000000 --- a/old_files/scripts.py +++ /dev/null @@ -1,599 +0,0 @@ -import estimator.estimator as est -import matplotlib.pyplot as plt -import numpy as np - -# define the four cost models used for Concrete (2 classical, 2 quantum) -# note that classical and quantum are the two models used in the "HE Std" - -def classical(beta, d, B): - return ZZ(2) ** RR(0.292 * beta + 16.4 + log(8 * d, 2)) - - -def quantum(beta, d, B): - return ZZ(2) ** RR(0.265 * beta + 16.4 + log(8 * d, 2)) - - -def classical_conservative(beta, d, B): - return ZZ(2) ** RR(0.292 * beta) - - -def quantum_conservative(beta, d, B): - return ZZ(2) ** RR(0.265 * beta) - -# we add an enumeration model for completeness -cost_models = [classical, quantum, classical_conservative, quantum_conservative, est.BKZ.enum] - - -def estimate_lwe_nocrash(n, alpha, q, secret_distribution, - reduction_cost_model=est.BKZ.sieve, m=oo): - """ - A function to estimate the complexity of LWE, whilst skipping over any attacks which crash - :param n : the LWE dimension - :param alpha : the noise rate of the error - :param q : the LWE ciphertext modulus - :param secret_distribution : the LWE secret distribution - :param reduction_cost_model: the BKZ reduction cost model - :param m : the number of available LWE samples - - EXAMPLE: - sage: estimate_lwe_nocrash(n = 256, q = 2**32, alpha = RR(8/2**32), secret_distribution = (0,1)) - sage: 39.46 - """ - - # the success value denotes whether we need to re-run the estimator, in the case of a crash - success = 0 - - try: - # we begin by trying all four attacks (usvp, dual, dec, mitm) - estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo, - skip={"bkw", "dec", "arora-gb"}) - success = 1 - - except Exception as e: - print(e) - - if success == 0: - try: - # dual crashes most often, so try skipping dual first - estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo, - skip={"bkw", "dec", "arora-gb", "dual"}) - success = 1 - - except Exception as e: - print(e) - - if success == 0: - try: - # next, skip mitm - estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo, - skip={"mitm", "bkw", "dec", "arora-gb", "dual"}) - - except Exception as e: - print(e) - - # the output security level is just the cost of the fastest attack - security_level = get_security_level(estimate) - - return security_level - - - -def get_security_level(estimate, decimal_places = 2): - """ Function to get the security level from an LWE Estimator output, - i.e. returns only the bit-security level (without the attack params) - :param estimate: the input estimate - :param decimal_places: the number of decimal places - - EXAMPLE: - sage: x = estimate_lwe(n = 256, q = 2**32, alpha = RR(8/2**32)) - sage: get_security_level(x) - 33.8016789754458 - """ - - levels = [] - - # use try/except to cover cases where we only consider one or two attacks - - try: - levels.append(estimate["usvp"]["rop"]) - - except: - pass - - try: - levels.append(estimate["dec"]["rop"]) - except: - pass - - try: - levels.append(estimate["dual"]["rop"]) - - except: - pass - - try: - levels.append(estimate["mitm"]["rop"]) - - except: - pass - - # take the minimum attack cost (in bits) - security_level = round(log(min(levels), 2), decimal_places) - - return security_level - - -def inequality(x, y): - """ A function which compresses the conditions - x < y and x > y into a single condition via a - multiplier - """ - if x <= y: - return 1 - - if x > y: - return -1 - - -def automated_param_select_n(sd, n=None, q=2 ** 32, reduction_cost_model=est.BKZ.sieve, secret_distribution=(0, 1), - target_security=128): - """ A function used to generate the smallest value of n which allows for - target_security bits of security, for the input values of (sd,q) - :param sd: the standard deviation of the error - :param n: an initial value of n to use in optimisation, guessed if None - :param q: the LWE modulus (q = 2**32, 2**64 in TFHE) - :param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default - :param secret_distribution: the LWE secret distribution - :param target_security: the target number of bits of security, 128 is default - - EXAMPLE: - sage: X = automated_param_select_n(sd = -25, q = 2**32) - sage: X - 1054 - """ - - if n is None: - # pick some random n which gets us close (based on concrete_LWE_params) - n = sd * (-25) * (target_security/80) - - sd = 2 ** sd * q - alpha = sqrt(2 * pi) * sd / RR(q) - - - # initial estimate, to determine if we are above or below the target security level - - security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo) - - z = inequality(security_level, target_security) - - while z * security_level < z * target_security and n > 80: - n += z * 8 - alpha = sqrt(2 * pi) * sd / RR(q) - security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo) - - if (-1 * sd > 0): - print("target security level is unatainable") - break - - # final estimate (we went too far in the above loop) - if security_level < target_security: - n -= z * 8 - alpha = sqrt(2 * pi) * sd / RR(q) - security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo) - - print("the finalised parameters are n = {}, log2(sd) = {}, log2(q) = {}, with a security level of {}-bits".format(n, - sd, - log(q, - 2), - security_level)) - - # final sanity check so we don't return insecure (or inf) parameters - if security_level < target_security or security_level == oo: - n = None - - return n - - -def automated_param_select_sd(n, sd=None, q=2**32, reduction_cost_model=est.BKZ.sieve, secret_distribution=(0, 1), - target_security=128): - """ A function used to generate the smallest value of sd which allows for - target_security bits of security, for the input values of (n,q) - :param n: the LWE dimension - :param sd: an initial value of sd to use in optimisation, guessed if None - :param q: the LWE modulus (q = 2**32, 2**64 in TFHE) - :param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default - :param secret_distribution: the LWE secret distribution - :param target_security: the target number of bits of security, 128 is default - - EXAMPLE - sage: X = automated_param_select_sd(n = 1054, q = 2**32) - sage: X - -26 - """ - - if sd is None: - # pick some random sd which gets us close (based on concrete_LWE_params) - sd = round(n * 80 / (target_security * (-25))) - - # make sure sd satisfies q * sd > 1 - sd = max(sd, -(log(q,2) - 2)) - - sd_ = (2 ** sd) * q - alpha = sqrt(2 * pi) * sd_ / RR(q) - - # initial estimate, to determine if we are above or below the target security level - security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo) - z = inequality(security_level, target_security) - - while z * security_level < z * target_security and sd > -log(q,2): - - sd += z * (0.5) - sd_ = (2 ** sd) * q - alpha = sqrt(2 * pi) * sd_ / RR(q) - security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo) - - ## THIS IS WHERE THE PROBLEM IS, CORRECT THIS CONDITION? - if (sd > log(q, 2)): - print("target security level is unatainable") - return None - - # final estimate (we went too far in the above loop) - if security_level < target_security: - sd -= z * (0.5) - sd_ = (2 ** sd) * q - alpha = sqrt(2 * pi) * sd_ / RR(q) - security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo) - - print("the finalised parameters are n = {}, log2(sd) = {}, log2(q) = {}, with a security level of {}-bits".format(n, - sd, - log(q, - 2), - security_level)) - - return sd - - -def generate_parameter_matrix(n_range, sd=None, q=2**32, reduction_cost_model=est.BKZ.sieve, - secret_distribution=(0, 1), target_security=128): - """ - :param n_range: a tuple (n_min, n_max) giving the values of n for which to generate parameters - :param sd: the standard deviation of the LWE error - :param q: the LWE modulus (q = 2**32, 2**64 in TFHE) - :param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default - :param secret_distribution: the LWE secret distribution - :param target_security: the target number of bits of security, 128 is default - - TODO: we should probably parallelise this function for speed - - EXAMPLE: - sage: X = generate_parameter_matrix([788, 790]) - sage: X - [(788, 4294967296, -20.0), (789, 4294967296, -20.0)] - """ - - RESULTS = [] - - # grab min and max value/s of n, with a granularity (if given as input) - try: - (n_min, n_max, gran) = n_range - except: - (n_min, n_max) = n_range - gran = 1 - - sd_ = sd - - for n in range(n_min, n_max + 1, gran): - sd = automated_param_select_sd(n, sd=sd_, q=q, reduction_cost_model=reduction_cost_model, - secret_distribution=secret_distribution, target_security=target_security) - sd_ = sd - RESULTS.append((n, q, sd)) - - return RESULTS - - -def generate_parameter_matrix_sd(sd_range, n=None, q=2**32, reduction_cost_model=est.BKZ.sieve, - secret_distribution=(0, 1), target_security=128): - """ - :param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters - :param sd: the standard deviation of the LWE error - :param q: the LWE modulus (q = 2**32, 2**64 in TFHE) - :param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default - :param secret_distribution: the LWE secret distribution - :param target_security: the target number of bits of security, 128 is default - - TODO: we should probably parallelise this function for speed - - EXAMPLE: - sage: X = generate_parameter_matrix([788, 790]) - sage: X - [(788, 4294967296, -20.0), (789, 4294967296, -20.0)] - """ - - RESULTS = [] - - # grab min and max value/s of n - (sd_min, sd_max) = sd_range - - n = n - - for sd in range(sd_min, sd_max + 1): - n = automated_param_select_n(sd, n=n, q=q, reduction_cost_model=reduction_cost_model, - secret_distribution=secret_distribution, target_security=target_security) - RESULTS.append((n, q, sd)) - - return RESULTS - - -def generate_parameter_step(results, label = None, torus_sd = True): - """ - Plot results - :param results: an output of generate_parameter_matrix - - returns: a step plot of chosen parameters - - EXAMPLE: - X = generate_parameter_matrix([700, 790]) - generate_parameter_step(X) - plt.show() - """ - - N = [] - SD = [] - - for (n, q, sd) in results: - N.append(n) - if torus_sd: - SD.append(sd) - else: - SD.append(sd + log(q,2)) - - plt.plot(N, SD, label = label) - plt.legend(loc = "upper right") - - return plt - - -def generate_iso_lines(N = [256, 2048], SD = [0, 32], q = 2**32): - - RESULTS = [] - - for n in range(N[0], N[1] + 1, 1): - for sd in range(SD[0], SD[1] + 1, 1): - sd = 2**sd - alpha = sqrt(2*pi) * sd / q - try: - est = est.estimate_lwe(n, alpha, q, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve, skip = ("bkw", "arora-gb", "dec")) - est = get_security_level(est, 2) - except: - est = est.estimate_lwe(n, alpha, q, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve, skip = ("bkw", "arora-gb", "dual", "dec")) - est = get_security_level(est, 2) - RESULTS.append((n, sd, est)) - - return RESULTS - - -def plot_iso_lines(results): - - x1 = [] - x2 = [] - - for z in results: - x1.append(z[0]) - # use log(q) - # use -ve values to match Pascal's diagram - x2.append(z[2]) - - plt.plot(x1, x2) - - return plt - - -def test_multiple_sd(n, q, secret_distribution, reduction_cost_model, split = 33, m = oo): - est = [] - Y = [] - for sd_ in np.linspace(0,32,split): - Y.append(sd_) - sd = (2** (-1 * sd_))* q - alpha = sqrt(2*pi) * sd / q - try: - es = est.estimate_lwe(n=512, alpha=alpha, q=q, secret_distribution=(0, 1), reduction_cost_model = reduction_cost_model, - skip=("bkw", "dec", "arora-gb"), m = m) - except: - es = est.estimate_lwe(n=512, alpha=alpha, q=q, secret_distribution=(0, 1), reduction_cost_model = reduction_cost_model, - skip=("bkw", "dec", "arora-gb", "dual"), m = m) - est.append(get_security_level(es,2)) - - return est, Y - - -## parameter curves - -def get_parameter_curves_data_sd(sec_levels, sd_range, q): - - Results = [] - for sec in sec_levels: - try: - result_sec = generate_parameter_matrix_sd(n = None, sd_range=sd_range, q=q, reduction_cost_model=est.BKZ.sieve, - secret_distribution=(0,1), target_security=sec) - Results.append(result_sec) - except: - pass - - return Results - - -def get_parameter_curves_data_n(sec_levels, n_range, q): - - Results = [] - for sec in sec_levels: - try: - result_sec = generate_parameter_matrix(n_range, sd = None, q=q, reduction_cost_model=est.BKZ.sieve, - secret_distribution=(0,1), target_security=sec) - Results.append(result_sec) - except: - pass - - return Results - - -def interpolate_result(result, log_q): - - # linear function interpolation - x = [] - y = [] - - # 1. filter out any points which reccomend sd = -log(q) + 2 - new_result= [] - for res in result: - if res[2] >= - log_q + 2: - new_result.append(res) - - result = new_result - for res in result: - x.append(res[0]) - y.append(res[2]) - - - (a,b) = np.polyfit(x, y, 1) - - return (a,b) - - -def plot_interpolants(interpolants, n_range, log_q, degree = 1): - for x in interpolants: - if degree == 1: - vals = [x[0] * n + x[1] for n in range(n_range[0],n_range[1])] - elif degree == 2: - vals = [x[0] * n**2 + x[1]*n + x[2] for n in range(n_range[0],n_range[1])] - # any values which fall outside of the range and edited to give at least two bits of noise. - - vvals = [] - for v in vals: - if v < -log_q + 2: - vvals.append(-log_q + 2) - else: - vvals.append(v) - - plt.plot(range(n_range[0], n_range[0] + len(vvals)), vvals) - - return 0 - - -## currently running -# sage: n_range = (256, 2048, 16) -# sage: sec_levels = [80 + 16*k for k in range(0,12)] -# sage: results = get_parameter_curves_data_n(sec_levels, n_range, q = 2**64) - -def verify_results(results, security_level, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve): - """ A function which verifies that a set of results match a given security level - :param results : a set of tuples of the form (n, q, sd) - :param security_level: the target security level for these params - """ - - estimates = [] - - # 1. Grab the parameters - for (n, q, sd) in results: - if sd is not None: - sd = 2**sd - alpha = sqrt(2*pi) * sd - - # 2. Test that these parameters satisfy the given security level - try: - estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo, skip = {"bkw","dec","arora-gb"}) - except: - estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo, - skip={"bkw", "dec", "arora-gb", "dual"}) - - estimates.append(estimate) - - return estimates - - -def verify_interpolants(interpolant, n_range, log_q, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve): - - estimates = [] - q = 2**log_q - (a, b) = interpolant - - for n in range(n_range[0], n_range[1]): - print(n) - # we take the max here to ensure that the "cut-off" for the minimal error is met. - sd = max(a * n + b, -log_q + 2) - sd = 2 ** sd - alpha = sqrt(2*pi) * sd - - security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution, - reduction_cost_model=reduction_cost_model, m=oo) - print(security_level) - if security_level == oo: - security_level = 0 - estimates.append(security_level) - - return estimates - -def get_zama_curves(): - - # hardcode the parameters for now - n_range = [128, 2048, 32] - sec_levels = [80 + 16*k for k in range(0,12)] - results = get_parameter_curves_data_n(sec_levels, n_range, q = 2**64) - - return results - - -def test_curves(): - - # a small hardcoded example for testing purposes - - n_range = [256, 1024, 128] - sec_levels = [80, 128, 256] - results = get_parameter_curves_data_n(sec_levels, n_range, q = 2**64) - - return results - -def find_nalpha(l, sec_lvl): - for j in range(len(l)): - if l[j] != oo and l[j] > sec_lvl: - return j - - -## we start with 80/128/192/256-bits of security - -## lambda = 80 -## z = verify_interpolants(interps[0], (128,2048), 64) -## i = 0 -## min(z[i:]) = 80.36 -## so the model is sd(n) = max(-0.04047677865612648 * n + 1.1433465085639063, log_q - 2), n >= 128 - - -## lambda = 128 -## z = verify_interpolants(interps[3], (128,2048), 64) -## i = 83 -## min(z[i:]) = 128.02 -## so the model is sd(n) = max(-0.026374888765705498 * n + 2.012143923330495, log_q - 2), n >= 211 ( = 128 + 83) - - -## lambda = 192 -## z = verify_interpolants(interps[7], (128,2048), 64) -## i = 304 -## min(z[i:]) = 192.19 -## so the model is sd(n) = max(-0.018504919354426233 * n + 2.6634073426215843, log_q - 2), n >= 432 ( = 128 + 212) - - -## lambda = 256 -## z = verify_interpolants(interps[-1], (128,2048), 64) -## i = 653 -## min(z[i:]) = 256.25 -## so the model is sd(n) = max(-0.014327640360322604 * n + 2.899270827311091), log_q - 2), n >= 781 ( = 128 + 653)