mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 13:39:25 +00:00
Compare commits
1431 Commits
tables
...
d4b22e5136
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4b22e5136 | ||
|
|
58fe79da10 | ||
|
|
3f07dd13c6 | ||
|
|
4e1d86dae2 | ||
|
|
7ef21830a5 | ||
|
|
8cb4c5d167 | ||
|
|
f9e4b420ed | ||
|
|
817b8e99d3 | ||
|
|
925cef17f2 | ||
|
|
3283cd79e4 | ||
|
|
51e2f24bc1 | ||
|
|
372e29fe08 | ||
|
|
fccb1aea51 | ||
|
|
a25e6330bd | ||
|
|
558a2bfa46 | ||
|
|
c73acb3d62 | ||
|
|
933b17aa91 | ||
|
|
5fa7e3d450 | ||
|
|
749d783b1e | ||
|
|
5a3ea80943 | ||
|
|
fddbebc7c0 | ||
|
|
e01848aa9e | ||
|
|
320b5627b5 | ||
|
|
be7780e69d | ||
|
|
0ddbaefb38 | ||
|
|
0f0db14f05 | ||
|
|
43083dfd49 | ||
|
|
523d2ac911 | ||
|
|
fd4f247917 | ||
|
|
ac9e356af4 | ||
|
|
bba7d2a356 | ||
|
|
4c349ae605 | ||
|
|
a4428761f7 | ||
|
|
940e9553fd | ||
|
|
593aefd229 | ||
|
|
5830c2463d | ||
|
|
bcc88c3e86 | ||
|
|
fea16df567 | ||
|
|
4960c3222e | ||
|
|
6b4df4f2c0 | ||
|
|
dac46c8d7d | ||
|
|
db2e8376df | ||
|
|
33dd412e67 | ||
|
|
fcad402186 | ||
|
|
ab4d79628d | ||
|
|
93be7a3067 | ||
|
|
63521f6a96 | ||
|
|
3d855c75be | ||
|
|
07df9aa035 | ||
|
|
bc44fbdbac | ||
|
|
4cacce5e55 | ||
|
|
7408e26781 | ||
|
|
1f92e1cbda | ||
|
|
333a9571b8 | ||
|
|
b7d49af1d5 | ||
|
|
5ea3b1bf97 | ||
|
|
2a31d8552e | ||
|
|
bca3728a10 | ||
|
|
4914420a37 | ||
|
|
f11a08c436 | ||
|
|
35b58a45bd | ||
|
|
af9b1ad5f9 | ||
|
|
e5afcda76b | ||
|
|
08c7c1b413 | ||
|
|
bdf5a66e95 | ||
|
|
e861859dec | ||
|
|
6658d95c85 | ||
|
|
2f07d04d88 | ||
|
|
e0259f2fe5 | ||
|
|
fab7a0a7cb | ||
|
|
84cee06ac1 | ||
|
|
c706d8664a | ||
|
|
1f2b9376f9 | ||
|
|
13b147cbf6 | ||
|
|
4a6496a90b | ||
|
|
9662d94bf9 | ||
|
|
233164cefd | ||
|
|
442d8c02fc | ||
|
|
d1be9eaa2d | ||
|
|
c32d3413ba | ||
|
|
a3a009a7e9 | ||
|
|
0889627e60 | ||
|
|
ace41c79fd | ||
|
|
f7d16b3fc5 | ||
|
|
157acc47ca | ||
|
|
ae0ecf9efe | ||
|
|
6374d9987e | ||
|
|
c93f6bf901 | ||
|
|
61a81e53e1 | ||
|
|
68dc872b88 | ||
|
|
89b237af7e | ||
|
|
2347bf5fd3 | ||
|
|
97f433c694 | ||
|
|
10f5ec51ca | ||
|
|
454bebaa77 | ||
|
|
0d569ff7a3 | ||
|
|
480acfd430 | ||
|
|
e266bc2e32 | ||
|
|
6c8a0bfda6 | ||
|
|
06c23368f2 | ||
|
|
5629c94b8b | ||
|
|
b427f4b8ab | ||
|
|
1096ddb7ea | ||
|
|
5487844b9e | ||
|
|
627e7e6210 | ||
|
|
019b42c0e0 | ||
|
|
079fddbaa6 | ||
|
|
92d8b91be9 | ||
|
|
4f1f7984a6 | ||
|
|
cda14ac8b9 | ||
|
|
6f5d794f10 | ||
|
|
34b93b882c | ||
|
|
0880453f82 | ||
|
|
ebdfc9afb4 | ||
|
|
f6409d08f3 | ||
|
|
c41a8ac8f2 | ||
|
|
d88aa90ec2 | ||
|
|
c05c511938 | ||
|
|
df85c09435 | ||
|
|
62a619a312 | ||
|
|
95b7460907 | ||
|
|
95c3cfc52e | ||
|
|
f0694172ef | ||
|
|
29633ada1b | ||
|
|
337e54c672 | ||
|
|
347d4cf413 | ||
|
|
aaff74575f | ||
|
|
ad0ecc5185 | ||
|
|
af12cec3b9 | ||
|
|
89788be034 | ||
|
|
745075af6e | ||
|
|
9b25d0dad7 | ||
|
|
2b76e41c9a | ||
|
|
05219c3ce8 | ||
|
|
cc75b52a43 | ||
|
|
4913873b10 | ||
|
|
0b8c7ade6e | ||
|
|
21262d41e6 | ||
|
|
508f7eb23a | ||
|
|
90df391170 | ||
|
|
9d3d47fc9f | ||
|
|
6691f16292 | ||
|
|
9c06cbccad | ||
|
|
c507ab9fd6 | ||
|
|
3aa8007700 | ||
|
|
1ba2d8d832 | ||
|
|
e7b0ed3e7e | ||
|
|
f3429ec1ef | ||
|
|
1cff9b4264 | ||
|
|
3c5a82e915 | ||
|
|
93e85c5ce6 | ||
|
|
617ec604ee | ||
|
|
265261d3ba | ||
|
|
7eb388e546 | ||
|
|
6c8040f723 | ||
|
|
02776c54a8 | ||
|
|
ec8dfd4639 | ||
|
|
99e05e4e5e | ||
|
|
a72b547824 | ||
|
|
bad3d210ba | ||
|
|
8c676d98c5 | ||
|
|
890b70212a | ||
|
|
9f7140c3db | ||
|
|
8b26a85faa | ||
|
|
24ea65eae9 | ||
|
|
fff8dcb827 | ||
|
|
2b23252b4c | ||
|
|
b493e3e31f | ||
|
|
00774c29d7 | ||
|
|
a4c82632fb | ||
|
|
c8747e23c5 | ||
|
|
008da698bc | ||
|
|
c2fffb9887 | ||
|
|
9c3329abeb | ||
|
|
065d314e2a | ||
|
|
ea3af28139 | ||
|
|
c40ce00955 | ||
|
|
74a68c6f68 | ||
|
|
2532423d42 | ||
|
|
b60e3c2524 | ||
|
|
77edd00725 | ||
|
|
884b6a6fec | ||
|
|
6a172825aa | ||
|
|
b297b79f07 | ||
|
|
3cf46338ee | ||
|
|
2d1443eb8a | ||
|
|
4de4c186b1 | ||
|
|
11fdb6da1d | ||
|
|
6caf45ea1d | ||
|
|
32bea92742 | ||
|
|
7122e0faf4 | ||
|
|
397fca748f | ||
|
|
16b22dd105 | ||
|
|
a6947d6d21 | ||
|
|
5c047ebe74 | ||
|
|
91a024e119 | ||
|
|
c511a54d18 | ||
|
|
6416e0079b | ||
|
|
7768ea90ad | ||
|
|
6e15bb2434 | ||
|
|
d1d5ee6b3d | ||
|
|
82f7342372 | ||
|
|
3a6c7ad796 | ||
|
|
62fa31de07 | ||
|
|
095ac50ba7 | ||
|
|
8cc0adf281 | ||
|
|
91905284bf | ||
|
|
4ebfae0b63 | ||
|
|
746bf5c6ad | ||
|
|
6e9ce3ac4f | ||
|
|
797ed49e7b | ||
|
|
99c6375605 | ||
|
|
6e8a5f9cb1 | ||
|
|
4446a369b1 | ||
|
|
ce038972df | ||
|
|
2f6fb93f87 | ||
|
|
1e6cb8044c | ||
|
|
1ca66b846a | ||
|
|
c82d1283af | ||
|
|
1c3e8af922 | ||
|
|
28c2b61933 | ||
|
|
cb1ef0d71f | ||
|
|
b823413c9b | ||
|
|
d1122a6535 | ||
|
|
8040fedddf | ||
|
|
51bb434239 | ||
|
|
f0ff3a18d2 | ||
|
|
695d1f0ecf | ||
|
|
571195bfda | ||
|
|
b79cf8abde | ||
|
|
c6c74684c9 | ||
|
|
de14687a0d | ||
|
|
d60e007126 | ||
|
|
b296be8515 | ||
|
|
6b2876351e | ||
|
|
0ea90d054d | ||
|
|
eb1d00aa55 | ||
|
|
372149c2cc | ||
|
|
c6cf33e370 | ||
|
|
f58478ad87 | ||
|
|
88a1726399 | ||
|
|
08e6669403 | ||
|
|
fcfdadc791 | ||
|
|
07c657306b | ||
|
|
9df8c9476e | ||
|
|
9ab2a2cfe0 | ||
|
|
a315040aee | ||
|
|
1822e31142 | ||
|
|
6efc313d76 | ||
|
|
4b85d4b03b | ||
|
|
05d8c32be8 | ||
|
|
8634d90b6b | ||
|
|
aa9daea6b0 | ||
|
|
942873f99d | ||
|
|
7c9b581723 | ||
|
|
b37a0db538 | ||
|
|
797604ad73 | ||
|
|
05b975dff9 | ||
|
|
74a8df4c7b | ||
|
|
bd14db9f76 | ||
|
|
25c02c1311 | ||
|
|
b018fc432c | ||
|
|
6e4ecbc90c | ||
|
|
be48dcc4a4 | ||
|
|
25066437da | ||
|
|
db49a63c2b | ||
|
|
ee50f584aa | ||
|
|
14f3f330db | ||
|
|
30a77d863f | ||
|
|
a03a1edbff | ||
|
|
c03afbe03e | ||
|
|
af611a39bf | ||
|
|
0d080e6d34 | ||
|
|
369af0fab5 | ||
|
|
d25e3d86a2 | ||
|
|
96f1d26f7a | ||
|
|
79e4cce2f6 | ||
|
|
0c341e3546 | ||
|
|
9f0790fb83 | ||
|
|
bb8e034e68 | ||
|
|
3f7bdaa64b | ||
|
|
351436a258 | ||
|
|
c328e5ea68 | ||
|
|
995734c960 | ||
|
|
54f1929078 | ||
|
|
d015ee96a3 | ||
|
|
a43815f101 | ||
|
|
7f1732c8c0 | ||
|
|
ed2445390f | ||
|
|
52a0c56016 | ||
|
|
42e8f2c8d8 | ||
|
|
b51204a4eb | ||
|
|
ec51fa233a | ||
|
|
ce4091695f | ||
|
|
24919cfc54 | ||
|
|
bf41009c5a | ||
|
|
e8e9e212df | ||
|
|
19187d2c30 | ||
|
|
43ae6794db | ||
|
|
978134a9d1 | ||
|
|
2eb155753a | ||
|
|
7d72e224f0 | ||
|
|
ffedba7a05 | ||
|
|
06e627a562 | ||
|
|
11f66c741d | ||
|
|
a0a2ef22e4 | ||
|
|
5e290a29d9 | ||
|
|
a688350f44 | ||
|
|
bc07e14b1e | ||
|
|
e1c07d89e0 | ||
|
|
56fd11ab8d | ||
|
|
c03fb6c71b | ||
|
|
96f94966b7 | ||
|
|
b65ba17007 | ||
|
|
c9003874ad | ||
|
|
205bec36e5 | ||
|
|
df8b455d54 | ||
|
|
84a0bcad51 | ||
|
|
b680bb532b | ||
|
|
b9983bf133 | ||
|
|
cddb44ae3f | ||
|
|
bd3272a9f2 | ||
|
|
de41be6e26 | ||
|
|
b8ac8e697b | ||
|
|
899a9604e1 | ||
|
|
facb5817c4 | ||
|
|
97fedf65d0 | ||
|
|
257323c1e5 | ||
|
|
deecd77aec | ||
|
|
360b264a0f | ||
|
|
e05b77d830 | ||
|
|
5970a455d0 | ||
|
|
4c9e3b085b | ||
|
|
a2089c61fb | ||
|
|
ae449535ff | ||
|
|
05dc474cb3 | ||
|
|
34bcb9eb01 | ||
|
|
2958f196fc | ||
|
|
92cebcd911 | ||
|
|
a3278dfb31 | ||
|
|
f02db19670 | ||
|
|
652c878f54 | ||
|
|
da01de08c9 | ||
|
|
052ef39a25 | ||
|
|
87fdc8ce35 | ||
|
|
86ff0ae71b | ||
|
|
3069138475 | ||
|
|
f22aedc007 | ||
|
|
7be20c451d | ||
|
|
0198d4cc46 | ||
|
|
7c10873cd5 | ||
|
|
08180cc563 | ||
|
|
c4bdbdde11 | ||
|
|
0d23964762 | ||
|
|
fbf51e53ec | ||
|
|
fd1826cca9 | ||
|
|
f561fa9ba1 | ||
|
|
f4fc539e14 | ||
|
|
0fff5391a8 | ||
|
|
a71a789912 | ||
|
|
83c41eccd4 | ||
|
|
e5113c333e | ||
|
|
6068978676 | ||
|
|
e7e30150f0 | ||
|
|
55fe27f41a | ||
|
|
d66a7ee43e | ||
|
|
a702d65c3d | ||
|
|
52eb68677a | ||
|
|
d29d19bdfe | ||
|
|
c3fdb9d9df | ||
|
|
46e1f85085 | ||
|
|
1bff2a0447 | ||
|
|
b66203ae3f | ||
|
|
43a182fc4c | ||
|
|
8ead7a2581 | ||
|
|
3797679755 | ||
|
|
c056b751fe | ||
|
|
5977121c48 | ||
|
|
7b6181ecdb | ||
|
|
f976bc86ac | ||
|
|
441bf62e11 | ||
|
|
4852dcaab7 | ||
|
|
d6bc1c1ea3 | ||
|
|
7b2dec63ce | ||
|
|
d833254b84 | ||
|
|
a1b2bdf0a2 | ||
|
|
43841f95fc | ||
|
|
fdfce9e207 | ||
|
|
3255c0ace5 | ||
|
|
057c3b7cf1 | ||
|
|
a29c410caf | ||
|
|
a6f40978cb | ||
|
|
bdf6afc144 | ||
|
|
a362b0a451 | ||
|
|
041ed46171 | ||
|
|
9accddb2d7 | ||
|
|
e06e4edc8e | ||
|
|
e4d59eeeca | ||
|
|
1f5b1bb514 | ||
|
|
1a3b6005c6 | ||
|
|
7dc1a24bce | ||
|
|
3483f7fa73 | ||
|
|
a300a1029a | ||
|
|
7409d0b3cf | ||
|
|
19e90b28b0 | ||
|
|
584943d1e9 | ||
|
|
62e1d63f47 | ||
|
|
e4adaa8947 | ||
|
|
3b3fdd104b | ||
|
|
5897efd7c7 | ||
|
|
863a7842ca | ||
|
|
f414735be5 | ||
|
|
5c5c097da9 | ||
|
|
7d4e8b59db | ||
|
|
e3e9939eaf | ||
|
|
530fba51dd | ||
|
|
cb61c9052a | ||
|
|
96cc5d0157 | ||
|
|
7275a95907 | ||
|
|
80e5ca9328 | ||
|
|
67951c4971 | ||
|
|
4143fe9f47 | ||
|
|
a73b19e2b8 | ||
|
|
97c328e5fb | ||
|
|
96c397caa0 | ||
|
|
d5c6ed1a03 | ||
|
|
f6e8bc3352 | ||
|
|
7d0d1dc382 | ||
|
|
d50fe87801 | ||
|
|
e6aa9df428 | ||
|
|
02edfd2935 | ||
|
|
9aeece5bf6 | ||
|
|
bb84f7cf1d | ||
|
|
bb25baf3bc | ||
|
|
013a0cddfc | ||
|
|
ed7300b406 | ||
|
|
88b5efda99 | ||
|
|
0712e6f107 | ||
|
|
6a4c57e86f | ||
|
|
b7746aa71d | ||
|
|
8dd41ee798 | ||
|
|
9a1d10f4ea | ||
|
|
6587590986 | ||
|
|
b0fcdd3367 | ||
|
|
15edea1389 | ||
|
|
1d9e2efc33 | ||
|
|
f25f5cd368 | ||
|
|
f847ac7077 | ||
|
|
29fcf6be4d | ||
|
|
108e2b57d9 | ||
|
|
3da5577950 | ||
|
|
f692047b8b | ||
|
|
2401266374 | ||
|
|
ed90d1752a | ||
|
|
3261fde853 | ||
|
|
7492adc473 | ||
|
|
04f9a1fa31 | ||
|
|
13cbc99149 | ||
|
|
1a0b4198ba | ||
|
|
22371a6585 | ||
|
|
b2d6a85ac0 | ||
|
|
44ca5e6520 | ||
|
|
83b7146e1a | ||
|
|
f193b896c1 | ||
|
|
985795e99d | ||
|
|
9bf8c92325 | ||
|
|
ab5af57dae | ||
|
|
98190b7b83 | ||
|
|
2f45bba2d4 | ||
|
|
30d0bad175 | ||
|
|
b8abc1e3cc | ||
|
|
b2ed2e961c | ||
|
|
9cdca1d3d6 | ||
|
|
4ee65ed243 | ||
|
|
aa59f53ead | ||
|
|
bd5491dfd5 | ||
|
|
0eff3d9453 | ||
|
|
0be567ff69 | ||
|
|
83b3a5c31c | ||
|
|
4d1212ec65 | ||
|
|
7e27315207 | ||
|
|
aa1faefe33 | ||
|
|
7d738a3677 | ||
|
|
4a32f22418 | ||
|
|
01a4b9e694 | ||
|
|
3b01d3039b | ||
|
|
40b7bc59d0 | ||
|
|
269db1c4be | ||
|
|
90318d7214 | ||
|
|
64d370ac11 | ||
|
|
db8dc1e864 | ||
|
|
086458d041 | ||
|
|
2e0f8138e2 | ||
|
|
32a9a33226 | ||
|
|
2508633de9 | ||
|
|
7312428a44 | ||
|
|
e1801b57c9 | ||
|
|
60491a091f | ||
|
|
9f3840d1cf | ||
|
|
7120bddc6f | ||
|
|
77f7794452 | ||
|
|
62a1a45135 | ||
|
|
0440e60645 | ||
|
|
4babf898d7 | ||
|
|
ca69f97fef | ||
|
|
fe19e8246e | ||
|
|
c62d9b448f | ||
|
|
98ab6acbd5 | ||
|
|
092f17932a | ||
|
|
e455332e01 | ||
|
|
a9468bf355 | ||
|
|
3d464c4736 | ||
|
|
142552f024 | ||
|
|
e3a7ee4927 | ||
|
|
9eaaa7d2e8 | ||
|
|
8adef705c3 | ||
|
|
3fd6d45b3e | ||
|
|
d263413e36 | ||
|
|
6f8a5d0ede | ||
|
|
24bdd7ed9b | ||
|
|
aa724c06bc | ||
|
|
1e6655408e | ||
|
|
9ab43407e1 | ||
|
|
7ac0de3a8d | ||
|
|
06a6cd29b0 | ||
|
|
2472ec7ba8 | ||
|
|
69c3fad7ce | ||
|
|
bd9a05feef | ||
|
|
7d8e08d5b4 | ||
|
|
f7e49e1f90 | ||
|
|
cd4c3a6c88 | ||
|
|
fddc605c65 | ||
|
|
2ad6b38be9 | ||
|
|
fda90e23c9 | ||
|
|
3f3f6b2d0c | ||
|
|
fa8ff62b09 | ||
|
|
5113ab9ec4 | ||
|
|
9b7cb688ed | ||
|
|
9a5f8fc5dd | ||
|
|
2dc35193c9 | ||
|
|
9bf24480f4 | ||
|
|
3af9dc5d6f | ||
|
|
148bc380fe | ||
|
|
8dad62f300 | ||
|
|
1e79de87e8 | ||
|
|
2f57a69cb6 | ||
|
|
493a222421 | ||
|
|
d5a19eca8c | ||
|
|
e9fca37181 | ||
|
|
83c25eff03 | ||
|
|
285422f71a | ||
|
|
1838c37ecf | ||
|
|
a3649b2062 | ||
|
|
1bd14163a0 | ||
|
|
89a6ee9290 | ||
|
|
a66994aade | ||
|
|
34ffd2fa76 | ||
|
|
775353f8cd | ||
|
|
c9b2490ab9 | ||
|
|
2db53d5434 | ||
|
|
6268bbd7c8 | ||
|
|
bc2f23f72b | ||
|
|
72337b17f5 | ||
|
|
36b193992f | ||
|
|
37b6de9c0c | ||
|
|
22f3c9e58f | ||
|
|
9adefa4c2c | ||
|
|
c245bcdc9b | ||
|
|
f249e20028 | ||
|
|
3745f8b6af | ||
|
|
ba46aa76f0 | ||
|
|
8c1d8a2658 | ||
|
|
2702384c70 | ||
|
|
d3093c92dc | ||
|
|
32df302cc4 | ||
|
|
ea8e26eca3 | ||
|
|
bccdabb53d | ||
|
|
b91bd44476 | ||
|
|
dc2656a538 | ||
|
|
67109c648c | ||
|
|
61418b4e9f | ||
|
|
506ded205a | ||
|
|
9801f9f753 | ||
|
|
8a4ef3ddae | ||
|
|
bfb5401336 | ||
|
|
f2872a2e07 | ||
|
|
c739f00d0b | ||
|
|
310a09b5a4 | ||
|
|
c65bb70741 | ||
|
|
718c44d50e | ||
|
|
1f45c2c6b5 | ||
|
|
76a30fd572 | ||
|
|
a52c86ad81 | ||
|
|
108504d6e2 | ||
|
|
27cd2ee2bb | ||
|
|
dc88b29b92 | ||
|
|
45ea805620 | ||
|
|
1e68cff6dc | ||
|
|
906d3b9a7c | ||
|
|
e319762c69 | ||
|
|
f161135119 | ||
|
|
d2a0ff13f2 | ||
|
|
498aa45619 | ||
|
|
39ce819876 | ||
|
|
8973eb8ac4 | ||
|
|
34397b31b1 | ||
|
|
96583da3b9 | ||
|
|
34c6974311 | ||
|
|
5a4db3efad | ||
|
|
28399b0310 | ||
|
|
426e89d6fb | ||
|
|
4850376664 | ||
|
|
f366d65d4b | ||
|
|
e680eabb62 | ||
|
|
a3441a6871 | ||
|
|
9895ec0f41 | ||
|
|
666bb3e96b | ||
|
|
acc19e2817 | ||
|
|
5e02f936e4 | ||
|
|
6b41c91dc2 | ||
|
|
8992504e69 | ||
|
|
e2901cab06 | ||
|
|
13a8b0afc1 | ||
|
|
7e71450dc4 | ||
|
|
049fefb5fd | ||
|
|
6a89cfcd08 | ||
|
|
4571a8ad85 | ||
|
|
96db784b10 | ||
|
|
dd523b22c2 | ||
|
|
fa406c507f | ||
|
|
ad51c123e3 | ||
|
|
f6f945e747 | ||
|
|
0dd8aed134 | ||
|
|
cee788eac3 | ||
|
|
bebe2fae0e | ||
|
|
adb48cd737 | ||
|
|
363a88c4ec | ||
|
|
5ba1dd2524 | ||
|
|
784c7b47a4 | ||
|
|
376b36974f | ||
|
|
38ad1d4bc4 | ||
|
|
aab8a417db | ||
|
|
d5c787fea2 | ||
|
|
e3a70ef0dc | ||
|
|
044b299cda | ||
|
|
53d86e2a29 | ||
|
|
c338b92067 | ||
|
|
3c38a0ec11 | ||
|
|
88f88b574c | ||
|
|
9f143a9742 | ||
|
|
2815046b21 | ||
|
|
d8033504c8 | ||
|
|
48ad5fe20b | ||
|
|
4c801df4f2 | ||
|
|
9b79c4dc0c | ||
|
|
e010d66c5d | ||
|
|
3d91fd88a3 | ||
|
|
f988c43f8d | ||
|
|
857e3ea72b | ||
|
|
8b14bb54bb | ||
|
|
f78332453b | ||
|
|
22da7aedde | ||
|
|
2641b83b3e | ||
|
|
4980e6b704 | ||
|
|
c091b86919 | ||
|
|
a8c7bb96c8 | ||
|
|
09a95c9bd2 | ||
|
|
101da0a641 | ||
|
|
6d5851a9ee | ||
|
|
64c309f8db | ||
|
|
e00aa3031c | ||
|
|
f8afb040dc | ||
|
|
7823ece4fe | ||
|
|
b205391b28 | ||
|
|
32a937ddb9 | ||
|
|
bdbeedc723 | ||
|
|
f306618e84 | ||
|
|
89865b549c | ||
|
|
39eae2795f | ||
|
|
0eb56406a4 | ||
|
|
afb385fba4 | ||
|
|
821f5d8de4 | ||
|
|
3862731a12 | ||
|
|
42eb674d1a | ||
|
|
1af5f1bcdc | ||
|
|
32435d8a4c | ||
|
|
49ce792b91 | ||
|
|
4949793c3f | ||
|
|
61d46dccd4 | ||
|
|
88a1fce15c | ||
|
|
a2493cfafc | ||
|
|
e3de64d5ff | ||
|
|
ecd0457d5b | ||
|
|
7990ee689a | ||
|
|
f05e909d0e | ||
|
|
5e565fa3ef | ||
|
|
6df1b46313 | ||
|
|
24dba66bad | ||
|
|
fd585d496c | ||
|
|
5703591eb2 | ||
|
|
9ac3b203c8 | ||
|
|
23e1c9769c | ||
|
|
8e6e05ae2d | ||
|
|
713660c79c | ||
|
|
cb8c8031b0 | ||
|
|
d07447fe97 | ||
|
|
c26beae0f9 | ||
|
|
818215b570 | ||
|
|
79943c3a6c | ||
|
|
076a8e4d62 | ||
|
|
ffd1457927 | ||
|
|
523a055b74 | ||
|
|
624fb2781d | ||
|
|
641077a089 | ||
|
|
298d1fd3ba | ||
|
|
92c3403698 | ||
|
|
37af8b51b3 | ||
|
|
900298b94b | ||
|
|
9effd5ccdc | ||
|
|
ceeb57470f | ||
|
|
69454fa9bb | ||
|
|
5121ca7519 | ||
|
|
1eb3b364f4 | ||
|
|
f66fe3c1cb | ||
|
|
6f9d02fdf8 | ||
|
|
28613400b8 | ||
|
|
b6579d5a2a | ||
|
|
c2f32e7882 | ||
|
|
228e36a12d | ||
|
|
38bcedbf11 | ||
|
|
98f9fc2c2f | ||
|
|
d5cfb3fb25 | ||
|
|
b1dbe1f50d | ||
|
|
1b57d655ed | ||
|
|
64402914ba | ||
|
|
a7c9c1ef55 | ||
|
|
a05961974a | ||
|
|
acc9495429 | ||
|
|
344ac9cbfc | ||
|
|
6ccac2d0ab | ||
|
|
56f7037084 | ||
|
|
a0f8214d48 | ||
|
|
5f93140ba5 | ||
|
|
712f11d879 | ||
|
|
808a633e4d | ||
|
|
0a367bfbda | ||
|
|
845c2842b5 | ||
|
|
8543487db2 | ||
|
|
a9072e6b1b | ||
|
|
b0c28a1cf0 | ||
|
|
23b9d57305 | ||
|
|
807ec30762 | ||
|
|
5424886d63 | ||
|
|
2bebe0755d | ||
|
|
f0ce6e6388 | ||
|
|
62504b2622 | ||
|
|
c9bb284570 | ||
|
|
6a4bccba74 | ||
|
|
df67b7d94c | ||
|
|
677b9b681f | ||
|
|
fa1b569b78 | ||
|
|
d75115ce13 | ||
|
|
3d00d405a3 | ||
|
|
3480fc5e16 | ||
|
|
7fa5d291b8 | ||
|
|
b54548b13a | ||
|
|
c878d38c60 | ||
|
|
5d9067b84d | ||
|
|
13f48a406e | ||
|
|
35fcd11096 | ||
|
|
93b1656f86 | ||
|
|
3c6cc42c23 | ||
|
|
93fe8a52dd | ||
|
|
249f7b904f | ||
|
|
8ce8657d34 | ||
|
|
e5a196504c | ||
|
|
b195db0929 | ||
|
|
89eef95fb3 | ||
|
|
0f80f6ec7d | ||
|
|
740274210b | ||
|
|
6ac57be4e3 | ||
|
|
08e7ca955b | ||
|
|
239800cfcf | ||
|
|
d49c636f0f | ||
|
|
30834fe4d2 | ||
|
|
d928b787f7 | ||
|
|
c7b232949a | ||
|
|
acf2469dd8 | ||
|
|
6267acf3df | ||
|
|
a95ecc2512 | ||
|
|
ac708b3b2a | ||
|
|
d25c668ee4 | ||
|
|
8ced63eaac | ||
|
|
f6a497f3ac | ||
|
|
790fe7ee23 | ||
|
|
8c020abb86 | ||
|
|
21f0bb2721 | ||
|
|
385ed2e97a | ||
|
|
fca567f61d | ||
|
|
dfa3106a38 | ||
|
|
c6982b5dfc | ||
|
|
1aa293cc4a | ||
|
|
8a24fc39a6 | ||
|
|
40b2920412 | ||
|
|
47f8766da6 | ||
|
|
663b5f4b50 | ||
|
|
227176e4b8 | ||
|
|
f069567f12 | ||
|
|
84c2d73093 | ||
|
|
4d50b6892c | ||
|
|
3eade48a6f | ||
|
|
89974c529a | ||
|
|
ffea02dfbf | ||
|
|
f55e9b40e6 | ||
|
|
a70df6a449 | ||
|
|
168f2899f0 | ||
|
|
c95bdb6752 | ||
|
|
88f0e89350 | ||
|
|
7b7ddbdd97 | ||
|
|
9175383e89 | ||
|
|
029b6c53a1 | ||
|
|
219adc7657 | ||
|
|
964fdee175 | ||
|
|
a7f2740dfb | ||
|
|
0c9c1aeff1 | ||
|
|
adfbde6e24 | ||
|
|
5765d1d278 | ||
|
|
78c00bde3d | ||
|
|
c0001f5ff2 | ||
|
|
6032af6692 | ||
|
|
7824b6cb8b | ||
|
|
78d5372fb7 | ||
|
|
cc531d630e | ||
|
|
09d96822ca | ||
|
|
7a8f8c2d3d | ||
|
|
e74b4ab94f | ||
|
|
e0820759c0 | ||
|
|
2feebe536e | ||
|
|
cc491ee1e1 | ||
|
|
14388e746c | ||
|
|
215155f84b | ||
|
|
c476f9b640 | ||
|
|
be8c25aef0 | ||
|
|
fb296a9c2e | ||
|
|
aa0ec4ac41 | ||
|
|
05b1fc5f05 | ||
|
|
72633d6421 | ||
|
|
ad5522d854 | ||
|
|
f2d9d70068 | ||
|
|
2b09309adc | ||
|
|
bf9ec410db | ||
|
|
e0dc5d29ad | ||
|
|
710e6e5217 | ||
|
|
3f6565588f | ||
|
|
af84b7f707 | ||
|
|
8c74576cf0 | ||
|
|
1e448dec21 | ||
|
|
ef0c901455 | ||
|
|
09c3c9cc9e | ||
|
|
a404944b90 | ||
|
|
70d866af6a | ||
|
|
f99a91b34d | ||
|
|
294ad08e00 | ||
|
|
a26ca1a92f | ||
|
|
9c2a44f9df | ||
|
|
8b5eaa8092 | ||
|
|
8041a0d845 | ||
|
|
9e1f3fc85c | ||
|
|
ee65e4df8f | ||
|
|
ff2febe5aa | ||
|
|
334873b6a5 | ||
|
|
21026136bd | ||
|
|
396e5322b4 | ||
|
|
9da0eb69c7 | ||
|
|
6f3b5f4535 | ||
|
|
e880ebb5a9 | ||
|
|
1036e673ce | ||
|
|
fd1bbec134 | ||
|
|
7579c71765 | ||
|
|
5a499de4ca | ||
|
|
e26b861d25 | ||
|
|
059e79c98a | ||
|
|
92a868e574 | ||
|
|
595cd6d404 | ||
|
|
4d43c04916 | ||
|
|
2604746586 | ||
|
|
36cdf6d4bf | ||
|
|
9676584ffe | ||
|
|
79655672ef | ||
|
|
fa2cf03e61 | ||
|
|
92ad689c7e | ||
|
|
b2169a7316 | ||
|
|
e2571a43aa | ||
|
|
e21fc5ff3c | ||
|
|
eafd054296 | ||
|
|
51bf51ae1e | ||
|
|
28b6bc99ac | ||
|
|
ce883104b7 | ||
|
|
f48022c6eb | ||
|
|
124b994c23 | ||
|
|
2e2bc59703 | ||
|
|
c032f66f8a | ||
|
|
695d923593 | ||
|
|
63318cb728 | ||
|
|
6f6c9f7cdf | ||
|
|
04e7863dbd | ||
|
|
a5002c50ec | ||
|
|
72dd665ebf | ||
|
|
8b1bce6abd | ||
|
|
e73a51bfa5 | ||
|
|
5858b6c03e | ||
|
|
9bea368d36 | ||
|
|
a509dbfad6 | ||
|
|
03a6470a5b | ||
|
|
997dd611d5 | ||
|
|
86cbf6e02e | ||
|
|
8c8232516d | ||
|
|
be947ce152 | ||
|
|
7c7f17aac6 | ||
|
|
ff5c240fcc | ||
|
|
d5a12a9b97 | ||
|
|
354ac856a5 | ||
|
|
402a7be966 | ||
|
|
119d25be49 | ||
|
|
2cfee536f6 | ||
|
|
90f67b5e54 | ||
|
|
4d17b922fe | ||
|
|
7488d23e0d | ||
|
|
a290b74805 | ||
|
|
61757d5e19 | ||
|
|
09f8ac37c4 | ||
|
|
c46cf47736 | ||
|
|
defce32ff1 | ||
|
|
de52c4db7f | ||
|
|
d74cbe2cce | ||
|
|
caa695511b | ||
|
|
7538c10159 | ||
|
|
90f2b03595 | ||
|
|
9e78c8fc9e | ||
|
|
d323fc8b7b | ||
|
|
82c34dcc76 | ||
|
|
bc19975a8a | ||
|
|
b9f38fb354 | ||
|
|
ccec529cee | ||
|
|
1c31ca7187 | ||
|
|
f6206b60ec | ||
|
|
96525330c2 | ||
|
|
7abc8f19cd | ||
|
|
bd06b95c05 | ||
|
|
648d237df5 | ||
|
|
3f4bab7f7b | ||
|
|
426346dd5a | ||
|
|
a4f64e2651 | ||
|
|
6fa405a728 | ||
|
|
ae4e98c052 | ||
|
|
30b8636641 | ||
|
|
1610383649 | ||
|
|
9615caf3bb | ||
|
|
8a70416fd0 | ||
|
|
4f28a38ce1 | ||
|
|
47be373eb0 | ||
|
|
79aff5d4c8 | ||
|
|
a9f6300e86 | ||
|
|
ff70cbb223 | ||
|
|
17818c2a02 | ||
|
|
aea6ac104f | ||
|
|
534e1bb11d | ||
|
|
c182b804bc | ||
|
|
9157f8d0a0 | ||
|
|
839734354a | ||
|
|
d954e67238 | ||
|
|
6a981dae6e | ||
|
|
397d79040c | ||
|
|
293731f739 | ||
|
|
8447021ba1 | ||
|
|
11a0803ea5 | ||
|
|
d58a7b0ebf | ||
|
|
952cf280c2 | ||
|
|
8d4d630e0f | ||
|
|
e1bb2c191b | ||
|
|
df2bb79a53 | ||
|
|
515587406f | ||
|
|
7fc8630d39 | ||
|
|
6a2a353b91 | ||
|
|
66eaf6ab61 | ||
|
|
597122b2e0 | ||
|
|
0aa6b561b7 | ||
|
|
60ca3a9599 | ||
|
|
59891594aa | ||
|
|
2fdf8f8285 | ||
|
|
55e0253225 | ||
|
|
918cce3494 | ||
|
|
6ac570365f | ||
|
|
0525ba2f62 | ||
|
|
9b47ad56bb | ||
|
|
5e771b1bea | ||
|
|
9952c67d98 | ||
|
|
f2218b4d4e | ||
|
|
780b79c3d8 | ||
|
|
ba82dac18c | ||
|
|
f374cd7398 | ||
|
|
ab1e5c372e | ||
|
|
67da08705e | ||
|
|
0d4b66dc2a | ||
|
|
4ed819fc7d | ||
|
|
74924095e1 | ||
|
|
d2c1592c61 | ||
|
|
64924835ad | ||
|
|
37e4f2cc50 | ||
|
|
caf37527eb | ||
|
|
669d2dbffc | ||
|
|
f4e2da2767 | ||
|
|
48078d0b4b | ||
|
|
0e0243639e | ||
|
|
14203bbb46 | ||
|
|
f5fa6f020d | ||
|
|
41a285ddfa | ||
|
|
36034c2f72 | ||
|
|
5e62072a0f | ||
|
|
e56495d624 | ||
|
|
71dbc798b5 | ||
|
|
4335baa43f | ||
|
|
77de28f77a | ||
|
|
ad470bc969 | ||
|
|
62dfc63532 | ||
|
|
1e201562df | ||
|
|
11114dcb74 | ||
|
|
837c776297 | ||
|
|
6bff3866ea | ||
|
|
b0730e3fdf | ||
|
|
2e78d61752 | ||
|
|
0b8a4ab3d0 | ||
|
|
c358090f16 | ||
|
|
adb5f34fda | ||
|
|
ed056cceaf | ||
|
|
2bad06e5d9 | ||
|
|
5a9a42f025 | ||
|
|
7d12c785b7 | ||
|
|
e08adcc1ac | ||
|
|
af5702fccd | ||
|
|
5037962d3c | ||
|
|
5b26115f81 | ||
|
|
1a99629a4a | ||
|
|
b1ea2dfba6 | ||
|
|
0e8c55e050 | ||
|
|
d36fc026dd | ||
|
|
0bbf511062 | ||
|
|
2729882d65 | ||
|
|
c37cc0b4e2 | ||
|
|
a053454ae4 | ||
|
|
20a33079f8 | ||
|
|
8307d4f6c8 | ||
|
|
db1fefe7c1 | ||
|
|
4a81640ab8 | ||
|
|
943438628d | ||
|
|
7efedb9a91 | ||
|
|
79124b9a33 | ||
|
|
6fec95b1a7 | ||
|
|
2f4f1de488 | ||
|
|
97374a3e24 | ||
|
|
530671795a | ||
|
|
8b7e7b1a1c | ||
|
|
053f07a281 | ||
|
|
08f9287107 | ||
|
|
35043d2889 | ||
|
|
1d2ebdca62 | ||
|
|
e5329b42e6 | ||
|
|
8144956f8a | ||
|
|
408494f8de | ||
|
|
8661111fc6 | ||
|
|
93d5f41917 | ||
|
|
15d6be1678 | ||
|
|
2fd5cd8161 | ||
|
|
c6284b85a4 | ||
|
|
a42a84e1e8 | ||
|
|
5a3406bb5f | ||
|
|
62b3036cbd | ||
|
|
6a15b21949 | ||
|
|
39b3452da1 | ||
|
|
7a05466049 | ||
|
|
6b5097f0c3 | ||
|
|
c1435a2045 | ||
|
|
969a5d94f2 | ||
|
|
93f7afec8b | ||
|
|
32c18cac84 | ||
|
|
65376e93e5 | ||
|
|
6104d606be | ||
|
|
1a6497f37a | ||
|
|
4d6a0bbd7d | ||
|
|
2d56d24d9c | ||
|
|
87dea5e455 | ||
|
|
8bee62609c | ||
|
|
d72c4ca4f7 | ||
|
|
d929a8d96e | ||
|
|
74647b1b52 | ||
|
|
40a6672547 | ||
|
|
686a5ee364 | ||
|
|
cb4ce5e354 | ||
|
|
ac0f5e9b2d | ||
|
|
18ac80671f | ||
|
|
8260ec1a9e | ||
|
|
07f424b484 | ||
|
|
5de8bf3295 | ||
|
|
82a096e90e | ||
|
|
c540f52dda | ||
|
|
264174644f | ||
|
|
df75782e54 | ||
|
|
86ad947261 | ||
|
|
a3267034b6 | ||
|
|
c6bd00e778 | ||
|
|
fba5b7fed4 | ||
|
|
affe4300e8 | ||
|
|
b6f9a1f8b6 | ||
|
|
9e01588b11 | ||
|
|
b0e0fc44cf | ||
|
|
a4fdff3e3b | ||
|
|
9241bdc3b5 | ||
|
|
b253529413 | ||
|
|
2ace339975 | ||
|
|
f12cc2cca6 | ||
|
|
19664967ed | ||
|
|
27f5881553 | ||
|
|
8ca90e7905 | ||
|
|
e13cf52c49 | ||
|
|
0346aa6964 | ||
|
|
7f8bb1aa9f | ||
|
|
3b920ad471 | ||
|
|
757adda2e0 | ||
|
|
a1ecdbf2f3 | ||
|
|
19ab49cb8c | ||
|
|
ced89332d2 | ||
|
|
be05e0dd47 | ||
|
|
8ef8b5ca6f | ||
|
|
50a4f5938a | ||
|
|
d37aff515d | ||
|
|
375887bb29 | ||
|
|
8ffa5553ff | ||
|
|
422a562c78 | ||
|
|
ea7c281a47 | ||
|
|
97a94a0bf8 | ||
|
|
6b591c0df9 | ||
|
|
4be3290e40 | ||
|
|
7b0b8a20ec | ||
|
|
7508106650 | ||
|
|
ccf4ca2215 | ||
|
|
1d6df0099c | ||
|
|
814a9a8d35 | ||
|
|
9662e9e1af | ||
|
|
b303649f9d | ||
|
|
a646ec5aaa | ||
|
|
e0deaa5539 | ||
|
|
f760c09006 | ||
|
|
daa88a051f | ||
|
|
e979883f2d | ||
|
|
52913a6e8d | ||
|
|
056dd1afcb | ||
|
|
bf09b5cc7e | ||
|
|
eeca440fa7 | ||
|
|
49c4acffbb | ||
|
|
5599a052ad | ||
|
|
bff5f33616 | ||
|
|
f10bcfddcb | ||
|
|
5b3c9bf5d0 | ||
|
|
3b4c600c60 | ||
|
|
bacf31378d | ||
|
|
da8e7e73e0 | ||
|
|
35a4f5bf9f | ||
|
|
445bb3786e | ||
|
|
6e518f5c22 | ||
|
|
e6bf3a4758 | ||
|
|
256d920835 | ||
|
|
b8db677d4c | ||
|
|
461504ccbf | ||
|
|
3ec5189fbf | ||
|
|
25f1549c6c | ||
|
|
9c65518dc3 | ||
|
|
2b042015b5 | ||
|
|
783a445a3e | ||
|
|
ace7506172 | ||
|
|
4871fc7441 | ||
|
|
259e5815c8 | ||
|
|
d5a5704ba4 | ||
|
|
9e82416e7d | ||
|
|
d32c865c9a | ||
|
|
cc917a217d | ||
|
|
af86b7a499 | ||
|
|
873d27685a | ||
|
|
12136a9409 | ||
|
|
4edba7eb7a | ||
|
|
bec92c10ad | ||
|
|
bade7a504e | ||
|
|
6787e44664 | ||
|
|
13977f6287 | ||
|
|
ba157ea84b | ||
|
|
7d3a06ac05 | ||
|
|
a3adf16b8c | ||
|
|
f136328c08 | ||
|
|
8f352353ba | ||
|
|
7a16ce78b0 | ||
|
|
6085a8bb9d | ||
|
|
e00c14afe5 | ||
|
|
8f4d6f79f3 | ||
|
|
31e8be7b6e | ||
|
|
ba0f119a53 | ||
|
|
0ca52a36ee | ||
|
|
0350cd803d | ||
|
|
f0957c8d52 | ||
|
|
3f503d92fb | ||
|
|
409d1151b2 | ||
|
|
00c1f21224 | ||
|
|
138bb46e19 | ||
|
|
56574f2f5b | ||
|
|
4a3178ed8f | ||
|
|
83060a914a | ||
|
|
6acbfdcc45 | ||
|
|
6f9cf510da | ||
|
|
b05a223b69 | ||
|
|
138f7cdfa4 | ||
|
|
fe1f28c77a | ||
|
|
b85801b524 | ||
|
|
c7121d96ac | ||
|
|
35ca220bcc | ||
|
|
3d9b9b178c | ||
|
|
7334ed1f43 | ||
|
|
84de427d72 | ||
|
|
d714f2202d | ||
|
|
be61bff074 | ||
|
|
8de465af87 | ||
|
|
65df18d285 | ||
|
|
953bece2ea | ||
|
|
5977aeb489 | ||
|
|
8e53522780 | ||
|
|
5df74ac9e2 | ||
|
|
16c51ce374 | ||
|
|
659ff280ac | ||
|
|
0fc336941c | ||
|
|
1a4f2d5621 | ||
|
|
8a20d90868 | ||
|
|
4101239e0d | ||
|
|
aa4b5e2ca3 | ||
|
|
6ef624ab7e | ||
|
|
e67e406d95 | ||
|
|
43e38e463f | ||
|
|
1464eefbe3 | ||
|
|
6eaed17952 | ||
|
|
2379855b31 | ||
|
|
cbceaff678 | ||
|
|
ccdb834e6e | ||
|
|
42a711ee51 | ||
|
|
7ecf8b22cf | ||
|
|
595b2da433 | ||
|
|
a03a04bcc6 | ||
|
|
22713493d8 | ||
|
|
b72af5e185 | ||
|
|
c7f8258e0c | ||
|
|
21fd56d4d3 | ||
|
|
f6bbc6c89e | ||
|
|
6c996fb3cd | ||
|
|
ec7d8ac67b | ||
|
|
5ce06cf1b7 | ||
|
|
4866a3e0e9 | ||
|
|
ad9bf9b240 | ||
|
|
19488cf446 | ||
|
|
65664dafa4 | ||
|
|
f50148d17a | ||
|
|
9d376d29e1 | ||
|
|
e5955f82c7 | ||
|
|
514563cef0 | ||
|
|
a245ee28c1 | ||
|
|
245dcc6083 | ||
|
|
826e27c80e | ||
|
|
d96cfdf485 | ||
|
|
4629e88a28 | ||
|
|
e9d5fb25c8 | ||
|
|
a0a54eb0de | ||
|
|
f9310a9968 | ||
|
|
26cbcfb78c | ||
|
|
7fa78a9ac2 | ||
|
|
b334c96906 | ||
|
|
482a8ec209 | ||
|
|
2c3f36fd3f | ||
|
|
9ff4cfab0d | ||
|
|
503ae02cae | ||
|
|
695f7ec5f9 | ||
|
|
95aa1ab827 | ||
|
|
8da0743361 | ||
|
|
2b7c9378c0 | ||
|
|
7870084b9e | ||
|
|
8d9315b797 | ||
|
|
ca091a5f04 | ||
|
|
19cd609cba | ||
|
|
8b0f0a3713 | ||
|
|
49749d96a0 | ||
|
|
fd48bbd15e | ||
|
|
f48a48ec3f | ||
|
|
5a4eb0a076 | ||
|
|
2edf61614f | ||
|
|
0431089592 | ||
|
|
a52b2e2148 | ||
|
|
2393cdfe8c | ||
|
|
34ffdfa8be | ||
|
|
b2f6c23e2f | ||
|
|
7e17b4f2db | ||
|
|
c96b595f42 | ||
|
|
65c20638ce | ||
|
|
fd6c58805f | ||
|
|
7d4fcdea9e | ||
|
|
978304e224 | ||
|
|
31b64b3082 | ||
|
|
73566e756d | ||
|
|
a59bbe7635 | ||
|
|
faa43b6874 | ||
|
|
cc0c6fb5ac | ||
|
|
c5256d9b06 | ||
|
|
6093f4ec93 | ||
|
|
139dcde69c | ||
|
|
09fdc1b77a | ||
|
|
8d56c43f5e | ||
|
|
91f62672d3 | ||
|
|
6045f4ae59 | ||
|
|
d620231530 | ||
|
|
ee6316b26b | ||
|
|
b97713aac7 | ||
|
|
d6a31863c4 | ||
|
|
081b9a1975 | ||
|
|
f71f19e26c | ||
|
|
33ee6b7a02 | ||
|
|
dbd39b557e | ||
|
|
a8a00598e4 | ||
|
|
4881ddae87 | ||
|
|
546b772be3 | ||
|
|
5b2940e161 | ||
|
|
5c106cecf6 | ||
|
|
5a1f011db8 | ||
|
|
5751204a98 | ||
|
|
19f5fd8fe9 | ||
|
|
f0b914c721 | ||
|
|
99b683b843 | ||
|
|
331aa6c644 | ||
|
|
3fffea178f | ||
|
|
d596eeee6e | ||
|
|
60d93c4b2d | ||
|
|
755d021f8e | ||
|
|
c5beee5648 | ||
|
|
d12507e612 | ||
|
|
8af3a9ea51 | ||
|
|
603a3f8c9f | ||
|
|
577fe99a08 | ||
|
|
00d61286b1 | ||
|
|
76db682a25 | ||
|
|
f319966dca | ||
|
|
74e2d230e8 | ||
|
|
c53e7ad6c7 | ||
|
|
a2aa182cc4 | ||
|
|
45912d6837 | ||
|
|
0543f3c469 | ||
|
|
f809827acd | ||
|
|
a73bcc908f | ||
|
|
75c3cdc5af | ||
|
|
062cd77a98 | ||
|
|
5d7798c5fb | ||
|
|
280fc441a7 | ||
|
|
454b73aec3 | ||
|
|
885d816309 | ||
|
|
6f776ff004 | ||
|
|
73205c5f96 | ||
|
|
a58b3a133c | ||
|
|
169d5e26ca | ||
|
|
96a49d8a88 | ||
|
|
a423c23c1e | ||
|
|
42a3d38b48 | ||
|
|
797be71eb3 | ||
|
|
a30568ff57 | ||
|
|
9221dbf048 | ||
|
|
d07fe34a24 | ||
|
|
1c4707136c | ||
|
|
6340607827 | ||
|
|
0453b6cbc1 | ||
|
|
8f76e67f57 | ||
|
|
aeb85b47ba | ||
|
|
534f951165 | ||
|
|
33c55b8506 | ||
|
|
bba93a64c2 | ||
|
|
755dc84859 | ||
|
|
023afaf7ce | ||
|
|
37b8e3c025 | ||
|
|
60e15d5160 | ||
|
|
7d9834be87 | ||
|
|
696da8228e | ||
|
|
ee29f6d6d8 | ||
|
|
3711e13009 | ||
|
|
f25bd88030 | ||
|
|
10ab467160 | ||
|
|
1362764b2b | ||
|
|
18a1d15f78 | ||
|
|
bb3ffa9021 | ||
|
|
5b80ead18c | ||
|
|
42d62c38b9 | ||
|
|
b80c1bec4c | ||
|
|
bd93d6ec8a | ||
|
|
76a7160ea5 | ||
|
|
3556584478 | ||
|
|
9f6eb205b0 | ||
|
|
e617783f09 | ||
|
|
146db6836e | ||
|
|
cd8b116fd8 | ||
|
|
a733bb5865 | ||
|
|
375967b165 | ||
|
|
4186bc93a8 | ||
|
|
1994dab634 | ||
|
|
d320af06a7 | ||
|
|
1b461ca5be | ||
|
|
895fbae2dc | ||
|
|
21e555192c | ||
|
|
aa0d364fc2 | ||
|
|
43c4487804 | ||
|
|
5583bf3447 | ||
|
|
be921ab2d3 | ||
|
|
c48992ab94 | ||
|
|
f7f67f72a2 | ||
|
|
c3ab201517 | ||
|
|
9cc35a06ab | ||
|
|
004086b85b | ||
|
|
ae3525ca2c | ||
|
|
194c5acebb | ||
|
|
c0cac7591d | ||
|
|
9cb2d8aa4a | ||
|
|
314c9cd8f7 | ||
|
|
2bddce2087 | ||
|
|
c589743e2b | ||
|
|
e67033a207 |
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
* text eol=lf
|
||||
|
||||
*.pdf binary
|
||||
21
.github/actions/LICENSE
vendored
Normal file
21
.github/actions/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2023 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
40
.github/actions/bitcoin/action.yml
vendored
Normal file
40
.github/actions/bitcoin/action.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: bitcoin-regtest
|
||||
description: Spawns a regtest Bitcoin daemon
|
||||
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to download and run"
|
||||
required: false
|
||||
default: "27.0"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Bitcoin Daemon Cache
|
||||
id: cache-bitcoind
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
||||
with:
|
||||
path: bitcoin.tar.gz
|
||||
key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||
|
||||
- name: Download the Bitcoin Daemon
|
||||
if: steps.cache-bitcoind.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
RUNNER_OS=linux
|
||||
RUNNER_ARCH=x86_64
|
||||
FILE=bitcoin-${{ inputs.version }}-$RUNNER_ARCH-$RUNNER_OS-gnu.tar.gz
|
||||
|
||||
wget https://bitcoincore.org/bin/bitcoin-core-${{ inputs.version }}/$FILE
|
||||
mv $FILE bitcoin.tar.gz
|
||||
|
||||
- name: Extract the Bitcoin Daemon
|
||||
shell: bash
|
||||
run: |
|
||||
tar xzvf bitcoin.tar.gz
|
||||
cd bitcoin-${{ inputs.version }}
|
||||
sudo mv bin/* /bin && sudo mv lib/* /lib
|
||||
|
||||
- name: Bitcoin Regtest Daemon
|
||||
shell: bash
|
||||
run: PATH=$PATH:/usr/bin ./orchestration/dev/coins/bitcoin/run.sh -daemon
|
||||
49
.github/actions/build-dependencies/action.yml
vendored
Normal file
49
.github/actions/build-dependencies/action.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: build-dependencies
|
||||
description: Installs build dependencies for Serai
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Remove unused packages
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt remove -y "*msbuild*" "*powershell*" "*nuget*" "*bazel*" "*ansible*" "*terraform*" "*heroku*" "*aws*" azure-cli
|
||||
sudo apt remove -y "*nodejs*" "*npm*" "*yarn*" "*java*" "*kotlin*" "*golang*" "*swift*" "*julia*" "*fortran*" "*android*"
|
||||
sudo apt remove -y "*apache2*" "*nginx*" "*firefox*" "*chromium*" "*chrome*" "*edge*"
|
||||
sudo apt remove -y "*qemu*" "*sql*" "*texinfo*" "*imagemagick*"
|
||||
sudo apt autoremove -y
|
||||
sudo apt clean
|
||||
docker system prune -a --volumes
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: Remove unused packages
|
||||
shell: bash
|
||||
run: |
|
||||
(gem uninstall -aIx) || (exit 0)
|
||||
brew uninstall --force "*msbuild*" "*powershell*" "*nuget*" "*bazel*" "*ansible*" "*terraform*" "*heroku*" "*aws*" azure-cli
|
||||
brew uninstall --force "*nodejs*" "*npm*" "*yarn*" "*java*" "*kotlin*" "*golang*" "*swift*" "*julia*" "*fortran*" "*android*"
|
||||
brew uninstall --force "*apache2*" "*nginx*" "*firefox*" "*chromium*" "*chrome*" "*edge*"
|
||||
brew uninstall --force "*qemu*" "*sql*" "*texinfo*" "*imagemagick*"
|
||||
brew cleanup
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo apt install -y ca-certificates protobuf-compiler
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
choco install protoc
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
brew install protobuf
|
||||
fi
|
||||
|
||||
- name: Install solc
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install svm-rs
|
||||
svm install 0.8.25
|
||||
svm use 0.8.25
|
||||
|
||||
# - name: Cache Rust
|
||||
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43
|
||||
49
.github/actions/monero-wallet-rpc/action.yml
vendored
Normal file
49
.github/actions/monero-wallet-rpc/action.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: monero-wallet-rpc
|
||||
description: Spawns a Monero Wallet-RPC.
|
||||
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to download and run"
|
||||
required: false
|
||||
default: v0.18.3.1
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Monero Wallet RPC Cache
|
||||
id: cache-monero-wallet-rpc
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
||||
with:
|
||||
path: monero-wallet-rpc
|
||||
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||
|
||||
- name: Download the Monero Wallet RPC
|
||||
if: steps.cache-monero-wallet-rpc.outputs.cache-hit != 'true'
|
||||
# Calculates OS/ARCH to demonstrate it, yet then locks to linux-x64 due
|
||||
# to the contained folder not following the same naming scheme and
|
||||
# requiring further expansion not worth doing right now
|
||||
shell: bash
|
||||
run: |
|
||||
RUNNER_OS=${{ runner.os }}
|
||||
RUNNER_ARCH=${{ runner.arch }}
|
||||
|
||||
RUNNER_OS=${RUNNER_OS,,}
|
||||
RUNNER_ARCH=${RUNNER_ARCH,,}
|
||||
|
||||
RUNNER_OS=linux
|
||||
RUNNER_ARCH=x64
|
||||
|
||||
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-${{ inputs.version }}.tar.bz2
|
||||
wget https://downloads.getmonero.org/cli/$FILE
|
||||
tar -xvf $FILE
|
||||
|
||||
mv monero-x86_64-linux-gnu-${{ inputs.version }}/monero-wallet-rpc monero-wallet-rpc
|
||||
|
||||
- name: Monero Wallet RPC
|
||||
shell: bash
|
||||
run: |
|
||||
./monero-wallet-rpc --allow-mismatched-daemon-version \
|
||||
--daemon-address 0.0.0.0:18081 --daemon-login serai:seraidex \
|
||||
--disable-rpc-login --rpc-bind-port 18082 \
|
||||
--wallet-dir ./ \
|
||||
--detach
|
||||
46
.github/actions/monero/action.yml
vendored
Normal file
46
.github/actions/monero/action.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: monero-regtest
|
||||
description: Spawns a regtest Monero daemon
|
||||
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to download and run"
|
||||
required: false
|
||||
default: v0.18.3.1
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Monero Daemon Cache
|
||||
id: cache-monerod
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
||||
with:
|
||||
path: /usr/bin/monerod
|
||||
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||
|
||||
- name: Download the Monero Daemon
|
||||
if: steps.cache-monerod.outputs.cache-hit != 'true'
|
||||
# Calculates OS/ARCH to demonstrate it, yet then locks to linux-x64 due
|
||||
# to the contained folder not following the same naming scheme and
|
||||
# requiring further expansion not worth doing right now
|
||||
shell: bash
|
||||
run: |
|
||||
RUNNER_OS=${{ runner.os }}
|
||||
RUNNER_ARCH=${{ runner.arch }}
|
||||
|
||||
RUNNER_OS=${RUNNER_OS,,}
|
||||
RUNNER_ARCH=${RUNNER_ARCH,,}
|
||||
|
||||
RUNNER_OS=linux
|
||||
RUNNER_ARCH=x64
|
||||
|
||||
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-${{ inputs.version }}.tar.bz2
|
||||
wget https://downloads.getmonero.org/cli/$FILE
|
||||
tar -xvf $FILE
|
||||
|
||||
sudo mv monero-x86_64-linux-gnu-${{ inputs.version }}/monerod /usr/bin/monerod
|
||||
sudo chmod 777 /usr/bin/monerod
|
||||
sudo chmod +x /usr/bin/monerod
|
||||
|
||||
- name: Monero Regtest Daemon
|
||||
shell: bash
|
||||
run: PATH=$PATH:/usr/bin ./orchestration/dev/coins/monero/run.sh --detach
|
||||
38
.github/actions/test-dependencies/action.yml
vendored
Normal file
38
.github/actions/test-dependencies/action.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: test-dependencies
|
||||
description: Installs test dependencies for Serai
|
||||
|
||||
inputs:
|
||||
monero-version:
|
||||
description: "Monero version to download and run as a regtest node"
|
||||
required: false
|
||||
default: v0.18.3.1
|
||||
|
||||
bitcoin-version:
|
||||
description: "Bitcoin version to download and run as a regtest node"
|
||||
required: false
|
||||
default: "27.0"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Install Foundry
|
||||
uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773
|
||||
with:
|
||||
version: nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9
|
||||
cache: false
|
||||
|
||||
- name: Run a Monero Regtest Node
|
||||
uses: ./.github/actions/monero
|
||||
with:
|
||||
version: ${{ inputs.monero-version }}
|
||||
|
||||
- name: Run a Bitcoin Regtest Node
|
||||
uses: ./.github/actions/bitcoin
|
||||
with:
|
||||
version: ${{ inputs.bitcoin-version }}
|
||||
|
||||
- name: Run a Monero Wallet-RPC
|
||||
uses: ./.github/actions/monero-wallet-rpc
|
||||
1
.github/nightly-version
vendored
Normal file
1
.github/nightly-version
vendored
Normal file
@@ -0,0 +1 @@
|
||||
nightly-2024-02-07
|
||||
36
.github/workflows/coins-tests.yml
vendored
Normal file
36
.github/workflows/coins-tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: coins/ Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-coins:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Test Dependencies
|
||||
uses: ./.github/actions/test-dependencies
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||
-p bitcoin-serai \
|
||||
-p alloy-simple-request-transport \
|
||||
-p ethereum-serai \
|
||||
-p monero-generators \
|
||||
-p monero-serai
|
||||
32
.github/workflows/common-tests.yml
vendored
Normal file
32
.github/workflows/common-tests.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: common/ Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-common:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||
-p std-shims \
|
||||
-p zalloc \
|
||||
-p serai-db \
|
||||
-p serai-env \
|
||||
-p simple-request
|
||||
40
.github/workflows/coordinator-tests.yml
vendored
Normal file
40
.github/workflows/coordinator-tests.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Coordinator Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "message-queue/**"
|
||||
- "coordinator/**"
|
||||
- "orchestration/**"
|
||||
- "tests/docker/**"
|
||||
- "tests/coordinator/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "message-queue/**"
|
||||
- "coordinator/**"
|
||||
- "orchestration/**"
|
||||
- "tests/docker/**"
|
||||
- "tests/coordinator/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Install Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run coordinator Docker tests
|
||||
run: cd tests/coordinator && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
||||
40
.github/workflows/crypto-tests.yml
vendored
Normal file
40
.github/workflows/crypto-tests.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: crypto/ Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-crypto:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||
-p flexible-transcript \
|
||||
-p ff-group-tests \
|
||||
-p dalek-ff-group \
|
||||
-p minimal-ed448 \
|
||||
-p ciphersuite \
|
||||
-p multiexp \
|
||||
-p schnorr-signatures \
|
||||
-p dleq \
|
||||
-p dkg \
|
||||
-p modular-frost \
|
||||
-p frost-schnorrkel
|
||||
24
.github/workflows/daily-deny.yml
vendored
Normal file
24
.github/workflows/daily-deny.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Daily Deny Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
deny:
|
||||
name: Run cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Advisory Cache
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
||||
with:
|
||||
path: ~/.cargo/advisory-db
|
||||
key: rust-advisory-db
|
||||
|
||||
- name: Install cargo deny
|
||||
run: cargo install --locked cargo-deny
|
||||
|
||||
- name: Run cargo deny
|
||||
run: cargo deny -L error --all-features check
|
||||
22
.github/workflows/full-stack-tests.yml
vendored
Normal file
22
.github/workflows/full-stack-tests.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Full Stack Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
pull_request:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Install Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Full Stack Docker tests
|
||||
run: cd tests/full-stack && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
||||
83
.github/workflows/lint.yml
vendored
Normal file
83
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-13, macos-14, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Get nightly version to use
|
||||
id: nightly
|
||||
shell: bash
|
||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Install nightly rust
|
||||
run: rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -t wasm32-unknown-unknown -c clippy
|
||||
|
||||
- name: Run Clippy
|
||||
run: cargo +${{ steps.nightly.outputs.version }} clippy --all-features --all-targets -- -D warnings -A clippy::items_after_test_module
|
||||
|
||||
# Also verify the lockfile isn't dirty
|
||||
# This happens when someone edits a Cargo.toml yet doesn't do anything
|
||||
# which causes the lockfile to be updated
|
||||
# The above clippy run will cause it to be updated, so checking there's
|
||||
# no differences present now performs the desired check
|
||||
- name: Verify lockfile
|
||||
shell: bash
|
||||
run: git diff | wc -l | LC_ALL="en_US.utf8" grep -x -e "^[ ]*0"
|
||||
|
||||
deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Advisory Cache
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
||||
with:
|
||||
path: ~/.cargo/advisory-db
|
||||
key: rust-advisory-db
|
||||
|
||||
- name: Install cargo deny
|
||||
run: cargo install --locked cargo-deny
|
||||
|
||||
- name: Run cargo deny
|
||||
run: cargo deny -L error --all-features check
|
||||
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Get nightly version to use
|
||||
id: nightly
|
||||
shell: bash
|
||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install nightly rust
|
||||
run: rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -c rustfmt
|
||||
|
||||
- name: Run rustfmt
|
||||
run: cargo +${{ steps.nightly.outputs.version }} fmt -- --check
|
||||
|
||||
machete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
- name: Verify all dependencies are in use
|
||||
run: |
|
||||
cargo install cargo-machete
|
||||
cargo machete
|
||||
36
.github/workflows/message-queue-tests.yml
vendored
Normal file
36
.github/workflows/message-queue-tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Message Queue Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "message-queue/**"
|
||||
- "orchestration/**"
|
||||
- "tests/docker/**"
|
||||
- "tests/message-queue/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "message-queue/**"
|
||||
- "orchestration/**"
|
||||
- "tests/docker/**"
|
||||
- "tests/message-queue/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Install Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run message-queue Docker tests
|
||||
run: cd tests/message-queue && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
||||
26
.github/workflows/mini-tests.yml
vendored
Normal file
26
.github/workflows/mini-tests.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: mini/ Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "mini/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "mini/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-common:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Tests
|
||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p mini-serai
|
||||
56
.github/workflows/monero-tests.yaml
vendored
Normal file
56
.github/workflows/monero-tests.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Monero Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "coins/monero/**"
|
||||
- "processor/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "coins/monero/**"
|
||||
- "processor/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# Only run these once since they will be consistent regardless of any node
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Test Dependencies
|
||||
uses: ./.github/actions/test-dependencies
|
||||
|
||||
- name: Run Unit Tests Without Features
|
||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --lib
|
||||
|
||||
# Doesn't run unit tests with features as the tests workflow will
|
||||
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
# Test against all supported protocol versions
|
||||
strategy:
|
||||
matrix:
|
||||
version: [v0.17.3.2, v0.18.2.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Test Dependencies
|
||||
uses: ./.github/actions/test-dependencies
|
||||
with:
|
||||
monero-version: ${{ matrix.version }}
|
||||
|
||||
- name: Run Integration Tests Without Features
|
||||
# Runs with the binaries feature so the binaries build
|
||||
# https://github.com/rust-lang/cargo/issues/8396
|
||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --features binaries --test '*'
|
||||
|
||||
- name: Run Integration Tests
|
||||
# Don't run if the the tests workflow also will
|
||||
if: ${{ matrix.version != 'v0.18.2.0' }}
|
||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
|
||||
53
.github/workflows/monthly-nightly-update.yml
vendored
Normal file
53
.github/workflows/monthly-nightly-update.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Monthly Nightly Update
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 1 * *"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
name: Update nightly
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Write nightly version
|
||||
run: echo $(date +"nightly-%Y-%m"-01) > .github/nightly-version
|
||||
|
||||
- name: Create the commit
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "<>"
|
||||
|
||||
git checkout -b $(date +"nightly-%Y-%m")
|
||||
|
||||
git add .github/nightly-version
|
||||
git commit -m "Update nightly"
|
||||
git push -u origin $(date +"nightly-%Y-%m")
|
||||
|
||||
- name: Pull Request
|
||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410
|
||||
with:
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
|
||||
const result = await github.rest.pulls.create({
|
||||
title: (new Date()).toLocaleString(
|
||||
false,
|
||||
{ month: "long", year: "numeric" }
|
||||
) + " - Rust Nightly Update",
|
||||
owner,
|
||||
repo,
|
||||
head: "nightly-" + (new Date()).toISOString().split("-").splice(0, 2).join("-"),
|
||||
base: "develop",
|
||||
body: "PR auto-generated by a GitHub workflow."
|
||||
});
|
||||
|
||||
github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: result.data.number,
|
||||
labels: ["improvement"]
|
||||
});
|
||||
35
.github/workflows/no-std.yml
vendored
Normal file
35
.github/workflows/no-std.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: no-std build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "tests/no-std/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "tests/no-std/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Install Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Install RISC-V Toolchain
|
||||
run: sudo apt update && sudo apt install -y gcc-riscv64-unknown-elf gcc-multilib && rustup target add riscv32imac-unknown-none-elf
|
||||
|
||||
- name: Verify no-std builds
|
||||
run: cd tests/no-std && CFLAGS=-I/usr/include cargo build --target riscv32imac-unknown-none-elf
|
||||
90
.github/workflows/pages.yml
vendored
Normal file
90
.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2022 just-the-docs
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
|
||||
name: Deploy Jekyll site to Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
paths:
|
||||
- "docs/**"
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
cache-version: 0
|
||||
working-directory: "${{ github.workspace }}/docs"
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Build with Jekyll
|
||||
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
|
||||
env:
|
||||
JEKYLL_ENV: production
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: "docs/_site/"
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
40
.github/workflows/processor-tests.yml
vendored
Normal file
40
.github/workflows/processor-tests.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Processor Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "message-queue/**"
|
||||
- "processor/**"
|
||||
- "orchestration/**"
|
||||
- "tests/docker/**"
|
||||
- "tests/processor/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "message-queue/**"
|
||||
- "processor/**"
|
||||
- "orchestration/**"
|
||||
- "tests/docker/**"
|
||||
- "tests/processor/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Install Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run processor Docker tests
|
||||
run: cd tests/processor && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
||||
36
.github/workflows/reproducible-runtime.yml
vendored
Normal file
36
.github/workflows/reproducible-runtime.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Reproducible Runtime
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "Cargo.lock"
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "substrate/**"
|
||||
- "orchestration/runtime/**"
|
||||
- "tests/reproducible-runtime/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "Cargo.lock"
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "substrate/**"
|
||||
- "orchestration/runtime/**"
|
||||
- "tests/reproducible-runtime/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Install Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Reproducible Runtime tests
|
||||
run: cd tests/reproducible-runtime && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
||||
83
.github/workflows/tests.yml
vendored
Normal file
83
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "message-queue/**"
|
||||
- "processor/**"
|
||||
- "coordinator/**"
|
||||
- "substrate/**"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "crypto/**"
|
||||
- "coins/**"
|
||||
- "message-queue/**"
|
||||
- "processor/**"
|
||||
- "coordinator/**"
|
||||
- "substrate/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-infra:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||
-p serai-message-queue \
|
||||
-p serai-processor-messages \
|
||||
-p serai-processor \
|
||||
-p tendermint-machine \
|
||||
-p tributary-chain \
|
||||
-p serai-coordinator \
|
||||
-p serai-orchestrator \
|
||||
-p serai-docker-tests
|
||||
|
||||
test-substrate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||
-p serai-primitives \
|
||||
-p serai-coins-primitives \
|
||||
-p serai-coins-pallet \
|
||||
-p serai-dex-pallet \
|
||||
-p serai-validator-sets-primitives \
|
||||
-p serai-validator-sets-pallet \
|
||||
-p serai-in-instructions-primitives \
|
||||
-p serai-in-instructions-pallet \
|
||||
-p serai-signals-primitives \
|
||||
-p serai-signals-pallet \
|
||||
-p serai-abi \
|
||||
-p serai-runtime \
|
||||
-p serai-node
|
||||
|
||||
test-serai-client:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||
|
||||
- name: Build Dependencies
|
||||
uses: ./.github/actions/build-dependencies
|
||||
|
||||
- name: Run Tests
|
||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,2 +1,7 @@
|
||||
target
|
||||
Cargo.lock
|
||||
Dockerfile
|
||||
Dockerfile.fast-epoch
|
||||
!orchestration/runtime/Dockerfile
|
||||
.test-logs
|
||||
|
||||
.vscode
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "coins/monero/c/monero"]
|
||||
path = coins/monero/c/monero
|
||||
url = https://github.com/monero-project/monero
|
||||
17
.rustfmt.toml
Normal file
17
.rustfmt.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
edition = "2021"
|
||||
tab_spaces = 2
|
||||
|
||||
max_width = 100
|
||||
# Let the developer decide based on the 100 char line limit
|
||||
use_small_heuristics = "Max"
|
||||
|
||||
error_on_line_overflow = true
|
||||
error_on_unformatted = true
|
||||
|
||||
imports_granularity = "Crate"
|
||||
reorder_imports = false
|
||||
reorder_modules = false
|
||||
|
||||
unstable_features = true
|
||||
spaces_around_ranges = true
|
||||
binop_separator = "Back"
|
||||
37
CONTRIBUTING.md
Normal file
37
CONTRIBUTING.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Contributing
|
||||
|
||||
Contributions come in a variety of forms. Developing Serai, helping document it,
|
||||
using its libraries in another project, using and testing it, and simply sharing
|
||||
it are all valuable ways of contributing.
|
||||
|
||||
This document will specifically focus on contributions to this repository in the
|
||||
form of code and documentation.
|
||||
|
||||
### Rules
|
||||
|
||||
- Stable native Rust, nightly wasm and tools.
|
||||
- `cargo fmt` must be used.
|
||||
- `cargo clippy` must pass, except for the ignored rules (`type_complexity` and
|
||||
`dead_code`).
|
||||
- The CI must pass.
|
||||
|
||||
- Only use uppercase variable names when relevant to cryptography.
|
||||
|
||||
- Use a two-space ident when possible.
|
||||
- Put a space after comment markers.
|
||||
- Don't use multiple newlines between sections of code.
|
||||
- Have a newline before EOF.
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Sort inputs as core, std, third party, and then Serai.
|
||||
- Comment code reasonably.
|
||||
- Include tests for new features.
|
||||
- Sign commits.
|
||||
|
||||
### Submission
|
||||
|
||||
All submissions should be through GitHub. Contributions to a crate will be
|
||||
licensed according to the crate's existing license, with the crate's copyright
|
||||
holders (distinct from authors) having the right to re-license the crate via a
|
||||
unanimous decision.
|
||||
10959
Cargo.lock
generated
Normal file
10959
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
168
Cargo.toml
168
Cargo.toml
@@ -1,22 +1,184 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
# Version patches
|
||||
"patches/zstd",
|
||||
"patches/rocksdb",
|
||||
"patches/proc-macro-crate",
|
||||
|
||||
# std patches
|
||||
"patches/matches",
|
||||
"patches/is-terminal",
|
||||
|
||||
# Rewrites/redirects
|
||||
"patches/option-ext",
|
||||
"patches/directories-next",
|
||||
|
||||
"common/std-shims",
|
||||
"common/zalloc",
|
||||
"common/db",
|
||||
"common/env",
|
||||
"common/request",
|
||||
|
||||
"crypto/transcript",
|
||||
|
||||
"crypto/ff-group-tests",
|
||||
"crypto/dalek-ff-group",
|
||||
"crypto/tables",
|
||||
"crypto/ed448",
|
||||
"crypto/ciphersuite",
|
||||
|
||||
"crypto/multiexp",
|
||||
|
||||
"crypto/schnorr",
|
||||
"crypto/dleq",
|
||||
"crypto/dkg",
|
||||
"crypto/frost",
|
||||
"crypto/schnorrkel",
|
||||
|
||||
"coins/bitcoin",
|
||||
"coins/ethereum/alloy-simple-request-transport",
|
||||
"coins/ethereum",
|
||||
"coins/monero/generators",
|
||||
"coins/monero",
|
||||
|
||||
"message-queue",
|
||||
|
||||
"processor/messages",
|
||||
"processor",
|
||||
|
||||
"coordinator/tributary/tendermint",
|
||||
"coordinator/tributary",
|
||||
"coordinator",
|
||||
|
||||
"substrate/primitives",
|
||||
|
||||
"substrate/coins/primitives",
|
||||
"substrate/coins/pallet",
|
||||
|
||||
"substrate/in-instructions/primitives",
|
||||
"substrate/in-instructions/pallet",
|
||||
|
||||
"substrate/validator-sets/primitives",
|
||||
"substrate/validator-sets/pallet",
|
||||
|
||||
"substrate/signals/primitives",
|
||||
"substrate/signals/pallet",
|
||||
|
||||
"substrate/abi",
|
||||
|
||||
"substrate/runtime",
|
||||
"substrate/consensus",
|
||||
"substrate/node"
|
||||
"substrate/node",
|
||||
|
||||
"substrate/client",
|
||||
|
||||
"orchestration",
|
||||
|
||||
"mini",
|
||||
|
||||
"tests/no-std",
|
||||
|
||||
"tests/docker",
|
||||
"tests/message-queue",
|
||||
"tests/processor",
|
||||
"tests/coordinator",
|
||||
"tests/full-stack",
|
||||
"tests/reproducible-runtime",
|
||||
]
|
||||
|
||||
# Always compile Monero (and a variety of dependencies) with optimizations due
|
||||
# to the extensive operations required for Bulletproofs
|
||||
[profile.dev.package]
|
||||
subtle = { opt-level = 3 }
|
||||
curve25519-dalek = { opt-level = 3 }
|
||||
|
||||
ff = { opt-level = 3 }
|
||||
group = { opt-level = 3 }
|
||||
|
||||
crypto-bigint = { opt-level = 3 }
|
||||
dalek-ff-group = { opt-level = 3 }
|
||||
minimal-ed448 = { opt-level = 3 }
|
||||
|
||||
multiexp = { opt-level = 3 }
|
||||
|
||||
monero-serai = { opt-level = 3 }
|
||||
|
||||
[profile.release]
|
||||
panic = "unwind"
|
||||
|
||||
[patch.crates-io]
|
||||
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
|
||||
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" }
|
||||
|
||||
# Needed due to dockertest's usage of `Rc`s when we need `Arc`s
|
||||
dockertest = { git = "https://github.com/kayabaNerve/dockertest-rs", branch = "arc" }
|
||||
|
||||
# wasmtime pulls in an old version for this
|
||||
zstd = { path = "patches/zstd" }
|
||||
# Needed for WAL compression
|
||||
rocksdb = { path = "patches/rocksdb" }
|
||||
# proc-macro-crate 2 binds to an old version of toml for msrv so we patch to 3
|
||||
proc-macro-crate = { path = "patches/proc-macro-crate" }
|
||||
|
||||
# is-terminal now has an std-based solution with an equivalent API
|
||||
is-terminal = { path = "patches/is-terminal" }
|
||||
# So does matches
|
||||
matches = { path = "patches/matches" }
|
||||
|
||||
# directories-next was created because directories was unmaintained
|
||||
# directories-next is now unmaintained while directories is maintained
|
||||
# The directories author pulls in ridiculously pointless crates and prefers
|
||||
# copyleft licenses
|
||||
# The following two patches resolve everything
|
||||
option-ext = { path = "patches/option-ext" }
|
||||
directories-next = { path = "patches/directories-next" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_or_default = "allow"
|
||||
borrow_as_ptr = "deny"
|
||||
cast_lossless = "deny"
|
||||
cast_possible_truncation = "deny"
|
||||
cast_possible_wrap = "deny"
|
||||
cast_precision_loss = "deny"
|
||||
cast_ptr_alignment = "deny"
|
||||
cast_sign_loss = "deny"
|
||||
checked_conversions = "deny"
|
||||
cloned_instead_of_copied = "deny"
|
||||
enum_glob_use = "deny"
|
||||
expl_impl_clone_on_copy = "deny"
|
||||
explicit_into_iter_loop = "deny"
|
||||
explicit_iter_loop = "deny"
|
||||
flat_map_option = "deny"
|
||||
float_cmp = "deny"
|
||||
fn_params_excessive_bools = "deny"
|
||||
ignored_unit_patterns = "deny"
|
||||
implicit_clone = "deny"
|
||||
inefficient_to_string = "deny"
|
||||
invalid_upcast_comparisons = "deny"
|
||||
large_stack_arrays = "deny"
|
||||
linkedlist = "deny"
|
||||
macro_use_imports = "deny"
|
||||
manual_instant_elapsed = "deny"
|
||||
manual_let_else = "deny"
|
||||
manual_ok_or = "deny"
|
||||
manual_string_new = "deny"
|
||||
map_unwrap_or = "deny"
|
||||
match_bool = "deny"
|
||||
match_same_arms = "deny"
|
||||
missing_fields_in_debug = "deny"
|
||||
needless_continue = "deny"
|
||||
needless_pass_by_value = "deny"
|
||||
ptr_cast_constness = "deny"
|
||||
range_minus_one = "deny"
|
||||
range_plus_one = "deny"
|
||||
redundant_closure_for_method_calls = "deny"
|
||||
redundant_else = "deny"
|
||||
string_add_assign = "deny"
|
||||
unchecked_duration_subtraction = "deny"
|
||||
uninlined_format_args = "deny"
|
||||
unnecessary_box_returns = "deny"
|
||||
unnecessary_join = "deny"
|
||||
unnecessary_wraps = "deny"
|
||||
unnested_or_patterns = "deny"
|
||||
unused_async = "deny"
|
||||
unused_self = "deny"
|
||||
zero_sized_map_values = "deny"
|
||||
|
||||
8
LICENSE
Normal file
8
LICENSE
Normal file
@@ -0,0 +1,8 @@
|
||||
Serai crates are licensed under one of two licenses, either MIT or AGPL-3.0,
|
||||
depending on the crate in question. Each crate declares their license in their
|
||||
`Cargo.toml` and includes a `LICENSE` file detailing its status. Additionally,
|
||||
a full copy of the AGPL-3.0 License is included in the root of this repository
|
||||
as a reference text. This copy should be provided with any distribution of a
|
||||
crate licensed under the AGPL-3.0, as per its terms.
|
||||
|
||||
The GitHub actions (`.github/actions`) are licensed under the MIT license.
|
||||
62
README.md
62
README.md
@@ -1,22 +1,66 @@
|
||||
# Serai
|
||||
|
||||
Serai is a new DEX, built from the ground up, initially planning on listing
|
||||
Bitcoin, Ethereum, Monero, DAI, and USDC, offering a liquidity pool trading
|
||||
experience. Funds are stored in an economically secured threshold multisig
|
||||
Bitcoin, Ethereum, DAI, and Monero, offering a liquidity-pool-based trading
|
||||
experience. Funds are stored in an economically secured threshold-multisig
|
||||
wallet.
|
||||
|
||||
[Getting Started](spec/Getting%20Started.md)
|
||||
|
||||
### Layout
|
||||
|
||||
- `docs` - Documentation on the Serai protocol.
|
||||
- `audits`: Audits for various parts of Serai.
|
||||
|
||||
- `coins` - Various coin libraries intended for usage in Serai yet also by the
|
||||
wider community. This means they will always support the functionality Serai
|
||||
needs, yet won't disadvantage other use cases when possible.
|
||||
- `spec`: The specification of the Serai protocol, both internally and as
|
||||
networked.
|
||||
|
||||
- `crypto` - A series of composable cryptographic libraries built around the
|
||||
`ff`/`group` APIs achieving a variety of tasks. These range from generic
|
||||
- `docs`: User-facing documentation on the Serai protocol.
|
||||
|
||||
- `common`: Crates containing utilities common to a variety of areas under
|
||||
Serai, none neatly fitting under another category.
|
||||
|
||||
- `crypto`: A series of composable cryptographic libraries built around the
|
||||
`ff`/`group` APIs, achieving a variety of tasks. These range from generic
|
||||
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
||||
needed for Bitcoin-Monero atomic swaps.
|
||||
|
||||
- `processor` - A generic chain processor to process data for Serai and process
|
||||
- `coins`: Various coin libraries intended for usage in Serai yet also by the
|
||||
wider community. This means they will always support the functionality Serai
|
||||
needs, yet won't disadvantage other use cases when possible.
|
||||
|
||||
- `message-queue`: An ordered message server so services can talk to each other,
|
||||
even when the other is offline.
|
||||
|
||||
- `processor`: A generic chain processor to process data for Serai and process
|
||||
events from Serai, executing transactions as expected and needed.
|
||||
|
||||
- `coordinator`: A service to manage processors and communicate over a P2P
|
||||
network with other validators.
|
||||
|
||||
- `substrate`: Substrate crates used to instantiate the Serai network.
|
||||
|
||||
- `orchestration`: Dockerfiles and scripts to deploy a Serai node/test
|
||||
environment.
|
||||
|
||||
- `tests`: Tests for various crates. Generally, `crate/src/tests` is used, or
|
||||
`crate/tests`, yet any tests requiring crates' binaries are placed here.
|
||||
|
||||
### Security
|
||||
|
||||
Serai hosts a bug bounty program via
|
||||
[Immunefi](https://immunefi.com/bounty/serai/). For in-scope critical
|
||||
vulnerabilities, we will reward whitehats with up to $30,000.
|
||||
|
||||
Anything not in-scope should still be submitted through Immunefi, with rewards
|
||||
issued at the discretion of the Immunefi program managers.
|
||||
|
||||
### Links
|
||||
|
||||
- [Website](https://serai.exchange/): https://serai.exchange/
|
||||
- [Immunefi](https://immunefi.com/bounty/serai/): https://immunefi.com/bounty/serai/
|
||||
- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX
|
||||
- [Mastodon](https://cryptodon.lol/@serai): https://cryptodon.lol/@serai
|
||||
- [Discord](https://discord.gg/mpEUtJR3vz): https://discord.gg/mpEUtJR3vz
|
||||
- [Matrix](https://matrix.to/#/#serai:matrix.org): https://matrix.to/#/#serai:matrix.org
|
||||
- [Reddit](https://www.reddit.com/r/SeraiDEX/): https://www.reddit.com/r/SeraiDEX/
|
||||
- [Telegram](https://t.me/SeraiDEX): https://t.me/SeraiDEX
|
||||
|
||||
BIN
audits/Cypher Stack coins bitcoin August 2023/Audit.pdf
Normal file
BIN
audits/Cypher Stack coins bitcoin August 2023/Audit.pdf
Normal file
Binary file not shown.
21
audits/Cypher Stack coins bitcoin August 2023/LICENSE
Normal file
21
audits/Cypher Stack coins bitcoin August 2023/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Cypher Stack
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6
audits/Cypher Stack coins bitcoin August 2023/README.md
Normal file
6
audits/Cypher Stack coins bitcoin August 2023/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Cypher Stack /coins/bitcoin Audit, August 2023
|
||||
|
||||
This audit was over the /coins/bitcoin folder. It is encompassing up to commit
|
||||
5121ca75199dff7bd34230880a1fdd793012068c.
|
||||
|
||||
Please see https://github.com/cypherstack/serai-btc-audit for provenance.
|
||||
BIN
audits/Cypher Stack crypto March 2023/Audit.pdf
Normal file
BIN
audits/Cypher Stack crypto March 2023/Audit.pdf
Normal file
Binary file not shown.
21
audits/Cypher Stack crypto March 2023/LICENSE
Normal file
21
audits/Cypher Stack crypto March 2023/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Cypher Stack
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
7
audits/Cypher Stack crypto March 2023/README.md
Normal file
7
audits/Cypher Stack crypto March 2023/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Cypher Stack /crypto Audit, March 2023
|
||||
|
||||
This audit was over the /crypto folder, excluding the ed448 crate, the `Ed448`
|
||||
ciphersuite in the ciphersuite crate, and the `dleq/experimental` feature. It is
|
||||
encompassing up to commit 669d2dbffc1dafb82a09d9419ea182667115df06.
|
||||
|
||||
Please see https://github.com/cypherstack/serai-audit for provenance.
|
||||
68
coins/bitcoin/Cargo.toml
Normal file
68
coins/bitcoin/Cargo.toml
Normal file
@@ -0,0 +1,68 @@
|
||||
[package]
|
||||
name = "bitcoin-serai"
|
||||
version = "0.3.0"
|
||||
description = "A Bitcoin library for FROST-signing transactions"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/bitcoin"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Vrx <vrx00@proton.me>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.74"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
std-shims = { version = "0.1.1", path = "../../common/std-shims", default-features = false }
|
||||
|
||||
thiserror = { version = "1", default-features = false, optional = true }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false }
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
bitcoin = { version = "0.31", default-features = false, features = ["no-std"] }
|
||||
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["secp256k1"], optional = true }
|
||||
|
||||
hex = { version = "0.4", default-features = false, optional = true }
|
||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", default-features = false, optional = true }
|
||||
simple-request = { path = "../../common/request", version = "0.1", default-features = false, features = ["tls", "basic-auth"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
secp256k1 = { version = "0.28", default-features = false, features = ["std"] }
|
||||
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
||||
|
||||
tokio = { version = "1", features = ["macros"] }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"std-shims/std",
|
||||
|
||||
"thiserror",
|
||||
|
||||
"zeroize/std",
|
||||
"rand_core/std",
|
||||
|
||||
"bitcoin/std",
|
||||
"bitcoin/serde",
|
||||
|
||||
"k256/std",
|
||||
|
||||
"transcript/std",
|
||||
"frost",
|
||||
|
||||
"hex/std",
|
||||
"serde/std",
|
||||
"serde_json/std",
|
||||
"simple-request",
|
||||
]
|
||||
hazmat = []
|
||||
default = ["std"]
|
||||
21
coins/bitcoin/LICENSE
Normal file
21
coins/bitcoin/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2023 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
coins/bitcoin/README.md
Normal file
4
coins/bitcoin/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# bitcoin-serai
|
||||
|
||||
An application of [modular-frost](https://docs.rs/modular-frost) to Bitcoin
|
||||
transactions, enabling extremely-efficient multisigs.
|
||||
166
coins/bitcoin/src/crypto.rs
Normal file
166
coins/bitcoin/src/crypto.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use k256::{
|
||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
||||
ProjectivePoint,
|
||||
};
|
||||
|
||||
use bitcoin::key::XOnlyPublicKey;
|
||||
|
||||
/// Get the x coordinate of a non-infinity, even point. Panics on invalid input.
|
||||
pub fn x(key: &ProjectivePoint) -> [u8; 32] {
|
||||
let encoded = key.to_encoded_point(true);
|
||||
assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key");
|
||||
(*encoded.x().expect("point at infinity")).into()
|
||||
}
|
||||
|
||||
/// Convert a non-infinity even point to a XOnlyPublicKey. Panics on invalid input.
|
||||
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
||||
XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd")
|
||||
}
|
||||
|
||||
/// Make a point even by adding the generator until it is even.
|
||||
///
|
||||
/// Returns the even point and the amount of additions required.
|
||||
#[cfg(any(feature = "std", feature = "hazmat"))]
|
||||
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
||||
let mut c = 0;
|
||||
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
|
||||
key += ProjectivePoint::GENERATOR;
|
||||
c += 1;
|
||||
}
|
||||
(key, c)
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
mod frost_crypto {
|
||||
use core::fmt::Debug;
|
||||
use std_shims::{vec::Vec, io};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use bitcoin::hashes::{HashEngine, Hash, sha256::Hash as Sha256};
|
||||
|
||||
use transcript::Transcript;
|
||||
|
||||
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
|
||||
|
||||
use frost::{
|
||||
curve::{Ciphersuite, Secp256k1},
|
||||
Participant, ThresholdKeys, ThresholdView, FrostError,
|
||||
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
||||
///
|
||||
/// If passed an odd nonce, it will have the generator added until it is even.
|
||||
///
|
||||
/// If the key is odd, this will panic.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Hram;
|
||||
#[allow(non_snake_case)]
|
||||
impl HramTrait<Secp256k1> for Hram {
|
||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
||||
// Convert the nonce to be even
|
||||
let (R, _) = make_even(*R);
|
||||
|
||||
const TAG_HASH: Sha256 = Sha256::const_hash(b"BIP0340/challenge");
|
||||
|
||||
let mut data = Sha256::engine();
|
||||
data.input(TAG_HASH.as_ref());
|
||||
data.input(TAG_HASH.as_ref());
|
||||
data.input(&x(&R));
|
||||
data.input(&x(A));
|
||||
data.input(m);
|
||||
|
||||
Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP-340 Schnorr signature algorithm.
|
||||
///
|
||||
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
||||
#[derive(Clone)]
|
||||
pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>);
|
||||
impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> {
|
||||
/// Construct a Schnorr algorithm continuing the specified transcript.
|
||||
pub fn new(transcript: T) -> Schnorr<T> {
|
||||
Schnorr(FrostSchnorr::new(transcript))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
|
||||
type Transcript = T;
|
||||
type Addendum = ();
|
||||
type Signature = [u8; 64];
|
||||
|
||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
||||
self.0.transcript()
|
||||
}
|
||||
|
||||
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
|
||||
self.0.nonces()
|
||||
}
|
||||
|
||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
keys: &ThresholdKeys<Secp256k1>,
|
||||
) {
|
||||
self.0.preprocess_addendum(rng, keys)
|
||||
}
|
||||
|
||||
fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
|
||||
self.0.read_addendum(reader)
|
||||
}
|
||||
|
||||
fn process_addendum(
|
||||
&mut self,
|
||||
view: &ThresholdView<Secp256k1>,
|
||||
i: Participant,
|
||||
addendum: (),
|
||||
) -> Result<(), FrostError> {
|
||||
self.0.process_addendum(view, i, addendum)
|
||||
}
|
||||
|
||||
fn sign_share(
|
||||
&mut self,
|
||||
params: &ThresholdView<Secp256k1>,
|
||||
nonce_sums: &[Vec<<Secp256k1 as Ciphersuite>::G>],
|
||||
nonces: Vec<Zeroizing<<Secp256k1 as Ciphersuite>::F>>,
|
||||
msg: &[u8],
|
||||
) -> <Secp256k1 as Ciphersuite>::F {
|
||||
self.0.sign_share(params, nonce_sums, nonces, msg)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn verify(
|
||||
&self,
|
||||
group_key: ProjectivePoint,
|
||||
nonces: &[Vec<ProjectivePoint>],
|
||||
sum: Scalar,
|
||||
) -> Option<Self::Signature> {
|
||||
self.0.verify(group_key, nonces, sum).map(|mut sig| {
|
||||
// Make the R of the final signature even
|
||||
let offset;
|
||||
(sig.R, offset) = make_even(sig.R);
|
||||
// s = r + cx. Since we added to the r, add to s
|
||||
sig.s += Scalar::from(offset);
|
||||
// Convert to a Bitcoin signature by dropping the byte for the point's sign bit
|
||||
sig.serialize()[1 ..].try_into().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_share(
|
||||
&self,
|
||||
verification_share: ProjectivePoint,
|
||||
nonces: &[Vec<ProjectivePoint>],
|
||||
share: Scalar,
|
||||
) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> {
|
||||
self.0.verify_share(verification_share, nonces, share)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "std")]
|
||||
pub use frost_crypto::*;
|
||||
24
coins/bitcoin/src/lib.rs
Normal file
24
coins/bitcoin/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
extern crate alloc;
|
||||
|
||||
/// The bitcoin Rust library.
|
||||
pub use bitcoin;
|
||||
|
||||
/// Cryptographic helpers.
|
||||
#[cfg(feature = "hazmat")]
|
||||
pub mod crypto;
|
||||
#[cfg(not(feature = "hazmat"))]
|
||||
pub(crate) mod crypto;
|
||||
|
||||
/// Wallet functionality to create transactions.
|
||||
pub mod wallet;
|
||||
/// A minimal asynchronous Bitcoin RPC client.
|
||||
#[cfg(feature = "std")]
|
||||
pub mod rpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
226
coins/bitcoin/src/rpc.rs
Normal file
226
coins/bitcoin/src/rpc.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use core::fmt::Debug;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use serde::{Deserialize, de::DeserializeOwned};
|
||||
use serde_json::json;
|
||||
|
||||
use simple_request::{hyper, Request, Client};
|
||||
|
||||
use bitcoin::{
|
||||
hashes::{Hash, hex::FromHex},
|
||||
consensus::encode,
|
||||
Txid, Transaction, BlockHash, Block,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
|
||||
pub struct Error {
|
||||
code: isize,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RpcResponse<T> {
|
||||
Ok { result: T },
|
||||
Err { error: Error },
|
||||
}
|
||||
|
||||
/// A minimal asynchronous Bitcoin RPC client.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Rpc {
|
||||
client: Client,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||
pub enum RpcError {
|
||||
#[error("couldn't connect to node")]
|
||||
ConnectionError,
|
||||
#[error("request had an error: {0:?}")]
|
||||
RequestError(Error),
|
||||
#[error("node replied with invalid JSON")]
|
||||
InvalidJson(serde_json::error::Category),
|
||||
#[error("node sent an invalid response ({0})")]
|
||||
InvalidResponse(&'static str),
|
||||
#[error("node was missing expected methods")]
|
||||
MissingMethods(HashSet<&'static str>),
|
||||
}
|
||||
|
||||
impl Rpc {
|
||||
/// Create a new connection to a Bitcoin RPC.
|
||||
///
|
||||
/// An RPC call is performed to ensure the node is reachable (and that an invalid URL wasn't
|
||||
/// provided).
|
||||
///
|
||||
/// Additionally, a set of expected methods is checked to be offered by the Bitcoin RPC. If these
|
||||
/// methods aren't provided, an error with the missing methods is returned. This ensures all RPC
|
||||
/// routes explicitly provided by this library are at least possible.
|
||||
///
|
||||
/// Each individual RPC route may still fail at time-of-call, regardless of the arguments
|
||||
/// provided to this library, if the RPC has an incompatible argument layout. That is not checked
|
||||
/// at time of RPC creation.
|
||||
pub async fn new(url: String) -> Result<Rpc, RpcError> {
|
||||
let rpc = Rpc { client: Client::with_connection_pool(), url };
|
||||
|
||||
// Make an RPC request to verify the node is reachable and sane
|
||||
let res: String = rpc.rpc_call("help", json!([])).await?;
|
||||
|
||||
// Verify all methods we expect are present
|
||||
// If we had a more expanded RPC, due to differences in RPC versions, it wouldn't make sense to
|
||||
// error if all methods weren't present
|
||||
// We only provide a very minimal set of methods which have been largely consistent, hence why
|
||||
// this is sane
|
||||
let mut expected_methods = HashSet::from([
|
||||
"help",
|
||||
"getblockcount",
|
||||
"getblockhash",
|
||||
"getblockheader",
|
||||
"getblock",
|
||||
"sendrawtransaction",
|
||||
"getrawtransaction",
|
||||
]);
|
||||
for line in res.split('\n') {
|
||||
// This doesn't check if the arguments are as expected
|
||||
// This is due to Bitcoin supporting a large amount of optional arguments, which
|
||||
// occasionally change, with their own mechanism of text documentation, making matching off
|
||||
// it a quite involved task
|
||||
// Instead, once we've confirmed the methods are present, we assume our arguments are aligned
|
||||
// Else we'll error at time of call
|
||||
if expected_methods.remove(line.split(' ').next().unwrap_or("")) &&
|
||||
expected_methods.is_empty()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !expected_methods.is_empty() {
|
||||
Err(RpcError::MissingMethods(expected_methods))?;
|
||||
};
|
||||
|
||||
Ok(rpc)
|
||||
}
|
||||
|
||||
/// Perform an arbitrary RPC call.
|
||||
pub async fn rpc_call<Response: DeserializeOwned + Debug>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: serde_json::Value,
|
||||
) -> Result<Response, RpcError> {
|
||||
let mut request = Request::from(
|
||||
hyper::Request::post(&self.url)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::to_vec(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
||||
.unwrap()
|
||||
.into(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
request.with_basic_auth();
|
||||
let mut res = self
|
||||
.client
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(|_| RpcError::ConnectionError)?
|
||||
.body()
|
||||
.await
|
||||
.map_err(|_| RpcError::ConnectionError)?;
|
||||
|
||||
let res: RpcResponse<Response> =
|
||||
serde_json::from_reader(&mut res).map_err(|e| RpcError::InvalidJson(e.classify()))?;
|
||||
match res {
|
||||
RpcResponse::Ok { result } => Ok(result),
|
||||
RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the latest block's number.
|
||||
///
|
||||
/// The genesis block's 'number' is zero. They increment from there.
|
||||
pub async fn get_latest_block_number(&self) -> Result<usize, RpcError> {
|
||||
// getblockcount doesn't return the amount of blocks on the current chain, yet the "height"
|
||||
// of the current chain. The "height" of the current chain is defined as the "height" of the
|
||||
// tip block of the current chain. The "height" of a block is defined as the amount of blocks
|
||||
// present when the block was created. Accordingly, the genesis block has height 0, and
|
||||
// getblockcount will return 0 when it's only the only block, despite their being one block.
|
||||
self.rpc_call("getblockcount", json!([])).await
|
||||
}
|
||||
|
||||
/// Get the hash of a block by the block's number.
|
||||
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
||||
let mut hash = self
|
||||
.rpc_call::<BlockHash>("getblockhash", json!([number]))
|
||||
.await?
|
||||
.as_raw_hash()
|
||||
.to_byte_array();
|
||||
// bitcoin stores the inner bytes in reverse order.
|
||||
hash.reverse();
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
/// Get a block's number by its hash.
|
||||
pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Number {
|
||||
height: usize,
|
||||
}
|
||||
Ok(self.rpc_call::<Number>("getblockheader", json!([hex::encode(hash)])).await?.height)
|
||||
}
|
||||
|
||||
/// Get a block by its hash.
|
||||
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
||||
let hex = self.rpc_call::<String>("getblock", json!([hex::encode(hash), 0])).await?;
|
||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the block"))?;
|
||||
let block: Block = encode::deserialize(&bytes)
|
||||
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized block"))?;
|
||||
|
||||
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
||||
block_hash.reverse();
|
||||
if hash != &block_hash {
|
||||
Err(RpcError::InvalidResponse("node replied with a different block"))?;
|
||||
}
|
||||
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// Publish a transaction.
|
||||
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
||||
let txid = match self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await {
|
||||
Ok(txid) => txid,
|
||||
Err(e) => {
|
||||
// A const from Bitcoin's bitcoin/src/rpc/protocol.h
|
||||
const RPC_VERIFY_ALREADY_IN_CHAIN: isize = -27;
|
||||
// If this was already successfully published, consider this having succeeded
|
||||
if let RpcError::RequestError(Error { code, .. }) = e {
|
||||
if code == RPC_VERIFY_ALREADY_IN_CHAIN {
|
||||
return Ok(tx.txid());
|
||||
}
|
||||
}
|
||||
Err(e)?
|
||||
}
|
||||
};
|
||||
if txid != tx.txid() {
|
||||
Err(RpcError::InvalidResponse("returned TX ID inequals calculated TX ID"))?;
|
||||
}
|
||||
Ok(txid)
|
||||
}
|
||||
|
||||
/// Get a transaction by its hash.
|
||||
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
||||
let hex = self.rpc_call::<String>("getrawtransaction", json!([hex::encode(hash)])).await?;
|
||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the transaction"))?;
|
||||
let tx: Transaction = encode::deserialize(&bytes)
|
||||
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized transaction"))?;
|
||||
|
||||
let mut tx_hash = *tx.txid().as_raw_hash().as_byte_array();
|
||||
tx_hash.reverse();
|
||||
if hash != &tx_hash {
|
||||
Err(RpcError::InvalidResponse("node replied with a different transaction"))?;
|
||||
}
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
46
coins/bitcoin/src/tests/crypto.rs
Normal file
46
coins/bitcoin/src/tests/crypto.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use rand_core::OsRng;
|
||||
|
||||
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
||||
|
||||
use k256::Scalar;
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use frost::{
|
||||
curve::Secp256k1,
|
||||
Participant,
|
||||
tests::{algorithm_machines, key_gen, sign},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
bitcoin::hashes::{Hash as HashTrait, sha256::Hash},
|
||||
crypto::{x_only, make_even, Schnorr},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_algorithm() {
|
||||
let mut keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||
const MESSAGE: &[u8] = b"Hello, World!";
|
||||
|
||||
for keys in keys.values_mut() {
|
||||
let (_, offset) = make_even(keys.group_key());
|
||||
*keys = keys.offset(Scalar::from(offset));
|
||||
}
|
||||
|
||||
let algo =
|
||||
Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
|
||||
let sig = sign(
|
||||
&mut OsRng,
|
||||
&algo,
|
||||
keys.clone(),
|
||||
algorithm_machines(&mut OsRng, &algo, &keys),
|
||||
Hash::hash(MESSAGE).as_ref(),
|
||||
);
|
||||
|
||||
BContext::new()
|
||||
.verify_schnorr(
|
||||
&Signature::from_slice(&sig)
|
||||
.expect("couldn't convert produced signature to secp256k1::Signature"),
|
||||
&Message::from(Hash::hash(MESSAGE)),
|
||||
&x_only(&keys[&Participant::new(1).unwrap()].group_key()),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
1
coins/bitcoin/src/tests/mod.rs
Normal file
1
coins/bitcoin/src/tests/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod crypto;
|
||||
188
coins/bitcoin/src/wallet/mod.rs
Normal file
188
coins/bitcoin/src/wallet/mod.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use std_shims::{
|
||||
vec::Vec,
|
||||
collections::HashMap,
|
||||
io::{self, Write},
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
use std_shims::io::Read;
|
||||
|
||||
use k256::{
|
||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
||||
Scalar, ProjectivePoint,
|
||||
};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use frost::{
|
||||
curve::{Ciphersuite, Secp256k1},
|
||||
ThresholdKeys,
|
||||
};
|
||||
|
||||
use bitcoin::{
|
||||
consensus::encode::serialize, key::TweakedPublicKey, address::Payload, OutPoint, ScriptBuf,
|
||||
TxOut, Transaction, Block,
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
use bitcoin::consensus::encode::Decodable;
|
||||
|
||||
use crate::crypto::x_only;
|
||||
#[cfg(feature = "std")]
|
||||
use crate::crypto::make_even;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
mod send;
|
||||
#[cfg(feature = "std")]
|
||||
pub use send::*;
|
||||
|
||||
/// Tweak keys to ensure they're usable with Bitcoin.
|
||||
///
|
||||
/// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're
|
||||
/// even.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
||||
let (_, offset) = make_even(keys.group_key());
|
||||
keys.offset(Scalar::from(offset))
|
||||
}
|
||||
|
||||
/// Return the Taproot address payload for a public key.
|
||||
///
|
||||
/// If the key is odd, this will return None.
|
||||
pub fn address_payload(key: ProjectivePoint) -> Option<Payload> {
|
||||
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key))))
|
||||
}
|
||||
|
||||
/// A spendable output.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ReceivedOutput {
|
||||
// The scalar offset to obtain the key usable to spend this output.
|
||||
offset: Scalar,
|
||||
// The output to spend.
|
||||
output: TxOut,
|
||||
// The TX ID and vout of the output to spend.
|
||||
outpoint: OutPoint,
|
||||
}
|
||||
|
||||
impl ReceivedOutput {
|
||||
/// The offset for this output.
|
||||
pub fn offset(&self) -> Scalar {
|
||||
self.offset
|
||||
}
|
||||
|
||||
/// The Bitcoin output for this output.
|
||||
pub fn output(&self) -> &TxOut {
|
||||
&self.output
|
||||
}
|
||||
|
||||
/// The outpoint for this output.
|
||||
pub fn outpoint(&self) -> &OutPoint {
|
||||
&self.outpoint
|
||||
}
|
||||
|
||||
/// The value of this output.
|
||||
pub fn value(&self) -> u64 {
|
||||
self.output.value.to_sat()
|
||||
}
|
||||
|
||||
/// Read a ReceivedOutput from a generic satisfying Read.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn read<R: Read>(r: &mut R) -> io::Result<ReceivedOutput> {
|
||||
Ok(ReceivedOutput {
|
||||
offset: Secp256k1::read_F(r)?,
|
||||
output: TxOut::consensus_decode(r).map_err(|_| io::Error::other("invalid TxOut"))?,
|
||||
outpoint: OutPoint::consensus_decode(r).map_err(|_| io::Error::other("invalid OutPoint"))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a ReceivedOutput to a generic satisfying Write.
|
||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||
w.write_all(&self.offset.to_bytes())?;
|
||||
w.write_all(&serialize(&self.output))?;
|
||||
w.write_all(&serialize(&self.outpoint))
|
||||
}
|
||||
|
||||
/// Serialize a ReceivedOutput to a `Vec<u8>`.
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
self.write(&mut res).unwrap();
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// A transaction scanner capable of being used with HDKD schemes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Scanner {
|
||||
key: ProjectivePoint,
|
||||
scripts: HashMap<ScriptBuf, Scalar>,
|
||||
}
|
||||
|
||||
impl Scanner {
|
||||
/// Construct a Scanner for a key.
|
||||
///
|
||||
/// Returns None if this key can't be scanned for.
|
||||
pub fn new(key: ProjectivePoint) -> Option<Scanner> {
|
||||
let mut scripts = HashMap::new();
|
||||
scripts.insert(address_payload(key)?.script_pubkey(), Scalar::ZERO);
|
||||
Some(Scanner { key, scripts })
|
||||
}
|
||||
|
||||
/// Register an offset to scan for.
|
||||
///
|
||||
/// Due to Bitcoin's requirement that points are even, not every offset may be used.
|
||||
/// If an offset isn't usable, it will be incremented until it is. If this offset is already
|
||||
/// present, None is returned. Else, Some(offset) will be, with the used offset.
|
||||
///
|
||||
/// This means offsets are surjective, not bijective, and the order offsets are registered in
|
||||
/// may determine the validity of future offsets.
|
||||
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
||||
// This loop will terminate as soon as an even point is found, with any point having a ~50%
|
||||
// chance of being even
|
||||
// That means this should terminate within a very small amount of iterations
|
||||
loop {
|
||||
match address_payload(self.key + (ProjectivePoint::GENERATOR * offset)) {
|
||||
Some(address) => {
|
||||
let script = address.script_pubkey();
|
||||
if self.scripts.contains_key(&script) {
|
||||
None?;
|
||||
}
|
||||
self.scripts.insert(script, offset);
|
||||
return Some(offset);
|
||||
}
|
||||
None => offset += Scalar::ONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan a transaction.
|
||||
pub fn scan_transaction(&self, tx: &Transaction) -> Vec<ReceivedOutput> {
|
||||
let mut res = Vec::new();
|
||||
for (vout, output) in tx.output.iter().enumerate() {
|
||||
// If the vout index exceeds 2**32, stop scanning outputs
|
||||
let Ok(vout) = u32::try_from(vout) else { break };
|
||||
|
||||
if let Some(offset) = self.scripts.get(&output.script_pubkey) {
|
||||
res.push(ReceivedOutput {
|
||||
offset: *offset,
|
||||
output: output.clone(),
|
||||
outpoint: OutPoint::new(tx.txid(), vout),
|
||||
});
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Scan a block.
|
||||
///
|
||||
/// This will also scan the coinbase transaction which is bound by maturity. If received outputs
|
||||
/// must be immediately spendable, a post-processing pass is needed to remove those outputs.
|
||||
/// Alternatively, scan_transaction can be called on `block.txdata[1 ..]`.
|
||||
pub fn scan_block(&self, block: &Block) -> Vec<ReceivedOutput> {
|
||||
let mut res = Vec::new();
|
||||
for tx in &block.txdata {
|
||||
res.extend(self.scan_transaction(tx));
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
446
coins/bitcoin/src/wallet/send.rs
Normal file
446
coins/bitcoin/src/wallet/send.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use std_shims::{
|
||||
io::{self, Read},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
|
||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
||||
|
||||
use bitcoin::{
|
||||
hashes::Hash,
|
||||
sighash::{TapSighashType, SighashCache, Prevouts},
|
||||
absolute::LockTime,
|
||||
script::{PushBytesBuf, ScriptBuf},
|
||||
transaction::{Version, Transaction},
|
||||
OutPoint, Sequence, Witness, TxIn, Amount, TxOut, Address,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
crypto::Schnorr,
|
||||
wallet::{ReceivedOutput, address_payload},
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.cpp#L26-L63
|
||||
// As the above notes, a lower amount may not be considered dust if contained in a SegWit output
|
||||
// This doesn't bother with delineation due to how marginal these values are, and because it isn't
|
||||
// worth the complexity to implement differentation
|
||||
pub const DUST: u64 = 546;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||
pub enum TransactionError {
|
||||
#[error("no inputs were specified")]
|
||||
NoInputs,
|
||||
#[error("no outputs were created")]
|
||||
NoOutputs,
|
||||
#[error("a specified payment's amount was less than bitcoin's required minimum")]
|
||||
DustPayment,
|
||||
#[error("too much data was specified")]
|
||||
TooMuchData,
|
||||
#[error("fee was too low to pass the default minimum fee rate")]
|
||||
TooLowFee,
|
||||
#[error("not enough funds for these payments")]
|
||||
NotEnoughFunds,
|
||||
#[error("transaction was too large")]
|
||||
TooLargeTransaction,
|
||||
}
|
||||
|
||||
/// A signable transaction, clone-able across attempts.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct SignableTransaction {
|
||||
tx: Transaction,
|
||||
offsets: Vec<Scalar>,
|
||||
prevouts: Vec<TxOut>,
|
||||
needed_fee: u64,
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 {
|
||||
// Expand this a full transaction in order to use the bitcoin library's weight function
|
||||
let mut tx = Transaction {
|
||||
version: Version(2),
|
||||
lock_time: LockTime::ZERO,
|
||||
input: vec![
|
||||
TxIn {
|
||||
// This is a fixed size
|
||||
// See https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
|
||||
previous_output: OutPoint::default(),
|
||||
// This is empty for a Taproot spend
|
||||
script_sig: ScriptBuf::new(),
|
||||
// This is fixed size, yet we do use Sequence::MAX
|
||||
sequence: Sequence::MAX,
|
||||
// Our witnesses contains a single 64-byte signature
|
||||
witness: Witness::from_slice(&[vec![0; 64]])
|
||||
};
|
||||
inputs
|
||||
],
|
||||
output: payments
|
||||
.iter()
|
||||
// The payment is a fixed size so we don't have to use it here
|
||||
// The script pub key is not of a fixed size and does have to be used here
|
||||
.map(|payment| TxOut {
|
||||
value: Amount::from_sat(payment.1),
|
||||
script_pubkey: payment.0.script_pubkey(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
if let Some(change) = change {
|
||||
// Use a 0 value since we're currently unsure what the change amount will be, and since
|
||||
// the value is fixed size (so any value could be used here)
|
||||
tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.script_pubkey() });
|
||||
}
|
||||
u64::from(tx.weight())
|
||||
}
|
||||
|
||||
/// Returns the fee necessary for this transaction to achieve the fee rate specified at
|
||||
/// construction.
|
||||
///
|
||||
/// The actual fee this transaction will use is `sum(inputs) - sum(outputs)`.
|
||||
pub fn needed_fee(&self) -> u64 {
|
||||
self.needed_fee
|
||||
}
|
||||
|
||||
/// Returns the fee this transaction will use.
|
||||
pub fn fee(&self) -> u64 {
|
||||
self.prevouts.iter().map(|prevout| prevout.value.to_sat()).sum::<u64>() -
|
||||
self.tx.output.iter().map(|prevout| prevout.value.to_sat()).sum::<u64>()
|
||||
}
|
||||
|
||||
/// Create a new SignableTransaction.
|
||||
///
|
||||
/// If a change address is specified, any leftover funds will be sent to it if the leftover funds
|
||||
/// exceed the minimum output amount. If a change address isn't specified, all leftover funds
|
||||
/// will become part of the paid fee.
|
||||
///
|
||||
/// If data is specified, an OP_RETURN output will be added with it.
|
||||
pub fn new(
|
||||
mut inputs: Vec<ReceivedOutput>,
|
||||
payments: &[(Address, u64)],
|
||||
change: Option<&Address>,
|
||||
data: Option<Vec<u8>>,
|
||||
fee_per_weight: u64,
|
||||
) -> Result<SignableTransaction, TransactionError> {
|
||||
if inputs.is_empty() {
|
||||
Err(TransactionError::NoInputs)?;
|
||||
}
|
||||
|
||||
if payments.is_empty() && change.is_none() && data.is_none() {
|
||||
Err(TransactionError::NoOutputs)?;
|
||||
}
|
||||
|
||||
for (_, amount) in payments {
|
||||
if *amount < DUST {
|
||||
Err(TransactionError::DustPayment)?;
|
||||
}
|
||||
}
|
||||
|
||||
if data.as_ref().map_or(0, Vec::len) > 80 {
|
||||
Err(TransactionError::TooMuchData)?;
|
||||
}
|
||||
|
||||
let input_sat = inputs.iter().map(|input| input.output.value.to_sat()).sum::<u64>();
|
||||
let offsets = inputs.iter().map(|input| input.offset).collect();
|
||||
let tx_ins = inputs
|
||||
.iter()
|
||||
.map(|input| TxIn {
|
||||
previous_output: input.outpoint,
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::MAX,
|
||||
witness: Witness::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
|
||||
let mut tx_outs = payments
|
||||
.iter()
|
||||
.map(|payment| TxOut {
|
||||
value: Amount::from_sat(payment.1),
|
||||
script_pubkey: payment.0.script_pubkey(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Add the OP_RETURN output
|
||||
if let Some(data) = data {
|
||||
tx_outs.push(TxOut {
|
||||
value: Amount::ZERO,
|
||||
script_pubkey: ScriptBuf::new_op_return(
|
||||
PushBytesBuf::try_from(data)
|
||||
.expect("data didn't fit into PushBytes depsite being checked"),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
|
||||
let mut needed_fee = fee_per_weight * weight;
|
||||
|
||||
// "Virtual transaction size" is weight ceildiv 4 per
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
||||
|
||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/
|
||||
// src/policy/policy.cpp#L295-L298
|
||||
// implements this as expected
|
||||
|
||||
// Technically, it takes whatever's greater, the weight or the amount of signature operations
|
||||
// multiplied by DEFAULT_BYTES_PER_SIGOP (20)
|
||||
// We only use 1 signature per input, and our inputs have a weight exceeding 20
|
||||
// Accordingly, our inputs' weight will always be greater than the cost of the signature ops
|
||||
let vsize = weight.div_ceil(4);
|
||||
debug_assert_eq!(
|
||||
u64::try_from(bitcoin::policy::get_virtual_tx_size(
|
||||
weight.try_into().unwrap(),
|
||||
tx_ins.len().try_into().unwrap()
|
||||
))
|
||||
.unwrap(),
|
||||
vsize
|
||||
);
|
||||
// Technically, if there isn't change, this TX may still pay enough of a fee to pass the
|
||||
// minimum fee. Such edge cases aren't worth programming when they go against intent, as the
|
||||
// specified fee rate is too low to be valid
|
||||
// bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE is in sats/kilo-vbyte
|
||||
if needed_fee < ((u64::from(bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE) * vsize) / 1000) {
|
||||
Err(TransactionError::TooLowFee)?;
|
||||
}
|
||||
|
||||
if input_sat < (payment_sat + needed_fee) {
|
||||
Err(TransactionError::NotEnoughFunds)?;
|
||||
}
|
||||
|
||||
// If there's a change address, check if there's change to give it
|
||||
if let Some(change) = change {
|
||||
let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(change));
|
||||
let fee_with_change = fee_per_weight * weight_with_change;
|
||||
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
|
||||
if value >= DUST {
|
||||
tx_outs
|
||||
.push(TxOut { value: Amount::from_sat(value), script_pubkey: change.script_pubkey() });
|
||||
weight = weight_with_change;
|
||||
needed_fee = fee_with_change;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tx_outs.is_empty() {
|
||||
Err(TransactionError::NoOutputs)?;
|
||||
}
|
||||
|
||||
if weight > u64::from(bitcoin::policy::MAX_STANDARD_TX_WEIGHT) {
|
||||
Err(TransactionError::TooLargeTransaction)?;
|
||||
}
|
||||
|
||||
Ok(SignableTransaction {
|
||||
tx: Transaction {
|
||||
version: Version(2),
|
||||
lock_time: LockTime::ZERO,
|
||||
input: tx_ins,
|
||||
output: tx_outs,
|
||||
},
|
||||
offsets,
|
||||
prevouts: inputs.drain(..).map(|input| input.output).collect(),
|
||||
needed_fee,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the TX ID of the transaction this will create.
|
||||
pub fn txid(&self) -> [u8; 32] {
|
||||
let mut res = self.tx.txid().to_byte_array();
|
||||
res.reverse();
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns the outputs this transaction will create.
|
||||
pub fn outputs(&self) -> &[TxOut] {
|
||||
&self.tx.output
|
||||
}
|
||||
|
||||
/// Create a multisig machine for this transaction.
|
||||
///
|
||||
/// Returns None if the wrong keys are used.
|
||||
pub fn multisig(
|
||||
self,
|
||||
keys: &ThresholdKeys<Secp256k1>,
|
||||
mut transcript: RecommendedTranscript,
|
||||
) -> Option<TransactionMachine> {
|
||||
transcript.domain_separate(b"bitcoin_transaction");
|
||||
transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
|
||||
|
||||
// Transcript the inputs and outputs
|
||||
let tx = &self.tx;
|
||||
for input in &tx.input {
|
||||
transcript.append_message(b"input_hash", input.previous_output.txid);
|
||||
transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes());
|
||||
}
|
||||
for payment in &tx.output {
|
||||
transcript.append_message(b"output_script", payment.script_pubkey.as_bytes());
|
||||
transcript.append_message(b"output_amount", payment.value.to_sat().to_le_bytes());
|
||||
}
|
||||
|
||||
let mut sigs = vec![];
|
||||
for i in 0 .. tx.input.len() {
|
||||
let mut transcript = transcript.clone();
|
||||
// This unwrap is safe since any transaction with this many inputs violates the maximum
|
||||
// size allowed under standards, which this lib will error on creation of
|
||||
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
||||
|
||||
let offset = keys.clone().offset(self.offsets[i]);
|
||||
if address_payload(offset.group_key())?.script_pubkey() != self.prevouts[i].script_pubkey {
|
||||
None?;
|
||||
}
|
||||
|
||||
sigs.push(AlgorithmMachine::new(
|
||||
Schnorr::new(transcript),
|
||||
keys.clone().offset(self.offsets[i]),
|
||||
));
|
||||
}
|
||||
|
||||
Some(TransactionMachine { tx: self, sigs })
|
||||
}
|
||||
}
|
||||
|
||||
/// A FROST signing machine to produce a Bitcoin transaction.
|
||||
///
|
||||
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
||||
/// This will panic if either `cache` is called or the message isn't empty.
|
||||
pub struct TransactionMachine {
|
||||
tx: SignableTransaction,
|
||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||
}
|
||||
|
||||
impl PreprocessMachine for TransactionMachine {
|
||||
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
|
||||
type Signature = Transaction;
|
||||
type SignMachine = TransactionSignMachine;
|
||||
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
rng: &mut R,
|
||||
) -> (Self::SignMachine, Self::Preprocess) {
|
||||
let mut preprocesses = Vec::with_capacity(self.sigs.len());
|
||||
let sigs = self
|
||||
.sigs
|
||||
.drain(..)
|
||||
.map(|sig| {
|
||||
let (sig, preprocess) = sig.preprocess(rng);
|
||||
preprocesses.push(preprocess);
|
||||
sig
|
||||
})
|
||||
.collect();
|
||||
|
||||
(TransactionSignMachine { tx: self.tx, sigs }, preprocesses)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransactionSignMachine {
|
||||
tx: SignableTransaction,
|
||||
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||
}
|
||||
|
||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
type Params = ();
|
||||
type Keys = ThresholdKeys<Secp256k1>;
|
||||
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
|
||||
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
|
||||
type SignatureMachine = TransactionSignatureMachine;
|
||||
|
||||
fn cache(self) -> CachedPreprocess {
|
||||
unimplemented!(
|
||||
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
||||
"being already bound to a specific transaction"
|
||||
);
|
||||
}
|
||||
|
||||
fn from_cache(
|
||||
(): (),
|
||||
_: ThresholdKeys<Secp256k1>,
|
||||
_: CachedPreprocess,
|
||||
) -> (Self, Self::Preprocess) {
|
||||
unimplemented!(
|
||||
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
||||
"being already bound to a specific transaction"
|
||||
);
|
||||
}
|
||||
|
||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
||||
self.sigs.iter().map(|sig| sig.read_preprocess(reader)).collect()
|
||||
}
|
||||
|
||||
fn sign(
|
||||
mut self,
|
||||
commitments: HashMap<Participant, Self::Preprocess>,
|
||||
msg: &[u8],
|
||||
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
|
||||
if !msg.is_empty() {
|
||||
panic!("message was passed to the TransactionSignMachine when it generates its own");
|
||||
}
|
||||
|
||||
let commitments = (0 .. self.sigs.len())
|
||||
.map(|c| {
|
||||
commitments
|
||||
.iter()
|
||||
.map(|(l, commitments)| (*l, commitments[c].clone()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut cache = SighashCache::new(&self.tx.tx);
|
||||
// Sign committing to all inputs
|
||||
let prevouts = Prevouts::All(&self.tx.prevouts);
|
||||
|
||||
let mut shares = Vec::with_capacity(self.sigs.len());
|
||||
let sigs = self
|
||||
.sigs
|
||||
.drain(..)
|
||||
.enumerate()
|
||||
.map(|(i, sig)| {
|
||||
let (sig, share) = sig.sign(
|
||||
commitments[i].clone(),
|
||||
cache
|
||||
.taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default)
|
||||
// This should never happen since the inputs align with the TX the cache was
|
||||
// constructed with, and because i is always < prevouts.len()
|
||||
.expect("taproot_key_spend_signature_hash failed to return a hash")
|
||||
.as_ref(),
|
||||
)?;
|
||||
shares.push(share);
|
||||
Ok(sig)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok((TransactionSignatureMachine { tx: self.tx.tx, sigs }, shares))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransactionSignatureMachine {
|
||||
tx: Transaction,
|
||||
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||
}
|
||||
|
||||
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
||||
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
|
||||
|
||||
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
|
||||
self.sigs.iter().map(|sig| sig.read_share(reader)).collect()
|
||||
}
|
||||
|
||||
fn complete(
|
||||
mut self,
|
||||
mut shares: HashMap<Participant, Self::SignatureShare>,
|
||||
) -> Result<Transaction, FrostError> {
|
||||
for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) {
|
||||
let sig = schnorr.complete(
|
||||
shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::<HashMap<_, _>>(),
|
||||
)?;
|
||||
|
||||
let mut witness = Witness::new();
|
||||
witness.push(sig);
|
||||
input.witness = witness;
|
||||
}
|
||||
|
||||
Ok(self.tx)
|
||||
}
|
||||
}
|
||||
25
coins/bitcoin/tests/rpc.rs
Normal file
25
coins/bitcoin/tests/rpc.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use bitcoin_serai::{bitcoin::hashes::Hash as HashTrait, rpc::RpcError};
|
||||
|
||||
mod runner;
|
||||
use runner::rpc;
|
||||
|
||||
async_sequential! {
|
||||
async fn test_rpc() {
|
||||
let rpc = rpc().await;
|
||||
|
||||
// Test get_latest_block_number and get_block_hash by round tripping them
|
||||
let latest = rpc.get_latest_block_number().await.unwrap();
|
||||
let hash = rpc.get_block_hash(latest).await.unwrap();
|
||||
assert_eq!(rpc.get_block_number(&hash).await.unwrap(), latest);
|
||||
|
||||
// Test this actually is the latest block number by checking asking for the next block's errors
|
||||
assert!(matches!(rpc.get_block_hash(latest + 1).await, Err(RpcError::RequestError(_))));
|
||||
|
||||
// Test get_block by checking the received block's hash matches the request
|
||||
let block = rpc.get_block(&hash).await.unwrap();
|
||||
// Hashes are stored in reverse. It's bs from Satoshi
|
||||
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
||||
block_hash.reverse();
|
||||
assert_eq!(hash, block_hash);
|
||||
}
|
||||
}
|
||||
48
coins/bitcoin/tests/runner.rs
Normal file
48
coins/bitcoin/tests/runner.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use bitcoin_serai::rpc::Rpc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
static SEQUENTIAL_CELL: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
#[allow(non_snake_case)]
|
||||
pub fn SEQUENTIAL() -> &'static Mutex<()> {
|
||||
SEQUENTIAL_CELL.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn rpc() -> Rpc {
|
||||
let rpc = Rpc::new("http://serai:seraidex@127.0.0.1:8332".to_string()).await.unwrap();
|
||||
|
||||
// If this node has already been interacted with, clear its chain
|
||||
if rpc.get_latest_block_number().await.unwrap() > 0 {
|
||||
rpc
|
||||
.rpc_call(
|
||||
"invalidateblock",
|
||||
serde_json::json!([hex::encode(rpc.get_block_hash(1).await.unwrap())]),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
rpc
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! async_sequential {
|
||||
($(async fn $name: ident() $body: block)*) => {
|
||||
$(
|
||||
#[tokio::test]
|
||||
async fn $name() {
|
||||
let guard = runner::SEQUENTIAL().lock().await;
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.run_until(async move {
|
||||
if let Err(err) = tokio::task::spawn_local(async move { $body }).await {
|
||||
drop(guard);
|
||||
Err(err).unwrap()
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
365
coins/bitcoin/tests/wallet.rs
Normal file
365
coins/bitcoin/tests/wallet.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use k256::{
|
||||
elliptic_curve::{
|
||||
group::{ff::Field, Group},
|
||||
sec1::{Tag, ToEncodedPoint},
|
||||
},
|
||||
Scalar, ProjectivePoint,
|
||||
};
|
||||
use frost::{
|
||||
curve::Secp256k1,
|
||||
Participant, ThresholdKeys,
|
||||
tests::{THRESHOLD, key_gen, sign_without_caching},
|
||||
};
|
||||
|
||||
use bitcoin_serai::{
|
||||
bitcoin::{
|
||||
hashes::Hash as HashTrait,
|
||||
blockdata::opcodes::all::OP_RETURN,
|
||||
script::{PushBytesBuf, Instruction, Instructions, Script},
|
||||
address::NetworkChecked,
|
||||
OutPoint, Amount, TxOut, Transaction, Network, Address,
|
||||
},
|
||||
wallet::{
|
||||
tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, SignableTransaction,
|
||||
},
|
||||
rpc::Rpc,
|
||||
};
|
||||
|
||||
mod runner;
|
||||
use runner::rpc;
|
||||
|
||||
const FEE: u64 = 20;
|
||||
|
||||
fn is_even(key: ProjectivePoint) -> bool {
|
||||
key.to_encoded_point(true).tag() == Tag::CompressedEvenY
|
||||
}
|
||||
|
||||
async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint) -> ReceivedOutput {
|
||||
let block_number = rpc.get_latest_block_number().await.unwrap() + 1;
|
||||
|
||||
rpc
|
||||
.rpc_call::<Vec<String>>(
|
||||
"generatetoaddress",
|
||||
serde_json::json!([
|
||||
1,
|
||||
Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap())
|
||||
]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Mine until maturity
|
||||
rpc
|
||||
.rpc_call::<Vec<String>>(
|
||||
"generatetoaddress",
|
||||
serde_json::json!([100, Address::p2sh(Script::new(), Network::Regtest).unwrap()]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let block = rpc.get_block(&rpc.get_block_hash(block_number).await.unwrap()).await.unwrap();
|
||||
|
||||
let mut outputs = scanner.scan_block(&block);
|
||||
assert_eq!(outputs, scanner.scan_transaction(&block.txdata[0]));
|
||||
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert_eq!(outputs[0].outpoint(), &OutPoint::new(block.txdata[0].txid(), 0));
|
||||
assert_eq!(outputs[0].value(), block.txdata[0].output[0].value.to_sat());
|
||||
|
||||
assert_eq!(
|
||||
ReceivedOutput::read::<&[u8]>(&mut outputs[0].serialize().as_ref()).unwrap(),
|
||||
outputs[0]
|
||||
);
|
||||
|
||||
outputs.swap_remove(0)
|
||||
}
|
||||
|
||||
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
||||
let mut keys = key_gen(&mut OsRng);
|
||||
for keys in keys.values_mut() {
|
||||
*keys = tweak_keys(keys);
|
||||
}
|
||||
let key = keys.values().next().unwrap().group_key();
|
||||
(keys, key)
|
||||
}
|
||||
|
||||
fn sign(
|
||||
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
||||
tx: &SignableTransaction,
|
||||
) -> Transaction {
|
||||
let mut machines = HashMap::new();
|
||||
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
||||
machines.insert(
|
||||
i,
|
||||
tx.clone()
|
||||
.multisig(&keys[&i].clone(), RecommendedTranscript::new(b"bitcoin-serai Test Transaction"))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
sign_without_caching(&mut OsRng, machines, &[])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tweak_keys() {
|
||||
let mut even = false;
|
||||
let mut odd = false;
|
||||
|
||||
// Generate keys until we get an even set and an odd set
|
||||
while !(even && odd) {
|
||||
let mut keys = key_gen(&mut OsRng).drain().next().unwrap().1;
|
||||
if is_even(keys.group_key()) {
|
||||
// Tweaking should do nothing
|
||||
assert_eq!(tweak_keys(&keys).group_key(), keys.group_key());
|
||||
|
||||
even = true;
|
||||
} else {
|
||||
let tweaked = tweak_keys(&keys).group_key();
|
||||
assert_ne!(tweaked, keys.group_key());
|
||||
// Tweaking should produce an even key
|
||||
assert!(is_even(tweaked));
|
||||
|
||||
// Verify it uses the smallest possible offset
|
||||
while keys.group_key().to_encoded_point(true).tag() == Tag::CompressedOddY {
|
||||
keys = keys.offset(Scalar::ONE);
|
||||
}
|
||||
assert_eq!(tweaked, keys.group_key());
|
||||
|
||||
odd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async_sequential! {
|
||||
async fn test_scanner() {
|
||||
// Test Scanners are creatable for even keys.
|
||||
for _ in 0 .. 128 {
|
||||
let key = ProjectivePoint::random(&mut OsRng);
|
||||
assert_eq!(Scanner::new(key).is_some(), is_even(key));
|
||||
}
|
||||
|
||||
let mut key = ProjectivePoint::random(&mut OsRng);
|
||||
while !is_even(key) {
|
||||
key += ProjectivePoint::GENERATOR;
|
||||
}
|
||||
|
||||
{
|
||||
let mut scanner = Scanner::new(key).unwrap();
|
||||
for _ in 0 .. 128 {
|
||||
let mut offset = Scalar::random(&mut OsRng);
|
||||
let registered = scanner.register_offset(offset).unwrap();
|
||||
// Registering this again should return None
|
||||
assert!(scanner.register_offset(offset).is_none());
|
||||
|
||||
// We can only register offsets resulting in even keys
|
||||
// Make this even
|
||||
while !is_even(key + (ProjectivePoint::GENERATOR * offset)) {
|
||||
offset += Scalar::ONE;
|
||||
}
|
||||
// Ensure it matches the registered offset
|
||||
assert_eq!(registered, offset);
|
||||
// Assert registering this again fails
|
||||
assert!(scanner.register_offset(offset).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
let rpc = rpc().await;
|
||||
let mut scanner = Scanner::new(key).unwrap();
|
||||
|
||||
assert_eq!(send_and_get_output(&rpc, &scanner, key).await.offset(), Scalar::ZERO);
|
||||
|
||||
// Register an offset and test receiving to it
|
||||
let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
||||
assert_eq!(
|
||||
send_and_get_output(&rpc, &scanner, key + (ProjectivePoint::GENERATOR * offset))
|
||||
.await
|
||||
.offset(),
|
||||
offset
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_transaction_errors() {
|
||||
let (_, key) = keys();
|
||||
|
||||
let rpc = rpc().await;
|
||||
let scanner = Scanner::new(key).unwrap();
|
||||
|
||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
||||
assert_eq!(output.offset(), Scalar::ZERO);
|
||||
|
||||
let inputs = vec![output];
|
||||
let addr = || Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap());
|
||||
let payments = vec![(addr(), 1000)];
|
||||
|
||||
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
|
||||
|
||||
assert_eq!(
|
||||
SignableTransaction::new(vec![], &payments, None, None, FEE),
|
||||
Err(TransactionError::NoInputs)
|
||||
);
|
||||
|
||||
// No change
|
||||
assert!(SignableTransaction::new(inputs.clone(), &[(addr(), 1000)], None, None, FEE).is_ok());
|
||||
// Consolidation TX
|
||||
assert!(SignableTransaction::new(inputs.clone(), &[], Some(&addr()), None, FEE).is_ok());
|
||||
// Data
|
||||
assert!(SignableTransaction::new(inputs.clone(), &[], None, Some(vec![]), FEE).is_ok());
|
||||
// No outputs
|
||||
assert_eq!(
|
||||
SignableTransaction::new(inputs.clone(), &[], None, None, FEE),
|
||||
Err(TransactionError::NoOutputs),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SignableTransaction::new(inputs.clone(), &[(addr(), 1)], None, None, FEE),
|
||||
Err(TransactionError::DustPayment),
|
||||
);
|
||||
|
||||
assert!(
|
||||
SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 80]), FEE).is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 81]), FEE),
|
||||
Err(TransactionError::TooMuchData),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SignableTransaction::new(inputs.clone(), &[], Some(&addr()), None, 0),
|
||||
Err(TransactionError::TooLowFee),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
||||
Err(TransactionError::NotEnoughFunds),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE),
|
||||
Err(TransactionError::TooLargeTransaction),
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_send() {
|
||||
let (keys, key) = keys();
|
||||
|
||||
let rpc = rpc().await;
|
||||
let mut scanner = Scanner::new(key).unwrap();
|
||||
|
||||
// Get inputs, one not offset and one offset
|
||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
||||
assert_eq!(output.offset(), Scalar::ZERO);
|
||||
|
||||
let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
||||
let offset_key = key + (ProjectivePoint::GENERATOR * offset);
|
||||
let offset_output = send_and_get_output(&rpc, &scanner, offset_key).await;
|
||||
assert_eq!(offset_output.offset(), offset);
|
||||
|
||||
// Declare payments, change, fee
|
||||
let payments = [
|
||||
(Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap()), 1005),
|
||||
(Address::<NetworkChecked>::new(Network::Regtest, address_payload(offset_key).unwrap()), 1007)
|
||||
];
|
||||
|
||||
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
||||
let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
|
||||
let change_addr =
|
||||
Address::<NetworkChecked>::new(Network::Regtest, address_payload(change_key).unwrap());
|
||||
|
||||
// Create and sign the TX
|
||||
let tx = SignableTransaction::new(
|
||||
vec![output.clone(), offset_output.clone()],
|
||||
&payments,
|
||||
Some(&change_addr),
|
||||
None,
|
||||
FEE
|
||||
).unwrap();
|
||||
let needed_fee = tx.needed_fee();
|
||||
let expected_id = tx.txid();
|
||||
let tx = sign(&keys, &tx);
|
||||
|
||||
assert_eq!(tx.output.len(), 3);
|
||||
|
||||
// Ensure we can scan it
|
||||
let outputs = scanner.scan_transaction(&tx);
|
||||
for (o, output) in outputs.iter().enumerate() {
|
||||
assert_eq!(output.outpoint(), &OutPoint::new(tx.txid(), u32::try_from(o).unwrap()));
|
||||
assert_eq!(&ReceivedOutput::read::<&[u8]>(&mut output.serialize().as_ref()).unwrap(), output);
|
||||
}
|
||||
|
||||
assert_eq!(outputs[0].offset(), Scalar::ZERO);
|
||||
assert_eq!(outputs[1].offset(), offset);
|
||||
assert_eq!(outputs[2].offset(), change_offset);
|
||||
|
||||
// Make sure the payments were properly created
|
||||
for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) {
|
||||
assert_eq!(
|
||||
output,
|
||||
&TxOut { script_pubkey: payment.0.script_pubkey(), value: Amount::from_sat(payment.1) },
|
||||
);
|
||||
assert_eq!(scanned.value(), payment.1 );
|
||||
}
|
||||
|
||||
// Make sure the change is correct
|
||||
assert_eq!(needed_fee, u64::from(tx.weight()) * FEE);
|
||||
let input_value = output.value() + offset_output.value();
|
||||
let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
|
||||
assert_eq!(input_value - output_value, needed_fee);
|
||||
|
||||
let change_amount =
|
||||
input_value - payments.iter().map(|payment| payment.1).sum::<u64>() - needed_fee;
|
||||
assert_eq!(
|
||||
tx.output[2],
|
||||
TxOut { script_pubkey: change_addr.script_pubkey(), value: Amount::from_sat(change_amount) },
|
||||
);
|
||||
|
||||
// This also tests send_raw_transaction and get_transaction, which the RPC test can't
|
||||
// effectively test
|
||||
rpc.send_raw_transaction(&tx).await.unwrap();
|
||||
let mut hash = *tx.txid().as_raw_hash().as_byte_array();
|
||||
hash.reverse();
|
||||
assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap());
|
||||
assert_eq!(expected_id, hash);
|
||||
}
|
||||
|
||||
async fn test_data() {
|
||||
let (keys, key) = keys();
|
||||
|
||||
let rpc = rpc().await;
|
||||
let scanner = Scanner::new(key).unwrap();
|
||||
|
||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
||||
assert_eq!(output.offset(), Scalar::ZERO);
|
||||
|
||||
let data_len = 60 + usize::try_from(OsRng.next_u64() % 21).unwrap();
|
||||
let mut data = vec![0; data_len];
|
||||
OsRng.fill_bytes(&mut data);
|
||||
|
||||
let tx = sign(
|
||||
&keys,
|
||||
&SignableTransaction::new(
|
||||
vec![output],
|
||||
&[],
|
||||
Some(&Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap())),
|
||||
Some(data.clone()),
|
||||
FEE
|
||||
).unwrap()
|
||||
);
|
||||
|
||||
assert!(tx.output[0].script_pubkey.is_op_return());
|
||||
let check = |mut instructions: Instructions| {
|
||||
assert_eq!(instructions.next().unwrap().unwrap(), Instruction::Op(OP_RETURN));
|
||||
assert_eq!(
|
||||
instructions.next().unwrap().unwrap(),
|
||||
Instruction::PushBytes(&PushBytesBuf::try_from(data.clone()).unwrap()),
|
||||
);
|
||||
assert!(instructions.next().is_none());
|
||||
};
|
||||
check(tx.output[0].script_pubkey.instructions());
|
||||
check(tx.output[0].script_pubkey.instructions_minimal());
|
||||
}
|
||||
}
|
||||
3
coins/ethereum/.gitignore
vendored
Normal file
3
coins/ethereum/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Solidity build outputs
|
||||
cache
|
||||
artifacts
|
||||
46
coins/ethereum/Cargo.toml
Normal file
46
coins/ethereum/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "ethereum-serai"
|
||||
version = "0.1.0"
|
||||
description = "An Ethereum library supporting Schnorr signing and on-chain verification"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/ethereum"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.74"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "1", default-features = false }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["recommended"] }
|
||||
|
||||
group = { version = "0.13", default-features = false }
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "ecdsa", "arithmetic"] }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["secp256k1"] }
|
||||
|
||||
alloy-core = { version = "0.7", default-features = false }
|
||||
alloy-sol-types = { version = "0.7", default-features = false, features = ["json"] }
|
||||
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false, features = ["k256"] }
|
||||
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
|
||||
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
|
||||
alloy-simple-request-transport = { path = "./alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["tests"] }
|
||||
|
||||
tokio = { version = "1", features = ["macros"] }
|
||||
|
||||
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
|
||||
|
||||
[features]
|
||||
tests = []
|
||||
15
coins/ethereum/LICENSE
Normal file
15
coins/ethereum/LICENSE
Normal file
@@ -0,0 +1,15 @@
|
||||
AGPL-3.0-only license
|
||||
|
||||
Copyright (c) 2022-2023 Luke Parker
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License Version 3 as
|
||||
published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
15
coins/ethereum/README.md
Normal file
15
coins/ethereum/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Ethereum
|
||||
|
||||
This package contains Ethereum-related functionality, specifically deploying and
|
||||
interacting with Serai contracts.
|
||||
|
||||
While `monero-serai` and `bitcoin-serai` are general purpose libraries,
|
||||
`ethereum-serai` is Serai specific. If any of the utilities are generally
|
||||
desired, please fork and maintain your own copy to ensure the desired
|
||||
functionality is preserved, or open an issue to request we make this library
|
||||
general purpose.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- solc
|
||||
- [Foundry](https://github.com/foundry-rs/foundry)
|
||||
29
coins/ethereum/alloy-simple-request-transport/Cargo.toml
Normal file
29
coins/ethereum/alloy-simple-request-transport/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "alloy-simple-request-transport"
|
||||
version = "0.1.0"
|
||||
description = "A transport for alloy based off simple-request"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/ethereum/alloy-simple-request-transport"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.74"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
tower = "0.4"
|
||||
|
||||
serde_json = { version = "1", default-features = false }
|
||||
simple-request = { path = "../../../common/request", default-features = false }
|
||||
|
||||
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
|
||||
alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "037dd4b20ec8533d6b6d5cf5e9489bbb182c18c6", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["tls"]
|
||||
tls = ["simple-request/tls"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Luke Parker
|
||||
Copyright (c) 2024 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
4
coins/ethereum/alloy-simple-request-transport/README.md
Normal file
4
coins/ethereum/alloy-simple-request-transport/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Alloy Simple Request Transport
|
||||
|
||||
A transport for alloy based on simple-request, a small HTTP client built around
|
||||
hyper.
|
||||
60
coins/ethereum/alloy-simple-request-transport/src/lib.rs
Normal file
60
coins/ethereum/alloy-simple-request-transport/src/lib.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use core::task;
|
||||
use std::io;
|
||||
|
||||
use alloy_json_rpc::{RequestPacket, ResponsePacket};
|
||||
use alloy_transport::{TransportError, TransportErrorKind, TransportFut};
|
||||
|
||||
use simple_request::{hyper, Request, Client};
|
||||
|
||||
use tower::Service;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SimpleRequest {
|
||||
client: Client,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl SimpleRequest {
|
||||
pub fn new(url: String) -> Self {
|
||||
Self { client: Client::with_connection_pool(), url }
|
||||
}
|
||||
}
|
||||
|
||||
impl Service<RequestPacket> for SimpleRequest {
|
||||
type Response = ResponsePacket;
|
||||
type Error = TransportError;
|
||||
type Future = TransportFut<'static>;
|
||||
|
||||
#[inline]
|
||||
fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll<Result<(), Self::Error>> {
|
||||
task::Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call(&mut self, req: RequestPacket) -> Self::Future {
|
||||
let inner = self.clone();
|
||||
Box::pin(async move {
|
||||
let packet = req.serialize().map_err(TransportError::SerError)?;
|
||||
let request = Request::from(
|
||||
hyper::Request::post(&inner.url)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_vec(&packet).map_err(TransportError::SerError)?.into())
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut res = inner
|
||||
.client
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(|e| TransportErrorKind::custom(io::Error::other(format!("{e:?}"))))?
|
||||
.body()
|
||||
.await
|
||||
.map_err(|e| TransportErrorKind::custom(io::Error::other(format!("{e:?}"))))?;
|
||||
|
||||
serde_json::from_reader(&mut res).map_err(|e| TransportError::deser_err(e, ""))
|
||||
})
|
||||
}
|
||||
}
|
||||
41
coins/ethereum/build.rs
Normal file
41
coins/ethereum/build.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=contracts/*");
|
||||
println!("cargo:rerun-if-changed=artifacts/*");
|
||||
|
||||
for line in String::from_utf8(Command::new("solc").args(["--version"]).output().unwrap().stdout)
|
||||
.unwrap()
|
||||
.lines()
|
||||
{
|
||||
if let Some(version) = line.strip_prefix("Version: ") {
|
||||
let version = version.split('+').next().unwrap();
|
||||
assert_eq!(version, "0.8.25");
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
let args = [
|
||||
"--base-path", ".",
|
||||
"-o", "./artifacts", "--overwrite",
|
||||
"--bin", "--abi",
|
||||
"--via-ir", "--optimize",
|
||||
|
||||
"./contracts/IERC20.sol",
|
||||
|
||||
"./contracts/Schnorr.sol",
|
||||
"./contracts/Deployer.sol",
|
||||
"./contracts/Sandbox.sol",
|
||||
"./contracts/Router.sol",
|
||||
|
||||
"./src/tests/contracts/Schnorr.sol",
|
||||
"./src/tests/contracts/ERC20.sol",
|
||||
|
||||
"--no-color",
|
||||
];
|
||||
let solc = Command::new("solc").args(args).output().unwrap();
|
||||
assert!(solc.status.success());
|
||||
for line in String::from_utf8(solc.stderr).unwrap().lines() {
|
||||
assert!(!line.starts_with("Error:"));
|
||||
}
|
||||
}
|
||||
52
coins/ethereum/contracts/Deployer.sol
Normal file
52
coins/ethereum/contracts/Deployer.sol
Normal file
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: AGPLv3
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/*
|
||||
The expected deployment process of the Router is as follows:
|
||||
|
||||
1) A transaction deploying Deployer is made. Then, a deterministic signature is
|
||||
created such that an account with an unknown private key is the creator of
|
||||
the contract. Anyone can fund this address, and once anyone does, the
|
||||
transaction deploying Deployer can be published by anyone. No other
|
||||
transaction may be made from that account.
|
||||
|
||||
2) Anyone deploys the Router through the Deployer. This uses a sequential nonce
|
||||
such that meet-in-the-middle attacks, with complexity 2**80, aren't feasible.
|
||||
While such attacks would still be feasible if the Deployer's address was
|
||||
controllable, the usage of a deterministic signature with a NUMS method
|
||||
prevents that.
|
||||
|
||||
This doesn't have any denial-of-service risks and will resolve once anyone steps
|
||||
forward as deployer. This does fail to guarantee an identical address across
|
||||
every chain, though it enables letting anyone efficiently ask the Deployer for
|
||||
the address (with the Deployer having an identical address on every chain).
|
||||
|
||||
Unfortunately, guaranteeing identical addresses aren't feasible. We'd need the
|
||||
Deployer contract to use a consistent salt for the Router, yet the Router must
|
||||
be deployed with a specific public key for Serai. Since Ethereum isn't able to
|
||||
determine a valid public key (one the result of a Serai DKG) from a dishonest
|
||||
public key, we have to allow multiple deployments with Serai being the one to
|
||||
determine which to use.
|
||||
|
||||
The alternative would be to have a council publish the Serai key on-Ethereum,
|
||||
with Serai verifying the published result. This would introduce a DoS risk in
|
||||
the council not publishing the correct key/not publishing any key.
|
||||
*/
|
||||
|
||||
contract Deployer {
|
||||
event Deployment(bytes32 indexed init_code_hash, address created);
|
||||
|
||||
error DeploymentFailed();
|
||||
|
||||
function deploy(bytes memory init_code) external {
|
||||
address created;
|
||||
assembly {
|
||||
created := create(0, add(init_code, 0x20), mload(init_code))
|
||||
}
|
||||
if (created == address(0)) {
|
||||
revert DeploymentFailed();
|
||||
}
|
||||
// These may be emitted out of order upon re-entrancy
|
||||
emit Deployment(keccak256(init_code), created);
|
||||
}
|
||||
}
|
||||
20
coins/ethereum/contracts/IERC20.sol
Normal file
20
coins/ethereum/contracts/IERC20.sol
Normal file
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: CC0
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
interface IERC20 {
|
||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
|
||||
function name() external view returns (string memory);
|
||||
function symbol() external view returns (string memory);
|
||||
function decimals() external view returns (uint8);
|
||||
|
||||
function totalSupply() external view returns (uint256);
|
||||
|
||||
function balanceOf(address owner) external view returns (uint256);
|
||||
function transfer(address to, uint256 value) external returns (bool);
|
||||
function transferFrom(address from, address to, uint256 value) external returns (bool);
|
||||
|
||||
function approve(address spender, uint256 value) external returns (bool);
|
||||
function allowance(address owner, address spender) external view returns (uint256);
|
||||
}
|
||||
222
coins/ethereum/contracts/Router.sol
Normal file
222
coins/ethereum/contracts/Router.sol
Normal file
@@ -0,0 +1,222 @@
|
||||
// SPDX-License-Identifier: AGPLv3
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./IERC20.sol";
|
||||
|
||||
import "./Schnorr.sol";
|
||||
import "./Sandbox.sol";
|
||||
|
||||
contract Router {
|
||||
// Nonce is incremented for each batch of transactions executed/key update
|
||||
uint256 public nonce;
|
||||
|
||||
// Current public key's x-coordinate
|
||||
// This key must always have the parity defined within the Schnorr contract
|
||||
bytes32 public seraiKey;
|
||||
|
||||
struct OutInstruction {
|
||||
address to;
|
||||
Call[] calls;
|
||||
|
||||
uint256 value;
|
||||
}
|
||||
|
||||
struct Signature {
|
||||
bytes32 c;
|
||||
bytes32 s;
|
||||
}
|
||||
|
||||
event SeraiKeyUpdated(
|
||||
uint256 indexed nonce,
|
||||
bytes32 indexed key,
|
||||
Signature signature
|
||||
);
|
||||
event InInstruction(
|
||||
address indexed from,
|
||||
address indexed coin,
|
||||
uint256 amount,
|
||||
bytes instruction
|
||||
);
|
||||
// success is a uint256 representing a bitfield of transaction successes
|
||||
event Executed(
|
||||
uint256 indexed nonce,
|
||||
bytes32 indexed batch,
|
||||
uint256 success,
|
||||
Signature signature
|
||||
);
|
||||
|
||||
// error types
|
||||
error InvalidKey();
|
||||
error InvalidSignature();
|
||||
error InvalidAmount();
|
||||
error FailedTransfer();
|
||||
error TooManyTransactions();
|
||||
|
||||
modifier _updateSeraiKeyAtEndOfFn(
|
||||
uint256 _nonce,
|
||||
bytes32 key,
|
||||
Signature memory sig
|
||||
) {
|
||||
if (
|
||||
(key == bytes32(0)) ||
|
||||
((bytes32(uint256(key) % Schnorr.Q)) != key)
|
||||
) {
|
||||
revert InvalidKey();
|
||||
}
|
||||
|
||||
_;
|
||||
|
||||
seraiKey = key;
|
||||
emit SeraiKeyUpdated(_nonce, key, sig);
|
||||
}
|
||||
|
||||
constructor(bytes32 _seraiKey) _updateSeraiKeyAtEndOfFn(
|
||||
0,
|
||||
_seraiKey,
|
||||
Signature({ c: bytes32(0), s: bytes32(0) })
|
||||
) {
|
||||
nonce = 1;
|
||||
}
|
||||
|
||||
// updateSeraiKey validates the given Schnorr signature against the current
|
||||
// public key, and if successful, updates the contract's public key to the
|
||||
// given one.
|
||||
function updateSeraiKey(
|
||||
bytes32 _seraiKey,
|
||||
Signature calldata sig
|
||||
) external _updateSeraiKeyAtEndOfFn(nonce, _seraiKey, sig) {
|
||||
bytes memory message =
|
||||
abi.encodePacked("updateSeraiKey", block.chainid, nonce, _seraiKey);
|
||||
nonce++;
|
||||
|
||||
if (!Schnorr.verify(seraiKey, message, sig.c, sig.s)) {
|
||||
revert InvalidSignature();
|
||||
}
|
||||
}
|
||||
|
||||
function inInstruction(
|
||||
address coin,
|
||||
uint256 amount,
|
||||
bytes memory instruction
|
||||
) external payable {
|
||||
if (coin == address(0)) {
|
||||
if (amount != msg.value) {
|
||||
revert InvalidAmount();
|
||||
}
|
||||
} else {
|
||||
(bool success, bytes memory res) =
|
||||
address(coin).call(
|
||||
abi.encodeWithSelector(
|
||||
IERC20.transferFrom.selector,
|
||||
msg.sender,
|
||||
address(this),
|
||||
amount
|
||||
)
|
||||
);
|
||||
|
||||
// Require there was nothing returned, which is done by some non-standard
|
||||
// tokens, or that the ERC20 contract did in fact return true
|
||||
bool nonStandardResOrTrue =
|
||||
(res.length == 0) || abi.decode(res, (bool));
|
||||
if (!(success && nonStandardResOrTrue)) {
|
||||
revert FailedTransfer();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Due to fee-on-transfer tokens, emitting the amount directly is frowned upon.
|
||||
The amount instructed to transfer may not actually be the amount
|
||||
transferred.
|
||||
|
||||
If we add nonReentrant to every single function which can effect the
|
||||
balance, we can check the amount exactly matches. This prevents transfers of
|
||||
less value than expected occurring, at least, not without an additional
|
||||
transfer to top up the difference (which isn't routed through this contract
|
||||
and accordingly isn't trying to artificially create events).
|
||||
|
||||
If we don't add nonReentrant, a transfer can be started, and then a new
|
||||
transfer for the difference can follow it up (again and again until a
|
||||
rounding error is reached). This contract would believe all transfers were
|
||||
done in full, despite each only being done in part (except for the last
|
||||
one).
|
||||
|
||||
Given fee-on-transfer tokens aren't intended to be supported, the only
|
||||
token planned to be supported is Dai and it doesn't have any fee-on-transfer
|
||||
logic, fee-on-transfer tokens aren't even able to be supported at this time,
|
||||
we simply classify this entire class of tokens as non-standard
|
||||
implementations which induce undefined behavior. It is the Serai network's
|
||||
role not to add support for any non-standard implementations.
|
||||
*/
|
||||
emit InInstruction(msg.sender, coin, amount, instruction);
|
||||
}
|
||||
|
||||
// execute accepts a list of transactions to execute as well as a signature.
|
||||
// if signature verification passes, the given transactions are executed.
|
||||
// if signature verification fails, this function will revert.
|
||||
function execute(
|
||||
OutInstruction[] calldata transactions,
|
||||
Signature calldata sig
|
||||
) external {
|
||||
if (transactions.length > 256) {
|
||||
revert TooManyTransactions();
|
||||
}
|
||||
|
||||
bytes memory message =
|
||||
abi.encode("execute", block.chainid, nonce, transactions);
|
||||
uint256 executed_with_nonce = nonce;
|
||||
// This prevents re-entrancy from causing double spends yet does allow
|
||||
// out-of-order execution via re-entrancy
|
||||
nonce++;
|
||||
|
||||
if (!Schnorr.verify(seraiKey, message, sig.c, sig.s)) {
|
||||
revert InvalidSignature();
|
||||
}
|
||||
|
||||
uint256 successes;
|
||||
for (uint256 i = 0; i < transactions.length; i++) {
|
||||
bool success;
|
||||
|
||||
// If there are no calls, send to `to` the value
|
||||
if (transactions[i].calls.length == 0) {
|
||||
(success, ) = transactions[i].to.call{
|
||||
value: transactions[i].value,
|
||||
gas: 5_000
|
||||
}("");
|
||||
} else {
|
||||
// If there are calls, ignore `to`. Deploy a new Sandbox and proxy the
|
||||
// calls through that
|
||||
//
|
||||
// We could use a single sandbox in order to reduce gas costs, yet that
|
||||
// risks one person creating an approval that's hooked before another
|
||||
// user's intended action executes, in order to drain their coins
|
||||
//
|
||||
// While technically, that would be a flaw in the sandboxed flow, this
|
||||
// is robust and prevents such flaws from being possible
|
||||
//
|
||||
// We also don't want people to set state via the Sandbox and expect it
|
||||
// future available when anyone else could set a distinct value
|
||||
Sandbox sandbox = new Sandbox();
|
||||
(success, ) = address(sandbox).call{
|
||||
value: transactions[i].value,
|
||||
// TODO: Have the Call specify the gas up front
|
||||
gas: 350_000
|
||||
}(
|
||||
abi.encodeWithSelector(
|
||||
Sandbox.sandbox.selector,
|
||||
transactions[i].calls
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
assembly {
|
||||
successes := or(successes, shl(i, success))
|
||||
}
|
||||
}
|
||||
emit Executed(
|
||||
executed_with_nonce,
|
||||
keccak256(message),
|
||||
successes,
|
||||
sig
|
||||
);
|
||||
}
|
||||
}
|
||||
48
coins/ethereum/contracts/Sandbox.sol
Normal file
48
coins/ethereum/contracts/Sandbox.sol
Normal file
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: AGPLv3
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
struct Call {
|
||||
address to;
|
||||
uint256 value;
|
||||
bytes data;
|
||||
}
|
||||
|
||||
// A minimal sandbox focused on gas efficiency.
|
||||
//
|
||||
// The first call is executed if any of the calls fail, making it a fallback.
|
||||
// All other calls are executed sequentially.
|
||||
contract Sandbox {
|
||||
error AlreadyCalled();
|
||||
error CallsFailed();
|
||||
|
||||
function sandbox(Call[] calldata calls) external payable {
|
||||
// Prevent re-entrancy due to this executing arbitrary calls from anyone
|
||||
// and anywhere
|
||||
bool called;
|
||||
assembly { called := tload(0) }
|
||||
if (called) {
|
||||
revert AlreadyCalled();
|
||||
}
|
||||
assembly { tstore(0, 1) }
|
||||
|
||||
// Execute the calls, starting from 1
|
||||
for (uint256 i = 1; i < calls.length; i++) {
|
||||
(bool success, ) =
|
||||
calls[i].to.call{ value: calls[i].value }(calls[i].data);
|
||||
|
||||
// If this call failed, execute the fallback (call 0)
|
||||
if (!success) {
|
||||
(success, ) =
|
||||
calls[0].to.call{ value: address(this).balance }(calls[0].data);
|
||||
// If this call also failed, revert entirely
|
||||
if (!success) {
|
||||
revert CallsFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't clear the re-entrancy guard as this contract should never be
|
||||
// called again, so there's no reason to spend the effort
|
||||
}
|
||||
}
|
||||
44
coins/ethereum/contracts/Schnorr.sol
Normal file
44
coins/ethereum/contracts/Schnorr.sol
Normal file
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: AGPLv3
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
// see https://github.com/noot/schnorr-verify for implementation details
|
||||
library Schnorr {
|
||||
// secp256k1 group order
|
||||
uint256 constant public Q =
|
||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
||||
|
||||
// Fixed parity for the public keys used in this contract
|
||||
// This avoids spending a word passing the parity in a similar style to
|
||||
// Bitcoin's Taproot
|
||||
uint8 constant public KEY_PARITY = 27;
|
||||
|
||||
error InvalidSOrA();
|
||||
error MalformedSignature();
|
||||
|
||||
// px := public key x-coord, where the public key has a parity of KEY_PARITY
|
||||
// message := 32-byte hash of the message
|
||||
// c := schnorr signature challenge
|
||||
// s := schnorr signature
|
||||
function verify(
|
||||
bytes32 px,
|
||||
bytes memory message,
|
||||
bytes32 c,
|
||||
bytes32 s
|
||||
) internal pure returns (bool) {
|
||||
// ecrecover = (m, v, r, s) -> key
|
||||
// We instead pass the following to obtain the nonce (not the key)
|
||||
// Then we hash it and verify it matches the challenge
|
||||
bytes32 sa = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
||||
bytes32 ca = bytes32(Q - mulmod(uint256(c), uint256(px), Q));
|
||||
|
||||
// For safety, we want each input to ecrecover to be 0 (sa, px, ca)
|
||||
// The ecreover precomple checks `r` and `s` (`px` and `ca`) are non-zero
|
||||
// That leaves us to check `sa` are non-zero
|
||||
if (sa == 0) revert InvalidSOrA();
|
||||
address R = ecrecover(sa, KEY_PARITY, px, ca);
|
||||
if (R == address(0)) revert MalformedSignature();
|
||||
|
||||
// Check the signature is correct by rebuilding the challenge
|
||||
return c == keccak256(abi.encodePacked(R, px, message));
|
||||
}
|
||||
}
|
||||
37
coins/ethereum/src/abi/mod.rs
Normal file
37
coins/ethereum/src/abi/mod.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use alloy_sol_types::sol;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(warnings)]
|
||||
#[allow(needless_pass_by_value)]
|
||||
#[allow(clippy::all)]
|
||||
#[allow(clippy::ignored_unit_patterns)]
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
mod erc20_container {
|
||||
use super::*;
|
||||
sol!("contracts/IERC20.sol");
|
||||
}
|
||||
pub use erc20_container::IERC20 as erc20;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(warnings)]
|
||||
#[allow(needless_pass_by_value)]
|
||||
#[allow(clippy::all)]
|
||||
#[allow(clippy::ignored_unit_patterns)]
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
mod deployer_container {
|
||||
use super::*;
|
||||
sol!("contracts/Deployer.sol");
|
||||
}
|
||||
pub use deployer_container::Deployer as deployer;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(warnings)]
|
||||
#[allow(needless_pass_by_value)]
|
||||
#[allow(clippy::all)]
|
||||
#[allow(clippy::ignored_unit_patterns)]
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
mod router_container {
|
||||
use super::*;
|
||||
sol!(Router, "artifacts/Router.abi");
|
||||
}
|
||||
pub use router_container::Router as router;
|
||||
185
coins/ethereum/src/crypto.rs
Normal file
185
coins/ethereum/src/crypto.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use group::ff::PrimeField;
|
||||
use k256::{
|
||||
elliptic_curve::{ops::Reduce, point::AffineCoordinates, sec1::ToEncodedPoint},
|
||||
ProjectivePoint, Scalar, U256 as KU256,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use k256::{elliptic_curve::point::DecompressPoint, AffinePoint};
|
||||
|
||||
use frost::{
|
||||
algorithm::{Hram, SchnorrSignature},
|
||||
curve::{Ciphersuite, Secp256k1},
|
||||
};
|
||||
|
||||
use alloy_core::primitives::{Parity, Signature as AlloySignature};
|
||||
use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
|
||||
|
||||
use crate::abi::router::{Signature as AbiSignature};
|
||||
|
||||
pub(crate) fn keccak256(data: &[u8]) -> [u8; 32] {
|
||||
alloy_core::primitives::keccak256(data).into()
|
||||
}
|
||||
|
||||
pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||
<Scalar as Reduce<KU256>>::reduce_bytes(&keccak256(data).into())
|
||||
}
|
||||
|
||||
pub fn address(point: &ProjectivePoint) -> [u8; 20] {
|
||||
let encoded_point = point.to_encoded_point(false);
|
||||
// Last 20 bytes of the hash of the concatenated x and y coordinates
|
||||
// We obtain the concatenated x and y coordinates via the uncompressed encoding of the point
|
||||
keccak256(&encoded_point.as_ref()[1 .. 65])[12 ..].try_into().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
|
||||
assert!(
|
||||
tx.chain_id.is_none(),
|
||||
"chain ID was Some when deterministically signing a TX (causing a non-deterministic signer)"
|
||||
);
|
||||
|
||||
let sig_hash = tx.signature_hash().0;
|
||||
let mut r = hash_to_scalar(&[sig_hash.as_slice(), b"r"].concat());
|
||||
let mut s = hash_to_scalar(&[sig_hash.as_slice(), b"s"].concat());
|
||||
loop {
|
||||
let r_bytes: [u8; 32] = r.to_repr().into();
|
||||
let s_bytes: [u8; 32] = s.to_repr().into();
|
||||
let v = Parity::NonEip155(false);
|
||||
let signature =
|
||||
AlloySignature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), v).unwrap();
|
||||
let tx = tx.clone().into_signed(signature);
|
||||
if tx.recover_signer().is_ok() {
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Re-hash until valid
|
||||
r = hash_to_scalar(r_bytes.as_ref());
|
||||
s = hash_to_scalar(s_bytes.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
/// The public key for a Schnorr-signing account.
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct PublicKey {
|
||||
pub(crate) A: ProjectivePoint,
|
||||
pub(crate) px: Scalar,
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// Construct a new `PublicKey`.
|
||||
///
|
||||
/// This will return None if the provided point isn't eligible to be a public key (due to
|
||||
/// bounds such as parity).
|
||||
#[allow(non_snake_case)]
|
||||
pub fn new(A: ProjectivePoint) -> Option<PublicKey> {
|
||||
let affine = A.to_affine();
|
||||
// Only allow even keys to save a word within Ethereum
|
||||
let is_odd = bool::from(affine.y_is_odd());
|
||||
if is_odd {
|
||||
None?;
|
||||
}
|
||||
|
||||
let x_coord = affine.x();
|
||||
let x_coord_scalar = <Scalar as Reduce<KU256>>::reduce_bytes(&x_coord);
|
||||
// Return None if a reduction would occur
|
||||
// Reductions would be incredibly unlikely and shouldn't be an issue, yet it's one less
|
||||
// headache/concern to have
|
||||
// This does ban a trivial amoount of public keys
|
||||
if x_coord_scalar.to_repr() != x_coord {
|
||||
None?;
|
||||
}
|
||||
|
||||
Some(PublicKey { A, px: x_coord_scalar })
|
||||
}
|
||||
|
||||
pub fn point(&self) -> ProjectivePoint {
|
||||
self.A
|
||||
}
|
||||
|
||||
pub(crate) fn eth_repr(&self) -> [u8; 32] {
|
||||
self.px.to_repr().into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_eth_repr(repr: [u8; 32]) -> Option<Self> {
|
||||
#[allow(non_snake_case)]
|
||||
let A = Option::<AffinePoint>::from(AffinePoint::decompress(&repr.into(), 0.into()))?.into();
|
||||
Option::from(Scalar::from_repr(repr.into())).map(|px| PublicKey { A, px })
|
||||
}
|
||||
}
|
||||
|
||||
/// The HRAm to use for the Schnorr contract.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct EthereumHram {}
|
||||
impl Hram<Secp256k1> for EthereumHram {
|
||||
#[allow(non_snake_case)]
|
||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
||||
let x_coord = A.to_affine().x();
|
||||
|
||||
let mut data = address(R).to_vec();
|
||||
data.extend(x_coord.as_slice());
|
||||
data.extend(m);
|
||||
|
||||
<Scalar as Reduce<KU256>>::reduce_bytes(&keccak256(&data).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A signature for the Schnorr contract.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Signature {
|
||||
pub(crate) c: Scalar,
|
||||
pub(crate) s: Scalar,
|
||||
}
|
||||
impl Signature {
|
||||
pub fn verify(&self, public_key: &PublicKey, message: &[u8]) -> bool {
|
||||
#[allow(non_snake_case)]
|
||||
let R = (Secp256k1::generator() * self.s) - (public_key.A * self.c);
|
||||
EthereumHram::hram(&R, &public_key.A, message) == self.c
|
||||
}
|
||||
|
||||
/// Construct a new `Signature`.
|
||||
///
|
||||
/// This will return None if the signature is invalid.
|
||||
pub fn new(
|
||||
public_key: &PublicKey,
|
||||
message: &[u8],
|
||||
signature: SchnorrSignature<Secp256k1>,
|
||||
) -> Option<Signature> {
|
||||
let c = EthereumHram::hram(&signature.R, &public_key.A, message);
|
||||
if !signature.verify(public_key.A, c) {
|
||||
None?;
|
||||
}
|
||||
|
||||
let res = Signature { c, s: signature.s };
|
||||
assert!(res.verify(public_key, message));
|
||||
Some(res)
|
||||
}
|
||||
|
||||
pub fn c(&self) -> Scalar {
|
||||
self.c
|
||||
}
|
||||
pub fn s(&self) -> Scalar {
|
||||
self.s
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 64] {
|
||||
let mut res = [0; 64];
|
||||
res[.. 32].copy_from_slice(self.c.to_repr().as_ref());
|
||||
res[32 ..].copy_from_slice(self.s.to_repr().as_ref());
|
||||
res
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: [u8; 64]) -> std::io::Result<Self> {
|
||||
let mut reader = bytes.as_slice();
|
||||
let c = Secp256k1::read_F(&mut reader)?;
|
||||
let s = Secp256k1::read_F(&mut reader)?;
|
||||
Ok(Signature { c, s })
|
||||
}
|
||||
}
|
||||
impl From<&Signature> for AbiSignature {
|
||||
fn from(sig: &Signature) -> AbiSignature {
|
||||
let c: [u8; 32] = sig.c.to_repr().into();
|
||||
let s: [u8; 32] = sig.s.to_repr().into();
|
||||
AbiSignature { c: c.into(), s: s.into() }
|
||||
}
|
||||
}
|
||||
119
coins/ethereum/src/deployer.rs
Normal file
119
coins/ethereum/src/deployer.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use alloy_core::primitives::{hex::FromHex, Address, B256, U256, Bytes, TxKind};
|
||||
use alloy_consensus::{Signed, TxLegacy};
|
||||
|
||||
use alloy_sol_types::{SolCall, SolEvent};
|
||||
|
||||
use alloy_rpc_types::{BlockNumberOrTag, Filter};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
crypto::{self, keccak256, PublicKey},
|
||||
router::Router,
|
||||
};
|
||||
pub use crate::abi::deployer as abi;
|
||||
|
||||
/// The Deployer contract for the Router contract.
|
||||
///
|
||||
/// This Deployer has a deterministic address, letting it be immediately identified on any
|
||||
/// compatible chain. It then supports retrieving the Router contract's address (which isn't
|
||||
/// deterministic) using a single log query.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Deployer;
|
||||
impl Deployer {
|
||||
/// Obtain the transaction to deploy this contract, already signed.
|
||||
///
|
||||
/// The account this transaction is sent from (which is populated in `from`) must be sufficiently
|
||||
/// funded for this transaction to be submitted. This account has no known private key to anyone,
|
||||
/// so ETH sent can be neither misappropriated nor returned.
|
||||
pub fn deployment_tx() -> Signed<TxLegacy> {
|
||||
let bytecode = include_str!("../artifacts/Deployer.bin");
|
||||
let bytecode =
|
||||
Bytes::from_hex(bytecode).expect("compiled-in Deployer bytecode wasn't valid hex");
|
||||
|
||||
let tx = TxLegacy {
|
||||
chain_id: None,
|
||||
nonce: 0,
|
||||
gas_price: 100_000_000_000u128,
|
||||
// TODO: Use a more accurate gas limit
|
||||
gas_limit: 1_000_000u128,
|
||||
to: TxKind::Create,
|
||||
value: U256::ZERO,
|
||||
input: bytecode,
|
||||
};
|
||||
|
||||
crypto::deterministically_sign(&tx)
|
||||
}
|
||||
|
||||
/// Obtain the deterministic address for this contract.
|
||||
pub fn address() -> [u8; 20] {
|
||||
let deployer_deployer =
|
||||
Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature");
|
||||
**Address::create(&deployer_deployer, 0)
|
||||
}
|
||||
|
||||
/// Construct a new view of the `Deployer`.
|
||||
pub async fn new(provider: Arc<RootProvider<SimpleRequest>>) -> Result<Option<Self>, Error> {
|
||||
let address = Self::address();
|
||||
#[cfg(not(test))]
|
||||
let required_block = BlockNumberOrTag::Finalized;
|
||||
#[cfg(test)]
|
||||
let required_block = BlockNumberOrTag::Latest;
|
||||
let code = provider
|
||||
.get_code_at(address.into(), required_block.into())
|
||||
.await
|
||||
.map_err(|_| Error::ConnectionError)?;
|
||||
// Contract has yet to be deployed
|
||||
if code.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(Self))
|
||||
}
|
||||
|
||||
/// Yield the `ContractCall` necessary to deploy the Router.
|
||||
pub fn deploy_router(&self, key: &PublicKey) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(Self::address().into()),
|
||||
input: abi::deployCall::new((Router::init_code(key).into(),)).abi_encode().into(),
|
||||
gas_limit: 1_000_000,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the first Router deployed with the specified key as its first key.
|
||||
///
|
||||
/// This is the Router Serai will use, and is the only way to construct a `Router`.
|
||||
pub async fn find_router(
|
||||
&self,
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
key: &PublicKey,
|
||||
) -> Result<Option<Router>, Error> {
|
||||
let init_code = Router::init_code(key);
|
||||
let init_code_hash = keccak256(&init_code);
|
||||
|
||||
#[cfg(not(test))]
|
||||
let to_block = BlockNumberOrTag::Finalized;
|
||||
#[cfg(test)]
|
||||
let to_block = BlockNumberOrTag::Latest;
|
||||
|
||||
// Find the first log using this init code (where the init code is binding to the key)
|
||||
let filter =
|
||||
Filter::new().from_block(0).to_block(to_block).address(Address::from(Self::address()));
|
||||
let filter = filter.event_signature(abi::Deployment::SIGNATURE_HASH);
|
||||
let filter = filter.topic1(B256::from(init_code_hash));
|
||||
let logs = provider.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
let Some(first_log) = logs.first() else { return Ok(None) };
|
||||
let router = first_log
|
||||
.log_decode::<abi::Deployment>()
|
||||
.map_err(|_| Error::ConnectionError)?
|
||||
.inner
|
||||
.data
|
||||
.created;
|
||||
|
||||
Ok(Some(Router::new(provider, router)))
|
||||
}
|
||||
}
|
||||
118
coins/ethereum/src/erc20.rs
Normal file
118
coins/ethereum/src/erc20.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use std::{sync::Arc, collections::HashSet};
|
||||
|
||||
use alloy_core::primitives::{Address, B256, U256};
|
||||
|
||||
use alloy_sol_types::{SolInterface, SolEvent};
|
||||
|
||||
use alloy_rpc_types::{BlockNumberOrTag, Filter};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use crate::Error;
|
||||
pub use crate::abi::erc20 as abi;
|
||||
use abi::{IERC20Calls, Transfer, transferCall, transferFromCall};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TopLevelErc20Transfer {
|
||||
pub id: [u8; 32],
|
||||
pub from: [u8; 20],
|
||||
pub amount: U256,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A view for an ERC20 contract.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ERC20(Arc<RootProvider<SimpleRequest>>, Address);
|
||||
impl ERC20 {
|
||||
/// Construct a new view of the specified ERC20 contract.
|
||||
///
|
||||
/// This checks a contract is deployed at that address yet does not check the contract is
|
||||
/// actually an ERC20.
|
||||
pub async fn new(
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
address: [u8; 20],
|
||||
) -> Result<Option<Self>, Error> {
|
||||
let code = provider
|
||||
.get_code_at(address.into(), BlockNumberOrTag::Finalized.into())
|
||||
.await
|
||||
.map_err(|_| Error::ConnectionError)?;
|
||||
// Contract has yet to be deployed
|
||||
if code.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(Self(provider.clone(), Address::from(&address))))
|
||||
}
|
||||
|
||||
pub async fn top_level_transfers(
|
||||
&self,
|
||||
block: u64,
|
||||
to: [u8; 20],
|
||||
) -> Result<Vec<TopLevelErc20Transfer>, Error> {
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
||||
let filter = filter.event_signature(Transfer::SIGNATURE_HASH);
|
||||
let mut to_topic = [0; 32];
|
||||
to_topic[12 ..].copy_from_slice(&to);
|
||||
let filter = filter.topic2(B256::from(to_topic));
|
||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
let mut handled = HashSet::new();
|
||||
|
||||
let mut top_level_transfers = vec![];
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.1 {
|
||||
Err(Error::ConnectionError)?;
|
||||
}
|
||||
|
||||
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?;
|
||||
let tx = self.0.get_transaction_by_hash(tx_id).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
// If this is a top-level call...
|
||||
if tx.to == Some(self.1) {
|
||||
// And we recognize the call...
|
||||
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due
|
||||
// to the InInstruction appended
|
||||
if let Ok(call) = IERC20Calls::abi_decode(&tx.input, false) {
|
||||
// Extract the top-level call's from/to/value
|
||||
let (from, call_to, value) = match call {
|
||||
IERC20Calls::transfer(transferCall { to: call_to, value }) => (tx.from, call_to, value),
|
||||
IERC20Calls::transferFrom(transferFromCall { from, to: call_to, value }) => {
|
||||
(from, call_to, value)
|
||||
}
|
||||
// Treat any other function selectors as unrecognized
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let log = log.log_decode::<Transfer>().map_err(|_| Error::ConnectionError)?.inner.data;
|
||||
|
||||
// Ensure the top-level transfer is equivalent, and this presumably isn't a log for an
|
||||
// internal transfer
|
||||
if (log.from != from) || (call_to != to) || (value != log.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now that the top-level transfer is confirmed to be equivalent to the log, ensure it's
|
||||
// the only log we handle
|
||||
if handled.contains(&tx_id) {
|
||||
continue;
|
||||
}
|
||||
handled.insert(tx_id);
|
||||
|
||||
// Read the data appended after
|
||||
let encoded = call.abi_encode();
|
||||
let data = tx.input.as_ref()[encoded.len() ..].to_vec();
|
||||
|
||||
// Push the transfer
|
||||
top_level_transfers.push(TopLevelErc20Transfer {
|
||||
// Since we'll only handle one log for this TX, set the ID to the TX ID
|
||||
id: *tx_id,
|
||||
from: *log.from.0,
|
||||
amount: log.value,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(top_level_transfers)
|
||||
}
|
||||
}
|
||||
30
coins/ethereum/src/lib.rs
Normal file
30
coins/ethereum/src/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub use alloy_core;
|
||||
pub use alloy_consensus;
|
||||
|
||||
pub use alloy_rpc_types;
|
||||
pub use alloy_simple_request_transport;
|
||||
pub use alloy_rpc_client;
|
||||
pub use alloy_provider;
|
||||
|
||||
pub mod crypto;
|
||||
|
||||
pub(crate) mod abi;
|
||||
|
||||
pub mod erc20;
|
||||
pub mod deployer;
|
||||
pub mod router;
|
||||
|
||||
pub mod machine;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("failed to verify Schnorr signature")]
|
||||
InvalidSignature,
|
||||
#[error("couldn't make call/send TX")]
|
||||
ConnectionError,
|
||||
}
|
||||
414
coins/ethereum/src/machine.rs
Normal file
414
coins/ethereum/src/machine.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
use std::{
|
||||
io::{self, Read},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use group::GroupEncoding;
|
||||
use frost::{
|
||||
curve::{Ciphersuite, Secp256k1},
|
||||
Participant, ThresholdKeys, FrostError,
|
||||
algorithm::Schnorr,
|
||||
sign::*,
|
||||
};
|
||||
|
||||
use alloy_core::primitives::U256;
|
||||
|
||||
use crate::{
|
||||
crypto::{PublicKey, EthereumHram, Signature},
|
||||
router::{
|
||||
abi::{Call as AbiCall, OutInstruction as AbiOutInstruction},
|
||||
Router,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Call {
|
||||
pub to: [u8; 20],
|
||||
pub value: U256,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
impl Call {
|
||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let mut to = [0; 20];
|
||||
reader.read_exact(&mut to)?;
|
||||
|
||||
let value = {
|
||||
let mut value_bytes = [0; 32];
|
||||
reader.read_exact(&mut value_bytes)?;
|
||||
U256::from_le_slice(&value_bytes)
|
||||
};
|
||||
|
||||
let mut data_len = {
|
||||
let mut data_len = [0; 4];
|
||||
reader.read_exact(&mut data_len)?;
|
||||
usize::try_from(u32::from_le_bytes(data_len)).expect("u32 couldn't fit within a usize")
|
||||
};
|
||||
|
||||
// A valid DoS would be to claim a 4 GB data is present for only 4 bytes
|
||||
// We read this in 1 KB chunks to only read data actually present (with a max DoS of 1 KB)
|
||||
let mut data = vec![];
|
||||
while data_len > 0 {
|
||||
let chunk_len = data_len.min(1024);
|
||||
let mut chunk = vec![0; chunk_len];
|
||||
reader.read_exact(&mut chunk)?;
|
||||
data.extend(&chunk);
|
||||
data_len -= chunk_len;
|
||||
}
|
||||
|
||||
Ok(Call { to, value, data })
|
||||
}
|
||||
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(&self.to)?;
|
||||
writer.write_all(&self.value.as_le_bytes())?;
|
||||
|
||||
let data_len = u32::try_from(self.data.len())
|
||||
.map_err(|_| io::Error::other("call data length exceeded 2**32"))?;
|
||||
writer.write_all(&data_len.to_le_bytes())?;
|
||||
writer.write_all(&self.data)
|
||||
}
|
||||
}
|
||||
impl From<Call> for AbiCall {
|
||||
fn from(call: Call) -> AbiCall {
|
||||
AbiCall { to: call.to.into(), value: call.value, data: call.data.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum OutInstructionTarget {
|
||||
Direct([u8; 20]),
|
||||
Calls(Vec<Call>),
|
||||
}
|
||||
impl OutInstructionTarget {
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let mut kind = [0xff];
|
||||
reader.read_exact(&mut kind)?;
|
||||
|
||||
match kind[0] {
|
||||
0 => {
|
||||
let mut addr = [0; 20];
|
||||
reader.read_exact(&mut addr)?;
|
||||
Ok(OutInstructionTarget::Direct(addr))
|
||||
}
|
||||
1 => {
|
||||
let mut calls_len = [0; 4];
|
||||
reader.read_exact(&mut calls_len)?;
|
||||
let calls_len = u32::from_le_bytes(calls_len);
|
||||
|
||||
let mut calls = vec![];
|
||||
for _ in 0 .. calls_len {
|
||||
calls.push(Call::read(reader)?);
|
||||
}
|
||||
Ok(OutInstructionTarget::Calls(calls))
|
||||
}
|
||||
_ => Err(io::Error::other("unrecognized OutInstructionTarget"))?,
|
||||
}
|
||||
}
|
||||
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
match self {
|
||||
OutInstructionTarget::Direct(addr) => {
|
||||
writer.write_all(&[0])?;
|
||||
writer.write_all(addr)?;
|
||||
}
|
||||
OutInstructionTarget::Calls(calls) => {
|
||||
writer.write_all(&[1])?;
|
||||
let call_len = u32::try_from(calls.len())
|
||||
.map_err(|_| io::Error::other("amount of calls exceeded 2**32"))?;
|
||||
writer.write_all(&call_len.to_le_bytes())?;
|
||||
for call in calls {
|
||||
call.write(writer)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct OutInstruction {
|
||||
pub target: OutInstructionTarget,
|
||||
pub value: U256,
|
||||
}
|
||||
impl OutInstruction {
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let target = OutInstructionTarget::read(reader)?;
|
||||
|
||||
let value = {
|
||||
let mut value_bytes = [0; 32];
|
||||
reader.read_exact(&mut value_bytes)?;
|
||||
U256::from_le_slice(&value_bytes)
|
||||
};
|
||||
|
||||
Ok(OutInstruction { target, value })
|
||||
}
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
self.target.write(writer)?;
|
||||
writer.write_all(&self.value.as_le_bytes())
|
||||
}
|
||||
}
|
||||
impl From<OutInstruction> for AbiOutInstruction {
|
||||
fn from(instruction: OutInstruction) -> AbiOutInstruction {
|
||||
match instruction.target {
|
||||
OutInstructionTarget::Direct(addr) => {
|
||||
AbiOutInstruction { to: addr.into(), calls: vec![], value: instruction.value }
|
||||
}
|
||||
OutInstructionTarget::Calls(calls) => AbiOutInstruction {
|
||||
to: [0; 20].into(),
|
||||
calls: calls.into_iter().map(Into::into).collect(),
|
||||
value: instruction.value,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum RouterCommand {
|
||||
UpdateSeraiKey { chain_id: U256, nonce: U256, key: PublicKey },
|
||||
Execute { chain_id: U256, nonce: U256, outs: Vec<OutInstruction> },
|
||||
}
|
||||
|
||||
impl RouterCommand {
|
||||
pub fn msg(&self) -> Vec<u8> {
|
||||
match self {
|
||||
RouterCommand::UpdateSeraiKey { chain_id, nonce, key } => {
|
||||
Router::update_serai_key_message(*chain_id, *nonce, key)
|
||||
}
|
||||
RouterCommand::Execute { chain_id, nonce, outs } => Router::execute_message(
|
||||
*chain_id,
|
||||
*nonce,
|
||||
outs.iter().map(|out| out.clone().into()).collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let mut kind = [0xff];
|
||||
reader.read_exact(&mut kind)?;
|
||||
|
||||
match kind[0] {
|
||||
0 => {
|
||||
let mut chain_id = [0; 32];
|
||||
reader.read_exact(&mut chain_id)?;
|
||||
|
||||
let mut nonce = [0; 32];
|
||||
reader.read_exact(&mut nonce)?;
|
||||
|
||||
let key = PublicKey::new(Secp256k1::read_G(reader)?)
|
||||
.ok_or(io::Error::other("key for RouterCommand doesn't have an eth representation"))?;
|
||||
Ok(RouterCommand::UpdateSeraiKey {
|
||||
chain_id: U256::from_le_slice(&chain_id),
|
||||
nonce: U256::from_le_slice(&nonce),
|
||||
key,
|
||||
})
|
||||
}
|
||||
1 => {
|
||||
let mut chain_id = [0; 32];
|
||||
reader.read_exact(&mut chain_id)?;
|
||||
let chain_id = U256::from_le_slice(&chain_id);
|
||||
|
||||
let mut nonce = [0; 32];
|
||||
reader.read_exact(&mut nonce)?;
|
||||
let nonce = U256::from_le_slice(&nonce);
|
||||
|
||||
let mut outs_len = [0; 4];
|
||||
reader.read_exact(&mut outs_len)?;
|
||||
let outs_len = u32::from_le_bytes(outs_len);
|
||||
|
||||
let mut outs = vec![];
|
||||
for _ in 0 .. outs_len {
|
||||
outs.push(OutInstruction::read(reader)?);
|
||||
}
|
||||
|
||||
Ok(RouterCommand::Execute { chain_id, nonce, outs })
|
||||
}
|
||||
_ => Err(io::Error::other("reading unknown type of RouterCommand"))?,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
match self {
|
||||
RouterCommand::UpdateSeraiKey { chain_id, nonce, key } => {
|
||||
writer.write_all(&[0])?;
|
||||
writer.write_all(&chain_id.as_le_bytes())?;
|
||||
writer.write_all(&nonce.as_le_bytes())?;
|
||||
writer.write_all(&key.A.to_bytes())
|
||||
}
|
||||
RouterCommand::Execute { chain_id, nonce, outs } => {
|
||||
writer.write_all(&[1])?;
|
||||
writer.write_all(&chain_id.as_le_bytes())?;
|
||||
writer.write_all(&nonce.as_le_bytes())?;
|
||||
writer.write_all(&u32::try_from(outs.len()).unwrap().to_le_bytes())?;
|
||||
for out in outs {
|
||||
out.write(writer)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = vec![];
|
||||
self.write(&mut res).unwrap();
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct SignedRouterCommand {
|
||||
command: RouterCommand,
|
||||
signature: Signature,
|
||||
}
|
||||
|
||||
impl SignedRouterCommand {
|
||||
pub fn new(key: &PublicKey, command: RouterCommand, signature: &[u8; 64]) -> Option<Self> {
|
||||
let c = Secp256k1::read_F(&mut &signature[.. 32]).ok()?;
|
||||
let s = Secp256k1::read_F(&mut &signature[32 ..]).ok()?;
|
||||
let signature = Signature { c, s };
|
||||
|
||||
if !signature.verify(key, &command.msg()) {
|
||||
None?
|
||||
}
|
||||
Some(SignedRouterCommand { command, signature })
|
||||
}
|
||||
|
||||
pub fn command(&self) -> &RouterCommand {
|
||||
&self.command
|
||||
}
|
||||
|
||||
pub fn signature(&self) -> &Signature {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let command = RouterCommand::read(reader)?;
|
||||
|
||||
let mut sig = [0; 64];
|
||||
reader.read_exact(&mut sig)?;
|
||||
let signature = Signature::from_bytes(sig)?;
|
||||
|
||||
Ok(SignedRouterCommand { command, signature })
|
||||
}
|
||||
|
||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
self.command.write(writer)?;
|
||||
writer.write_all(&self.signature.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RouterCommandMachine {
|
||||
key: PublicKey,
|
||||
command: RouterCommand,
|
||||
machine: AlgorithmMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, EthereumHram>>,
|
||||
}
|
||||
|
||||
impl RouterCommandMachine {
|
||||
pub fn new(keys: ThresholdKeys<Secp256k1>, command: RouterCommand) -> Option<Self> {
|
||||
// The Schnorr algorithm should be fine without this, even when using the IETF variant
|
||||
// If this is better and more comprehensive, we should do it, even if not necessary
|
||||
let mut transcript = RecommendedTranscript::new(b"ethereum-serai RouterCommandMachine v0.1");
|
||||
let key = keys.group_key();
|
||||
transcript.append_message(b"key", key.to_bytes());
|
||||
transcript.append_message(b"command", command.serialize());
|
||||
|
||||
Some(Self {
|
||||
key: PublicKey::new(key)?,
|
||||
command,
|
||||
machine: AlgorithmMachine::new(Schnorr::new(transcript), keys),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PreprocessMachine for RouterCommandMachine {
|
||||
type Preprocess = Preprocess<Secp256k1, ()>;
|
||||
type Signature = SignedRouterCommand;
|
||||
type SignMachine = RouterCommandSignMachine;
|
||||
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> (Self::SignMachine, Self::Preprocess) {
|
||||
let (machine, preprocess) = self.machine.preprocess(rng);
|
||||
|
||||
(RouterCommandSignMachine { key: self.key, command: self.command, machine }, preprocess)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RouterCommandSignMachine {
|
||||
key: PublicKey,
|
||||
command: RouterCommand,
|
||||
machine: AlgorithmSignMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, EthereumHram>>,
|
||||
}
|
||||
|
||||
impl SignMachine<SignedRouterCommand> for RouterCommandSignMachine {
|
||||
type Params = ();
|
||||
type Keys = ThresholdKeys<Secp256k1>;
|
||||
type Preprocess = Preprocess<Secp256k1, ()>;
|
||||
type SignatureShare = SignatureShare<Secp256k1>;
|
||||
type SignatureMachine = RouterCommandSignatureMachine;
|
||||
|
||||
fn cache(self) -> CachedPreprocess {
|
||||
unimplemented!(
|
||||
"RouterCommand machines don't support caching their preprocesses due to {}",
|
||||
"being already bound to a specific command"
|
||||
);
|
||||
}
|
||||
|
||||
fn from_cache(
|
||||
(): (),
|
||||
_: ThresholdKeys<Secp256k1>,
|
||||
_: CachedPreprocess,
|
||||
) -> (Self, Self::Preprocess) {
|
||||
unimplemented!(
|
||||
"RouterCommand machines don't support caching their preprocesses due to {}",
|
||||
"being already bound to a specific command"
|
||||
);
|
||||
}
|
||||
|
||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
||||
self.machine.read_preprocess(reader)
|
||||
}
|
||||
|
||||
fn sign(
|
||||
self,
|
||||
commitments: HashMap<Participant, Self::Preprocess>,
|
||||
msg: &[u8],
|
||||
) -> Result<(RouterCommandSignatureMachine, Self::SignatureShare), FrostError> {
|
||||
if !msg.is_empty() {
|
||||
panic!("message was passed to a RouterCommand machine when it generates its own");
|
||||
}
|
||||
|
||||
let (machine, share) = self.machine.sign(commitments, &self.command.msg())?;
|
||||
|
||||
Ok((RouterCommandSignatureMachine { key: self.key, command: self.command, machine }, share))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RouterCommandSignatureMachine {
|
||||
key: PublicKey,
|
||||
command: RouterCommand,
|
||||
machine:
|
||||
AlgorithmSignatureMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, EthereumHram>>,
|
||||
}
|
||||
|
||||
impl SignatureMachine<SignedRouterCommand> for RouterCommandSignatureMachine {
|
||||
type SignatureShare = SignatureShare<Secp256k1>;
|
||||
|
||||
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
|
||||
self.machine.read_share(reader)
|
||||
}
|
||||
|
||||
fn complete(
|
||||
self,
|
||||
shares: HashMap<Participant, Self::SignatureShare>,
|
||||
) -> Result<SignedRouterCommand, FrostError> {
|
||||
let sig = self.machine.complete(shares)?;
|
||||
let signature = Signature::new(&self.key, &self.command.msg(), sig)
|
||||
.expect("machine produced an invalid signature");
|
||||
Ok(SignedRouterCommand { command: self.command, signature })
|
||||
}
|
||||
}
|
||||
426
coins/ethereum/src/router.rs
Normal file
426
coins/ethereum/src/router.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
use std::{sync::Arc, io, collections::HashSet};
|
||||
|
||||
use k256::{
|
||||
elliptic_curve::{group::GroupEncoding, sec1},
|
||||
ProjectivePoint,
|
||||
};
|
||||
|
||||
use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
|
||||
#[cfg(test)]
|
||||
use alloy_core::primitives::B256;
|
||||
use alloy_consensus::TxLegacy;
|
||||
|
||||
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
|
||||
|
||||
use alloy_rpc_types::Filter;
|
||||
#[cfg(test)]
|
||||
use alloy_rpc_types::{BlockId, TransactionRequest, TransactionInput};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
pub use crate::{
|
||||
Error,
|
||||
crypto::{PublicKey, Signature},
|
||||
abi::{erc20::Transfer, router as abi},
|
||||
};
|
||||
use abi::{SeraiKeyUpdated, InInstruction as InInstructionEvent, Executed as ExecutedEvent};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum Coin {
|
||||
Ether,
|
||||
Erc20([u8; 20]),
|
||||
}
|
||||
|
||||
impl Coin {
|
||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let mut kind = [0xff];
|
||||
reader.read_exact(&mut kind)?;
|
||||
Ok(match kind[0] {
|
||||
0 => Coin::Ether,
|
||||
1 => {
|
||||
let mut address = [0; 20];
|
||||
reader.read_exact(&mut address)?;
|
||||
Coin::Erc20(address)
|
||||
}
|
||||
_ => Err(io::Error::other("unrecognized Coin type"))?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
match self {
|
||||
Coin::Ether => writer.write_all(&[0]),
|
||||
Coin::Erc20(token) => {
|
||||
writer.write_all(&[1])?;
|
||||
writer.write_all(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct InInstruction {
|
||||
pub id: ([u8; 32], u64),
|
||||
pub from: [u8; 20],
|
||||
pub coin: Coin,
|
||||
pub amount: U256,
|
||||
pub data: Vec<u8>,
|
||||
pub key_at_end_of_block: ProjectivePoint,
|
||||
}
|
||||
|
||||
impl InInstruction {
|
||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let id = {
|
||||
let mut id_hash = [0; 32];
|
||||
reader.read_exact(&mut id_hash)?;
|
||||
let mut id_pos = [0; 8];
|
||||
reader.read_exact(&mut id_pos)?;
|
||||
let id_pos = u64::from_le_bytes(id_pos);
|
||||
(id_hash, id_pos)
|
||||
};
|
||||
|
||||
let mut from = [0; 20];
|
||||
reader.read_exact(&mut from)?;
|
||||
|
||||
let coin = Coin::read(reader)?;
|
||||
let mut amount = [0; 32];
|
||||
reader.read_exact(&mut amount)?;
|
||||
let amount = U256::from_le_slice(&amount);
|
||||
|
||||
let mut data_len = [0; 4];
|
||||
reader.read_exact(&mut data_len)?;
|
||||
let data_len = usize::try_from(u32::from_le_bytes(data_len))
|
||||
.map_err(|_| io::Error::other("InInstruction data exceeded 2**32 in length"))?;
|
||||
let mut data = vec![0; data_len];
|
||||
reader.read_exact(&mut data)?;
|
||||
|
||||
let mut key_at_end_of_block = <ProjectivePoint as GroupEncoding>::Repr::default();
|
||||
reader.read_exact(&mut key_at_end_of_block)?;
|
||||
let key_at_end_of_block = Option::from(ProjectivePoint::from_bytes(&key_at_end_of_block))
|
||||
.ok_or(io::Error::other("InInstruction had key at end of block which wasn't valid"))?;
|
||||
|
||||
Ok(InInstruction { id, from, coin, amount, data, key_at_end_of_block })
|
||||
}
|
||||
|
||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(&self.id.0)?;
|
||||
writer.write_all(&self.id.1.to_le_bytes())?;
|
||||
|
||||
writer.write_all(&self.from)?;
|
||||
|
||||
self.coin.write(writer)?;
|
||||
writer.write_all(&self.amount.as_le_bytes())?;
|
||||
|
||||
writer.write_all(
|
||||
&u32::try_from(self.data.len())
|
||||
.map_err(|_| {
|
||||
io::Error::other("InInstruction being written had data exceeding 2**32 in length")
|
||||
})?
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
writer.write_all(&self.data)?;
|
||||
|
||||
writer.write_all(&self.key_at_end_of_block.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Executed {
|
||||
pub tx_id: [u8; 32],
|
||||
pub nonce: u64,
|
||||
pub signature: [u8; 64],
|
||||
}
|
||||
|
||||
/// The contract Serai uses to manage its state.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Router(Arc<RootProvider<SimpleRequest>>, Address);
|
||||
impl Router {
|
||||
pub(crate) fn code() -> Vec<u8> {
|
||||
let bytecode = include_str!("../artifacts/Router.bin");
|
||||
Bytes::from_hex(bytecode).expect("compiled-in Router bytecode wasn't valid hex").to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn init_code(key: &PublicKey) -> Vec<u8> {
|
||||
let mut bytecode = Self::code();
|
||||
// Append the constructor arguments
|
||||
bytecode.extend((abi::constructorCall { _seraiKey: key.eth_repr().into() }).abi_encode());
|
||||
bytecode
|
||||
}
|
||||
|
||||
// This isn't pub in order to force users to use `Deployer::find_router`.
|
||||
pub(crate) fn new(provider: Arc<RootProvider<SimpleRequest>>, address: Address) -> Self {
|
||||
Self(provider, address)
|
||||
}
|
||||
|
||||
pub fn address(&self) -> [u8; 20] {
|
||||
**self.1
|
||||
}
|
||||
|
||||
/// Get the key for Serai at the specified block.
|
||||
#[cfg(test)]
|
||||
pub async fn serai_key(&self, at: [u8; 32]) -> Result<PublicKey, Error> {
|
||||
let call = TransactionRequest::default()
|
||||
.to(Some(self.1))
|
||||
.input(TransactionInput::new(abi::seraiKeyCall::new(()).abi_encode().into()));
|
||||
let bytes = self
|
||||
.0
|
||||
.call(&call, Some(BlockId::Hash(B256::from(at).into())))
|
||||
.await
|
||||
.map_err(|_| Error::ConnectionError)?;
|
||||
let res =
|
||||
abi::seraiKeyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;
|
||||
PublicKey::from_eth_repr(res._0.0).ok_or(Error::ConnectionError)
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to update the key for Serai.
|
||||
pub(crate) fn update_serai_key_message(chain_id: U256, nonce: U256, key: &PublicKey) -> Vec<u8> {
|
||||
let mut buffer = b"updateSeraiKey".to_vec();
|
||||
buffer.extend(&chain_id.to_be_bytes::<32>());
|
||||
buffer.extend(&nonce.to_be_bytes::<32>());
|
||||
buffer.extend(&key.eth_repr());
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Update the key representing Serai.
|
||||
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
||||
// TODO: Set a more accurate gas
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::updateSeraiKeyCall::new((public_key.eth_repr().into(), sig.into()))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
gas_limit: 100_000,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current nonce for the published batches.
|
||||
#[cfg(test)]
|
||||
pub async fn nonce(&self, at: [u8; 32]) -> Result<U256, Error> {
|
||||
let call = TransactionRequest::default()
|
||||
.to(Some(self.1))
|
||||
.input(TransactionInput::new(abi::nonceCall::new(()).abi_encode().into()));
|
||||
let bytes = self
|
||||
.0
|
||||
.call(&call, Some(BlockId::Hash(B256::from(at).into())))
|
||||
.await
|
||||
.map_err(|_| Error::ConnectionError)?;
|
||||
let res =
|
||||
abi::nonceCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;
|
||||
Ok(res._0)
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to update the key for Serai.
|
||||
pub(crate) fn execute_message(
|
||||
chain_id: U256,
|
||||
nonce: U256,
|
||||
outs: Vec<abi::OutInstruction>,
|
||||
) -> Vec<u8> {
|
||||
("execute".to_string(), chain_id, nonce, outs).abi_encode_params()
|
||||
}
|
||||
|
||||
/// Execute a batch of `OutInstruction`s.
|
||||
pub fn execute(&self, outs: &[abi::OutInstruction], sig: &Signature) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::executeCall::new((outs.to_vec(), sig.into())).abi_encode().into(),
|
||||
// TODO
|
||||
gas_limit: 100_000 + ((200_000 + 10_000) * u128::try_from(outs.len()).unwrap()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn in_instructions(
|
||||
&self,
|
||||
block: u64,
|
||||
allowed_tokens: &HashSet<[u8; 20]>,
|
||||
) -> Result<Vec<InInstruction>, Error> {
|
||||
let key_at_end_of_block = {
|
||||
let filter = Filter::new().from_block(0).to_block(block).address(self.1);
|
||||
let filter = filter.event_signature(SeraiKeyUpdated::SIGNATURE_HASH);
|
||||
let all_keys = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
let last_key_x_coordinate_log = all_keys.last().ok_or(Error::ConnectionError)?;
|
||||
let last_key_x_coordinate = last_key_x_coordinate_log
|
||||
.log_decode::<SeraiKeyUpdated>()
|
||||
.map_err(|_| Error::ConnectionError)?
|
||||
.inner
|
||||
.data
|
||||
.key;
|
||||
|
||||
let mut compressed_point = <ProjectivePoint as GroupEncoding>::Repr::default();
|
||||
compressed_point[0] = u8::from(sec1::Tag::CompressedEvenY);
|
||||
compressed_point[1 ..].copy_from_slice(last_key_x_coordinate.as_slice());
|
||||
|
||||
ProjectivePoint::from_bytes(&compressed_point).expect("router's last key wasn't a valid key")
|
||||
};
|
||||
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
||||
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
|
||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
let mut transfer_check = HashSet::new();
|
||||
let mut in_instructions = vec![];
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.1 {
|
||||
Err(Error::ConnectionError)?;
|
||||
}
|
||||
|
||||
let id = (
|
||||
log.block_hash.ok_or(Error::ConnectionError)?.into(),
|
||||
log.log_index.ok_or(Error::ConnectionError)?,
|
||||
);
|
||||
|
||||
let tx_hash = log.transaction_hash.ok_or(Error::ConnectionError)?;
|
||||
let tx = self.0.get_transaction_by_hash(tx_hash).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
let log =
|
||||
log.log_decode::<InInstructionEvent>().map_err(|_| Error::ConnectionError)?.inner.data;
|
||||
|
||||
let coin = if log.coin.0 == [0; 20] {
|
||||
Coin::Ether
|
||||
} else {
|
||||
let token = *log.coin.0;
|
||||
|
||||
if !allowed_tokens.contains(&token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this also counts as a top-level transfer via the token, drop it
|
||||
//
|
||||
// Necessary in order to handle a potential edge case with some theoretical token
|
||||
// implementations
|
||||
//
|
||||
// This will either let it be handled by the top-level transfer hook or will drop it
|
||||
// entirely on the side of caution
|
||||
if tx.to == Some(token.into()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all logs for this TX
|
||||
let receipt = self
|
||||
.0
|
||||
.get_transaction_receipt(tx_hash)
|
||||
.await
|
||||
.map_err(|_| Error::ConnectionError)?
|
||||
.ok_or(Error::ConnectionError)?;
|
||||
let tx_logs = receipt.inner.logs();
|
||||
|
||||
// Find a matching transfer log
|
||||
let mut found_transfer = false;
|
||||
for tx_log in tx_logs {
|
||||
let log_index = tx_log.log_index.ok_or(Error::ConnectionError)?;
|
||||
// Ensure we didn't already use this transfer to check a distinct InInstruction event
|
||||
if transfer_check.contains(&log_index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this log is from the token we expected to be transferred
|
||||
if tx_log.address().0 != token {
|
||||
continue;
|
||||
}
|
||||
// Check if this is a transfer log
|
||||
// https://github.com/alloy-rs/core/issues/589
|
||||
if tx_log.topics()[0] != Transfer::SIGNATURE_HASH {
|
||||
continue;
|
||||
}
|
||||
let Ok(transfer) = Transfer::decode_log(&tx_log.inner.clone(), true) else { continue };
|
||||
// Check if this is a transfer to us for the expected amount
|
||||
if (transfer.to == self.1) && (transfer.value == log.amount) {
|
||||
transfer_check.insert(log_index);
|
||||
found_transfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_transfer {
|
||||
// This shouldn't be a ConnectionError
|
||||
// This is an exploit, a non-conforming ERC20, or an invalid connection
|
||||
// This should halt the process which is sufficient, yet this is sub-optimal
|
||||
// TODO
|
||||
Err(Error::ConnectionError)?;
|
||||
}
|
||||
|
||||
Coin::Erc20(token)
|
||||
};
|
||||
|
||||
in_instructions.push(InInstruction {
|
||||
id,
|
||||
from: *log.from.0,
|
||||
coin,
|
||||
amount: log.amount,
|
||||
data: log.instruction.as_ref().to_vec(),
|
||||
key_at_end_of_block,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(in_instructions)
|
||||
}
|
||||
|
||||
pub async fn executed_commands(&self, block: u64) -> Result<Vec<Executed>, Error> {
|
||||
let mut res = vec![];
|
||||
|
||||
{
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
||||
let filter = filter.event_signature(SeraiKeyUpdated::SIGNATURE_HASH);
|
||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.1 {
|
||||
Err(Error::ConnectionError)?;
|
||||
}
|
||||
|
||||
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?.into();
|
||||
|
||||
let log =
|
||||
log.log_decode::<SeraiKeyUpdated>().map_err(|_| Error::ConnectionError)?.inner.data;
|
||||
|
||||
let mut signature = [0; 64];
|
||||
signature[.. 32].copy_from_slice(log.signature.c.as_ref());
|
||||
signature[32 ..].copy_from_slice(log.signature.s.as_ref());
|
||||
res.push(Executed {
|
||||
tx_id,
|
||||
nonce: log.nonce.try_into().map_err(|_| Error::ConnectionError)?,
|
||||
signature,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
||||
let filter = filter.event_signature(ExecutedEvent::SIGNATURE_HASH);
|
||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
for log in logs {
|
||||
// Double check the address which emitted this log
|
||||
if log.address() != self.1 {
|
||||
Err(Error::ConnectionError)?;
|
||||
}
|
||||
|
||||
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?.into();
|
||||
|
||||
let log = log.log_decode::<ExecutedEvent>().map_err(|_| Error::ConnectionError)?.inner.data;
|
||||
|
||||
let mut signature = [0; 64];
|
||||
signature[.. 32].copy_from_slice(log.signature.c.as_ref());
|
||||
signature[32 ..].copy_from_slice(log.signature.s.as_ref());
|
||||
res.push(Executed {
|
||||
tx_id,
|
||||
nonce: log.nonce.try_into().map_err(|_| Error::ConnectionError)?,
|
||||
signature,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tests")]
|
||||
pub fn key_updated_filter(&self) -> Filter {
|
||||
Filter::new().address(self.1).event_signature(SeraiKeyUpdated::SIGNATURE_HASH)
|
||||
}
|
||||
#[cfg(feature = "tests")]
|
||||
pub fn executed_filter(&self) -> Filter {
|
||||
Filter::new().address(self.1).event_signature(ExecutedEvent::SIGNATURE_HASH)
|
||||
}
|
||||
}
|
||||
13
coins/ethereum/src/tests/abi/mod.rs
Normal file
13
coins/ethereum/src/tests/abi/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use alloy_sol_types::sol;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(warnings)]
|
||||
#[allow(needless_pass_by_value)]
|
||||
#[allow(clippy::all)]
|
||||
#[allow(clippy::ignored_unit_patterns)]
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
mod schnorr_container {
|
||||
use super::*;
|
||||
sol!("src/tests/contracts/Schnorr.sol");
|
||||
}
|
||||
pub(crate) use schnorr_container::TestSchnorr as schnorr;
|
||||
51
coins/ethereum/src/tests/contracts/ERC20.sol
Normal file
51
coins/ethereum/src/tests/contracts/ERC20.sol
Normal file
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: AGPLv3
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract TestERC20 {
|
||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
|
||||
function name() public pure returns (string memory) {
|
||||
return "Test ERC20";
|
||||
}
|
||||
function symbol() public pure returns (string memory) {
|
||||
return "TEST";
|
||||
}
|
||||
function decimals() public pure returns (uint8) {
|
||||
return 18;
|
||||
}
|
||||
|
||||
function totalSupply() public pure returns (uint256) {
|
||||
return 1_000_000 * 10e18;
|
||||
}
|
||||
|
||||
mapping(address => uint256) balances;
|
||||
mapping(address => mapping(address => uint256)) allowances;
|
||||
|
||||
constructor() {
|
||||
balances[msg.sender] = totalSupply();
|
||||
}
|
||||
|
||||
function balanceOf(address owner) public view returns (uint256) {
|
||||
return balances[owner];
|
||||
}
|
||||
function transfer(address to, uint256 value) public returns (bool) {
|
||||
balances[msg.sender] -= value;
|
||||
balances[to] += value;
|
||||
return true;
|
||||
}
|
||||
function transferFrom(address from, address to, uint256 value) public returns (bool) {
|
||||
allowances[from][msg.sender] -= value;
|
||||
balances[from] -= value;
|
||||
balances[to] += value;
|
||||
return true;
|
||||
}
|
||||
|
||||
function approve(address spender, uint256 value) public returns (bool) {
|
||||
allowances[msg.sender][spender] = value;
|
||||
return true;
|
||||
}
|
||||
function allowance(address owner, address spender) public view returns (uint256) {
|
||||
return allowances[owner][spender];
|
||||
}
|
||||
}
|
||||
15
coins/ethereum/src/tests/contracts/Schnorr.sol
Normal file
15
coins/ethereum/src/tests/contracts/Schnorr.sol
Normal file
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: AGPLv3
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "../../../contracts/Schnorr.sol";
|
||||
|
||||
contract TestSchnorr {
|
||||
function verify(
|
||||
bytes32 px,
|
||||
bytes calldata message,
|
||||
bytes32 c,
|
||||
bytes32 s
|
||||
) external pure returns (bool) {
|
||||
return Schnorr.verify(px, message, c, s);
|
||||
}
|
||||
}
|
||||
105
coins/ethereum/src/tests/crypto.rs
Normal file
105
coins/ethereum/src/tests/crypto.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use rand_core::OsRng;
|
||||
|
||||
use group::ff::{Field, PrimeField};
|
||||
use k256::{
|
||||
ecdsa::{
|
||||
self, hazmat::SignPrimitive, signature::hazmat::PrehashVerifier, SigningKey, VerifyingKey,
|
||||
},
|
||||
Scalar, ProjectivePoint,
|
||||
};
|
||||
|
||||
use frost::{
|
||||
curve::{Ciphersuite, Secp256k1},
|
||||
algorithm::{Hram, IetfSchnorr},
|
||||
tests::{algorithm_machines, sign},
|
||||
};
|
||||
|
||||
use crate::{crypto::*, tests::key_gen};
|
||||
|
||||
// The ecrecover opcode, yet with parity replacing v
|
||||
pub(crate) fn ecrecover(message: Scalar, odd_y: bool, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
||||
let sig = ecdsa::Signature::from_scalars(r, s).ok()?;
|
||||
let message: [u8; 32] = message.to_repr().into();
|
||||
alloy_core::primitives::Signature::from_signature_and_parity(
|
||||
sig,
|
||||
alloy_core::primitives::Parity::Parity(odd_y),
|
||||
)
|
||||
.ok()?
|
||||
.recover_address_from_prehash(&alloy_core::primitives::B256::from(message))
|
||||
.ok()
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecrecover() {
|
||||
let private = SigningKey::random(&mut OsRng);
|
||||
let public = VerifyingKey::from(&private);
|
||||
|
||||
// Sign the signature
|
||||
const MESSAGE: &[u8] = b"Hello, World!";
|
||||
let (sig, recovery_id) = private
|
||||
.as_nonzero_scalar()
|
||||
.try_sign_prehashed(
|
||||
<Secp256k1 as Ciphersuite>::F::random(&mut OsRng),
|
||||
&keccak256(MESSAGE).into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Sanity check the signature verifies
|
||||
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
||||
{
|
||||
assert_eq!(public.verify_prehash(&keccak256(MESSAGE), &sig).unwrap(), ());
|
||||
}
|
||||
|
||||
// Perform the ecrecover
|
||||
assert_eq!(
|
||||
ecrecover(
|
||||
hash_to_scalar(MESSAGE),
|
||||
u8::from(recovery_id.unwrap().is_y_odd()) == 1,
|
||||
*sig.r(),
|
||||
*sig.s()
|
||||
)
|
||||
.unwrap(),
|
||||
address(&ProjectivePoint::from(public.as_affine()))
|
||||
);
|
||||
}
|
||||
|
||||
// Run the sign test with the EthereumHram
|
||||
#[test]
|
||||
fn test_signing() {
|
||||
let (keys, _) = key_gen();
|
||||
|
||||
const MESSAGE: &[u8] = b"Hello, World!";
|
||||
|
||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||
let _sig =
|
||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn preprocess_signature_for_ecrecover(
|
||||
R: ProjectivePoint,
|
||||
public_key: &PublicKey,
|
||||
m: &[u8],
|
||||
s: Scalar,
|
||||
) -> (Scalar, Scalar) {
|
||||
let c = EthereumHram::hram(&R, &public_key.A, m);
|
||||
let sa = -(s * public_key.px);
|
||||
let ca = -(c * public_key.px);
|
||||
(sa, ca)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecrecover_hack() {
|
||||
let (keys, public_key) = key_gen();
|
||||
|
||||
const MESSAGE: &[u8] = b"Hello, World!";
|
||||
|
||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||
let sig =
|
||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
||||
|
||||
let (sa, ca) = preprocess_signature_for_ecrecover(sig.R, &public_key, MESSAGE, sig.s);
|
||||
let q = ecrecover(sa, false, public_key.px, ca).unwrap();
|
||||
assert_eq!(q, address(&sig.R));
|
||||
}
|
||||
127
coins/ethereum/src/tests/mod.rs
Normal file
127
coins/ethereum/src/tests/mod.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
use rand_core::OsRng;
|
||||
|
||||
use k256::{Scalar, ProjectivePoint};
|
||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, tests::key_gen as frost_key_gen};
|
||||
|
||||
use alloy_core::{
|
||||
primitives::{Address, U256, Bytes, TxKind},
|
||||
hex::FromHex,
|
||||
};
|
||||
use alloy_consensus::{SignableTransaction, TxLegacy};
|
||||
|
||||
use alloy_rpc_types::TransactionReceipt;
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use crate::crypto::{address, deterministically_sign, PublicKey};
|
||||
|
||||
mod crypto;
|
||||
|
||||
mod abi;
|
||||
mod schnorr;
|
||||
mod router;
|
||||
|
||||
pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey) {
|
||||
let mut keys = frost_key_gen::<_, Secp256k1>(&mut OsRng);
|
||||
let mut group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||
|
||||
let mut offset = Scalar::ZERO;
|
||||
while PublicKey::new(group_key).is_none() {
|
||||
offset += Scalar::ONE;
|
||||
group_key += ProjectivePoint::GENERATOR;
|
||||
}
|
||||
for keys in keys.values_mut() {
|
||||
*keys = keys.offset(offset);
|
||||
}
|
||||
let public_key = PublicKey::new(group_key).unwrap();
|
||||
|
||||
(keys, public_key)
|
||||
}
|
||||
|
||||
// TODO: Use a proper error here
|
||||
pub async fn send(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
wallet: &k256::ecdsa::SigningKey,
|
||||
mut tx: TxLegacy,
|
||||
) -> Option<TransactionReceipt> {
|
||||
let verifying_key = *wallet.verifying_key().as_affine();
|
||||
let address = Address::from(address(&verifying_key.into()));
|
||||
|
||||
// https://github.com/alloy-rs/alloy/issues/539
|
||||
// let chain_id = provider.get_chain_id().await.unwrap();
|
||||
// tx.chain_id = Some(chain_id);
|
||||
tx.chain_id = None;
|
||||
tx.nonce = provider.get_transaction_count(address, None).await.unwrap();
|
||||
// 100 gwei
|
||||
tx.gas_price = 100_000_000_000u128;
|
||||
|
||||
let sig = wallet.sign_prehash_recoverable(tx.signature_hash().as_ref()).unwrap();
|
||||
assert_eq!(address, tx.clone().into_signed(sig.into()).recover_signer().unwrap());
|
||||
assert!(
|
||||
provider.get_balance(address, None).await.unwrap() >
|
||||
((U256::from(tx.gas_price) * U256::from(tx.gas_limit)) + tx.value)
|
||||
);
|
||||
|
||||
let mut bytes = vec![];
|
||||
tx.encode_with_signature_fields(&sig.into(), &mut bytes);
|
||||
let pending_tx = provider.send_raw_transaction(&bytes).await.ok()?;
|
||||
pending_tx.get_receipt().await.ok()
|
||||
}
|
||||
|
||||
pub async fn fund_account(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
wallet: &k256::ecdsa::SigningKey,
|
||||
to_fund: Address,
|
||||
value: U256,
|
||||
) -> Option<()> {
|
||||
let funding_tx =
|
||||
TxLegacy { to: TxKind::Call(to_fund), gas_limit: 21_000, value, ..Default::default() };
|
||||
assert!(send(provider, wallet, funding_tx).await.unwrap().status());
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
// TODO: Use a proper error here
|
||||
pub async fn deploy_contract(
|
||||
client: Arc<RootProvider<SimpleRequest>>,
|
||||
wallet: &k256::ecdsa::SigningKey,
|
||||
name: &str,
|
||||
) -> Option<Address> {
|
||||
let hex_bin_buf = std::fs::read_to_string(format!("./artifacts/{name}.bin")).unwrap();
|
||||
let hex_bin =
|
||||
if let Some(stripped) = hex_bin_buf.strip_prefix("0x") { stripped } else { &hex_bin_buf };
|
||||
let bin = Bytes::from_hex(hex_bin).unwrap();
|
||||
|
||||
let deployment_tx = TxLegacy {
|
||||
chain_id: None,
|
||||
nonce: 0,
|
||||
// 100 gwei
|
||||
gas_price: 100_000_000_000u128,
|
||||
gas_limit: 1_000_000,
|
||||
to: TxKind::Create,
|
||||
value: U256::ZERO,
|
||||
input: bin,
|
||||
};
|
||||
|
||||
let deployment_tx = deterministically_sign(&deployment_tx);
|
||||
|
||||
// Fund the deployer address
|
||||
fund_account(
|
||||
&client,
|
||||
wallet,
|
||||
deployment_tx.recover_signer().unwrap(),
|
||||
U256::from(deployment_tx.tx().gas_limit) * U256::from(deployment_tx.tx().gas_price),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (deployment_tx, sig, _) = deployment_tx.into_parts();
|
||||
let mut bytes = vec![];
|
||||
deployment_tx.encode_with_signature_fields(&sig, &mut bytes);
|
||||
let pending_tx = client.send_raw_transaction(&bytes).await.ok()?;
|
||||
let receipt = pending_tx.get_receipt().await.ok()?;
|
||||
assert!(receipt.status());
|
||||
|
||||
Some(receipt.contract_address.unwrap())
|
||||
}
|
||||
183
coins/ethereum/src/tests/router.rs
Normal file
183
coins/ethereum/src/tests/router.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use std::{convert::TryFrom, sync::Arc, collections::HashMap};
|
||||
|
||||
use rand_core::OsRng;
|
||||
|
||||
use group::Group;
|
||||
use k256::ProjectivePoint;
|
||||
use frost::{
|
||||
curve::Secp256k1,
|
||||
Participant, ThresholdKeys,
|
||||
algorithm::IetfSchnorr,
|
||||
tests::{algorithm_machines, sign},
|
||||
};
|
||||
|
||||
use alloy_core::primitives::{Address, U256};
|
||||
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||
|
||||
use crate::{
|
||||
crypto::*,
|
||||
deployer::Deployer,
|
||||
router::{Router, abi as router},
|
||||
tests::{key_gen, send, fund_account},
|
||||
};
|
||||
|
||||
async fn setup_test() -> (
|
||||
AnvilInstance,
|
||||
Arc<RootProvider<SimpleRequest>>,
|
||||
u64,
|
||||
Router,
|
||||
HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
||||
PublicKey,
|
||||
) {
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
let provider = RootProvider::new(
|
||||
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
||||
);
|
||||
let chain_id = provider.get_chain_id().await.unwrap();
|
||||
let wallet = anvil.keys()[0].clone().into();
|
||||
let client = Arc::new(provider);
|
||||
|
||||
// Make sure the Deployer constructor returns None, as it doesn't exist yet
|
||||
assert!(Deployer::new(client.clone()).await.unwrap().is_none());
|
||||
|
||||
// Deploy the Deployer
|
||||
let tx = Deployer::deployment_tx();
|
||||
fund_account(
|
||||
&client,
|
||||
&wallet,
|
||||
tx.recover_signer().unwrap(),
|
||||
U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (tx, sig, _) = tx.into_parts();
|
||||
let mut bytes = vec![];
|
||||
tx.encode_with_signature_fields(&sig, &mut bytes);
|
||||
|
||||
let pending_tx = client.send_raw_transaction(&bytes).await.unwrap();
|
||||
let receipt = pending_tx.get_receipt().await.unwrap();
|
||||
assert!(receipt.status());
|
||||
let deployer =
|
||||
Deployer::new(client.clone()).await.expect("network error").expect("deployer wasn't deployed");
|
||||
|
||||
let (keys, public_key) = key_gen();
|
||||
|
||||
// Verify the Router constructor returns None, as it doesn't exist yet
|
||||
assert!(deployer.find_router(client.clone(), &public_key).await.unwrap().is_none());
|
||||
|
||||
// Deploy the router
|
||||
let receipt = send(&client, &anvil.keys()[0].clone().into(), deployer.deploy_router(&public_key))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(receipt.status());
|
||||
let contract = deployer.find_router(client.clone(), &public_key).await.unwrap().unwrap();
|
||||
|
||||
(anvil, client, chain_id, contract, keys, public_key)
|
||||
}
|
||||
|
||||
async fn latest_block_hash(client: &RootProvider<SimpleRequest>) -> [u8; 32] {
|
||||
client
|
||||
.get_block(client.get_block_number().await.unwrap().into(), false)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.header
|
||||
.hash
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deploy_contract() {
|
||||
let (_anvil, client, _, router, _, public_key) = setup_test().await;
|
||||
|
||||
let block_hash = latest_block_hash(&client).await;
|
||||
assert_eq!(router.serai_key(block_hash).await.unwrap(), public_key);
|
||||
assert_eq!(router.nonce(block_hash).await.unwrap(), U256::try_from(1u64).unwrap());
|
||||
// TODO: Check it emitted SeraiKeyUpdated(public_key) at its genesis
|
||||
}
|
||||
|
||||
pub fn hash_and_sign(
|
||||
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
||||
public_key: &PublicKey,
|
||||
message: &[u8],
|
||||
) -> Signature {
|
||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||
let sig =
|
||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, keys), message);
|
||||
|
||||
Signature::new(public_key, message, sig).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_router_update_serai_key() {
|
||||
let (anvil, client, chain_id, contract, keys, public_key) = setup_test().await;
|
||||
|
||||
let next_key = loop {
|
||||
let point = ProjectivePoint::random(&mut OsRng);
|
||||
let Some(next_key) = PublicKey::new(point) else { continue };
|
||||
break next_key;
|
||||
};
|
||||
|
||||
let message = Router::update_serai_key_message(
|
||||
U256::try_from(chain_id).unwrap(),
|
||||
U256::try_from(1u64).unwrap(),
|
||||
&next_key,
|
||||
);
|
||||
let sig = hash_and_sign(&keys, &public_key, &message);
|
||||
|
||||
let first_block_hash = latest_block_hash(&client).await;
|
||||
assert_eq!(contract.serai_key(first_block_hash).await.unwrap(), public_key);
|
||||
|
||||
let receipt =
|
||||
send(&client, &anvil.keys()[0].clone().into(), contract.update_serai_key(&next_key, &sig))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(receipt.status());
|
||||
|
||||
let second_block_hash = latest_block_hash(&client).await;
|
||||
assert_eq!(contract.serai_key(second_block_hash).await.unwrap(), next_key);
|
||||
// Check this does still offer the historical state
|
||||
assert_eq!(contract.serai_key(first_block_hash).await.unwrap(), public_key);
|
||||
// TODO: Check logs
|
||||
|
||||
println!("gas used: {:?}", receipt.gas_used);
|
||||
// println!("logs: {:?}", receipt.logs);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_router_execute() {
|
||||
let (anvil, client, chain_id, contract, keys, public_key) = setup_test().await;
|
||||
|
||||
let to = Address::from([0; 20]);
|
||||
let value = U256::ZERO;
|
||||
let tx = router::OutInstruction { to, value, calls: vec![] };
|
||||
let txs = vec![tx];
|
||||
|
||||
let first_block_hash = latest_block_hash(&client).await;
|
||||
let nonce = contract.nonce(first_block_hash).await.unwrap();
|
||||
assert_eq!(nonce, U256::try_from(1u64).unwrap());
|
||||
|
||||
let message = Router::execute_message(U256::try_from(chain_id).unwrap(), nonce, txs.clone());
|
||||
let sig = hash_and_sign(&keys, &public_key, &message);
|
||||
|
||||
let receipt =
|
||||
send(&client, &anvil.keys()[0].clone().into(), contract.execute(&txs, &sig)).await.unwrap();
|
||||
assert!(receipt.status());
|
||||
|
||||
let second_block_hash = latest_block_hash(&client).await;
|
||||
assert_eq!(contract.nonce(second_block_hash).await.unwrap(), U256::try_from(2u64).unwrap());
|
||||
// Check this does still offer the historical state
|
||||
assert_eq!(contract.nonce(first_block_hash).await.unwrap(), U256::try_from(1u64).unwrap());
|
||||
// TODO: Check logs
|
||||
|
||||
println!("gas used: {:?}", receipt.gas_used);
|
||||
// println!("logs: {:?}", receipt.logs);
|
||||
}
|
||||
93
coins/ethereum/src/tests/schnorr.rs
Normal file
93
coins/ethereum/src/tests/schnorr.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use rand_core::OsRng;
|
||||
|
||||
use group::ff::PrimeField;
|
||||
use k256::Scalar;
|
||||
|
||||
use frost::{
|
||||
curve::Secp256k1,
|
||||
algorithm::IetfSchnorr,
|
||||
tests::{algorithm_machines, sign},
|
||||
};
|
||||
|
||||
use alloy_core::primitives::Address;
|
||||
|
||||
use alloy_sol_types::SolCall;
|
||||
|
||||
use alloy_rpc_types::{TransactionInput, TransactionRequest};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
crypto::*,
|
||||
tests::{key_gen, deploy_contract, abi::schnorr as abi},
|
||||
};
|
||||
|
||||
async fn setup_test() -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Address) {
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
let provider = RootProvider::new(
|
||||
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
||||
);
|
||||
let wallet = anvil.keys()[0].clone().into();
|
||||
let client = Arc::new(provider);
|
||||
|
||||
let address = deploy_contract(client.clone(), &wallet, "TestSchnorr").await.unwrap();
|
||||
(anvil, client, address)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deploy_contract() {
|
||||
setup_test().await;
|
||||
}
|
||||
|
||||
pub async fn call_verify(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
contract: Address,
|
||||
public_key: &PublicKey,
|
||||
message: &[u8],
|
||||
signature: &Signature,
|
||||
) -> Result<(), Error> {
|
||||
let px: [u8; 32] = public_key.px.to_repr().into();
|
||||
let c_bytes: [u8; 32] = signature.c.to_repr().into();
|
||||
let s_bytes: [u8; 32] = signature.s.to_repr().into();
|
||||
let call = TransactionRequest::default().to(Some(contract)).input(TransactionInput::new(
|
||||
abi::verifyCall::new((px.into(), message.to_vec().into(), c_bytes.into(), s_bytes.into()))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
));
|
||||
let bytes = provider.call(&call, None).await.map_err(|_| Error::ConnectionError)?;
|
||||
let res =
|
||||
abi::verifyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;
|
||||
|
||||
if res._0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::InvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ecrecover_hack() {
|
||||
let (_anvil, client, contract) = setup_test().await;
|
||||
|
||||
let (keys, public_key) = key_gen();
|
||||
|
||||
const MESSAGE: &[u8] = b"Hello, World!";
|
||||
|
||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||
let sig =
|
||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
||||
let sig = Signature::new(&public_key, MESSAGE, sig).unwrap();
|
||||
|
||||
call_verify(&client, contract, &public_key, MESSAGE, &sig).await.unwrap();
|
||||
// Test an invalid signature fails
|
||||
let mut sig = sig;
|
||||
sig.s += Scalar::ONE;
|
||||
assert!(call_verify(&client, contract, &public_key, MESSAGE, &sig).await.is_err());
|
||||
}
|
||||
1
coins/monero/.gitignore
vendored
1
coins/monero/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
c/.build
|
||||
@@ -1,52 +1,105 @@
|
||||
[package]
|
||||
name = "monero-serai"
|
||||
version = "0.1.0"
|
||||
description = "A modern Monero wallet library"
|
||||
version = "0.1.4-alpha"
|
||||
description = "A modern Monero transaction library"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.74"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
hex-literal = "0.3"
|
||||
lazy_static = "1"
|
||||
thiserror = "1"
|
||||
std-shims = { path = "../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||
|
||||
rand_core = "0.6"
|
||||
rand_chacha = { version = "0.3", optional = true }
|
||||
rand = "0.8"
|
||||
rand_distr = "0.4"
|
||||
async-trait = { version = "0.1", default-features = false }
|
||||
thiserror = { version = "1", default-features = false, optional = true }
|
||||
|
||||
subtle = "2.4"
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
subtle = { version = "^2.4", default-features = false }
|
||||
|
||||
tiny-keccak = { version = "2", features = ["keccak"] }
|
||||
blake2 = { version = "0.10", optional = true }
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
# Used to send transactions
|
||||
rand = { version = "0.8", default-features = false }
|
||||
rand_chacha = { version = "0.3", default-features = false }
|
||||
# Used to select decoys
|
||||
rand_distr = { version = "0.4", default-features = false }
|
||||
|
||||
curve25519-dalek = { version = "3", features = ["std"] }
|
||||
sha3 = { version = "0.10", default-features = false }
|
||||
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
|
||||
|
||||
group = { version = "0.12" }
|
||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group" }
|
||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
|
||||
dleq = { package = "dleq-serai", path = "../../crypto/dleq", features = ["serialize"], optional = true }
|
||||
# Used for the hash to curve, along with the more complicated proofs
|
||||
group = { version = "0.13", default-features = false }
|
||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
||||
multiexp = { path = "../../crypto/multiexp", version = "0.4", default-features = false, features = ["batch"] }
|
||||
|
||||
hex = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
# Needed for multisig
|
||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["ed25519"], optional = true }
|
||||
|
||||
base58-monero = "1"
|
||||
monero-epee-bin-serde = "1.0"
|
||||
monero = "0.16"
|
||||
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
||||
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
hex-literal = "0.4"
|
||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
|
||||
serde_json = { version = "1", default-features = false, features = ["alloc"] }
|
||||
|
||||
[features]
|
||||
experimental = []
|
||||
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]
|
||||
base58-monero = { version = "2", default-features = false, features = ["check"] }
|
||||
|
||||
# Used for the provided HTTP RPC
|
||||
digest_auth = { version = "0.3", default-features = false, optional = true }
|
||||
simple-request = { path = "../../common/request", version = "0.1", default-features = false, features = ["tls"], optional = true }
|
||||
tokio = { version = "1", default-features = false, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
||||
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
sha2 = "0.10"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio = { version = "1", features = ["sync", "macros"] }
|
||||
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"std-shims/std",
|
||||
|
||||
"thiserror",
|
||||
|
||||
"zeroize/std",
|
||||
"subtle/std",
|
||||
|
||||
"rand_core/std",
|
||||
"rand/std",
|
||||
"rand_chacha/std",
|
||||
"rand_distr/std",
|
||||
|
||||
"sha3/std",
|
||||
"pbkdf2/std",
|
||||
|
||||
"multiexp/std",
|
||||
|
||||
"transcript/std",
|
||||
|
||||
"monero-generators/std",
|
||||
|
||||
"hex/std",
|
||||
"serde/std",
|
||||
"serde_json/std",
|
||||
|
||||
"base58-monero/std",
|
||||
]
|
||||
|
||||
http-rpc = ["digest_auth", "simple-request", "tokio"]
|
||||
multisig = ["transcript", "frost", "std"]
|
||||
binaries = ["tokio/rt-multi-thread", "tokio/macros", "http-rpc"]
|
||||
|
||||
default = ["std", "http-rpc"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Luke Parker
|
||||
Copyright (c) 2022-2023 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -4,4 +4,58 @@ A modern Monero transaction library intended for usage in wallets. It prides
|
||||
itself on accuracy, correctness, and removing common pit falls developers may
|
||||
face.
|
||||
|
||||
Threshold multisignature support is available via the `multisig` feature.
|
||||
monero-serai also offers the following features:
|
||||
|
||||
- Featured Addresses
|
||||
- A FROST-based multisig orders of magnitude more performant than Monero's
|
||||
|
||||
### Purpose and support
|
||||
|
||||
monero-serai was written for Serai, a decentralized exchange aiming to support
|
||||
Monero. Despite this, monero-serai is intended to be a widely usable library,
|
||||
accurate to Monero. monero-serai guarantees the functionality needed for Serai,
|
||||
yet will not deprive functionality from other users.
|
||||
|
||||
Various legacy transaction formats are not currently implemented, yet we are
|
||||
willing to add support for them. There aren't active development efforts around
|
||||
them however.
|
||||
|
||||
### Caveats
|
||||
|
||||
This library DOES attempt to do the following:
|
||||
|
||||
- Create on-chain transactions identical to how wallet2 would (unless told not
|
||||
to)
|
||||
- Not be detectable as monero-serai when scanning outputs
|
||||
- Not reveal spent outputs to the connected RPC node
|
||||
|
||||
This library DOES NOT attempt to do the following:
|
||||
|
||||
- Have identical RPC behavior when creating transactions
|
||||
- Be a wallet
|
||||
|
||||
This means that monero-serai shouldn't be fingerprintable on-chain. It also
|
||||
shouldn't be fingerprintable if a targeted attack occurs to detect if the
|
||||
receiving wallet is monero-serai or wallet2. It also should be generally safe
|
||||
for usage with remote nodes.
|
||||
|
||||
It won't hide from remote nodes it's monero-serai however, potentially
|
||||
allowing a remote node to profile you. The implications of this are left to the
|
||||
user to consider.
|
||||
|
||||
It also won't act as a wallet, just as a transaction library. wallet2 has
|
||||
several *non-transaction-level* policies, such as always attempting to use two
|
||||
inputs to create transactions. These are considered out of scope to
|
||||
monero-serai.
|
||||
|
||||
### Feature flags
|
||||
monero-serai has certain functionality behind feature flags:
|
||||
|
||||
- `std:` Enables usage of Rust's `std` and several other functionality. See `Cargo.toml` for the full list.
|
||||
- `http-rpc`: Enables an HTTP(S) transport type within the `rpc` module
|
||||
- `multisig`: Enables multi-signature features within the `wallet` module
|
||||
- `binaries`: TODO
|
||||
|
||||
The features enabled by default are:
|
||||
- `std`
|
||||
- `http-rpc`
|
||||
@@ -1,72 +1,67 @@
|
||||
use std::{env, path::Path, process::Command};
|
||||
use std::{
|
||||
io::Write,
|
||||
env,
|
||||
path::Path,
|
||||
fs::{File, remove_file},
|
||||
};
|
||||
|
||||
use dalek_ff_group::EdwardsPoint;
|
||||
|
||||
use monero_generators::bulletproofs_generators;
|
||||
|
||||
fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) {
|
||||
for generator in points {
|
||||
generators_string.extend(
|
||||
format!(
|
||||
"
|
||||
dalek_ff_group::EdwardsPoint(
|
||||
curve25519_dalek::edwards::CompressedEdwardsY({:?}).decompress().unwrap()
|
||||
),
|
||||
",
|
||||
generator.compress().to_bytes()
|
||||
)
|
||||
.chars(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn generators(prefix: &'static str, path: &str) {
|
||||
let generators = bulletproofs_generators(prefix.as_bytes());
|
||||
#[allow(non_snake_case)]
|
||||
let mut G_str = String::new();
|
||||
serialize(&mut G_str, &generators.G);
|
||||
#[allow(non_snake_case)]
|
||||
let mut H_str = String::new();
|
||||
serialize(&mut H_str, &generators.H);
|
||||
|
||||
let path = Path::new(&env::var("OUT_DIR").unwrap()).join(path);
|
||||
let _ = remove_file(&path);
|
||||
File::create(&path)
|
||||
.unwrap()
|
||||
.write_all(
|
||||
format!(
|
||||
"
|
||||
pub(crate) static GENERATORS_CELL: OnceLock<Generators> = OnceLock::new();
|
||||
pub fn GENERATORS() -> &'static Generators {{
|
||||
GENERATORS_CELL.get_or_init(|| Generators {{
|
||||
G: vec![
|
||||
{G_str}
|
||||
],
|
||||
H: vec![
|
||||
{H_str}
|
||||
],
|
||||
}})
|
||||
}}
|
||||
",
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if !Command::new("git").args(&["submodule", "update", "--init", "--recursive"]).status().unwrap().success() {
|
||||
panic!("git failed to init submodules");
|
||||
}
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
if !Command ::new("mkdir").args(&["-p", ".build"])
|
||||
.current_dir(&Path::new("c")).status().unwrap().success() {
|
||||
panic!("failed to create a directory to track build progress");
|
||||
}
|
||||
|
||||
let out_dir = &env::var("OUT_DIR").unwrap();
|
||||
|
||||
// Use a file to signal if Monero was already built, as that should never be rebuilt
|
||||
// If the signaling file was deleted, run this script again to rebuild Monero though
|
||||
println!("cargo:rerun-if-changed=c/.build/monero");
|
||||
if !Path::new("c/.build/monero").exists() {
|
||||
if !Command::new("make").arg(format!("-j{}", &env::var("THREADS").unwrap_or("2".to_string())))
|
||||
.current_dir(&Path::new("c/monero")).status().unwrap().success() {
|
||||
panic!("make failed to build Monero. Please check your dependencies");
|
||||
}
|
||||
|
||||
if !Command::new("touch").arg("monero")
|
||||
.current_dir(&Path::new("c/.build")).status().unwrap().success() {
|
||||
panic!("failed to create a file to label Monero as built");
|
||||
}
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=c/wrapper.cpp");
|
||||
cc::Build::new()
|
||||
.static_flag(true)
|
||||
.warnings(false)
|
||||
.extra_warnings(false)
|
||||
.flag("-Wno-deprecated-declarations")
|
||||
|
||||
.include("c/monero/external/supercop/include")
|
||||
.include("c/monero/contrib/epee/include")
|
||||
.include("c/monero/src")
|
||||
.include("c/monero/build/release/generated_include")
|
||||
|
||||
.define("AUTO_INITIALIZE_EASYLOGGINGPP", None)
|
||||
.include("c/monero/external/easylogging++")
|
||||
.file("c/monero/external/easylogging++/easylogging++.cc")
|
||||
|
||||
.file("c/monero/src/common/aligned.c")
|
||||
.file("c/monero/src/common/perf_timer.cpp")
|
||||
|
||||
.include("c/monero/src/crypto")
|
||||
.file("c/monero/src/crypto/crypto-ops-data.c")
|
||||
.file("c/monero/src/crypto/crypto-ops.c")
|
||||
.file("c/monero/src/crypto/keccak.c")
|
||||
.file("c/monero/src/crypto/hash.c")
|
||||
|
||||
.include("c/monero/src/device")
|
||||
.file("c/monero/src/device/device_default.cpp")
|
||||
|
||||
.include("c/monero/src/ringct")
|
||||
.file("c/monero/src/ringct/rctCryptoOps.c")
|
||||
.file("c/monero/src/ringct/rctTypes.cpp")
|
||||
.file("c/monero/src/ringct/rctOps.cpp")
|
||||
.file("c/monero/src/ringct/multiexp.cc")
|
||||
.file("c/monero/src/ringct/bulletproofs.cc")
|
||||
.file("c/monero/src/ringct/rctSigs.cpp")
|
||||
|
||||
.file("c/wrapper.cpp")
|
||||
.compile("wrapper");
|
||||
|
||||
println!("cargo:rustc-link-search={}", out_dir);
|
||||
println!("cargo:rustc-link-lib=wrapper");
|
||||
println!("cargo:rustc-link-lib=stdc++");
|
||||
generators("bulletproof", "generators.rs");
|
||||
generators("bulletproof_plus", "generators_plus.rs");
|
||||
}
|
||||
|
||||
Submodule coins/monero/c/monero deleted from 424e4de16b
@@ -1,158 +0,0 @@
|
||||
#include <mutex>
|
||||
|
||||
#include "device/device_default.hpp"
|
||||
|
||||
#include "ringct/bulletproofs.h"
|
||||
#include "ringct/rctSigs.h"
|
||||
|
||||
typedef std::lock_guard<std::mutex> lock;
|
||||
|
||||
std::mutex rng_mutex;
|
||||
uint8_t rng_entropy[64];
|
||||
|
||||
extern "C" {
|
||||
void rng(uint8_t* seed) {
|
||||
// Set the first half to the seed
|
||||
memcpy(rng_entropy, seed, 32);
|
||||
// Set the second half to the hash of a DST to ensure a lack of collisions
|
||||
crypto::cn_fast_hash("RNG_entropy_seed", 16, (char*) &rng_entropy[32]);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void monero_wide_reduce(uint8_t* value);
|
||||
namespace crypto {
|
||||
void generate_random_bytes_not_thread_safe(size_t n, void* value) {
|
||||
size_t written = 0;
|
||||
while (written != n) {
|
||||
uint8_t hash[32];
|
||||
crypto::cn_fast_hash(rng_entropy, 64, (char*) hash);
|
||||
// Step the RNG by setting the latter half to the most recent result
|
||||
// Does not leak the RNG, even if the values are leaked (which they are
|
||||
// expected to be) due to the first half remaining constant and
|
||||
// undisclosed
|
||||
memcpy(&rng_entropy[32], hash, 32);
|
||||
|
||||
size_t next = n - written;
|
||||
if (next > 32) {
|
||||
next = 32;
|
||||
}
|
||||
memcpy(&((uint8_t*) value)[written], hash, next);
|
||||
written += next;
|
||||
}
|
||||
}
|
||||
|
||||
void random32_unbiased(unsigned char *bytes) {
|
||||
uint8_t value[64];
|
||||
generate_random_bytes_not_thread_safe(64, value);
|
||||
monero_wide_reduce(value);
|
||||
memcpy(bytes, value, 32);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
void c_hash_to_point(uint8_t* point) {
|
||||
rct::key key_point;
|
||||
ge_p3 e_p3;
|
||||
memcpy(key_point.bytes, point, 32);
|
||||
rct::hash_to_p3(e_p3, key_point);
|
||||
ge_p3_tobytes(point, &e_p3);
|
||||
}
|
||||
|
||||
uint8_t* c_generate_bp(uint8_t* seed, uint8_t len, uint64_t* a, uint8_t* m) {
|
||||
lock guard(rng_mutex);
|
||||
rng(seed);
|
||||
|
||||
rct::keyV masks;
|
||||
std::vector<uint64_t> amounts;
|
||||
masks.resize(len);
|
||||
amounts.resize(len);
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
memcpy(masks[i].bytes, m + (i * 32), 32);
|
||||
amounts[i] = a[i];
|
||||
}
|
||||
|
||||
rct::Bulletproof bp = rct::bulletproof_PROVE(amounts, masks);
|
||||
|
||||
std::stringstream ss;
|
||||
binary_archive<true> ba(ss);
|
||||
::serialization::serialize(ba, bp);
|
||||
uint8_t* res = (uint8_t*) calloc(ss.str().size(), 1);
|
||||
memcpy(res, ss.str().data(), ss.str().size());
|
||||
return res;
|
||||
}
|
||||
|
||||
bool c_verify_bp(
|
||||
uint8_t* seed,
|
||||
uint s_len,
|
||||
uint8_t* s,
|
||||
uint8_t c_len,
|
||||
uint8_t* c
|
||||
) {
|
||||
// BPs are batch verified which use RNG based weights to ensure individual
|
||||
// integrity
|
||||
// That's why this must also have control over RNG, to prevent interrupting
|
||||
// multisig signing while not using known seeds. Considering this doesn't
|
||||
// actually define a batch, and it's only verifying a single BP,
|
||||
// it'd probably be fine, but...
|
||||
lock guard(rng_mutex);
|
||||
rng(seed);
|
||||
|
||||
rct::Bulletproof bp;
|
||||
std::stringstream ss;
|
||||
std::string str;
|
||||
str.assign((char*) s, (size_t) s_len);
|
||||
ss << str;
|
||||
binary_archive<false> ba(ss);
|
||||
::serialization::serialize(ba, bp);
|
||||
if (!ss.good()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bp.V.resize(c_len);
|
||||
for (uint8_t i = 0; i < c_len; i++) {
|
||||
memcpy(bp.V[i].bytes, &c[i * 32], 32);
|
||||
}
|
||||
|
||||
try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; }
|
||||
}
|
||||
|
||||
bool c_verify_clsag(
|
||||
uint s_len,
|
||||
uint8_t* s,
|
||||
uint8_t k_len,
|
||||
uint8_t* k,
|
||||
uint8_t* I,
|
||||
uint8_t* p,
|
||||
uint8_t* m
|
||||
) {
|
||||
rct::clsag clsag;
|
||||
std::stringstream ss;
|
||||
std::string str;
|
||||
str.assign((char*) s, (size_t) s_len);
|
||||
ss << str;
|
||||
binary_archive<false> ba(ss);
|
||||
::serialization::serialize(ba, clsag);
|
||||
if (!ss.good()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rct::ctkeyV keys;
|
||||
keys.resize(k_len);
|
||||
for (uint8_t i = 0; i < k_len; i++) {
|
||||
memcpy(keys[i].dest.bytes, &k[(i * 2) * 32], 32);
|
||||
memcpy(keys[i].mask.bytes, &k[((i * 2) + 1) * 32], 32);
|
||||
}
|
||||
|
||||
memcpy(clsag.I.bytes, I, 32);
|
||||
|
||||
rct::key pseudo_out;
|
||||
memcpy(pseudo_out.bytes, p, 32);
|
||||
|
||||
rct::key msg;
|
||||
memcpy(msg.bytes, m, 32);
|
||||
|
||||
try {
|
||||
return verRctCLSAGSimple(msg, clsag, keys, pseudo_out);
|
||||
} catch(...) { return false; }
|
||||
}
|
||||
}
|
||||
34
coins/monero/generators/Cargo.toml
Normal file
34
coins/monero/generators/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "monero-generators"
|
||||
version = "0.4.0"
|
||||
description = "Monero's hash_to_point and generators"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/generators"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||
|
||||
subtle = { version = "^2.4", default-features = false }
|
||||
|
||||
sha3 = { version = "0.10", default-features = false }
|
||||
|
||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
||||
|
||||
group = { version = "0.13", default-features = false }
|
||||
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
|
||||
[features]
|
||||
std = ["std-shims/std", "subtle/std", "sha3/std", "dalek-ff-group/std"]
|
||||
default = ["std"]
|
||||
21
coins/monero/generators/LICENSE
Normal file
21
coins/monero/generators/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2023 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
7
coins/monero/generators/README.md
Normal file
7
coins/monero/generators/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Monero Generators
|
||||
|
||||
Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
||||
An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
||||
`hash_to_point` here, is included, as needed to generate generators.
|
||||
|
||||
This library is usable under no-std when the `std` feature is disabled.
|
||||
60
coins/monero/generators/src/hash_to_point.rs
Normal file
60
coins/monero/generators/src/hash_to_point.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use subtle::ConditionallySelectable;
|
||||
|
||||
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||
|
||||
use group::ff::{Field, PrimeField};
|
||||
use dalek_ff_group::FieldElement;
|
||||
|
||||
use crate::hash;
|
||||
|
||||
/// Decompress canonically encoded ed25519 point
|
||||
/// It does not check if the point is in the prime order subgroup
|
||||
pub fn decompress_point(bytes: [u8; 32]) -> Option<EdwardsPoint> {
|
||||
CompressedEdwardsY(bytes)
|
||||
.decompress()
|
||||
// Ban points which are either unreduced or -0
|
||||
.filter(|point| point.compress().to_bytes() == bytes)
|
||||
}
|
||||
|
||||
/// Monero's hash to point function, as named `hash_to_ec`.
|
||||
pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
|
||||
#[allow(non_snake_case)]
|
||||
let A = FieldElement::from(486662u64);
|
||||
|
||||
let v = FieldElement::from_square(hash(&bytes)).double();
|
||||
let w = v + FieldElement::ONE;
|
||||
let x = w.square() + (-A.square() * v);
|
||||
|
||||
// This isn't the complete X, yet its initial value
|
||||
// We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X
|
||||
// While inefficient, it solves API boundaries and reduces the amount of work done here
|
||||
#[allow(non_snake_case)]
|
||||
let X = {
|
||||
let u = w;
|
||||
let v = x;
|
||||
let v3 = v * v * v;
|
||||
let uv3 = u * v3;
|
||||
let v7 = v3 * v3 * v;
|
||||
let uv7 = u * v7;
|
||||
uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap())
|
||||
};
|
||||
let x = X.square() * x;
|
||||
|
||||
let y = w - x;
|
||||
let non_zero_0 = !y.is_zero();
|
||||
let y_if_non_zero_0 = w + x;
|
||||
let sign = non_zero_0 & (!y_if_non_zero_0.is_zero());
|
||||
|
||||
let mut z = -A;
|
||||
z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign);
|
||||
#[allow(non_snake_case)]
|
||||
let Z = z + w;
|
||||
#[allow(non_snake_case)]
|
||||
let mut Y = z - w;
|
||||
|
||||
Y *= Z.invert().unwrap();
|
||||
let mut bytes = Y.to_repr();
|
||||
bytes[31] |= sign.unwrap_u8() << 7;
|
||||
|
||||
decompress_point(bytes).unwrap().mul_by_cofactor()
|
||||
}
|
||||
79
coins/monero/generators/src/lib.rs
Normal file
79
coins/monero/generators/src/lib.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
||||
//!
|
||||
//! An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
||||
//! `hash_to_point` here, is included, as needed to generate generators.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use std_shims::{sync::OnceLock, vec::Vec};
|
||||
|
||||
use sha3::{Digest, Keccak256};
|
||||
|
||||
use curve25519_dalek::edwards::{EdwardsPoint as DalekPoint};
|
||||
|
||||
use group::{Group, GroupEncoding};
|
||||
use dalek_ff_group::EdwardsPoint;
|
||||
|
||||
mod varint;
|
||||
use varint::write_varint;
|
||||
|
||||
mod hash_to_point;
|
||||
pub use hash_to_point::{hash_to_point, decompress_point};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
fn hash(data: &[u8]) -> [u8; 32] {
|
||||
Keccak256::digest(data).into()
|
||||
}
|
||||
|
||||
static H_CELL: OnceLock<DalekPoint> = OnceLock::new();
|
||||
/// Monero's alternate generator `H`, used for amounts in Pedersen commitments.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn H() -> DalekPoint {
|
||||
*H_CELL.get_or_init(|| {
|
||||
decompress_point(hash(&EdwardsPoint::generator().to_bytes())).unwrap().mul_by_cofactor()
|
||||
})
|
||||
}
|
||||
|
||||
static H_POW_2_CELL: OnceLock<[DalekPoint; 64]> = OnceLock::new();
|
||||
/// Monero's alternate generator `H`, multiplied by 2**i for i in 1 ..= 64.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn H_pow_2() -> &'static [DalekPoint; 64] {
|
||||
H_POW_2_CELL.get_or_init(|| {
|
||||
let mut res = [H(); 64];
|
||||
for i in 1 .. 64 {
|
||||
res[i] = res[i - 1] + res[i - 1];
|
||||
}
|
||||
res
|
||||
})
|
||||
}
|
||||
|
||||
const MAX_M: usize = 16;
|
||||
const N: usize = 64;
|
||||
const MAX_MN: usize = MAX_M * N;
|
||||
|
||||
/// Container struct for Bulletproofs(+) generators.
|
||||
#[allow(non_snake_case)]
|
||||
pub struct Generators {
|
||||
pub G: Vec<EdwardsPoint>,
|
||||
pub H: Vec<EdwardsPoint>,
|
||||
}
|
||||
|
||||
/// Generate generators as needed for Bulletproofs(+), as Monero does.
|
||||
pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators {
|
||||
let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) };
|
||||
for i in 0 .. MAX_MN {
|
||||
let i = 2 * i;
|
||||
|
||||
let mut even = H().compress().to_bytes().to_vec();
|
||||
even.extend(dst);
|
||||
let mut odd = even.clone();
|
||||
|
||||
write_varint(&i.try_into().unwrap(), &mut even).unwrap();
|
||||
write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap();
|
||||
res.H.push(EdwardsPoint(hash_to_point(hash(&even))));
|
||||
res.G.push(EdwardsPoint(hash_to_point(hash(&odd))));
|
||||
}
|
||||
res
|
||||
}
|
||||
38
coins/monero/generators/src/tests/hash_to_point.rs
Normal file
38
coins/monero/generators/src/tests/hash_to_point.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::{decompress_point, hash_to_point};
|
||||
|
||||
#[test]
|
||||
fn crypto_tests() {
|
||||
// tests.txt file copied from monero repo
|
||||
// https://github.com/monero-project/monero/
|
||||
// blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/tests/crypto/tests.txt
|
||||
let reader = include_str!("./tests.txt");
|
||||
|
||||
for line in reader.lines() {
|
||||
let mut words = line.split_whitespace();
|
||||
let command = words.next().unwrap();
|
||||
|
||||
match command {
|
||||
"check_key" => {
|
||||
let key = words.next().unwrap();
|
||||
let expected = match words.next().unwrap() {
|
||||
"true" => true,
|
||||
"false" => false,
|
||||
_ => unreachable!("invalid result"),
|
||||
};
|
||||
|
||||
let actual = decompress_point(hex::decode(key).unwrap().try_into().unwrap());
|
||||
|
||||
assert_eq!(actual.is_some(), expected);
|
||||
}
|
||||
"hash_to_ec" => {
|
||||
let bytes = words.next().unwrap();
|
||||
let expected = words.next().unwrap();
|
||||
|
||||
let actual = hash_to_point(hex::decode(bytes).unwrap().try_into().unwrap());
|
||||
|
||||
assert_eq!(hex::encode(actual.compress().to_bytes()), expected);
|
||||
}
|
||||
_ => unreachable!("unknown command"),
|
||||
}
|
||||
}
|
||||
}
|
||||
1
coins/monero/generators/src/tests/mod.rs
Normal file
1
coins/monero/generators/src/tests/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod hash_to_point;
|
||||
628
coins/monero/generators/src/tests/tests.txt
Normal file
628
coins/monero/generators/src/tests/tests.txt
Normal file
@@ -0,0 +1,628 @@
|
||||
check_key c2cb3cf3840aa9893e00ec77093d3d44dba7da840b51c48462072d58d8efd183 false
|
||||
check_key bd85a61bae0c101d826cbed54b1290f941d26e70607a07fc6f0ad611eb8f70a6 true
|
||||
check_key 328f81cad4eba24ab2bad7c0e56b1e2e7346e625bcb06ae649aef3ffa0b8bef3 false
|
||||
check_key 6016a5463b9e5a58c3410d3f892b76278883473c3f0b69459172d3de49e85abe true
|
||||
check_key 4c71282b2add07cdc6898a2622553f1ca4eb851e5cb121181628be5f3814c5b1 false
|
||||
check_key 69393c25c3b50e177f81f20f852dd604e768eb30052e23108b3cfa1a73f2736e true
|
||||
check_key 3d5a89b676cb84c2be3428d20a660dc6a37cae13912e127888a5132e8bac2163 true
|
||||
check_key 78cd665deb28cebc6208f307734c56fccdf5fa7e2933fadfcdd2b6246e9ae95c false
|
||||
check_key e03b2414e260580f86ee294cd4c636a5b153e617f704e81dad248fbf715b2ee4 true
|
||||
check_key 28c3503ce82d7cdc8e0d96c4553bcf0352bbcfc73925495dbe541e7e1df105fc false
|
||||
check_key 06855c3c3e0d03fec354059bda319b39916bdc10b6581e3f41b335ee7b014fd5 false
|
||||
check_key 556381485df0d7d5a268ab5ecfb2984b060acc63471183fcf538bf273b0c0cb5 true
|
||||
check_key c7f76d82ac64b1e7fdc32761ff00d6f0f7ada4cf223aa5a11187e3a02e1d5319 true
|
||||
check_key cfa85d8bdb6f633fcf031adee3a299ac42eeb6bd707744049f652f6322f5aa47 true
|
||||
check_key 91e9b63ced2b08979fee713365464cc3417c4f238f9bdd3396efbb3c58e195ee true
|
||||
check_key 7b56e76fe94bd30b3b2f2c4ba5fe4c504821753a8965eb1cbcf8896e2d6aba19 true
|
||||
check_key 7338df494bc416cf5edcc02069e067f39cb269ce67bd9faba956021ce3b3de3a false
|
||||
check_key f9a1f27b1618342a558379f4815fa5039a8fe9d98a09f45c1af857ba99231dc1 false
|
||||
check_key b2a1f37718180d4448a7fcb5f788048b1a7132dde1cfd25f0b9b01776a21c687 true
|
||||
check_key 0d3a0f9443a8b24510ad1e76a8117cca03bce416edfe35e3c2a2c2712454f8dc false
|
||||
check_key d8d3d806a76f120c4027dc9c9d741ad32e06861b9cfbc4ce39289c04e251bb3c false
|
||||
check_key 1e9e3ba7bc536cd113606842835d1f05b4b9e65875742f3a35bfb2d63164b5d5 true
|
||||
check_key 5c52d0087997a2cdf1d01ed0560d94b4bfd328cb741cb9a8d46ff50374b35a57 true
|
||||
check_key bb669d4d7ffc4b91a14defedcdbd96b330108b01adc63aa685e2165284c0033b false
|
||||
check_key d2709ae751a0a6fd796c98456fa95a7b64b75a3434f1caa3496eeaf5c14109b4 true
|
||||
check_key e0c238cba781684e655b10a7d4af04ab7ff2e7022182d7ed2279d6adf36b3e7a false
|
||||
check_key 34ebb4bf871572cee5c6935716fab8c8ec28feef4f039763d8f039b84a50bf4c false
|
||||
check_key 4730d4f38ec3f3b83e32e6335d2506df4ee39858848842c5a0184417fcc639e4 true
|
||||
check_key d42cf7fdf5e17e0a8a7f88505a2b7a3d297113bd93d3c20fa87e11509ec905a2 true
|
||||
check_key b757c95059cefabb0080d3a8ebca82e46efecfd29881be3121857f9d915e388c false
|
||||
check_key bbe777aaf04d02b96c0632f4b1c6f35f1c7bcbc5f22af192f92c077709a2b50b false
|
||||
check_key 73518522aabd28566f858c33fccb34b7a4de0e283f6f783f625604ee647afad9 true
|
||||
check_key f230622c4a8f6e516590466bd10f86b64fbef61695f6a054d37604e0b024d5af false
|
||||
check_key bc6b9a8379fd6c369f7c3bd9ddce58db6b78f27a41d798bb865c3920824d0943 false
|
||||
check_key 45a4f87c25898cd6be105fa1602b85c4d862782adaac8b85c996c4a2bcd8af47 true
|
||||
check_key eb4ad3561d21c4311affbd7cc2c7ff5fd509f72f88ba67dc097a75c31fdbd990 false
|
||||
check_key 2f34f4630c09a23b7ecc19f02b4190a26df69e07e13de8069ae5ff80d23762fc true
|
||||
check_key 2ea4e4fb5085eb5c8adee0d5ab7d35c67d74d343bd816cd13924536cffc2527c true
|
||||
check_key 5d35467ee6705a0d35818aa9ae94e4603c3e5500bfc4cf4c4f77a7160a597aa6 true
|
||||
check_key 8ff42bc76796e20c99b6e879369bd4b46a256db1366416291de9166e39d5a093 true
|
||||
check_key 0262ba718850df6c621e8a24cd9e4831c047e38818a89e15c7a06a489a4558e1 false
|
||||
check_key 58b29b2ba238b534b08fb46f05f430e61cb77dc251b0bb50afec1b6061fd9247 false
|
||||
check_key 153170e3dc2b0e1b368fc0d0e31053e872f094cdace9a2846367f0d9245a109b false
|
||||
check_key 40419d309d07522d493bb047ca9b5fb6c401aae226eefae6fd395f5bb9114200 true
|
||||
check_key 713068818d256ef69c78cd6082492013fbd48de3c9e7e076415dd0a692994504 true
|
||||
check_key a7218ee08e50781b0c87312d5e0031467e863c10081668e3792d96cbcee4e474 true
|
||||
check_key 356ce516b00e674ef1729c75b0a68090e7265cef675bbf32bf809495b67e9342 false
|
||||
check_key 52a5c053293675e3efd2c585047002ea6d77931cbf38f541b9070d319dc0d237 false
|
||||
check_key 77c0080bf157e069b18c4c604cc9505c5ec6f0f9930e087592d70507ca1b5534 false
|
||||
check_key e733bc41f880a4cfb1ca6f397916504130807289cacfca10b15f5b8d058ed1bf false
|
||||
check_key c4f1d3c884908a574ecea8be10e02277de35ef84a1d10f105f2be996f285161f true
|
||||
check_key aed677f7f69e146aa0863606ac580fc0bbdc22a88c4b4386abaa4bdfff66bcc9 false
|
||||
check_key 6ad0edf59769599af8caa986f502afc67aecbebb8107aaf5e7d3ae51d5cf8dd8 false
|
||||
check_key 64a0a70e99be1f775c222ee9cd6f1bee6f632cb9417899af398ff9aff70661c6 true
|
||||
check_key c63afaa03bb5c4ed7bc77aac175dbfb73f904440b2e3056a65850ac1bd261332 false
|
||||
check_key a4e89cd2471c26951513b1cfbdcf053a86575e095af52495276aa56ede8ce344 false
|
||||
check_key 2ce935d97f7c3ddb973de685d20f58ee39938fe557216328045ec2b83f3132be true
|
||||
check_key 3e3d38b1fca93c1559ac030d586616354c668aa76245a09e3fa6de55ac730973 true
|
||||
check_key 8b81b9681f76a4254007fd07ed1ded25fc675973ccb23afd06074805194733a4 false
|
||||
check_key 26d1c15dfc371489439e29bcef2afcf7ed01fac24960fdc2e7c20847a8067588 true
|
||||
check_key 85c1199b5a4591fc4cc36d23660648c1b9cfbb0e9c47199fa3eea33299a3dcec false
|
||||
check_key 60830ba5449c1f04ac54675dfc7cac7510106c4b7549852551f8fe65971123e2 false
|
||||
check_key 3e43c28c024597b3b836e4bc16905047cbf6e841b80e0b8cd6a325049070c2a5 false
|
||||
check_key 474792c16a0032343a6f28f4cb564747c3b1ea0b6a6b9a42f7c71d7cc3dd3b44 true
|
||||
check_key c8ec5e67cb5786673085191881950a3ca20dde88f46851b01dd91c695cfbad16 true
|
||||
check_key 861c4b24b24a87b8559e0bb665f84dcc506c147a909f335ae4573b92299f042f false
|
||||
check_key 2c9e0fe3e4983d79f86c8c36928528f1bc90d94352ce427032cdef6906d84d0b true
|
||||
check_key 9293742822c2dff63fdc1bf6645c864fd527cea2ddba6d4f3048d202fc340c9a true
|
||||
check_key 3956422ad380ef19cb9fe360ef09cc7aaec7163eea4114392a7a0b2e2671914e true
|
||||
check_key 5ae8e72cadda85e525922fec11bd53a261cf26ee230fe85a1187f831b1b2c258 false
|
||||
check_key 973feca43a0baf450c30ace5dc19015e19400f0898316e28d9f3c631da31f99a true
|
||||
check_key dd946c91a2077f45c5c16939e53859d9beabaf065e7b1b993d5e5cd385f8716e true
|
||||
check_key b3928f2d67e47f6bd6da81f72e64908d8ff391af5689f0202c4c6fec7666ffe8 true
|
||||
check_key 313382e82083697d7f9d256c3b3800b099b56c3ef33cacdccbd40a65622e25fc false
|
||||
check_key 7d65380c12144802d39ed9306eed79fe165854273700437c0b4b50559800c058 true
|
||||
check_key 4db5c20a49422fd27739c9ca80e2271a8a125dfcead22cb8f035d0e1b7b163be true
|
||||
check_key dd76a9f565ef0e44d1531349ec4c5f7c3c387c2f5823e693b4952f4b0b70808c true
|
||||
check_key 66430bf628eae23918c3ed17b42138db1f98c24819e55fc4a07452d0c85603eb true
|
||||
check_key 9f0b677830c3f089c27daf724bb10be848537f8285de83ab0292d35afb617f77 false
|
||||
check_key cbf98287391fb00b1e68ad64e9fb10198025864c099b8b9334d840457e673874 true
|
||||
check_key a42552e9446e49a83aed9e3370506671216b2d1471392293b8fc2b81c81a73ee false
|
||||
check_key fb3de55ac81a923d506a514602d65d004ec9d13e8b47e82d73af06da73006673 false
|
||||
check_key e17abb78e58a4b72ff4ad7387b290f2811be880b394b8bcaae7748ac09930169 false
|
||||
check_key 9ffbda7ace69753761cdb5eb01f75433efa5cdb6a4f1b664874182c6a95adcba true
|
||||
check_key 507123c979179ea0a3f7f67fb485f71c8636ec4ec70aa47b92f3c707e7541a54 false
|
||||
check_key f1d0b156571994ef578c61cb6545d34f834eb30e4357539a5633c862d4dffa91 false
|
||||
check_key 3de62311ec14f9ee95828c190b2dc3f03059d6119e8dfccb7323efc640e07c75 false
|
||||
check_key 5e50bb48bc9f6dd11d52c1f0d10d8ae5674d7a4af89cbbce178dafc8a562e5fe false
|
||||
check_key 20b2c16497be101995391ceefb979814b0ea76f1ed5b6987985bcdcd17b36a81 false
|
||||
check_key d63bff73b914ce791c840e99bfae0d47afdb99c2375e33c8f149d0df03d97873 false
|
||||
check_key 3f24b3d94b5ddd244e4c4e67a6d9f533f0396ca30454aa0ca799f21328b81d47 true
|
||||
check_key 6a44c016f09225a6d2e830290719d33eb29b53b553eea7737ed3a6e297b2e7d2 true
|
||||
check_key ff0f34df0c76c207b8340be2009db72f730c69c2bbfeea2013105eaccf1d1f8e true
|
||||
check_key 4baf559869fe4e915e219c3c8d9a2330fc91e542a5a2a7311d4d59fee996f807 true
|
||||
check_key 1632207dfef26e97d13b0d0035ea9468fc5a8a89b0990fce77bb143c9d7f3b67 true
|
||||
check_key fcb3dee3993d1a47630f29410903dd03706bd5e81c5802e6f1b9095cbdb404d3 true
|
||||
check_key fb527092b9809e3d27d7588c7ef89915a769b99c1e03e7f72bbead9ed837daae false
|
||||
check_key 902b118d27d40ab9cbd55edd375801ce302cdb59e09c8659a3ea1401918d8bba false
|
||||
check_key 4d6fbf25ca51e263a700f1abf84f758dde3d11b632e908b3093d64fe2e70ea0a true
|
||||
check_key f4c3211ec70affc1c9a94a6589460ee8360dad5f8c679152f16994038532e3fc true
|
||||
check_key c2b3d73ac14956d7fdf12fa92235af1bb09e1566a6a6ffd0025682c750abdd69 false
|
||||
check_key b7e68c12207d2e2104fb2ca224829b6fccc1c0e2154e8a931e3c837a945f4430 false
|
||||
check_key 56ca0ca227708f1099bda1463db9559541c8c11ffad7b3d95c717471f25a01bf true
|
||||
check_key 3eef3a46833e4d851671182a682e344e36bea7211a001f3b8af1093a9c83f1b2 true
|
||||
check_key bd1f4a4f26cab7c1cbc0e17049b90854d6d28d2d55181e1b5f7a8045fcdfa06e true
|
||||
check_key 8537b01c87e7c184d9555e8d93363dcd9b60a8acc94cd3e41eb7525fd3e1d35a false
|
||||
check_key 68ace49179d549bad391d98ab2cc8afee65f98ce14955c3c1b16e850fabec231 true
|
||||
check_key f9922f8a660e7c3e4f3735a817d18b72f59166a0be2d99795f953cf233a27e24 true
|
||||
check_key 036b6be3da26e80508d5a5a6a5999a1fe0db1ac4e9ade8f1ea2eaf2ea9b1a70e true
|
||||
check_key 5e595e886ce16b5ea31f53bcb619f16c8437276618c595739fece6339731feb0 false
|
||||
check_key 4ee2cebae3476ed2eeb7efef9d20958538b3642f938403302682a04115c0f8ed false
|
||||
check_key 519eedbd0da8676063ce7d5a605b3fc27afeecded857afa24b894ad248c87b5d false
|
||||
check_key ce2b627c0accf4a3105796680c37792b30c6337d2d4fea11678282455ff82ff7 false
|
||||
check_key aa26ed99071a8416215e8e7ded784aa7c2b303aab67e66f7539905d7e922eb4d false
|
||||
check_key 435ae49c9ca26758aa103bdcca8d51393b1906fe27a61c5245361e554f335ec2 true
|
||||
check_key 42568af395bd30024f6ccc95205c0e11a6ad1a7ee100f0ec46fcdf0af88e91fb false
|
||||
check_key 0b4a78d1fde56181445f04ca4780f0725daa9c375b496fab6c037d6b2c2275db true
|
||||
check_key 2f82d2a3c8ce801e1ad334f9e074a4fbf76ffac4080a7331dc1359c2b4f674a4 false
|
||||
check_key 24297d8832d733ed052dd102d4c40e813f702006f325644ccf0cb2c31f77953f false
|
||||
check_key 5231a53f6bea7c75b273bde4a9f673044ed87796f20e0909978f29d98fc8d4f0 true
|
||||
check_key 94b5affcf78be5cf62765c32a0794bc06b4900e8a47ddba0e166ec20cec05935 true
|
||||
check_key c14b4d846ea52ffbbb36aa62f059453af3cfae306280dada185d2d385ef8f317 true
|
||||
check_key cceb34fddf01a6182deb79c6000a998742d4800d23d1d8472e3f43cd61f94508 true
|
||||
check_key 1faffa33407fba1634d4136cf9447896776c16293b033c6794f06774b514744c true
|
||||
check_key faaac98f644a2b77fb09ba0ebf5fcddf3ff55f6604c0e9e77f0278063e25113a true
|
||||
check_key 09e8525b00bea395978279ca979247a76f38f86dce4465eb76c140a7f904c109 true
|
||||
check_key 2d797fc725e7fb6d3b412694e7386040effe4823cdf01f6ec7edea4bc0e77e20 false
|
||||
check_key bbb74dabee651a65f46bca472df6a8a749cc4ba5ca35078df5f6d27a772f922a false
|
||||
check_key 77513ca00f3866607c3eff5c2c011beffa775c0022c5a4e7de1120a27e6687fd true
|
||||
check_key 10064c14ace2a998fc2843eeeb62884fe3f7ab331ca70613d6a978f44d9868eb false
|
||||
check_key 026ae84beb5e54c62629a7b63702e85044e38cadfc9a1fcabee6099ba185005c false
|
||||
check_key aef91536292b7ba34a3e787fb019523c2fa7a0d56fca069cc82ccb6b02a45b14 false
|
||||
check_key 147bb1a82c623c722540feaad82b7adf4b85c6ec0cbcef3ca52906f3e85617ac true
|
||||
check_key fc9fb281a0847d58dc9340ef35ef02f7d20671142f12bdd1bfb324ab61d03911 false
|
||||
check_key b739801b9455ac617ca4a7190e2806669f638d4b2f9288171afb55e1542c8d71 false
|
||||
check_key 494cc1e2ee997eb1eb051f83c4c89968116714ddf74e460d4fa1c6e7c72e3eb3 true
|
||||
check_key ed2fbdf2b727ed9284db90ec900a942224787a880bc41d95c4bc4cf136260fd7 true
|
||||
check_key 02843d3e6fc6835ad03983670a592361a26948eb3e31648d572416a944d4909e true
|
||||
check_key c14fea556a7e1b6b6c3d4e2e38a4e7e95d834220ff0140d3f7f561a34e460801 true
|
||||
check_key 5f8f82a35452d0b0d09ffb40a1154641916c31e161ad1a6ab8cfddc2004efdf6 false
|
||||
check_key 7b93d72429fab07b49956007eba335bb8c5629fbf9e7a601eaa030f196934a56 true
|
||||
check_key 6a63ed96d2e46c2874beaf82344065d94b1e5c04406997f94caf4ccd97cfbab9 false
|
||||
check_key c915f409e1e0f776d1f440aa6969cfec97559ef864b07d8c0d7c1163871b4603 true
|
||||
check_key d06bc33630fc94303c2c369481308f805f5ce53c40141160aa4a1f072967617e false
|
||||
check_key 1aafb14ca15043c2589bcd32c7c5f29479216a1980e127e9536729faf1c40266 true
|
||||
check_key 58c115624a20f4b0c152ccd048c54a28a938556863ab8521b154d3165d3649cd false
|
||||
check_key 9001ba086e8aa8a67e128f36d700cc641071556306db7ec9b8ac12a6256b27b7 false
|
||||
check_key 898c468541634fb0def11f82c781341fce0def7b15695af4e642e397218c730c true
|
||||
check_key 47ea6539e65b7b611b0e1ae9ee170adf7c31581ca9f78796d8ebbcc5cd74b712 false
|
||||
check_key 0c60952a64eeac446652f5d3c136fd36966cf66310c15ee6ab2ecbf981461257 false
|
||||
check_key 682264c4686dc7736b6e46bdc8ab231239bc5dac3f5cb9681a1e97a527945e8e true
|
||||
check_key 276006845ca0ea4238b231434e20ad8b8b2a36876effbe1d1e3ffb1f14973397 true
|
||||
check_key eecd3a49e55e32446f86c045dce123ef6fe2e5c57db1d850644b3c56ec689fce true
|
||||
check_key a4dced63589118db3d5aebf6b5670e71250f07485ca4bb6dddf9cce3e4c227a1 false
|
||||
check_key b8ade608ba43d55db7ab481da88b74a9be513fca651c03e04d30cc79f50e0276 false
|
||||
check_key 0d91de88d007a03fe782f904808b036ff63dec6b73ce080c55231afd4ed261c3 true
|
||||
check_key 87c59becb52dd16501edadbb0e06b0406d69541c4d46115351e79951a8dd9c28 true
|
||||
check_key 9aee723be2265171fe10a86d1d3e9cf5a4e46178e859db83f86d1c6db104a247 false
|
||||
check_key 509d34ae5bf56db011845b8cdf0cc7729ed602fce765e9564cb433b4d4421a43 false
|
||||
check_key 06e766d9a6640558767c2aab29f73199130bfdc07fd858a73e6ae8e7b7ba23ba false
|
||||
check_key 801c4fe5ab3e7cf13f7aa2ca3bc57cc8eba587d21f8bc4cd40b1e98db7aec8d9 false
|
||||
check_key d85ad63aeb7d2faa22e5c9b87cd27f45b01e6d0fdc4c3ddf105584ac0a021465 false
|
||||
check_key a7ca13051eb2baeb5befa5e236e482e0bb71803ad06a6eae3ae48742393329d2 true
|
||||
check_key 5a9ba3ec20f116173d933bf5cf35c320ed3751432f3ab453e4a6c51c1d243257 false
|
||||
check_key a4091add8a6710c03285a422d6e67863a48b818f61c62e989b1e9b2ace240a87 false
|
||||
check_key bdee0c6442e6808f25bb18e21b19032cf93a55a5f5c6426fba2227a41c748684 true
|
||||
check_key d4aeb6cdad9667ec3b65c7fbc5bfd1b82bba1939c6bb448a86e40aec42be5f25 false
|
||||
check_key 73525b30a77f1212f7e339ec11f48c453e476f3669e6e70bebabc2fe9e37c160 true
|
||||
check_key 45501f2dc4d0a3131f9e0fe37a51c14869ab610abd8bf0158111617924953629 false
|
||||
check_key 07d0e4c592aa3676adf81cca31a95d50c8c269d995a78cde27b2a9a7a93083a6 false
|
||||
check_key a1797d6178c18add443d22fdbf45ca5e49ead2f78b70bdf1500f570ee90adca5 true
|
||||
check_key 0961e82e6e7855d7b7bf96777e14ae729f91c5bbd20f805bd7daac5ccbec4bab false
|
||||
check_key 57f5ba0ad36e997a4fb585cd2fc81b9cc5418db702c4d1e366639bb432d37c73 true
|
||||
check_key 82b005be61580856841e042ee8be74ae4ca66bb6733478e81ca1e56213de5c05 false
|
||||
check_key d7733dcae1874c93e9a2bd46385f720801f913744d60479930dad7d56c767cdc false
|
||||
check_key b8b8b698609ac3f1bd8f4965151b43b362e6c5e3d1c1feae312c1d43976d59ab true
|
||||
check_key 4bba7815a9a1b86a5b80b17ac0b514e2faa7a24024f269b330e5b7032ae8c04e true
|
||||
check_key 0f70da8f8266b58acda259935ef1a947c923f8698622c5503520ff31162e877b false
|
||||
check_key 233eaa3db80f314c6c895d1328a658a9175158fa2483ed216670c288a04b27bc false
|
||||
check_key a889f124fabfd7a1e2d176f485be0cbd8b3eeaafeee4f40e99e2a56befb665be true
|
||||
check_key 2b7b8abc198b11cf7efa21bc63ec436f790fe1f9b8c044440f183ab291af61d6 true
|
||||
check_key 2491804714f7938cf501fb2adf07597b4899b919cabbaab49518b8f8767fdc6a true
|
||||
check_key 52744a54fcb00dc930a5d7c2bc866cbfc1e75dd38b38021fd792bb0ca9f43164 true
|
||||
check_key e42cbf70b81ba318419104dffbb0cdc3b7e7d4698e422206b753a4e2e6fc69bb false
|
||||
check_key 2faff73e4fed62965f3dbf2e6446b5fea0364666cc8c9450b6ed63bbb6f5f0e7 true
|
||||
check_key 8b963928d75be661c3c18ddd4f4d1f37ebc095ce1edc13fe8b23784c8f416dfd false
|
||||
check_key b1162f952808434e4d2562ffda98bd311613d655d8cf85dc86e0a6c59f7158bc true
|
||||
check_key 5a69adcd9e4f5b0020467e968d85877cb3aa04fa86088d4499b57ca65a665836 true
|
||||
check_key 61ab47da432c829d0bc9d4fdb59520b135428eec665ad509678188b81c7adf49 false
|
||||
check_key 154bb547f22f65a87c0c3f56294f5791d04a3c14c8125d256aeed8ec54c4a06e true
|
||||
check_key 0a78197861c30fd3547b5f2eabd96d3ac22ac0632f03b7afd9d5d2bfc2db352f true
|
||||
check_key 8bdeadcca1f1f8a4a67b01ed2f10ef31aba7b034e8d1df3a69fe9aebf32454e0 false
|
||||
check_key f4b17dfca559be7d5cea500ac01e834624fed9befae3af746b39073d5f63190d true
|
||||
check_key 622c52821e16ddc63b58f3ec2b959fe8c6ea6b1a596d9a58fd81178963f41c01 true
|
||||
check_key 07bedd5d55c937ef5e23a56c6e58f31adb91224d985285d7fef39ede3a9efb17 false
|
||||
check_key 5179bf3b7458648e57dc20f003c6bbfd55e8cd7c0a6e90df6ef8e8183b46f99d true
|
||||
check_key 683c80c3f304f10fdd53a84813b5c25b1627ebd14eb29b258b41cd14396ef41f true
|
||||
check_key c266244ed597c438170875fe7874f81258a830105ca1108131e6b8fea95eb8ba true
|
||||
check_key 0c1cdc693df29c2d1e66b2ce3747e34a30287d5eb6c302495634ec856593fe8e true
|
||||
check_key 28950f508f6a0d4c20ab5e4d55b80565a6a539092e72b7eb0ed9fa5017ecef88 false
|
||||
check_key 8328a2a5fcfc4433b1c283539a8943e6eb8cc16c59f29dedc3af2c77cfd56f25 true
|
||||
check_key 5d0f82319676d4d3636ff5dc2a38ea5ec8aeaac4835fdcab983ab35d76b7967b false
|
||||
check_key cafcc75e94a014115f25c23aaae86e67352f928f468d4312b92240ff0f3a4481 false
|
||||
check_key 3e5fdd8072574218f389d018e959669e8ca4ef20b114ea7dce7bfb32339f9f42 true
|
||||
check_key 591763e3390a78ccb529ceea3d3a97165878b179ad2edaa166fd3c78ec69d391 true
|
||||
check_key 7a0a196935bf79dc2b1c3050e8f2bf0665f7773fc07511b828ec1c4b1451d317 false
|
||||
check_key 9cf0c034162131fbaa94a608f58546d0acbcc2e67b62a0b2be2ce75fc8c25b9a false
|
||||
check_key e3840846e3d32644d45654b96def09a5d6968caca9048c13fcaab7ae8851c316 false
|
||||
check_key a4e330253739af588d70fbda23543f6df7d76d894a486d169e5fedf7ed32d2e2 false
|
||||
check_key cfb41db7091223865f7ecbdda92b9a6fb08887827831451de5bcb3165395d95d true
|
||||
check_key 3d10bd023cef8ae30229fdbfa7446a3c218423d00f330857ff6adde080749015 false
|
||||
check_key 4403b53b8d4112bb1727bb8b5fd63d1f79f107705ffe17867704e70a61875328 false
|
||||
check_key 121ef0813a9f76b7a9c045058557c5072de6a102f06a9b103ead6af079420c29 true
|
||||
check_key 386204cf473caf3854351dda55844a41162eb9ce4740e1e31cfef037b41bc56e false
|
||||
check_key eb5872300dc658161df469364283e4658f37f6a1349976f8973bd6b5d1d57a39 true
|
||||
check_key b8f32188f0fc62eeb38a561ff7b7f3c94440e6d366a05ef7636958bc97834d02 false
|
||||
check_key a817f129a8292df79eef8531736fdebb2e985304653e7ef286574d0703b40fb4 false
|
||||
check_key 2c06595bc103447b9c20a71cd358c704cb43b0b34c23fb768e6730ac9494f39e true
|
||||
check_key dd84bc4c366ced4f65c50c26beb8a9bc26c88b7d4a77effbb0f7af1b28e25734 false
|
||||
check_key 76b4d33810eed637f90d49a530ac5415df97cafdac6f17eda1ba7eb9a14e5886 true
|
||||
check_key 926ce5161c4c92d90ec4efc58e5f449a2c385766c42d2e60af16b7362097aef5 false
|
||||
check_key 20c661f1e95e94a745eb9ec7a4fa719eff2f64052968e448d4734f90952aefee false
|
||||
check_key 671b50abbd119c756010416e15fcdcc9a8e92eed0f67cbca240c3a9154db55c0 false
|
||||
check_key df7aeee8458433e5c68253b8ef006a1c74ce3aef8951056f1fa918a8eb855213 false
|
||||
check_key 70c81a38b92849cf547e3d5a6570d78e5228d4eaf9c8fdd15959edc9eb750daf false
|
||||
check_key 55a512100b72d4ae0cfc16c75566fcaa3a7bb9116840db1559c71fd0e961cc36 false
|
||||
check_key dbfbec4d0d2433a794ad40dc0aea965b6582875805c9a7351b47377403296acd true
|
||||
check_key 0a7fe09eb9342214f98b38964f72ae3c787c19e5d7e256af9216f108f88b00a3 true
|
||||
check_key a82e54681475f53ced9730ee9e3a607e341014d9403f5a42f3dbdbe8fc52e842 true
|
||||
check_key 4d1f90059f7895a3f89abf16162e8d69b399c417f515ccb43b83144bbe8105f6 true
|
||||
check_key 94e5c5b8486b1f2ff4e98ddf3b9295787eb252ba9b408ca4d7724595861da834 false
|
||||
check_key d16e3e8dfa6d33d1d2db21c651006ccddbf4ce2e556594de5a22ae433e774ae6 false
|
||||
check_key a1b203ec5e36098a3af08d6077068fec57eab3a754cbb5f8192983f37191c2df false
|
||||
check_key 5378bb3ec8b4e49849bd7477356ed86f40757dd1ea3cee1e5183c7e7be4c3406 false
|
||||
check_key 541a4162edeb57130295441dc1cb604072d7323b6c7dffa02ea5e4fed1d2ee9e true
|
||||
check_key d8e86e189edcc4b5c262c26004691edd7bd909090997f886b00ed4b6af64d547 false
|
||||
check_key 18a8731d1983d1df2ce2703b4c85e7357b6356634ac1412e6c2ac33ad35f8364 false
|
||||
check_key b21212eac1eb11e811022514c5041233c4a07083a5b20acd7d632a938dc627de true
|
||||
check_key 50efcfac1a55e9829d89334513d6d921abeb237594174015d154512054e4f9d1 true
|
||||
check_key 9c44e8bcba31ddb4e67808422e42062540742ebd73439da0ba7837bf26649ec4 true
|
||||
check_key b068a4f90d5bd78fd350daa129de35e5297b0ad6be9c85c7a6f129e3760a1482 false
|
||||
check_key e9df93932f0096fcf2055564457c6dc685051673a4a6cd87779924be5c4abead true
|
||||
check_key eddab2fc52dac8ed12914d1eb5b0da9978662c4d35b388d64ddf8f065606acaf true
|
||||
check_key 54d3e6b3f2143d9083b4c98e4c22d98f99d274228050b2dc11695bf86631e89f true
|
||||
check_key 6da1d5ef1827de8bbf886623561b058032e196d17f983cbc52199b31b2acc75b true
|
||||
check_key e2a2df18e2235ebd743c9714e334f415d4ca4baf7ad1b335fb45021353d5117f true
|
||||
check_key f34cb7d6e861c8bfe6e15ac19de68e74ccc9b345a7b751a10a5c7f85a99dfeb6 false
|
||||
check_key f36e2f5967eb56244f9e4981a831f4d19c805e31983662641fe384e68176604a true
|
||||
check_key c7e2dc9e8aa6f9c23d379e0f5e3057a69b931b886bbb74ded9f660c06d457463 true
|
||||
check_key b97324364941e06f2ab4f5153a368f9b07c524a89e246720099042ad9e8c1c5b false
|
||||
check_key eff75c70d425f5bba0eef426e116a4697e54feefac870660d9cf24c685078d75 false
|
||||
check_key 161f3cd1a5873788755437e399136bcbf51ff5534700b3a8064f822995a15d24 false
|
||||
check_key 63d6d3d2c21e88b06c9ff856809572024d86c85d85d6d62a52105c0672d92e66 false
|
||||
check_key 1dc19b610b293de602f43dca6c204ce304702e6dc15d2a9337da55961bd26834 false
|
||||
check_key 28a16d02405f509e1cfef5236c0c5f73c3bcadcd23c8eff377253941f82769db true
|
||||
check_key 682d9cc3b65d149b8c2e54d6e20101e12b7cf96be90c9458e7a69699ec0c8ed7 false
|
||||
check_key 0000000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0000000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0100000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0100000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 0200000000000000000000000000000000000000000000000000000000000000 false
|
||||
check_key 0200000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 0300000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0300000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0400000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0400000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0500000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0500000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0600000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0600000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0700000000000000000000000000000000000000000000000000000000000000 false
|
||||
check_key 0700000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 0800000000000000000000000000000000000000000000000000000000000000 false
|
||||
check_key 0800000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 0900000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0900000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0a00000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0a00000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0b00000000000000000000000000000000000000000000000000000000000000 false
|
||||
check_key 0b00000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 0c00000000000000000000000000000000000000000000000000000000000000 false
|
||||
check_key 0c00000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 0d00000000000000000000000000000000000000000000000000000000000000 false
|
||||
check_key 0d00000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 0e00000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0e00000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 0f00000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 0f00000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 1000000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 1000000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 1100000000000000000000000000000000000000000000000000000000000000 false
|
||||
check_key 1100000000000000000000000000000000000000000000000000000000000080 false
|
||||
check_key 1200000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 1200000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key 1300000000000000000000000000000000000000000000000000000000000000 true
|
||||
check_key 1300000000000000000000000000000000000000000000000000000000000080 true
|
||||
check_key daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key dcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key dcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key ddffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key ddffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key deffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key deffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key dfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key dfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key e0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key e0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key e1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key e1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key e2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key e2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key e3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key e3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key e4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key e4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key e5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key e5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key e6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key e6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key e7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key e7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key e8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key e8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key e9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key e9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key eaffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key eaffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff true
|
||||
check_key ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f true
|
||||
check_key ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f4ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key f9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key f9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key faffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key faffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key fbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key fbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key fcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key fcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key fdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key fdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key feffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key feffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
check_key ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f false
|
||||
check_key ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff false
|
||||
hash_to_ec da66e9ba613919dec28ef367a125bb310d6d83fb9052e71034164b6dc4f392d0 52b3f38753b4e13b74624862e253072cf12f745d43fcfafbe8c217701a6e5875
|
||||
hash_to_ec a7fbdeeccb597c2d5fdaf2ea2e10cbfcd26b5740903e7f6d46bcbf9a90384fc6 f055ba2d0d9828ce2e203d9896bfda494d7830e7e3a27fa27d5eaa825a79a19c
|
||||
hash_to_ec ed6e6579368caba2cc4851672972e949c0ee586fee4d6d6a9476d4a908f64070 da3ceda9a2ef6316bf9272566e6dffd785ac71f57855c0202f422bbb86af4ec0
|
||||
hash_to_ec 9ae78e5620f1c4e6b29d03da006869465b3b16dae87ab0a51f4e1b74bc8aa48b 72d8720da66f797f55fbb7fa538af0b4a4f5930c8289c991472c37dc5ec16853
|
||||
hash_to_ec ab49eb4834d24db7f479753217b763f70604ecb79ed37e6c788528720f424e5b 45914ba926a1a22c8146459c7f050a51ef5f560f5b74bae436b93a379866e6b8
|
||||
hash_to_ec 5b79158ef2341180b8327b976efddbf364620b7e88d2e0707fa56f3b902c34b3 eac991dcbba39cb3bd166906ab48e2c3c3f4cd289a05e1c188486d348ede7c2e
|
||||
hash_to_ec f21daa7896c81d3a7a2e9df721035d3c3902fe546c9d739d0c334ed894fb1d21 a6bedc5ffcc867d0c13a88a03360c8c83a9e4ddf339851bd3768c53a124378ec
|
||||
hash_to_ec 3dae79aaca1abe6aecea7b0d38646c6b013d40053c7cdde2bed094497d925d2b 1a442546a35860a4ab697a36b158ded8e001bbfe20aef1c63e2840e87485c613
|
||||
hash_to_ec 3d219463a55c24ac6f55706a6e46ade3fcd1edc87bade7b967129372036aca63 b252922ab64e32968735b8ade861445aa8dc02b763bd249bff121d10829f7c52
|
||||
hash_to_ec bc5db69aced2b3197398eaf7cf60fd782379874b5ca27cb21bd23692c3c885cc ae072a43f78a0f29dc9822ae5e70865bbd151236a6d7fe4ae3e8f8961e19b0e5
|
||||
hash_to_ec 98a6ed760b225976f8ada0579540e35da643089656695b5d0b8c7265a37e2342 6a99dbfa8ead6228910498cc3ff3fb18cb8627c5735e4b8657da846c16d2dcad
|
||||
hash_to_ec e9cdc9fd9425a4a2389a5d60f76a2d839f0afbf66330f079a88fe23d73eae930 8aa518d091928668f3ca40e71e14b2698f6cae097b8120d7f6ae9afba8fd3d60
|
||||
hash_to_ec a50c026c0af2f9f9884c2e9b8464724ac83bef546fec2c86b7de0880980d24fb b07433f8df39da2453a1e13fd413123a158feae602d822b724d42ef6c8e443bf
|
||||
hash_to_ec bf180e20d160fa23ccfa6993febe22b920160efc5a9614245f1a3a360076e87a 9d6454ff69779ce978ea5fb3be88576dc8feaedf151e93b70065f92505f2e800
|
||||
hash_to_ec b2b64dfeb1d58c6afbf5a56d8c0c42012175ebb4b7df30f26a67b66be8c34614 0523b22e7f220c939b604a15780abc5816709b91b81d9ee1541d44bd2586bbd8
|
||||
hash_to_ec 463fc877f4279740020d10652c950f088ebdebeae34aa7a366c92c9c8773f63a daa5fa72e70c4d3af407b8f2f3364708029b2d4863bbdde54bd67bd08db0fcad
|
||||
hash_to_ec 721842f3809982e7b96a806ae1f162d98ae6911d476307ad1e4f24522fd26f55 4397c300a8cfcb42e7cc310bc975dc975ec2d191eaa7e0462998eb2830c34126
|
||||
hash_to_ec 384da8d9b83972af8cbefc2da5efc744037c8ef40efa4b3bacc3238a6232963d 3c80f107e6868f73ef600ab9229a3f4bbe24f4adce52e6ab3a66d5d510e0670d
|
||||
hash_to_ec e26f8adef5b6fe5bb01466bff0455ca23fda07e200133697b3b6430ca3332bde e262a58bcc1f8baf1980e00d5d40ba00803690174d14fb4c0f608429ce3df773
|
||||
hash_to_ec 6e275b4ea4f085a5d3151aa08cf16a8c60b078e70be7ce5dac75b5d7b0eebe7c cb21b5a7744b4fcdc92ead4be0b04bcb9145e7bb4b06eff3bb2f0fe429b85108
|
||||
hash_to_ec a0dde4561ad9daa796d9cd8a3c34fd41687cee76d128bf2e2252466e3ef3b068 79a2eb06bb7647f5d0aae5da7cf2e2b2d2ce890f25f2b1f81bfc5fef8c87a7d3
|
||||
hash_to_ec dbaf63830e037b4c329969d1d85e58cb6c4f56014fd08eb38219bd20031ae27c 079c93ae27cd98075a487fd3f7457ad2fb57cdf12ec8651fedd944d765d07549
|
||||
hash_to_ec 1e87ba8a9acf96948bc199ae55c83ab3277be152c6d0b1d68a07955768d81171 5c6339f834116791f9ea22fcc3970346aaeddacf13fbd0a7d4005fbd469492ca
|
||||
hash_to_ec 5a544088e63ddf5b9f444ed75a75bc9315c4c50439522f06b4823ecaf5e8a08d e95ca0730d57c6469be3a0f3c94382f8490257e2e546de86c650bdbc6482eaee
|
||||
hash_to_ec e4e06d92ebb036a5e4bb547dbaa43fd70db3929eef2702649455c86d7e59aa46 e26210ff8ee28e24ef2613df40aa8a874b5e3c1d07ae14acc59220615aa334dc
|
||||
hash_to_ec 5793b8b32dcc0f204501647f2976493c4f8f1fa5132315226f99f29a5a6fdfce 656e390086906d99852c9696e831f62cb56fc8f85f9a5c936c327f23c7faf4fe
|
||||
hash_to_ec 84f56fa4d7f12e0efd48b1f7c81c15d6e3843ebb419f4a27ec97028d4f9da19e 0cbd4f0cd288e1e071cce800877de6aef97b63fff867424a4f2b2bab25602608
|
||||
hash_to_ec 242683ddf0a9fc55f6585de3aa64ea17c9c544896ff7677cd82c98f833bdf2ca 38c36d52314549213df7c7201ab7749a4724cbea92812f583bb48cabc20816ad
|
||||
hash_to_ec a93ee320dc030aa382168c2eb6d75fce6e5a63a81f15632d514c6de8a7cfa5ee bd0a2facaa95bc95215a94be21996e46f789ee8beb38e75a1173b75fc686c505
|
||||
hash_to_ec e36136601d84475d25c3f14efe030363d646658937a8a8a19a812d5e6deb5944 2fb93d78fae299c9f6b22346acfb829796ee7a47ec71db5456d8201bec6c35a3
|
||||
hash_to_ec ba4b67d3d387c66baa4a32ec8b1db7681087e85076e71bab10036388c3aeb011 cc01329ce56f963bf444a124751c45b2c779ccb6dea16ca05251baca246b5401
|
||||
hash_to_ec 3fbc91896a2585154d6f7094c5ab9c487e29a27951c226eec1235f618e44946b 7d983acbb901bf5497d0708392e5e742ec8c8036cbb0d03403e9929da8cc85a7
|
||||
hash_to_ec a2da289fed650e9901f69a5f33535eb47c6bd07798633cbf6c00ce3172df76ac dca8a4d30ec2d657fefd0dba9c1c5fd45a79f665048b3cf72ac2c3b7363da1ac
|
||||
hash_to_ec 99025d2d493f768e273ed66cacd3a5b392761e6bd158ca09c8fba84631ea1534 7ef5af79ab155ab7e1770a47fcd7f194aca43d79ec6e303c7ce18c6a20279b04
|
||||
hash_to_ec 3cf1d01d0b70fb31f2a2f979c1bae812381430f474247d0b018167f2a2cd9a9f 7c53d799ec938a21bb305a6b5ca0a7a355fa9a68b01d289c4f22b36ce3738f95
|
||||
hash_to_ec 639c421b49636b2a1f8416c5d6e64425fe51e3b52584c265502379189895668e 0b47216ae5e6e03667143a6cf8894d9d73e3152c64fb455631d81a424410e871
|
||||
hash_to_ec 4ccf2c973348b7cc4b14f846f9bfcdcb959b7429accf6dede96248946841d990 7fd41f5b97ba42ed03947dd953f8e69770c92cc34b16236edad7ab3c78cbbb2e
|
||||
hash_to_ec f76ae09fff537f8919fd1a43ff9b8922b6a77e9e30791c82cf2c4b8acb51363e 8e2c6bf86461ad2c230c496ee3896da33c11cc020fd4c70faa3645b329049234
|
||||
hash_to_ec 98932da7450f15db6c1eef78359904915c31c2aa7572366ec8855180edb81e3a 86180adddfac0b4d1fb41d58e98445dde1da605b380d392e9386bd445f1d821c
|
||||
hash_to_ec ab26a1660988ec7aba91fc01f7aa9a157bbc12927f5b197062b922a5c0c7f8dd 2c44a43eda0d0aad055f18333e761f2f2ec11c585ec7339081c19266af918e4f
|
||||
hash_to_ec 4465d0c1b4930cc718252efd87d11d04162d2a321b9b850c4a19a6acdfca24f4 b03806287d804188a4d679a0ecee66f399d7bdc3bd1494f9b2b0772bbb5a034f
|
||||
hash_to_ec 0f2a7867864ed00e5c40082df0a0b031c89fa5f978d9beb2fde75153f51cfb75 5c471e1b118ef9d76c93aec70e0578f46e8db1d55affd447c1f64c0ad9a5caa5
|
||||
hash_to_ec 5c2808c07d8175f332cae050ce13bec4254870d76abff68faf34b0b8d3ad5000 eeff1d9a5aa428b7aecc575e63dde17294072eb246568493e1ed88ce5c95b779
|
||||
hash_to_ec 36300a21601fad00d00da45e27b36c11923b857f97e50303bd01f21998eaef95 b33b077871e6f5dad8ff6bc621c1b6dedcf700777d996c8c02d73f7297108b7e
|
||||
hash_to_ec 9e1afb76d6c480816d2cedd7f2ab08a36c309efaa3764dcdb51bad6049683805 4cd96ba7b543b1a224b8670bf20b3733e3910711d32456d3e58e920215788adf
|
||||
hash_to_ec 685f152704664495459b76c81567a4b571e8b307dd0e3c9b08ee95651a006047 80dd6b637580cb3be76025867f1525852b65a7a66066993fda3af7eb187dc1a5
|
||||
hash_to_ec 0b216444391a1163c14f7b27f9135e9747978c0e426dce1fa65c657f3e9146be 021259695a6854a4a03e8c74d09ab9630a401bfca06172a733fe122f01af90b4
|
||||
hash_to_ec cfcb35e98f71226c3558eaa9cf620db5ae207ece081ab13ddea4b1f122850a5a 46763d2742e2cdffe80bb3d056f4d3a1565aa83f19aab0a1f89e54ad81ae0814
|
||||
hash_to_ec 07e7292da8cdcdb58ee30c3fa16f1d609e9b3b1110dd6fa9b2cc18f4103a1c12 fe949ca251ac66f13a8925ae624a09cdbf6696d3c110442338d37700536e8ec7
|
||||
hash_to_ec 813bc7e3749e658190cf2a4e358bc07a6671f262e2c4eef9f44c66066a72e6a7 6b92fbda984bd0e6f4af7a5e04c2b66b6f0f9d197a9694362a8556e5b7439f8a
|
||||
hash_to_ec 89c50a1e5497156e0fae20d99f5e33e330362b962c9ca00eaf084fe91aaec71d ef36cb75eb95fb761a8fa8c376e9c4447bcd61421250f7a711bd289e6ed78a9b
|
||||
hash_to_ec d9bd9ff2dd807eb25de7c5de865dbc43cce2466389cedbc92b90aab0eb014f81 30104771ff961cd1861cd053689feab888c57b8a4a2e3989646ea7dea40f3c04
|
||||
hash_to_ec b8c837501b6ca3e118db9848717c847c062bf0ebeca5a7c211726c1426878af5 19a1e204b4a32ce9cccf5d96a541eb76a78789dceaf4fe69964e58ff96c29b63
|
||||
hash_to_ec 84376c5350a42c07ac9f96e8d5c35a8c7f62c639a1834b09e4331b5962ecace8 ba1e4437d5048bd1294eadc502092eafc470b99fde82649e84a52225e68e88f2
|
||||
hash_to_ec a3345e4a4cfc369bf0e7d11f49aed0d2a6ded00e3ff8c7605db9a919cf730640 0d318705c16e943c0fdcde134aaf6e4ccce9f3d9161d001861656fc7ea77a0b1
|
||||
hash_to_ec 3c994dfb9c71e4f401e65fd552dc9f49885f88b8b3588e24e1d2e9b8870ffab1 984157de5d7c2c4b43b2bffea171809165d7bb442baea88e83b27f839ebdb939
|
||||
hash_to_ec 153674c1c1b18a646f564af77c5bd7de452dc3f3e1e2326bfe9c57745b69ec5c e9a4a1e225ae472d1b3168c99f8ba1943ad2ed84ef29598f3f96314f22db9ef2
|
||||
hash_to_ec 2d46a705d4fe5d8b5a1f4e9ef46d9e06467450eb357b6d39faa000995314e871 b9d1aec540bf6a9c0e1b325ab87d4fbe66b1df48986dde3cb62e66e136eba107
|
||||
hash_to_ec 6764c3767f16ec8faecc62f9f76735f76b11d7556aeb61066aeaeaad4fc9042f 3a5c68fb94b023488fb5940e07d1005e7c18328e7a84f673ccd536c07560a57b
|
||||
hash_to_ec c99c6ee5804d4b13a445bc03eaa07a6ef5bcb2fff0f71678dd3bd66b822f8be8 a9e1ce91deed4136e6e53e143d1c0af106abde9d77c066c78ebbf5d227f9dde0
|
||||
hash_to_ec 3009182e1efac085c7eba24a7d9ef28ace98ebafa72211e73a41c935c37e6768 e55431a4c89d38bd95f8092cdf6e44d164ad5855677aba17ec262abc8c217c86
|
||||
hash_to_ec e7153acd114a7636a207be0b67fa86fee56dd318f2808a81e35dd13d4251b2d0 ff2b98d257e4d4ff7379e8871441ca7d26e73f78f3f5afcf421d78c9799ba677
|
||||
hash_to_ec 6378586744b721c5003976e3e18351c49cd28154c821bc45338892e5efedd197 3d765fb7bb4e165a3fa6ea00b5b5e22250f3861f0db0099626d9a9020443dda2
|
||||
hash_to_ec 5be49aba389b7e3ad6def3ba3c7dbec0a11a3c36fc9d441130ef370b8a8d29c2 2d61faf38062dc98ae1aaafec05e90a925c9769df5b8b8f7090d9e91b2a11151
|
||||
hash_to_ec f7bc382178d38e1b9a1a995bd8347c1283d8a2e8d150379faa53fd125e903d2b 544c815da65c3c5994b0ac7d6455578d03a2bc7cf558b788bcdb3430e231635a
|
||||
hash_to_ec c28b5c4b6662eebb3ec358600644849ebeb59d827ed589c161d900ca18715fa8 a2d64db3c0e0353c257aadf9abc12ac779654d364f348b9f8e429aa7571203db
|
||||
hash_to_ec 3a4792e5df9b2416a785739b9cf4e0d68aef600fa756a399cc949dd1fff5033a 4b54591bd79c30640b700dfb7f20158f692f467b6af70bd8a4e739c14a66c86a
|
||||
hash_to_ec 002e70f25e1ceaf35cc14b2c6975a4c777b284a695550541e6f5424b962c19f5 73987e9342e338eb57a7a9e03bd33144db37c1091e952a10bd243c5bb295c18a
|
||||
hash_to_ec 7eb671319f212c9cae0975571b6af109124724ba182937a9066546c92bdeff0c 49b46da3be0df1d141d2a323d5af82202afa2947a95b9f3df47722337f0d5798
|
||||
hash_to_ec ca093712559c8edd5c51689e2ddcb8641c2960e5d9c8b03a44926bb798a0c8dc b9ef9cf0f8e4a3d123db565afafb1102338bfb75498444ac0a25c5ed70d615da
|
||||
hash_to_ec cfea0a08a72777ff3aa7be0d8934587fa4127cd49a1a938232815dc3fd8b23ac b4de604b3d712f1ef578195fb0e53c865d41e2dfe425202c6cfe6f10e4404eb5
|
||||
hash_to_ec aa0122ae258d6db21a26a31c0c92d8a0e3fdb46594aed41d561e069687dedcd6 5247eaec346de1c6cddf0ab04c12cd1d85cdb6d3a2fba2a5f9a5fe461abef5eb
|
||||
hash_to_ec b3941734f4d3ba34ccaf03c4c737ac5a1e036eb74309300ce44d73aca24fef08 535938985c936e3780c61fe29a4121d6cb89a05080b6c2147031ea0c2b5b9829
|
||||
hash_to_ec 8c2ee1041a2743b30dcbf413cc9232099b9268f82a5a21a09b63e7aff750882f 6ad0d4b3a65b522dfad0e9ac814b1fb939bc4910bd780943c72f57f362754cca
|
||||
hash_to_ec 4b6829a2a2d46c8f0d0c23db0f735fcf976524bf39ccb623b919dd3b28ad5193 2e0097d7f92993bc45ba06baf4ca63d64899d86760adc4eb5eeefb4a78561050
|
||||
hash_to_ec 9c1407cb6bba11e7b4c1d274d772f074f410d6fe9a1ee7a22cddf379257877d9 692261c7d6a9a7031c67d033f6d82a68ef3c27bd51a5666e55972238769821cd
|
||||
hash_to_ec 638c42e4997abf8a4a9bffd040e31bd695d590cde8afbd7efd16ffdbae63bf66 793024c8ce196a2419f761dde8734734af6bd9eb772b30cc78f2cb89598dce97
|
||||
hash_to_ec 1fb60d79600de151a1cf8a2334deb5828632cbd91cb5b3d45ae06e08187ae23d ff2542cde5bc2562e69471a31cfc3d0c26e2f6ccc1891a633b07a3968e42521c
|
||||
hash_to_ec d2fdbbae4e38a1b734151c3df52540feb2d3ff74edfef2f740e49a5c363406ee 344c83ba6ff4e38b257077623d298d2f2b52002645021241bc9389f81b29ad12
|
||||
hash_to_ec 836c27a6ddfe1a24aba3d6022dff6dfe970f142d8b4ac6afb8efcba5a051942f b8af481d33726b3f875268282d621e4c63f891a09f920b8f2f49080f3a507387
|
||||
hash_to_ec 46281153ddcdf2e79d459693b6fe318c1969538dd59a750b790bfff6e9481abf 8eaf534919ab6573ba4e0fbde0e370ae01eae0763335177aa429f61c4295e9d4
|
||||
hash_to_ec d57b789e050bf3db462b79a997dac76aa048d4be05f133c66edee56afd3dbe66 0c5a294cb2cbb6d9d1c0a1d57d938278f674867f612ed89dcbe4533449f1a131
|
||||
hash_to_ec 548d524d03ac22da18ff4201ce8dbee83ad9af54ee4e26791d26ed2ab8f9bfc7 c6609d9e7d9fd982dec8a166ff4fb6f7d195b413aad2df85f73d555349134f3b
|
||||
hash_to_ec cc920690422e307357f573b87a6e0e65f432c6ec12a604eb718b66ba18897a56 6f11c466d1c72fccd81e51d9bda03b6e8d6a395e1d931b2a84e392dc9a3efa18
|
||||
hash_to_ec c7fb8a51f5fcd8824fc0875d4eb57ab4917cb97090a6e2288f852f2bb449edd9 45543fea6eed461016e48598b521f18ff70178afea18032b188deea3e56052fc
|
||||
hash_to_ec c681bb1b829e24b1c52cb890036b89f0029d261c6a15e5b2c684ee7dfe91e746 263006fe2c6b08f1ab29cdf442472c298e2faf225bbf5c32399d3745cd3904bd
|
||||
hash_to_ec e06411c542312fdd305e17e46be14c63bab5836dc8751da06164b1ae22d4e20f 901871be7a7ff5aecade2acff869846f3c50de69307ac155f2aa3a74d5472ef2
|
||||
hash_to_ec 9c725a2acb80fa712f9781da510e5163b1b30f4e1c064c26b5185e537f0614ea 02420d49257846eb39fddd196d3171679f6be21d9adac667786b65a6e90f57b1
|
||||
hash_to_ec 22792772820feafa85c5cb3fa8f876105251bef08617d389619697f47dff54f2 a3ad444e7811693687f3925e7c315ae55d08d9f4b0a29876bc2a891ab941c1c3
|
||||
hash_to_ec 0587b790121395d0f4f39093d10b4817f58a1e80621a24eea22b3c127d6ac5a2 86c417c695c64c7becaad0d59ddbb2bca4cb2b409a21253d680aac1a08617095
|
||||
hash_to_ec fa0b5f28399bef0cd87bfe6b8a2b69e9c5506fb4bacd22deba8049615a5db526 ede0ea240036ff75d075258a053f3ce5d6f77925d358dbe33c06509fc9b12111
|
||||
hash_to_ec 62a3274fc0bed109d5057b865c2ba6b6a5a417cb90a3425674102fcd457ede2d ff7e46751bb4dcd1e800a8feab7cf6771f42dc0cfed7084c23b8a5d255a6f34e
|
||||
hash_to_ec a6fcd4aecaaaf281563b9b7cd6fbc7b1829654f644f4165942669a2ef632b2bf 28f136be0eb957a5b36f8ec294399c9f73ad3a3c9bb953ad191758ced554a233
|
||||
hash_to_ec 01baa4c06d6676c9b286cda76ed949fd80a408b3309500ba84a5bb7e3dce58e2 a943d1afa2efce284740e7db21ea02db70b124808be2ff80cbf9b9cb96c7b73e
|
||||
hash_to_ec dd9aff9c006ba514cef8fae665657bc9813fe2715467cf479643ea4c4e365d6d 68de2f7d49de4004286ce0989a06a686b15d0f463a02ffd448a18914e1ddf713
|
||||
hash_to_ec 3df3513d5e539161761ce7992ab9935f649bc934bed0da3c5e1095344b733bb9 e9c2dd747d7b2482474325943cd850102b8093164678362c7621993a790e2a8a
|
||||
hash_to_ec 7680cfb244dc8ef37c671fff176be1a3dad00e5d283f93145d0cbee74cca2df4 a0fd8c3cca16a130eaa5864cbe8152b7adfbf09e8cf72244b2fc8364c3b20bf4
|
||||
hash_to_ec 8a547c38bd6b219ea0d612d4a155eba9c56034a1405dcf4b608de787f37e0fd8 76bf0dc40fd0a5508c5e091d8bb7eccfa28b331e72c6a0d4ac0e05a3d651850b
|
||||
hash_to_ec dd93901621f58465e9791012afa76908f1e80ad80e52b809dc7fc32bb004f0a8 09a0b7ecfe8058b1e9ee01c9b523826867ca97a32efad29ac8ceebca67a4ea00
|
||||
hash_to_ec b643010220f1f4ee6c7565f6e1b3dc84c18274ede363ac36b6af3707e69a1542 233c9ff8de59e5f96c2f91892a71d9d93fa7316319f30d1615f10ac1e01f9285
|
||||
hash_to_ec c2637b2299dfc1fd7e953e39a582bafd19e6e7fff3642978eb092b900dbfea80 339587ba1c05e2cba44196a4be1fd218b772199e2c61c3c0ff21dcd54b570c43
|
||||
hash_to_ec 1f36d3a7e7c468eb000937de138809e381ad2e23414cbbaac49b7f33533ed486 7e5b0a96051c77237a027a79764c2763487af88121c7774645e97827fb744888
|
||||
hash_to_ec 8c142a55f60b2edbe03335b7f90aa2bd63e567048a65d61c70cb28779c5200af d3d6d5563b3d81c8c91cf9806bb13b2850fb7c162c610fd2f5b83c464add8182
|
||||
hash_to_ec 99e7b98293c9de1f81aff1376485a990014b8b176521b2a68cdbde6300190398 119cbc01a1d9b9fb4759031d3a70685aebea0f01bc5ee082ce824265fd21b3b4
|
||||
hash_to_ec 9753bd38be072b51490290be6207ca4545e3541bdf194e0850ae0a9f9e64b8ba 1ad3aa759863153606fa6570f0e1290baded4c8c1f2ba0f67c1911bfc8ccd7a0
|
||||
hash_to_ec 322703864ceee19b7f17cec2a822f310f0c4da3ff98b0be61a6fd30ac4db649c 89d9e7a5947e1cde874e4030de278070aae363063cd3592ce5411821474f0816
|
||||
hash_to_ec c1acd01e1e535fad273a8b757d981470f43dd7d95af732901fbba16b6e245761 57e80445248111150da5e63c706b4abbf3eef2cc508bd0347ff6b81e8c59f5bc
|
||||
hash_to_ec 492473559f181bbe78f60215bc6d3a5168435ea2fc0a508372d6f5ca126e9767 df3965f137cf6f60c56ebd7c8f246281fd6dc92ce23a37e9f846f8452c884e01
|
||||
hash_to_ec afa9d6e0e2fb972ee806beb450c2c0165e58234b0676a4ec0ca19b6e710d7c35 669a57e69dd2845a5e50ed8e5d8423ac9ae792a43c7738554d6c5e765a7b088a
|
||||
hash_to_ec 094de050bdadef3b7dbaeeca29381c667e63e71220970149d97b95db8f4db61b 0cf5d03530c5e97850d0964c6a394de9cde1e8e498f8c0e173c518242c07f99a
|
||||
hash_to_ec 2ce583724bc699ad800b33176a1d983512fe3cb3afa65d99224b23dae223efb7 e1548fd563c75ae5b5366dbab4cb73c54e7d5e087c9e5453125ff8fbe6c83a5c
|
||||
hash_to_ec 8064974b976ff5ef6adaade6196ab69cda6970cd74f7f5899181805f691ad970 98ae63c47331a4ac433cb2f17230c525982d89d21e2838515a36ec5744ec2d15
|
||||
hash_to_ec 384911047de609c6ae8438c745897357989363885cef2381a8a00a090cf04a58 4692ec3a0a03263620841c108538d584322fdd24d221a74bf1e1f407f83828af
|
||||
hash_to_ec 0e1b1ced5ae997ef9c10b72cfc6d8c36d7433c01fc04f4083447f87243282528 6ee443ab0637702b7340bd4a908b9e2e63df0cc423c409fb320eb3f383118b80
|
||||
hash_to_ec 5a7aea70c85c040af6ff3384bcaa63ec45c015b55b44fffa37ab982a00dc57c5 2df2e20137cefd166c767646ecd2e386d28f405aebe43d739aa55beba04ed407
|
||||
hash_to_ec 3e878a3567487f20f7c98ea0488a40b87f1ba99e50bbfe9f00a423f927cbd898 697c7e60e4bf8c429ba7ac22b11a4b248d7465fc6abe597ec6d1e1c973330688
|
||||
hash_to_ec c0bb08350d8a4bb6bf8745f6440e9bd254653102a81c79d6528da2810da758e4 396a872ac9147a69b27223bf4ec4198345b26576b3690f233b832395f2598235
|
||||
hash_to_ec 6c3026a9284053a4ddb754818f9ae306ffa96eb7003bd03826eeccc9a0cf656e bef73da51d3ba9972a33d1afb7d263094b66ab6dbe3988161b08c17f8c69c2d5
|
||||
hash_to_ec f80b7d8f5a80d321af3a42130db199d9edcb8f5a82507d8bfca6d002d65458b6 aa59c167ea60ee024421bfbd00adbb3cbfc20e16bd3c9b172a6bef4d47ca7f57
|
||||
hash_to_ec bc0ffc24615aa02fafef447f17e7b776489cd2cc909f71e8344e01cad9f1610d 5c4195cc8dc3518143f06a9c228ae59ec9a6425a8fab89bfc638ad997cf35220
|
||||
hash_to_ec b15fad558737229f8816fcba8fbef805bd420c03e392d118c69bdf01890c4924 f5810477e37554728837f097e1b170d1d8c95351c7fff8abbbfc624e1a50c1b9
|
||||
hash_to_ec ec8c1f10d8e9da9cf0d57c4a1f2c402771bed7970109f3cf21ad32111f1f198f a697e0a3f09827b0cf3a4ffb6386388feda80d30ffffcbd54443dafcba162b28
|
||||
hash_to_ec a989647bf0d70fdb7533b8c303a2a07f5e42e26a45ffc4e48cff5ba88643a201 450fd73e636f94d0d232600dd39031386b0e2ecde4105124fc451341da9803db
|
||||
hash_to_ec 7159971b03c365480d91d625a0fadc8e3a632c518acf0dbec87dd659da70e168 377bc43c038ac46cf6565aa0a6d6bf39968c0c1142755dba3141eeebf0acdf5d
|
||||
hash_to_ec e39089a64fedac4b2c25e36312b33f79d02bf75a883f450f910915b8560a3b06 77efa7db1be020e77596f550de45626824a8268095d56a0991696b211cb329cc
|
||||
hash_to_ec 2056b3c6347611bb0929dad00ec932a4d9bec0f06b2d57f17e01ffa1528a719e b6072c2be2ce928e8cbbb87e8eb7e06975c0f93b309dd3b6a29edaad2b56f99b
|
||||
hash_to_ec 2c026793146e81b889fc741d62e06c341ce263560d57cd46d0376f5b29174489 8f1f64b67762aa784969e954c196a2c6610addc3604aa3291eb0b80304dfe9ef
|
||||
hash_to_ec be6026d6704379c489fa7749832b58bdb1a9685a5ffb68c438537f2f76e0011f 0072569a4090a9ad383a205bb092196c9de871c22506e3bb63d6b9d1b2357c96
|
||||
hash_to_ec f4db802d5c6b7d7b53663b03d988b4cd0c7cad6c26612c5307754a93ebdc9710 f21bc9be4cb28761f6fe1d0a555ad5e9748375a2e9faea25a1df75cc8d273e18
|
||||
hash_to_ec c27d79a564c56b00956a55090481e85fbc837fd5fb5e8311ecb436e300c07e3a 1b1891e6abec74621501450cd68bb1eeaa5b2fffff4ec441a55d1235ff3a0842
|
||||
hash_to_ec a1e2f93c717cad32af386efa624198973df5a710963dd19d4c3ac40032a3a286 69c60571e3f9f63d2bfb359386ae3b8cd9e49a2e9127753002866e85c0443573
|
||||
hash_to_ec 76920d7b1763474bc94a16433c3c28241a9acdee3ff2b2cb0e6757ba415310aa c1b409169f102b696fc7fa1aa9c48631e58e08b5132b6aadf43407627bb1b499
|
||||
hash_to_ec 57ac654b29fa227c181fff2121491fcb283af6cbe932c8199c946862c0e90cb2 a204e8d327ea93b0b1bd74a78ffc370b20cea6455e209f2bc258114baa16d728
|
||||
hash_to_ec 88e66cfaef6432b759c50efce885097d1752252b479dac5ed822fa6c85d56427 6fb84790d3749a5c1088209ee3823848d9c19bf1524215c44031143dd8080d70
|
||||
hash_to_ec c1e55da929c4f8f793696fc77ff4e1c317c34852d98403bfd15dd388ee7df0df 2f41e76f15c5b480665bd84067e3b543b85ce6de02be9da7a550b5e1ead94d34
|
||||
hash_to_ec 29e9ace5aa3c5a572b13f4b62b738a764d90c8c293ccb062ad798acbab7c5ef4 bce791aba1edc2a66079628fd838799489ab16b0a475ce7fe62e24cc56fe131c
|
||||
hash_to_ec f25b2340689dadacaa9a0ef08aee8447d80b982e8a1ea42cf0500a1b9d85b37d f7f53aa117e6772a9abc452b3931b0a99405ac45147e7c550ac9fcf7ffe377b5
|
||||
hash_to_ec 0cb6c47fc8478063b33f5aed615a05bcc84d782c497b6cc8e76ec1fa11edbfdb 7a0b58b03147e7c9be1d98de49ead2ce738d0071b0af8ca03cc92ceb26fc2246
|
||||
hash_to_ec 7bd7287d7c4b596fe46fe57a6982c959653487bea843a77dd47d40986200d576 343084618c58284c64a5ff076f891be64885dc2ac73fa1567f7b39fde6b91542
|
||||
hash_to_ec e4984bf330708152254fb18ecef12d546afd24898a3cf00fba866957b6ee1b82 c70e88b061656181fbd6ff12aca578fb66de5553c756ea4698a248b177185bc6
|
||||
hash_to_ec cefd6c3cb9754ea632d6aea140af017de5ea12e5184f868936b74d9aa349d603 4b476502a8a483aadd50667f262f95351901628dd3a2aac1a5a41c4ea03f1647
|
||||
hash_to_ec da5d0f33344ee7f3345204badf183491b9452b84bccc907602c7bad43e5cf43e 9561b9e61241625e028361494d4fa5cd78df4c7219fa64c8fede6d8421b8904a
|
||||
hash_to_ec d6f0a4f8c770a1274a76fd7ae4e5faf7779249263e1aaecc6f815cf376f5c302 cd5c55820be10f0d38feb81363ede3716a9168601a0dd1ce3109aab81367d698
|
||||
hash_to_ec b6bf32491d12a41c275d8518fc534d9a0d17aade509e7e8b8409a95c86167307 4aae534abbd67a9a8f2974154606c0e9be8932e920c7a5e931b46a92859acf82
|
||||
hash_to_ec 0f930beaad041f9cefd867bc194027dd651fb3c9bda5944ececdba8a7136b6d3 521708f8149891b418d0920369569a9d578029c78f8e41c68a0bb68d3ad5df60
|
||||
hash_to_ec 49b1fe0f97be74b81e0b047027b3e9f726fa5e90a67dafa877309397291c06c5 0852e59dfae5ec32cce606c119376597bce5cd4d04879d329f74e3ec66414cd3
|
||||
hash_to_ec 4d57647d03f2cfbd4782fcc933e0683b52d35fc8d37283e6c7de522ddfa7e698 cbeb9ebfbbc49ec81fac3b7b063fecac1bb40ea686d3ffb08f82b291715cd87f
|
||||
hash_to_ec 4ea3238c06fc9346c7421ff85bc0244b893860b94bc437378472814d09b2e99f a1fbae941adc344031bbdf53385dfdc012311490a4eb5e9a2749a21b27ce917a
|
||||
hash_to_ec 0cd3609f5c78b318cb853d189b73b1ee2d00edd4e5fce2812027daa3fcb1fed1 0c7a7241b16e3c47d41f5abbf205797bd4b63fc425a7120cb2a4bf324e08ae74
|
||||
hash_to_ec d74ab71428e36943c9868f70d3243469babd27988a1666a06f499a5741a52e3e 65b7c259f3b4547c082b2a7669b2b363668c4d87ac14e80471317b03b34e5216
|
||||
hash_to_ec f6b151998365e7d69bcbce383dd2e8b5bf93b8b72f029ff942588208c1619591 6ce840ce5dfbca238665c1e6eddb8b045aa85c69b5976fc55ab57e66d3d0a791
|
||||
hash_to_ec 207751de234b2bd7ec20bdd8326210c23aa68f04875c94ad7e256a96520f25d6 fc8f79ab3af317c38bfb88f40fb84422995a0479cfa6b03fa6df7f4e5f2813fb
|
||||
hash_to_ec 62291e2873f38c0a234b77d1964205f3f91905c261d3c06f81051a9b0cb787cb 076d1d767457518e6777cb3bd4df22c8a19eb617e4bbccd1b0bd37522d6597a5
|
||||
hash_to_ec 4b060df2d2854036751d00190ee821cb0066d256d4172539fdfa6fbd1cdfe1f9 59866e927c69e7de5df00dc46c0d2a1ddf799d901128ff040cebb8fd61b95da4
|
||||
hash_to_ec ac8daf73f9c609bb36bce4fdeec1e50be5f22de38c3904fabcf758f0fc180bc7 7d8dc4e956363b652468a5fecafd7c08d48a2297e93b8edcb38e595fdd5a1fde
|
||||
hash_to_ec fef7b6563fd27f3aab1d659806b26b8f2ec38bc8feefad50288383c001d1c20f e6e42547f12df431439d45103d2c5a583248f44554a98a3a433cf8c38b11805d
|
||||
hash_to_ec 40a3d6871c76ecc6bb7b28324478733e196cc11d062dd4c9265cf31be5cf5a97 8c55a3811c241a020b1be202a58d5defbc4c8945d73b132570b47dd7c019ccf0
|
||||
hash_to_ec 0cd71e7e562b2b47f4bc8640caf20e69d3a62f10231b4c7a372c9691cff9ac3c fb8e4e3de479b3bf1f4f13b4ed5507df1e80bd9250567b9d021b03339d6e7197
|
||||
hash_to_ec 40a4e62800a99b7a26e0b507ffb29592e5bdba25284dc473048f24b27d25b40a 90ae131d29ee4a71cd764ab26f1ca4e6d09a40db98f8692b345c3a0e130dc860
|
||||
hash_to_ec 1ddf35193cf52860bfe3e41060a7f44281241c6ae49cd541d24c1aca679b7501 3b4f50013895c522776ced456329c4e727de03575f6b99ae7d238a9f70862121
|
||||
hash_to_ec 014e0fa8ce9d5df262b9a1765725fde354a855de8aef3fc23684e05dd1ba8d34 3857f57776a3cb68721bcb7f1533a5f9fb416a1dc8824d719399b63a142d24de
|
||||
hash_to_ec 09987979b0e98d1d5355df8a8698b8f54d3a037d12745c0a4317fe519c3df9cc 32a181e2b754aeced214c73ac459c97d99e63317be3eb923344c64a396173bca
|
||||
hash_to_ec 51e9e8ec4413e92dbaaba067824c32b018487a8d16412ed310507b4741e18eed 0356b209156b4993fd5d5630308298429a1b0021c19bedecb7719ac607cfa644
|
||||
hash_to_ec 14d91313dfe46e353310e6a4a23ee15d7a4e1f431700a444be8520e6043d08d9 6f345f4018b5d178d9f61894d9f46ac09ff639483727b0d113943507cee88cfd
|
||||
hash_to_ec 0d5af9ace87382acfffb9ab1a34b6e921881aa015d4f6d9c73171b2b0a97600d a8dbf36c85bebe6a7b3733e70cd3cd9ed0eb282ca470f344e5fcf9fe959f2e6e
|
||||
hash_to_ec 996690caac7328b19d20ed28eb0003d675b1a9ff79055ab530e3bf170eb22a94 14340d7d935cffce74b8b2f325c9d92ce0238b51807ef2c1512935bb843194ce
|
||||
hash_to_ec ad839c4b4c278c8ebe16ff137a558255a1f74646aa87c6cd99e994c7bb97ce8a d4f2da327ffded913b50577be0e583db2b237b5ca74da648e9b985c247073b76
|
||||
hash_to_ec 26fc2eeeee983e1300d72362fdff42edf08038e4eee277a6e2dbd1bd8c9d6560 3468b8269728c2c0bfc2e53b1575415124798bc0f59b60ea2f14967fc0ca19ce
|
||||
hash_to_ec db33cecaf4ee6f0ceba338cc5fabfb7462cd952a9c9007357ff3f0ca8336f8bc 0bab38f58686d0ff770f770a297971510bc83e2ff2dfead34823d1c4d67f11af
|
||||
hash_to_ec a0ee84b3c646526fb8787d26dcd9b7fe9dc713c8a6c1a4ea640465a9f36a64df 4d7a638f6759d3ec45339cd1300e1239cca5f0f658ca3cd29bc9bdb32f44faf0
|
||||
hash_to_ec 6a702e7899fcf3988e2b6b55654c22e54f43d3fa29de19177bdff5b2295fe27f 145d5748d6054fb586568e276f6925aef593a5b9c8249ad3dbef510af99b4307
|
||||
hash_to_ec 30ce0fd4f1fac8b62d613b8ee4a66deef6eb7094bd8466531050b837460f6971 f3aa850d593ba7cef01389f7e1916e57617f1d75cd42f64ce8f5f272384b148c
|
||||
hash_to_ec 3aa31d4ad7046ad13d83eb11c9a6e90eb8483a374a77a9a7b2a7cc0978fefa76 2fe0827dc080d9c1e7ec475a78aa7ae3c86d1a35f4c3f25f4a1f7299cacf018a
|
||||
hash_to_ec 8562a5a91e763b98014523ebb6e49120979098f89c31df1fde9eb3a49a15b20f ae223bf85e2009a9daf5fd8a14685e2e1e625fc88818b2fd437dd7e109a48f59
|
||||
hash_to_ec ccf9c313a47b8dbf7ce42c94b785818bc24134d95b6d22acc53c1ec2be29cf27 3e79fce6fe5aa14251b6560df4b76e811d7739eec097f27052c4403a283be71d
|
||||
hash_to_ec d1e33cd6f8918618d5fb6d67ad8de939db8beaec4f115551eac64479b739b773 613fffcbe1bf48bb2d7bfd64fd97790a06025f8f2429edddb9ac145707847ecf
|
||||
hash_to_ec 81eaeced34dd44e448d5dafa5715225e4956c90911c964a96ff7aa5b86b969bc 8f81177495d120a1357380164d677509b167f2958eb8b962b616c3951d426d8c
|
||||
hash_to_ec 2bc001a29f8eab1c7377de69957ba365fb5bdaf9c2c220889709af920dfe27d3 9bcb3010038f366fa4c280eed6e914a23bfc402594d0b83d0e66730a465a565b
|
||||
hash_to_ec 6feeb703c05e86c58d9fc5623f1af8657ecd1e75a14d18c4eedb642a8a393d16 6544628ba67ed0e14854961739c4d467fcf49d6361e39d32ea73dabeae51e6c3
|
||||
hash_to_ec e8ff145a7c26897f2c1639edd333a5412f87752f110079f581ccdc87fcce208c d4b5a6e06069c7e012e32119f8eda08ff04a8dfa784e1cf1bced455a4d41d905
|
||||
hash_to_ec 80488131dcb2018527908dbf8cdf4b823ef0806dc1d360f4da671004ef7ff74d 9984a79d9fd4f317768b442161116eef84e2ca49e938642b268fd64312d59a27
|
||||
hash_to_ec d8c4ca60446849a784d1462aa26a3b93073ff6841cb2da3ef52ab9785b00b1fd da5ec1562e7de2382d35728312f4eea3608d4dba775c1c108de510e1ce97d059
|
||||
hash_to_ec 68645728dfc6b9358dfb426493238ba38f24a2f46a3e89edb47d212549939cb7 d3253aa7235113dcc1b577d3bb80be34f528398815a653dbdbacbcbdfd5887a1
|
||||
hash_to_ec 4e8eb97ba2d1046e1b42e67530a61441e31c84e5e5e448d8e8dbe75d104eaccb de94f73e83222aa0e39b559d4fef70387b0815b9b2f6beff5da67262d8f0eb3e
|
||||
hash_to_ec 104ff03122ffdf59b22b8c0fe3d8f2ef67d02328e4d5181916d3d2a92f9a0bb7 1517ccf69c0328327e1cf581f16944ff66bc91c37e1cd68a99525415e00b7c9f
|
||||
hash_to_ec 80f23aae7356ae9a2f9f7504495a731214d26f870fb7df68fdc00b233494156f 7aef046b0a70f84e8d239aa95e192b5a3fffa0fae5090c91273e8996beca9e38
|
||||
hash_to_ec 2424b33235955a737ebddbf1c6c59cd8778af74da3bd3e658447666a2ab2f557 d19e2be8d482950fbdae429618da7a9daedb8c5944dea19cd1b6b274e792231b
|
||||
hash_to_ec 0adc839d2b8f099e4341a4763b074c06318d6bcbd1ec558d20a9820c4a426463 cea5da12a84e5c20011726d9224a9930bec30f9571762dd7ca857b86bd37d056
|
||||
hash_to_ec 46c84d53951f1ba23c46a23d5d96bf019c559aa5d2d79e4535cfcdb36f38ce25 2a913a01a6f7dd78a43cdd5354d1160d9a5f0d824c489a892c80eba798a77567
|
||||
hash_to_ec 99bdaaf68555ccdc93d97c3a0fb4c126a1aa8b1202194a1a753401a6cae21055 1f645efe173577a092f2d847cc966e28ba3b36397fe84c96dfa4724ed4fcfdf9
|
||||
hash_to_ec c540ff78f1e063ad26ffa69febb8818c9f2a325072c566091ad816e40fe39af4 de7a762262c91ab4beccc0713233cb91163aec43e34de0dbcfad0c431e8a9722
|
||||
hash_to_ec de8b1ff8978cd5e02681521542b7b6c3c2f8f4602065059f83594809d04e3dda 290601e75207085bff3e016746e55a80310a76dea9ef566c24181079c76da11c
|
||||
hash_to_ec d555994c8a022e52602d2a8bdd01fc1bfa6b9ab6734ff72a1bd5f937de4627f8 5f6794e874f48c4b362d0a24207374c2d274e28de86351afc6ddb95d8cc2fd62
|
||||
hash_to_ec 19db72f703fe6f1b73f21b6ba133ae6b111ae8cc496d3aa32e02411e34c0d8d7 42f159f43d2d62b8cf8a47d5f1340c5cf070e9860fc60de647c55d50fe9f5607
|
||||
hash_to_ec 23a87a258c2a5d1353aa2d5946f9e5749b92f85e3c58e1d177c3b6c3dcac809c e5685016f79d5e87d1fecb3e2a0fe64e4875f7accd2f6649d7f6b16317549cb1
|
||||
hash_to_ec 43e1738d7d1b5b565f5fc78e81480f7edf9a4dc18f104fc4be95135b98931b17 650f5b682e45f2d0c5d5e8bcfd9e0cda7d9071b55ecbfaf5e3b59941cd7479f2
|
||||
hash_to_ec a9d644de0804edf62dee613efa2547e510990a9b7a987ebe55ec74c23873a878 52ad329f88499a4f110e6a6cba1f820012d8db6ccb8f6495ab1e3eb5a24786e1
|
||||
hash_to_ec 11f2b5d89a0350d7c8727becf0f4dd19bd90f8c94ff207132ab13282dd9b94e6 b798a47bb98dc2a8f99deaf64d27638e33a0d504c5d2fbee477a2bc9b89e2838
|
||||
hash_to_ec 5e206e3190b3b715d125f1a11fff424fb33e36e534c99ddde2a3517068b7dcc4 2738e9571c96b2ddf93cb5f4a72b1ea78d3731d9555b830494513c0683c950ca
|
||||
hash_to_ec efc3d65a43d4f10795c7265a76671348f80173e0f507c812f7ae76793b99c529 cf4434d18ce8167b51f117fe930860143c46e1739a8db1fba73b6b0de830d707
|
||||
hash_to_ec 81f00469788aad6631cf75b585ae06d43ec81c20479925a2009afac9687dff60 c335b5889b36ba4b4175bb0d986807e8eedb6f6b7329b70b922e2ab729c4202a
|
||||
hash_to_ec 9ef5ff329b525ee8f5c3ac38e1dba7cb19985617341d356707c67ff273aed02d bef9f9e051ba0e24d1fdf72099cf43ecdd250d047fb329855b5372d5c422db9e
|
||||
hash_to_ec 3fa1401bd63132cf8b385c0fa65f0715ba1fe6161e41d59f8033ae2b22f63fa1 8289a1cb3c2dae48879bb8913fafe2d196cc2fdab5f2a77607910efd33eae6df
|
||||
hash_to_ec 6559836fd0081fa38a3f8d8408b564e5698b9797cf5e15f7f12a7d2c84511989 28d405a6687d2ecc90c1c66bf0454d58f3fa38835743075e1db58c658e15a104
|
||||
hash_to_ec 8e0882d45f0e4c2fb2839d3be86ff699d4b2242f5b25ac5a3c2f65297c7d2032 2771fdcf9135a62007adb5f0004d8222f0e42f819c81710aa4dc3ab2042bebf3
|
||||
hash_to_ec 1d91dc4dd9bd82646029d13aca1af96830c1d8a0400ddebeb14b00c93501c039 7792c62e897f32cbc9c4229f0d28f7882ceeae120329a1cd35f76a75ac704e93
|
||||
hash_to_ec 09527f9052acbbdd7676cbbd9534780865f04a27aaadad2b7d4f1dac68883cf0 b934220cde1327f2dc6af67bcb4124bf424d5084ef4da945e4daad1717cd0bb8
|
||||
hash_to_ec 2362e1abe73e64cdd2ca7f6c5ea9f467213747dd3f2b7c6e5df9cb21e03307d7 676b7122b96564358bbaaf77e3a5a4db1767e4f9a50f6ddd1c69df4566755af9
|
||||
hash_to_ec 26c2dd2356e9b6c68a415b25f91d18614dc8500c66f346d28489da543ee75a94 0f4fd7086acd68eb7c9fa2410e2ecf18e34654eb44e979bc03ce436e992d5feb
|
||||
hash_to_ec 422dc0a09d6a45a8e0b563eeb6a5ee84b08abd3a8cb34ff93f77ba3b163f4042 631f1b412ff5a0fccbe53a02b4a3deaa93a0418ed9874df401eb698ef75d7441
|
||||
hash_to_ec ceecdf46f57ef3f36ff30a1a3579b609340282d1b26ab5ddef2f53514e91bab1 9bc6f981fe98d14a2fc5b01a8134b6d35e123ec9ab8a3f303e0a5abb28150e2e
|
||||
hash_to_ec 024a9e6e0d73f28aa6207fb1e02ce86d444d2d46f8211e8aaab54f459db91a5a 5fb0c1d2c3b30f399102104ea1874099fa83110b3d9c1fcfffb2981c98bf8cdf
|
||||
hash_to_ec 5b8e45e269c9ccac4c68e532a72b29346d218f4606f37a14064826a62050e3a8 c7be46a871b77fc05ce891d24bd6bd54d9775b7ef573c6bc2d92b67f3604c1d1
|
||||
hash_to_ec 9a6593a385c266389eef14237874b97bdcd1823c3199311667d4853c2d12aa81 9f55ee9d94102d2b9c5670f30586cf9823bf205b4d4fe088c323e87c4e10f26f
|
||||
hash_to_ec 27377e2811598c3569b92990865d39b72c7a5533e1be30f77330863187c11875 abd82bc726f2710a8b87e4c1cf5a069f0ae800de614468d3ff35639983020197
|
||||
hash_to_ec 7cacfaa135fb7d568b8dce8ea9136498b1b28c6d1020af45d376288d78d411f0 229fccd49744c0692508af329224553d21561ee6062b2b8a21f080f73da5bd97
|
||||
hash_to_ec 52abd90a5542d6496b8dec9567b020f30058e29458d64f2d4f3ad6f3bfc1a5a0 874e82ced7cf77577b3374087fb08a2300b7f403de628310c26bdb3be869d309
|
||||
hash_to_ec 5c8eebe9d12309187afa8d0d5191de3fdb84e5a05485d7cd62e8804ce7fdc0bc 12b7537643488aa8b9dcc4bae040cd491f8b466163b7988157b0502fb6c9177f
|
||||
hash_to_ec 6ca3dd5c7a21a6bf65d6eefbe20a66e9b1d6b64196344be0c075f47aea48e3aa 5e1d0705ee24675238293b73ab1d98359119d4b328275be2460cc6ee4d19cc88
|
||||
hash_to_ec d7e6cd0d39b4308c2a5ee547c4569c8bb3887e49cedece62d218d7c3c5277797 793dc4397112dfd9a8f4e061f457eb6d6fbb1d7a58c40bad5f16002c64914186
|
||||
hash_to_ec 9cb6de8ba967cca0f0f861c6e20546f8958446595c01c28dae7ba6cfa09d6b14 ba1a2f7502b58fee3499c20e35fa01bb932e7a7c4a925dc04fbf5d90f33cfb5e
|
||||
hash_to_ec 8ef9c7366733a1edcd116238cdbd177d61222d5c3e05b30ef6b85014cbcb6b79 8fc89664722947164ac9b77086aed319897612068f56ecd57f47029f14671603
|
||||
hash_to_ec 7f317a34e4fb7de9f69cb107ffc0e57fd9f5c85b85ccb5319d05cebfc169924a 4b71c42339c73db7d710cd63f374d478a6c13bdc352cff40e967282268965ba7
|
||||
hash_to_ec 15beef8d9687b92918a903b01d594859db4e7128263c8db0cae9d423ff962c1e cd75e6323952f6ac88f138f391b69f38c46d70b7eda61f9e431725b6f1d514a5
|
||||
hash_to_ec 7a1c04c9af8fc6649833fe81e96f0199fcfe94959256cbe1490075fc5be0904e 0368270cd979439ae0a9552a5d6c9f959e4247fcf920d9e071464582e79c04b1
|
||||
hash_to_ec c854c583d338615f85f69061e0fa9c9d7c5bbbfe562e8774fef3be556fe8bb63 061620171d7320f64bee98414ff7200a1f481521d202fb281cab06be73b80402
|
||||
hash_to_ec 0fb8af5aba05ad2503edf1cfad5a451da088e7e974772057cd991a4e0601a3eb d3cbc20384a4420143fcce2cb763b0c15bec4f3267d1bdad3c34c1ee6b790f5e
|
||||
hash_to_ec 9a251cf59e84a9da5630642f9671c732440caa8fcf4c92446a7e5f5ef99da46c 9b9679086a433f2077f40bcd4c7545fb5cc87e7dbb8bba468d53cb04a74361a0
|
||||
hash_to_ec 8c632e357cef00e0911eb566f8cc809136b3f5ac1e82d183e4d645cef89fa155 5e06b0f4f278fa1ccb5431866e0b35171cdb814e2e82b9189ce01d8d8a1b2408
|
||||
hash_to_ec 4aa4c31463475086a5d96b3ff550340567ab3b4a86fa3f01cfe9be18bc4dcb54 76a2916cfc093f27992e1f07b50f431d61d58e255507e208cd29ea4d3bc56623
|
||||
hash_to_ec 1d33d9aadb949346e3c78d065a0f5262374524f4cb97a7390c8cdaede7ca6578 9ad2f757f499359903031adea6126c577469c4e834a2959e3ac08ee74b13783c
|
||||
hash_to_ec d9217b9a070df20c4d2f0db42ff0bb36bfba9f51b0b6df8fdfe150405dce4934 65a843c522b4b8ec081a696a0d2dd8dfdfea45db201de7a5889a1446c6dff8c7
|
||||
hash_to_ec b665b2ca8a285e44ba84e785533b56496a5319730dbb95bc14d3bdfece7544dc 8a804cd13457497b0a29eeca2cecfaa858766ec1d270a0e0c6785b43fd49b824
|
||||
hash_to_ec 43b5cbcc21b3404bca97fa9a661940fe64d40f3ca569310e50b1bb0173c4d5ee 6c12fffb540d536060bb8b96cf635c1b2cbaa4d875a8d2fb0bf79a690363df19
|
||||
hash_to_ec 11c58f20562c00dec5bb4456be07cd98186837e9af38d50d45f5e7b6f0f9000d cee76b567586f66dadd38c01213bfc1a17d38e96a495efb4c26063dc498ba209
|
||||
hash_to_ec b069a980b51d8e030262db0b30069e660f4a3f6f8075d1790c153ba12b879f8b 262391b00bdee71d1d827b2cfe50b46c29e265934dc91959bd369aca0cc6444e
|
||||
hash_to_ec 75274bfd79bf33eb2f9ab046d34528af9a71811e7e3d55c20eb049c81ac692d8 cb93c850e36896fe6626e97c53652af6736ec3ba0641c7765d0cca2bad2352de
|
||||
hash_to_ec 5cdb6a24d9736a00f197d9707949fedc5405f367744fe8c83b7cff650302b589 8b4ac03123fab9275dcf340345a1b11fba48ef106d410ba2e0e6f6457037a419
|
||||
hash_to_ec 07fdc85f809f95a07b59b084402bf91c512ebbe05c7657d6ba27a9e7e121e3e2 61182b3def063630e11de648a278032bcb75949f3a24ef5a133da87830ae5c4e
|
||||
hash_to_ec a4188ca634cbb796f9927822e343d7b267e0a609c1a0ffa4dcf3726b9ffcc8a2 a911e4899fda28fd6337d708d34553ac5e810ee4938f6f7d9d6e521cab069edb
|
||||
hash_to_ec 3c128ec5c955ea189a5789df2c892e94193a534a9d5801b8f75df870bc492a69 59eef5ee9df0f681df5b5c67ead1f06b059a8a843837b67f20cce15779608170
|
||||
hash_to_ec 51a4cc7ec4a14a98c0731e9de7f3ce0779123222d95455e940f2014a23729ec8 105863ccda076af7290d1bf9ec828651dc5811159839044d23f1c3e31a11c5e2
|
||||
hash_to_ec 1b901a31acbb7807c3309facdc7d04bc3b5a4aa714e6e346bd1c6ad4634e6534 01b3c0000b6c6b471c67c6ab3f9c7a500beaea5edb5c8f2b34df91b69ff67f21
|
||||
hash_to_ec d2f2c8d79cfa2e7cb2db80568ba62ca0576741acfbe5e2baa0d9b3c424a7c84d 7df9d9088022bd1ce6814d6f8051eef27a650ee38e789b184da2691efd27139d
|
||||
hash_to_ec 04dcb7644fdfc12d8e34d6e57d7769db939b4a149ed2b81aa51a74ee90babe19 6cff0ab2dd3b32ba1bd1a78e3661722f3f10003a01ce83e430970557decedb2c
|
||||
hash_to_ec 222798c6841eeaa07e7b7e29686942d7c7f9afc38d09360c8e1f52f2b7debd12 133e3a04ec82aa9b8dbbec18cadbafff446d1270bf7c6f3f97ddd3906dae2468
|
||||
hash_to_ec 4f7277c3ef247a0689b486ad965f969c433fc63e95d7310e789c4708418ccabc 7e0f2c984dd3cffb35458938c95fe92acf2e697aed060b0e3377c7a07e53c494
|
||||
hash_to_ec 359b4d6709413243ae2c5409ea02714a9f8961bbbb64a91e81daf01e18c981bf eab69af2cb7f113ad6a27035c0399853d10bd0b99291fad37794d100f7530431
|
||||
hash_to_ec 6cea3c6a9eb38f60329537170aa4db8dbb869af2040061e53b10c267daf6568c da9a97f4fa96bd05dade5e2704a6a633ba4dbe5080a1e831cda888e9d4f86615
|
||||
hash_to_ec 3dddecb954ef0209bcf61fd5b46b6c94f2384ef281c48a20ffee74f90788172d af9899c31f944617af54712f93d1a2b4944e48867f480d0d1aec61f3b713e32d
|
||||
hash_to_ec 9605247462f50bdf7ff57fe966abbefe8b6efa0b65b5116252f0ec723717013f fc8f10904d42a74e09310ccf63db31a90f1dab88b278f15e3364a2356810f7e9
|
||||
hash_to_ec a005143c4d299933f866db41d0a0b8c67264f5d4ea840dd243cb10c3526bc077 928df1fe9404ffa9c1f4a1c8b2d43ab9b81c5615c8330d2dc2074ac66d4d5200
|
||||
hash_to_ec f45ce88065c34a163f8e77b6fb583502ed0eb1f490f63f76065a9d97e214e3a9 41bd6784270af4154f2f24f118617e2d7f5b7771a409f08b0f2b7bbcb5e3d666
|
||||
hash_to_ec 7b40ac30ed02b12ff592a5479c80cf5a7673abfdd4dd38810e40e63275bc2eed 6c6bf5961d83851c9728801093d9af04e5a693bc6cbad237b9ac4b0ed580a771
|
||||
hash_to_ec 9f985005794d3052a63361413a9820d2ce903198d6d5195b3f20a68f146c6d5c 88bcac53ba5b1c5b44730a24b4cc2cd782298fc70dc9d777b577a2b33b256449
|
||||
hash_to_ec 31b8e37d01fd5669de4ebf78889d749bc44ffe997186ace56f1fb3e60b8742d2 776366b44170efb130a5045597db5675c6c0b56f3def84863c6b6358aa8dcf40
|
||||
16
coins/monero/generators/src/varint.rs
Normal file
16
coins/monero/generators/src/varint.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std_shims::io::{self, Write};
|
||||
|
||||
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
||||
pub(crate) fn write_varint<W: Write>(varint: &u64, w: &mut W) -> io::Result<()> {
|
||||
let mut varint = *varint;
|
||||
while {
|
||||
let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap();
|
||||
varint >>= 7;
|
||||
if varint != 0 {
|
||||
b |= VARINT_CONTINUATION_MASK;
|
||||
}
|
||||
w.write_all(&[b])?;
|
||||
varint != 0
|
||||
} {}
|
||||
Ok(())
|
||||
}
|
||||
321
coins/monero/src/bin/reserialize_chain.rs
Normal file
321
coins/monero/src/bin/reserialize_chain.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
#[cfg(feature = "binaries")]
|
||||
mod binaries {
|
||||
pub(crate) use std::sync::Arc;
|
||||
|
||||
pub(crate) use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
||||
|
||||
pub(crate) use multiexp::BatchVerifier;
|
||||
|
||||
pub(crate) use serde::Deserialize;
|
||||
pub(crate) use serde_json::json;
|
||||
|
||||
pub(crate) use monero_serai::{
|
||||
Commitment,
|
||||
ringct::RctPrunable,
|
||||
transaction::{Input, Transaction},
|
||||
block::Block,
|
||||
rpc::{RpcError, Rpc, HttpRpc},
|
||||
};
|
||||
|
||||
pub(crate) use monero_generators::decompress_point;
|
||||
|
||||
pub(crate) use tokio::task::JoinHandle;
|
||||
|
||||
pub(crate) async fn check_block(rpc: Arc<Rpc<HttpRpc>>, block_i: usize) {
|
||||
let hash = loop {
|
||||
match rpc.get_block_hash(block_i).await {
|
||||
Ok(hash) => break hash,
|
||||
Err(RpcError::ConnectionError(e)) => {
|
||||
println!("get_block_hash ConnectionError: {e}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => panic!("couldn't get block {block_i}'s hash: {e:?}"),
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Grab the JSON to also check it was deserialized correctly
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct BlockResponse {
|
||||
blob: String,
|
||||
}
|
||||
let res: BlockResponse = loop {
|
||||
match rpc.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await {
|
||||
Ok(res) => break res,
|
||||
Err(RpcError::ConnectionError(e)) => {
|
||||
println!("get_block ConnectionError: {e}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => panic!("couldn't get block {block_i} via block.hash(): {e:?}"),
|
||||
}
|
||||
};
|
||||
|
||||
let blob = hex::decode(res.blob).expect("node returned non-hex block");
|
||||
let block = Block::read(&mut blob.as_slice())
|
||||
.unwrap_or_else(|e| panic!("couldn't deserialize block {block_i}: {e}"));
|
||||
assert_eq!(block.hash(), hash, "hash differs");
|
||||
assert_eq!(block.serialize(), blob, "serialization differs");
|
||||
|
||||
let txs_len = 1 + block.txs.len();
|
||||
|
||||
if !block.txs.is_empty() {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct TransactionResponse {
|
||||
tx_hash: String,
|
||||
as_hex: String,
|
||||
}
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct TransactionsResponse {
|
||||
#[serde(default)]
|
||||
missed_tx: Vec<String>,
|
||||
txs: Vec<TransactionResponse>,
|
||||
}
|
||||
|
||||
let mut hashes_hex = block.txs.iter().map(hex::encode).collect::<Vec<_>>();
|
||||
let mut all_txs = vec![];
|
||||
while !hashes_hex.is_empty() {
|
||||
let txs: TransactionsResponse = loop {
|
||||
match rpc
|
||||
.rpc_call(
|
||||
"get_transactions",
|
||||
Some(json!({
|
||||
"txs_hashes": hashes_hex.drain(.. hashes_hex.len().min(100)).collect::<Vec<_>>(),
|
||||
})),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(txs) => break txs,
|
||||
Err(RpcError::ConnectionError(e)) => {
|
||||
println!("get_transactions ConnectionError: {e}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => panic!("couldn't call get_transactions: {e:?}"),
|
||||
}
|
||||
};
|
||||
assert!(txs.missed_tx.is_empty());
|
||||
all_txs.extend(txs.txs);
|
||||
}
|
||||
|
||||
let mut batch = BatchVerifier::new(block.txs.len());
|
||||
for (tx_hash, tx_res) in block.txs.into_iter().zip(all_txs) {
|
||||
assert_eq!(
|
||||
tx_res.tx_hash,
|
||||
hex::encode(tx_hash),
|
||||
"node returned a transaction with different hash"
|
||||
);
|
||||
|
||||
let tx = Transaction::read(
|
||||
&mut hex::decode(&tx_res.as_hex).expect("node returned non-hex transaction").as_slice(),
|
||||
)
|
||||
.expect("couldn't deserialize transaction");
|
||||
|
||||
assert_eq!(
|
||||
hex::encode(tx.serialize()),
|
||||
tx_res.as_hex,
|
||||
"Transaction serialization was different"
|
||||
);
|
||||
assert_eq!(tx.hash(), tx_hash, "Transaction hash was different");
|
||||
|
||||
if matches!(tx.rct_signatures.prunable, RctPrunable::Null) {
|
||||
assert_eq!(tx.prefix.version, 1);
|
||||
assert!(!tx.signatures.is_empty());
|
||||
continue;
|
||||
}
|
||||
|
||||
let sig_hash = tx.signature_hash();
|
||||
// Verify all proofs we support proving for
|
||||
// This is due to having debug_asserts calling verify within their proving, and CLSAG
|
||||
// multisig explicitly calling verify as part of its signing process
|
||||
// Accordingly, making sure our signature_hash algorithm is correct is great, and further
|
||||
// making sure the verification functions are valid is appreciated
|
||||
match tx.rct_signatures.prunable {
|
||||
RctPrunable::Null |
|
||||
RctPrunable::AggregateMlsagBorromean { .. } |
|
||||
RctPrunable::MlsagBorromean { .. } => {}
|
||||
RctPrunable::MlsagBulletproofs { bulletproofs, .. } => {
|
||||
assert!(bulletproofs.batch_verify(
|
||||
&mut rand_core::OsRng,
|
||||
&mut batch,
|
||||
(),
|
||||
&tx.rct_signatures.base.commitments
|
||||
));
|
||||
}
|
||||
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => {
|
||||
assert!(bulletproofs.batch_verify(
|
||||
&mut rand_core::OsRng,
|
||||
&mut batch,
|
||||
(),
|
||||
&tx.rct_signatures.base.commitments
|
||||
));
|
||||
|
||||
for (i, clsag) in clsags.into_iter().enumerate() {
|
||||
let (amount, key_offsets, image) = match &tx.prefix.inputs[i] {
|
||||
Input::Gen(_) => panic!("Input::Gen"),
|
||||
Input::ToKey { amount, key_offsets, key_image } => (amount, key_offsets, key_image),
|
||||
};
|
||||
|
||||
let mut running_sum = 0;
|
||||
let mut actual_indexes = vec![];
|
||||
for offset in key_offsets {
|
||||
running_sum += offset;
|
||||
actual_indexes.push(running_sum);
|
||||
}
|
||||
|
||||
async fn get_outs(
|
||||
rpc: &Rpc<HttpRpc>,
|
||||
amount: u64,
|
||||
indexes: &[u64],
|
||||
) -> Vec<[EdwardsPoint; 2]> {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Out {
|
||||
key: String,
|
||||
mask: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Outs {
|
||||
outs: Vec<Out>,
|
||||
}
|
||||
|
||||
let outs: Outs = loop {
|
||||
match rpc
|
||||
.rpc_call(
|
||||
"get_outs",
|
||||
Some(json!({
|
||||
"get_txid": true,
|
||||
"outputs": indexes.iter().map(|o| json!({
|
||||
"amount": amount,
|
||||
"index": o
|
||||
})).collect::<Vec<_>>()
|
||||
})),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(outs) => break outs,
|
||||
Err(RpcError::ConnectionError(e)) => {
|
||||
println!("get_outs ConnectionError: {e}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => panic!("couldn't connect to RPC to get outs: {e:?}"),
|
||||
}
|
||||
};
|
||||
|
||||
let rpc_point = |point: &str| {
|
||||
decompress_point(
|
||||
hex::decode(point)
|
||||
.expect("invalid hex for ring member")
|
||||
.try_into()
|
||||
.expect("invalid point len for ring member"),
|
||||
)
|
||||
.expect("invalid point for ring member")
|
||||
};
|
||||
|
||||
outs
|
||||
.outs
|
||||
.iter()
|
||||
.map(|out| {
|
||||
let mask = rpc_point(&out.mask);
|
||||
if amount != 0 {
|
||||
assert_eq!(mask, Commitment::new(Scalar::from(1u8), amount).calculate());
|
||||
}
|
||||
[rpc_point(&out.key), mask]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
clsag
|
||||
.verify(
|
||||
&get_outs(&rpc, amount.unwrap_or(0), &actual_indexes).await,
|
||||
image,
|
||||
&pseudo_outs[i],
|
||||
&sig_hash,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(batch.verify_vartime());
|
||||
}
|
||||
|
||||
println!("Deserialized, hashed, and reserialized {block_i} with {txs_len} TXs");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "binaries")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use binaries::*;
|
||||
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
|
||||
// Read start block as the first arg
|
||||
let mut block_i = args[1].parse::<usize>().expect("invalid start block");
|
||||
|
||||
// How many blocks to work on at once
|
||||
let async_parallelism: usize =
|
||||
args.get(2).unwrap_or(&"8".to_string()).parse::<usize>().expect("invalid parallelism argument");
|
||||
|
||||
// Read further args as RPC URLs
|
||||
let default_nodes = vec![
|
||||
"http://xmr-node.cakewallet.com:18081".to_string(),
|
||||
"https://node.sethforprivacy.com".to_string(),
|
||||
];
|
||||
let mut specified_nodes = vec![];
|
||||
{
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let Some(node) = args.get(3 + i) else { break };
|
||||
specified_nodes.push(node.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
let nodes = if specified_nodes.is_empty() { default_nodes } else { specified_nodes };
|
||||
|
||||
let rpc = |url: String| async move {
|
||||
HttpRpc::new(url.clone())
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("couldn't create HttpRpc connected to {url}"))
|
||||
};
|
||||
let main_rpc = rpc(nodes[0].clone()).await;
|
||||
let mut rpcs = vec![];
|
||||
for i in 0 .. async_parallelism {
|
||||
rpcs.push(Arc::new(rpc(nodes[i % nodes.len()].clone()).await));
|
||||
}
|
||||
|
||||
let mut rpc_i = 0;
|
||||
let mut handles: Vec<JoinHandle<()>> = vec![];
|
||||
let mut height = 0;
|
||||
loop {
|
||||
let new_height = main_rpc.get_height().await.expect("couldn't call get_height");
|
||||
if new_height == height {
|
||||
break;
|
||||
}
|
||||
height = new_height;
|
||||
|
||||
while block_i < height {
|
||||
if handles.len() >= async_parallelism {
|
||||
// Guarantee one handle is complete
|
||||
handles.swap_remove(0).await.unwrap();
|
||||
|
||||
// Remove all of the finished handles
|
||||
let mut i = 0;
|
||||
while i < handles.len() {
|
||||
if handles[i].is_finished() {
|
||||
handles.swap_remove(i).await.unwrap();
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
handles.push(tokio::spawn(check_block(rpcs[rpc_i].clone(), block_i)));
|
||||
rpc_i = (rpc_i + 1) % rpcs.len();
|
||||
block_i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "binaries"))]
|
||||
fn main() {
|
||||
panic!("To run binaries, please build with `--feature binaries`.");
|
||||
}
|
||||
@@ -1,19 +1,58 @@
|
||||
use crate::{
|
||||
serialize::*,
|
||||
transaction::Transaction
|
||||
use std_shims::{
|
||||
vec::Vec,
|
||||
io::{self, Read, Write},
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
use crate::{
|
||||
hash,
|
||||
merkle::merkle_root,
|
||||
serialize::*,
|
||||
transaction::{Input, Transaction},
|
||||
};
|
||||
|
||||
const CORRECT_BLOCK_HASH_202612: [u8; 32] =
|
||||
hex_literal::hex!("426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a");
|
||||
const EXISTING_BLOCK_HASH_202612: [u8; 32] =
|
||||
hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698");
|
||||
|
||||
/// The header of a [`Block`].
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct BlockHeader {
|
||||
pub major_version: u64,
|
||||
pub minor_version: u64,
|
||||
/// This represents the hardfork number of the block.
|
||||
pub major_version: u8,
|
||||
/// This field is used to vote for a particular [hardfork](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_basic/cryptonote_basic.h#L460).
|
||||
pub minor_version: u8,
|
||||
/// The UNIX time at which the block was mined.
|
||||
pub timestamp: u64,
|
||||
/// The previous [`Block::hash`].
|
||||
pub previous: [u8; 32],
|
||||
pub nonce: u32
|
||||
/// The block's nonce.
|
||||
pub nonce: u32,
|
||||
}
|
||||
|
||||
impl BlockHeader {
|
||||
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||
/// Serialize [`Self`] into the writer `w`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::block::*;
|
||||
/// # fn main() -> std::io::Result<()> {
|
||||
/// let block_header = BlockHeader {
|
||||
/// major_version: 1,
|
||||
/// minor_version: 2,
|
||||
/// timestamp: 3,
|
||||
/// previous: [4; 32],
|
||||
/// nonce: 5,
|
||||
/// };
|
||||
///
|
||||
/// let mut writer = vec![];
|
||||
/// block_header.write(&mut writer)?;
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// This function returns any errors from the writer itself.
|
||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||
write_varint(&self.major_version, w)?;
|
||||
write_varint(&self.minor_version, w)?;
|
||||
write_varint(&self.timestamp, w)?;
|
||||
@@ -21,46 +60,165 @@ impl BlockHeader {
|
||||
w.write_all(&self.nonce.to_le_bytes())
|
||||
}
|
||||
|
||||
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<BlockHeader> {
|
||||
Ok(
|
||||
BlockHeader {
|
||||
major_version: read_varint(r)?,
|
||||
minor_version: read_varint(r)?,
|
||||
timestamp: read_varint(r)?,
|
||||
previous: { let mut previous = [0; 32]; r.read_exact(&mut previous)?; previous },
|
||||
nonce: { let mut nonce = [0; 4]; r.read_exact(&mut nonce)?; u32::from_le_bytes(nonce) }
|
||||
}
|
||||
)
|
||||
/// Serialize [`Self`] into a new byte buffer.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::block::*;
|
||||
/// # fn main() -> std::io::Result<()> {
|
||||
/// let block_header = BlockHeader {
|
||||
/// major_version: 1,
|
||||
/// minor_version: 2,
|
||||
/// timestamp: 3,
|
||||
/// previous: [4; 32],
|
||||
/// nonce: 5,
|
||||
/// };
|
||||
///
|
||||
/// let mut writer = vec![];
|
||||
/// block_header.write(&mut writer)?;
|
||||
///
|
||||
/// let serialized = block_header.serialize();
|
||||
/// assert_eq!(serialized, writer);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut serialized = vec![];
|
||||
self.write(&mut serialized).unwrap();
|
||||
serialized
|
||||
}
|
||||
|
||||
/// Create [`Self`] from the reader `r`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::block::*;
|
||||
/// # fn main() -> std::io::Result<()> {
|
||||
/// let block_header = BlockHeader {
|
||||
/// major_version: 1,
|
||||
/// minor_version: 2,
|
||||
/// timestamp: 3,
|
||||
/// previous: [4; 32],
|
||||
/// nonce: 5,
|
||||
/// };
|
||||
///
|
||||
/// let mut vec = vec![];
|
||||
/// block_header.write(&mut vec)?;
|
||||
///
|
||||
/// let read = BlockHeader::read(& vec)?;
|
||||
/// assert_eq!(read, block_header);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// This function returns an error if either the reader failed,
|
||||
/// or if the data could not be deserialized into a [`Self`].
|
||||
pub fn read<R: Read>(r: &mut R) -> io::Result<BlockHeader> {
|
||||
Ok(BlockHeader {
|
||||
major_version: read_varint(r)?,
|
||||
minor_version: read_varint(r)?,
|
||||
timestamp: read_varint(r)?,
|
||||
previous: read_bytes(r)?,
|
||||
nonce: read_bytes(r).map(u32::from_le_bytes)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
/// Block on the Monero blockchain.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Block {
|
||||
/// The header of this block.
|
||||
pub header: BlockHeader,
|
||||
/// The miner/coinbase transaction.
|
||||
pub miner_tx: Transaction,
|
||||
pub txs: Vec<[u8; 32]>
|
||||
/// Hashes of all the transactions within this block.
|
||||
pub txs: Vec<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||
self.header.serialize(w)?;
|
||||
self.miner_tx.serialize(w)?;
|
||||
write_varint(&self.txs.len().try_into().unwrap(), w)?;
|
||||
/// Return the amount of Monero generated in this block in atomic units.
|
||||
pub fn number(&self) -> Option<u64> {
|
||||
match self.miner_tx.prefix.inputs.first() {
|
||||
Some(Input::Gen(number)) => Some(*number),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize [`Self`] into the writer `w`.
|
||||
///
|
||||
/// # Errors
|
||||
/// This function returns any errors from the writer itself.
|
||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||
self.header.write(w)?;
|
||||
self.miner_tx.write(w)?;
|
||||
write_varint(&self.txs.len(), w)?;
|
||||
for tx in &self.txs {
|
||||
w.write_all(tx)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Block> {
|
||||
Ok(
|
||||
Block {
|
||||
header: BlockHeader::deserialize(r)?,
|
||||
miner_tx: Transaction::deserialize(r)?,
|
||||
txs: (0 .. read_varint(r)?).map(
|
||||
|_| { let mut tx = [0; 32]; r.read_exact(&mut tx).map(|_| tx) }
|
||||
).collect::<Result<_, _>>()?
|
||||
}
|
||||
)
|
||||
/// Return the merkle root of this block.
|
||||
///
|
||||
/// In the case that this block has no transactions other than
|
||||
/// the miner transaction, the miner transaction hash is returned,
|
||||
/// i.e. the [`Transaction::hash`] of [`Self::miner_tx`] is returned.
|
||||
fn tx_merkle_root(&self) -> [u8; 32] {
|
||||
merkle_root(self.miner_tx.hash(), &self.txs)
|
||||
}
|
||||
|
||||
/// Serialize the block as required for the proof of work hash.
|
||||
///
|
||||
/// This is distinct from the serialization required for the block hash. To get the block hash,
|
||||
/// use the [`Block::hash`] function.
|
||||
pub fn serialize_hashable(&self) -> Vec<u8> {
|
||||
let mut blob = self.header.serialize();
|
||||
blob.extend_from_slice(&self.tx_merkle_root());
|
||||
write_varint(&(1 + u64::try_from(self.txs.len()).unwrap()), &mut blob).unwrap();
|
||||
|
||||
blob
|
||||
}
|
||||
|
||||
/// Calculate the hash of this block.
|
||||
pub fn hash(&self) -> [u8; 32] {
|
||||
let mut hashable = self.serialize_hashable();
|
||||
// Monero pre-appends a VarInt of the block hashing blobs length before getting the block hash
|
||||
// but doesn't do this when getting the proof of work hash :)
|
||||
let mut hashing_blob = Vec::with_capacity(8 + hashable.len());
|
||||
write_varint(&u64::try_from(hashable.len()).unwrap(), &mut hashing_blob).unwrap();
|
||||
hashing_blob.append(&mut hashable);
|
||||
|
||||
let hash = hash(&hashing_blob);
|
||||
if hash == CORRECT_BLOCK_HASH_202612 {
|
||||
return EXISTING_BLOCK_HASH_202612;
|
||||
};
|
||||
|
||||
hash
|
||||
}
|
||||
|
||||
/// Serialize [`Self`] into a new byte buffer.
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut serialized = vec![];
|
||||
self.write(&mut serialized).unwrap();
|
||||
serialized
|
||||
}
|
||||
|
||||
/// Create [`Self`] from the reader `r`.
|
||||
///
|
||||
/// # Errors
|
||||
/// This function returns an error if either the reader failed,
|
||||
/// or if the data could not be deserialized into a [`Self`].
|
||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Block> {
|
||||
let header = BlockHeader::read(r)?;
|
||||
|
||||
let miner_tx = Transaction::read(r)?;
|
||||
if !matches!(miner_tx.prefix.inputs.as_slice(), &[Input::Gen(_)]) {
|
||||
Err(io::Error::other("Miner transaction has incorrect input type."))?;
|
||||
}
|
||||
|
||||
Ok(Block {
|
||||
header,
|
||||
miner_tx,
|
||||
txs: (0_usize..read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use std::io::Read;
|
||||
|
||||
use thiserror::Error;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
||||
|
||||
use group::{Group, GroupEncoding};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use dalek_ff_group as dfg;
|
||||
use dleq::DLEqProof;
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum MultisigError {
|
||||
#[error("internal error ({0})")]
|
||||
InternalError(String),
|
||||
#[error("invalid discrete log equality proof")]
|
||||
InvalidDLEqProof(u16),
|
||||
#[error("invalid key image {0}")]
|
||||
InvalidKeyImage(u16)
|
||||
}
|
||||
|
||||
fn transcript() -> RecommendedTranscript {
|
||||
RecommendedTranscript::new(b"monero_key_image_dleq")
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn write_dleq<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
H: EdwardsPoint,
|
||||
x: Scalar
|
||||
) -> Vec<u8> {
|
||||
let mut res = Vec::with_capacity(64);
|
||||
DLEqProof::prove(
|
||||
rng,
|
||||
// Doesn't take in a larger transcript object due to the usage of this
|
||||
// Every prover would immediately write their own DLEq proof, when they can only do so in
|
||||
// the proper order if they want to reach consensus
|
||||
// It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to try to
|
||||
// merge later in some form, when it should instead just merge xH (as it does)
|
||||
&mut transcript(),
|
||||
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)],
|
||||
dfg::Scalar(x)
|
||||
).serialize(&mut res).unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn read_dleq<Re: Read>(
|
||||
serialized: &mut Re,
|
||||
H: EdwardsPoint,
|
||||
l: u16,
|
||||
xG: dfg::EdwardsPoint
|
||||
) -> Result<dfg::EdwardsPoint, MultisigError> {
|
||||
let mut bytes = [0; 32];
|
||||
serialized.read_exact(&mut bytes).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
|
||||
// dfg ensures the point is torsion free
|
||||
let xH = Option::<dfg::EdwardsPoint>::from(
|
||||
dfg::EdwardsPoint::from_bytes(&bytes)).ok_or(MultisigError::InvalidDLEqProof(l)
|
||||
)?;
|
||||
// Ensure this is a canonical point
|
||||
if xH.to_bytes() != bytes {
|
||||
Err(MultisigError::InvalidDLEqProof(l))?;
|
||||
}
|
||||
|
||||
DLEqProof::<dfg::EdwardsPoint>::deserialize(
|
||||
serialized
|
||||
).map_err(|_| MultisigError::InvalidDLEqProof(l))?.verify(
|
||||
&mut transcript(),
|
||||
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)],
|
||||
&[xG, xH]
|
||||
).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
|
||||
|
||||
Ok(xH)
|
||||
}
|
||||
@@ -1,100 +1,308 @@
|
||||
use std::slice;
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
use std_shims::{sync::OnceLock, io};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use subtle::ConstantTimeEq;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use tiny_keccak::{Hasher, Keccak};
|
||||
use sha3::{Digest, Keccak256};
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
constants::{ED25519_BASEPOINT_TABLE, ED25519_BASEPOINT_POINT},
|
||||
scalar::Scalar,
|
||||
edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY}
|
||||
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
|
||||
traits::VartimePrecomputedMultiscalarMul,
|
||||
};
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
pub mod frost;
|
||||
pub use monero_generators::{H, decompress_point};
|
||||
|
||||
mod merkle;
|
||||
|
||||
mod serialize;
|
||||
use serialize::{read_byte, read_u16};
|
||||
|
||||
/// UnreducedScalar struct with functionality for recovering incorrectly reduced scalars.
|
||||
mod unreduced_scalar;
|
||||
|
||||
/// Ring Signature structs and functionality.
|
||||
pub mod ring_signatures;
|
||||
|
||||
/// RingCT structs and functionality.
|
||||
pub mod ringct;
|
||||
use ringct::RctType;
|
||||
|
||||
/// Transaction structs.
|
||||
pub mod transaction;
|
||||
/// Block structs.
|
||||
pub mod block;
|
||||
|
||||
/// Monero daemon RPC interface.
|
||||
pub mod rpc;
|
||||
/// Wallet functionality, enabling scanning and sending transactions.
|
||||
pub mod wallet;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
lazy_static! {
|
||||
static ref H: EdwardsPoint = CompressedEdwardsY(
|
||||
hex::decode("8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94").unwrap().try_into().unwrap()
|
||||
).decompress().unwrap();
|
||||
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&*H);
|
||||
/// Default block lock time for transactions.
|
||||
///
|
||||
/// This is the amount of new blocks that must
|
||||
/// pass before a new transaction can be spent.
|
||||
///
|
||||
/// Equivalent to Monero's [`CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE`](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_config.h#L49).
|
||||
pub const DEFAULT_LOCK_WINDOW: usize = 10;
|
||||
/// Block lock time for coinbase transactions.
|
||||
///
|
||||
/// This is the amount of new blocks that must
|
||||
/// pass before a coinbase/miner transaction can be spent.
|
||||
///
|
||||
/// Equivalent to Monero's [`CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW`](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_config.h#L44).
|
||||
pub const COINBASE_LOCK_WINDOW: usize = 60;
|
||||
/// Average amount of seconds it takes for a block to be mined.
|
||||
///
|
||||
/// This is target amount of seconds mining difficulty will adjust to,
|
||||
/// i.e. a block will be mined every `BLOCK_TIME` seconds on average.
|
||||
///
|
||||
/// Equivalent to Monero's [`DIFFICULTY_TARGET_V2`](https://github.com/monero-project/monero/blob/c8214782fb2a769c57382a999eaf099691c836e7/src/cryptonote_config.h#L44).
|
||||
pub const BLOCK_TIME: usize = 120;
|
||||
|
||||
static INV_EIGHT_CELL: OnceLock<Scalar> = OnceLock::new();
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn INV_EIGHT() -> Scalar {
|
||||
*INV_EIGHT_CELL.get_or_init(|| Scalar::from(8u8).invert())
|
||||
}
|
||||
|
||||
// Function from libsodium our subsection of Monero relies on. Implementing it here means we don't
|
||||
// need to link against libsodium
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn crypto_verify_32(a: *const u8, b: *const u8) -> isize {
|
||||
isize::from(
|
||||
slice::from_raw_parts(a, 32).ct_eq(slice::from_raw_parts(b, 32)).unwrap_u8()
|
||||
) - 1
|
||||
static BASEPOINT_PRECOMP_CELL: OnceLock<VartimeEdwardsPrecomputation> = OnceLock::new();
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn BASEPOINT_PRECOMP() -> &'static VartimeEdwardsPrecomputation {
|
||||
BASEPOINT_PRECOMP_CELL
|
||||
.get_or_init(|| VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT]))
|
||||
}
|
||||
|
||||
// Offer a wide reduction to C. Our seeded RNG prevented Monero from defining an unbiased scalar
|
||||
// generation function, and in order to not use Monero code (which would require propagating its
|
||||
// license), the function was rewritten. It was rewritten with wide reduction, instead of rejection
|
||||
// sampling however, hence the need for this function
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn monero_wide_reduce(value: *mut u8) {
|
||||
let res = Scalar::from_bytes_mod_order_wide(
|
||||
std::slice::from_raw_parts(value, 64).try_into().unwrap()
|
||||
);
|
||||
for (i, b) in res.to_bytes().iter().enumerate() {
|
||||
value.add(i).write(*b);
|
||||
/// Monero protocol version.
|
||||
///
|
||||
/// v15 is omitted as v15 was simply v14 and v16 being active at the same time, with regards to the
|
||||
/// transactions supported. Accordingly, v16 should be used during v15.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Protocol {
|
||||
/// Version 14.
|
||||
v14,
|
||||
/// Version 16.
|
||||
v16,
|
||||
/// A custom version with customized properties.
|
||||
Custom {
|
||||
/// See [`Self::ring_len`].
|
||||
ring_len: usize,
|
||||
/// See [`Self::bp_plus`].
|
||||
bp_plus: bool,
|
||||
/// See [`Self::optimal_rct_type`].
|
||||
optimal_rct_type: RctType,
|
||||
/// See [`Self::view_tags`].
|
||||
view_tags: bool,
|
||||
/// See [`Self::v16_fee`].
|
||||
v16_fee: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Protocol {
|
||||
/// Amount of ring members under this protocol version.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::*;
|
||||
/// assert_eq!(Protocol::v14.ring_len(), 11);
|
||||
/// assert_eq!(Protocol::v16.ring_len(), 16);
|
||||
/// ```
|
||||
pub fn ring_len(&self) -> usize {
|
||||
match self {
|
||||
Protocol::v14 => 11,
|
||||
Protocol::v16 => 16,
|
||||
Protocol::Custom { ring_len, .. } => *ring_len,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the specified version uses Bulletproofs or Bulletproofs+.
|
||||
///
|
||||
/// This method will likely be reworked when versions not using Bulletproofs at all are added.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::*;
|
||||
/// assert_eq!(Protocol::v14.bp_plus(), false);
|
||||
/// assert_eq!(Protocol::v16.bp_plus(), true);
|
||||
/// ```
|
||||
pub fn bp_plus(&self) -> bool {
|
||||
match self {
|
||||
Protocol::v14 => false,
|
||||
Protocol::v16 => true,
|
||||
Protocol::Custom { bp_plus, .. } => *bp_plus,
|
||||
}
|
||||
}
|
||||
|
||||
/// The optimal RingCT type for this version.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::{*, ringct::*};
|
||||
/// assert_eq!(Protocol::v14.optimal_rct_type(), RctType::Clsag);
|
||||
/// assert_eq!(Protocol::v16.optimal_rct_type(), RctType::BulletproofsPlus);
|
||||
/// ```
|
||||
// TODO: Make this an Option when we support pre-RCT protocols
|
||||
pub fn optimal_rct_type(&self) -> RctType {
|
||||
match self {
|
||||
Protocol::v14 => RctType::Clsag,
|
||||
Protocol::v16 => RctType::BulletproofsPlus,
|
||||
Protocol::Custom { optimal_rct_type, .. } => *optimal_rct_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the specified version uses view tags.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::{*, ringct::*};
|
||||
/// assert_eq!(Protocol::v14.view_tags(), false);
|
||||
/// assert_eq!(Protocol::v16.view_tags(), true);
|
||||
/// ```
|
||||
pub fn view_tags(&self) -> bool {
|
||||
match self {
|
||||
Protocol::v14 => false,
|
||||
Protocol::v16 => true,
|
||||
Protocol::Custom { view_tags, .. } => *view_tags,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the specified version uses the fee algorithm from Monero
|
||||
/// hard fork version 16 (released in v18 binaries).
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use monero_serai::{*, ringct::*};
|
||||
/// assert_eq!(Protocol::v14.v16_fee(), false);
|
||||
/// assert_eq!(Protocol::v16.v16_fee(), true);
|
||||
/// ```
|
||||
pub fn v16_fee(&self) -> bool {
|
||||
match self {
|
||||
Protocol::v14 => false,
|
||||
Protocol::v16 => true,
|
||||
Protocol::Custom { v16_fee, .. } => *v16_fee,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
||||
match self {
|
||||
Protocol::v14 => w.write_all(&[0, 14]),
|
||||
Protocol::v16 => w.write_all(&[0, 16]),
|
||||
Protocol::Custom { ring_len, bp_plus, optimal_rct_type, view_tags, v16_fee } => {
|
||||
// Custom, version 0
|
||||
w.write_all(&[1, 0])?;
|
||||
w.write_all(&u16::try_from(*ring_len).unwrap().to_le_bytes())?;
|
||||
w.write_all(&[u8::from(*bp_plus)])?;
|
||||
w.write_all(&[optimal_rct_type.to_byte()])?;
|
||||
w.write_all(&[u8::from(*view_tags)])?;
|
||||
w.write_all(&[u8::from(*v16_fee)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read<R: io::Read>(r: &mut R) -> io::Result<Protocol> {
|
||||
Ok(match read_byte(r)? {
|
||||
// Monero protocol
|
||||
0 => match read_byte(r)? {
|
||||
14 => Protocol::v14,
|
||||
16 => Protocol::v16,
|
||||
_ => Err(io::Error::other("unrecognized monero protocol"))?,
|
||||
},
|
||||
// Custom
|
||||
1 => match read_byte(r)? {
|
||||
0 => Protocol::Custom {
|
||||
ring_len: read_u16(r)?.into(),
|
||||
bp_plus: match read_byte(r)? {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => Err(io::Error::other("invalid bool serialization"))?,
|
||||
},
|
||||
optimal_rct_type: RctType::from_byte(read_byte(r)?)
|
||||
.ok_or_else(|| io::Error::other("invalid RctType serialization"))?,
|
||||
view_tags: match read_byte(r)? {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => Err(io::Error::other("invalid bool serialization"))?,
|
||||
},
|
||||
v16_fee: match read_byte(r)? {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => Err(io::Error::other("invalid bool serialization"))?,
|
||||
},
|
||||
},
|
||||
_ => Err(io::Error::other("unrecognized custom protocol serialization"))?,
|
||||
},
|
||||
_ => Err(io::Error::other("unrecognized protocol serialization"))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Transparent structure representing a [https://web.getmonero.org/resources/moneropedia/pedersen-commitment.html]()'s contents.
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Commitment {
|
||||
/// The value used to mask the `amount`.
|
||||
pub mask: Scalar,
|
||||
pub amount: u64
|
||||
/// The value being masked.
|
||||
///
|
||||
/// In Monero's case, this is the amount of XMR in atomic units.
|
||||
pub amount: u64,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Commitment {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||
fmt.debug_struct("Commitment").field("amount", &self.amount).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Commitment {
|
||||
/// A commitment to zero, defined with a mask of 1 (as to not be the identity).
|
||||
pub fn zero() -> Commitment {
|
||||
Commitment { mask: Scalar::one(), amount: 0}
|
||||
Commitment { mask: Scalar::ONE, amount: 0 }
|
||||
}
|
||||
|
||||
/// Create a new [`Self`].
|
||||
pub fn new(mask: Scalar, amount: u64) -> Commitment {
|
||||
Commitment { mask, amount }
|
||||
}
|
||||
|
||||
/// Calculate a Pedersen commitment, as a point, from the transparent structure.
|
||||
pub fn calculate(&self) -> EdwardsPoint {
|
||||
(&self.mask * &ED25519_BASEPOINT_TABLE) + (&Scalar::from(self.amount) * &*H_TABLE)
|
||||
(&self.mask * ED25519_BASEPOINT_TABLE) + (Scalar::from(self.amount) * H())
|
||||
}
|
||||
}
|
||||
|
||||
// Allows using a modern rand as dalek's is notoriously dated
|
||||
/// Support generating a random scalar using a modern rand, as dalek's is notoriously dated.
|
||||
pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
|
||||
let mut r = [0; 64];
|
||||
rng.fill_bytes(&mut r);
|
||||
Scalar::from_bytes_mod_order_wide(&r)
|
||||
}
|
||||
|
||||
pub fn hash(data: &[u8]) -> [u8; 32] {
|
||||
let mut keccak = Keccak::v256();
|
||||
keccak.update(data);
|
||||
let mut res = [0; 32];
|
||||
keccak.finalize(&mut res);
|
||||
res
|
||||
pub(crate) fn hash(data: &[u8]) -> [u8; 32] {
|
||||
Keccak256::digest(data).into()
|
||||
}
|
||||
|
||||
/// Hash the provided data to a scalar via keccak256(data) % l.
|
||||
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||
Scalar::from_bytes_mod_order(hash(&data))
|
||||
let scalar = Scalar::from_bytes_mod_order(hash(data));
|
||||
// Monero will explicitly error in this case
|
||||
// This library acknowledges its practical impossibility of it occurring, and doesn't bother to
|
||||
// code in logic to handle it. That said, if it ever occurs, something must happen in order to
|
||||
// not generate/verify a proof we believe to be valid when it isn't
|
||||
assert!(scalar != Scalar::ZERO, "ZERO HASH: {data:?}");
|
||||
scalar
|
||||
}
|
||||
|
||||
55
coins/monero/src/merkle.rs
Normal file
55
coins/monero/src/merkle.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std_shims::vec::Vec;
|
||||
|
||||
use crate::hash;
|
||||
|
||||
pub(crate) fn merkle_root(root: [u8; 32], leafs: &[[u8; 32]]) -> [u8; 32] {
|
||||
match leafs.len() {
|
||||
0 => root,
|
||||
1 => hash(&[root, leafs[0]].concat()),
|
||||
_ => {
|
||||
let mut hashes = Vec::with_capacity(1 + leafs.len());
|
||||
hashes.push(root);
|
||||
hashes.extend(leafs);
|
||||
|
||||
// Monero preprocess this so the length is a power of 2
|
||||
let mut high_pow_2 = 4; // 4 is the lowest value this can be
|
||||
while high_pow_2 < hashes.len() {
|
||||
high_pow_2 *= 2;
|
||||
}
|
||||
let low_pow_2 = high_pow_2 / 2;
|
||||
|
||||
// Merge right-most hashes until we're at the low_pow_2
|
||||
{
|
||||
let overage = hashes.len() - low_pow_2;
|
||||
let mut rightmost = hashes.drain((low_pow_2 - overage) ..);
|
||||
// This is true since we took overage from beneath and above low_pow_2, taking twice as
|
||||
// many elements as overage
|
||||
debug_assert_eq!(rightmost.len() % 2, 0);
|
||||
|
||||
let mut paired_hashes = Vec::with_capacity(overage);
|
||||
while let Some(left) = rightmost.next() {
|
||||
let right = rightmost.next().unwrap();
|
||||
paired_hashes.push(hash(&[left.as_ref(), &right].concat()));
|
||||
}
|
||||
drop(rightmost);
|
||||
|
||||
hashes.extend(paired_hashes);
|
||||
assert_eq!(hashes.len(), low_pow_2);
|
||||
}
|
||||
|
||||
// Do a traditional pairing off
|
||||
let mut new_hashes = Vec::with_capacity(hashes.len() / 2);
|
||||
while hashes.len() > 1 {
|
||||
let mut i = 0;
|
||||
while i < hashes.len() {
|
||||
new_hashes.push(hash(&[hashes[i], hashes[i + 1]].concat()));
|
||||
i += 2;
|
||||
}
|
||||
|
||||
hashes = new_hashes;
|
||||
new_hashes = Vec::with_capacity(hashes.len() / 2);
|
||||
}
|
||||
hashes[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user