mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Compare commits
968 Commits
dkg-exampl
...
polkadot-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35265c615b | ||
|
|
bbad7738d9 | ||
|
|
d149d8a08a | ||
|
|
ffae13b3be | ||
|
|
5e42fd2848 | ||
|
|
d62f311b93 | ||
|
|
ec0d38a90d | ||
|
|
d8c7465722 | ||
|
|
0983a49b29 | ||
|
|
08adb3a199 | ||
|
|
f03ca53509 | ||
|
|
1820e916ca | ||
|
|
a5065e52e9 | ||
|
|
e78fd12da6 | ||
|
|
b8e5fa7ff2 | ||
|
|
d038aa95fd | ||
|
|
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 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,3 +1,5 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
# Auto detect text files and perform LF normalization
|
||||||
* text=auto
|
* text=auto
|
||||||
* text eol=lf
|
* text eol=lf
|
||||||
|
|
||||||
|
*.pdf binary
|
||||||
|
|||||||
47
.github/actions/bitcoin/action.yml
vendored
Normal file
47
.github/actions/bitcoin/action.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: bitcoin-regtest
|
||||||
|
description: Spawns a regtest Bitcoin daemon
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version to download and run"
|
||||||
|
required: false
|
||||||
|
default: 24.0.1
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Bitcoin Daemon Cache
|
||||||
|
id: cache-bitcoind
|
||||||
|
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
||||||
|
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: |
|
||||||
|
RPC_USER=serai
|
||||||
|
RPC_PASS=seraidex
|
||||||
|
|
||||||
|
bitcoind -txindex -regtest \
|
||||||
|
-rpcuser=$RPC_USER -rpcpassword=$RPC_PASS \
|
||||||
|
-rpcbind=127.0.0.1 -rpcbind=$(hostname) -rpcallowip=0.0.0.0/0 \
|
||||||
|
-daemon
|
||||||
51
.github/actions/build-dependencies/action.yml
vendored
51
.github/actions/build-dependencies/action.yml
vendored
@@ -7,44 +7,35 @@ inputs:
|
|||||||
require: true
|
require: true
|
||||||
default:
|
default:
|
||||||
|
|
||||||
rust-toolchain:
|
|
||||||
description: "Rust toolchain to install"
|
|
||||||
required: false
|
|
||||||
default: stable
|
|
||||||
|
|
||||||
rust-components:
|
|
||||||
description: "Rust components to install"
|
|
||||||
required: false
|
|
||||||
default:
|
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
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
|
||||||
|
|
||||||
|
- name: Install apt dependencies
|
||||||
|
shell: bash
|
||||||
|
run: sudo apt install -y ca-certificates
|
||||||
|
|
||||||
- name: Install Protobuf
|
- name: Install Protobuf
|
||||||
uses: arduino/setup-protoc@master
|
uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ inputs.github-token }}
|
repo-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
- name: Install solc
|
- name: Install solc
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pip3 install solc-select==0.2.1
|
cargo install svm-rs
|
||||||
solc-select install 0.8.16
|
svm install 0.8.16
|
||||||
solc-select use 0.8.16
|
svm use 0.8.16
|
||||||
|
|
||||||
- name: Install Rust
|
# - name: Cache Rust
|
||||||
uses: dtolnay/rust-toolchain@master
|
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43
|
||||||
with:
|
|
||||||
toolchain: ${{ inputs.rust-toolchain }}
|
|
||||||
components: ${{ inputs.rust-components }}
|
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
shell: bash
|
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Install WASM toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@master
|
|
||||||
with:
|
|
||||||
toolchain: ${{ steps.nightly.outputs.version }}
|
|
||||||
targets: wasm32-unknown-unknown
|
|
||||||
|
|||||||
4
.github/actions/monero-wallet-rpc/action.yml
vendored
4
.github/actions/monero-wallet-rpc/action.yml
vendored
@@ -5,14 +5,14 @@ inputs:
|
|||||||
version:
|
version:
|
||||||
description: "Version to download and run"
|
description: "Version to download and run"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.1.2
|
default: v0.18.2.0
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Monero Wallet RPC Cache
|
- name: Monero Wallet RPC Cache
|
||||||
id: cache-monero-wallet-rpc
|
id: cache-monero-wallet-rpc
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
||||||
with:
|
with:
|
||||||
path: monero-wallet-rpc
|
path: monero-wallet-rpc
|
||||||
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||||
|
|||||||
4
.github/actions/monero/action.yml
vendored
4
.github/actions/monero/action.yml
vendored
@@ -5,14 +5,14 @@ inputs:
|
|||||||
version:
|
version:
|
||||||
description: "Version to download and run"
|
description: "Version to download and run"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.1.2
|
default: v0.18.2.0
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Monero Daemon Cache
|
- name: Monero Daemon Cache
|
||||||
id: cache-monerod
|
id: cache-monerod
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
||||||
with:
|
with:
|
||||||
path: monerod
|
path: monerod
|
||||||
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||||
|
|||||||
17
.github/actions/test-dependencies/action.yml
vendored
17
.github/actions/test-dependencies/action.yml
vendored
@@ -10,7 +10,12 @@ inputs:
|
|||||||
monero-version:
|
monero-version:
|
||||||
description: "Monero version to download and run as a regtest node"
|
description: "Monero version to download and run as a regtest node"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.0.0
|
default: v0.18.2.0
|
||||||
|
|
||||||
|
bitcoin-version:
|
||||||
|
description: "Bitcoin version to download and run as a regtest node"
|
||||||
|
required: false
|
||||||
|
default: 24.0.1
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@@ -21,14 +26,20 @@ runs:
|
|||||||
github-token: ${{ inputs.github-token }}
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
- name: Install Foundry
|
- name: Install Foundry
|
||||||
uses: foundry-rs/foundry-toolchain@v1
|
uses: foundry-rs/foundry-toolchain@cb603ca0abb544f301eaed59ac0baf579aa6aecf
|
||||||
with:
|
with:
|
||||||
version: nightly
|
version: nightly-09fe3e041369a816365a020f715ad6f94dbce9f2
|
||||||
|
cache: false
|
||||||
|
|
||||||
- name: Run a Monero Regtest Node
|
- name: Run a Monero Regtest Node
|
||||||
uses: ./.github/actions/monero
|
uses: ./.github/actions/monero
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.monero-version }}
|
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
|
- name: Run a Monero Wallet-RPC
|
||||||
uses: ./.github/actions/monero-wallet-rpc
|
uses: ./.github/actions/monero-wallet-rpc
|
||||||
|
|||||||
2
.github/nightly-version
vendored
2
.github/nightly-version
vendored
@@ -1 +1 @@
|
|||||||
nightly-2022-12-01
|
nightly-2023-12-04
|
||||||
|
|||||||
37
.github/workflows/coins-tests.yml
vendored
Normal file
37
.github/workflows/coins-tests.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||||
|
-p bitcoin-serai \
|
||||||
|
-p ethereum-serai \
|
||||||
|
-p monero-generators \
|
||||||
|
-p monero-serai
|
||||||
33
.github/workflows/common-tests.yml
vendored
Normal file
33
.github/workflows/common-tests.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- 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
|
||||||
44
.github/workflows/coordinator-tests.yml
vendored
Normal file
44
.github/workflows/coordinator-tests.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Coordinator Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "coins/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "orchestration/message-queue/**"
|
||||||
|
- "coordinator/**"
|
||||||
|
- "orchestration/coordinator/**"
|
||||||
|
- "tests/docker/**"
|
||||||
|
- "tests/coordinator/**"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "coins/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "orchestration/message-queue/**"
|
||||||
|
- "coordinator/**"
|
||||||
|
- "orchestration/coordinator/**"
|
||||||
|
- "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
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- name: Run coordinator Docker tests
|
||||||
|
run: cd tests/coordinator && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
||||||
42
.github/workflows/crypto-tests.yml
vendored
Normal file
42
.github/workflows/crypto-tests.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- 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
|
||||||
7
.github/workflows/daily-deny.yml
vendored
7
.github/workflows/daily-deny.yml
vendored
@@ -9,17 +9,14 @@ jobs:
|
|||||||
name: Run cargo deny
|
name: Run cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
- name: Advisory Cache
|
- name: Advisory Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/advisory-db
|
path: ~/.cargo/advisory-db
|
||||||
key: rust-advisory-db
|
key: rust-advisory-db
|
||||||
|
|
||||||
- name: Install cargo
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install cargo deny
|
- name: Install cargo deny
|
||||||
run: cargo install --locked cargo-deny
|
run: cargo install --locked cargo-deny
|
||||||
|
|
||||||
|
|||||||
24
.github/workflows/full-stack-tests.yml
vendored
Normal file
24
.github/workflows/full-stack-tests.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- name: Run Full Stack Docker tests
|
||||||
|
run: cd tests/full-stack && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
||||||
86
.github/workflows/lint.yml
vendored
Normal file
86
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
clippy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
|
- name: Get nightly version to use
|
||||||
|
id: nightly
|
||||||
|
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build Dependencies
|
||||||
|
uses: ./.github/actions/build-dependencies
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install nightly rust
|
||||||
|
run: rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -t wasm32-unknown-unknown -c rust-src -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
|
||||||
|
run: git diff | wc -l | grep -x "0"
|
||||||
|
|
||||||
|
deny:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
|
- name: Advisory Cache
|
||||||
|
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
dockerfiles:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
- name: Verify Dockerfiles are up to date
|
||||||
|
# Runs the file which generates them and checks the diff has no lines
|
||||||
|
run: cd orchestration && ./dockerfiles.sh && git diff | wc -l | grep -x "0"
|
||||||
|
|
||||||
|
machete:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
- name: Verify all dependencies are in use
|
||||||
|
run: |
|
||||||
|
cargo install cargo-machete
|
||||||
|
cargo machete
|
||||||
38
.github/workflows/message-queue-tests.yml
vendored
Normal file
38
.github/workflows/message-queue-tests.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Message Queue Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "orchestration/message-queue/**"
|
||||||
|
- "tests/docker/**"
|
||||||
|
- "tests/message-queue/**"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "orchestration/message-queue/**"
|
||||||
|
- "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
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- name: Run message-queue Docker tests
|
||||||
|
run: cd tests/message-queue && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
||||||
28
.github/workflows/mini-tests.yml
vendored
Normal file
28
.github/workflows/mini-tests.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p mini-serai
|
||||||
21
.github/workflows/monero-tests.yaml
vendored
21
.github/workflows/monero-tests.yaml
vendored
@@ -6,17 +6,21 @@ on:
|
|||||||
- develop
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- "coins/monero/**"
|
- "coins/monero/**"
|
||||||
|
- "processor/**"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "coins/monero/**"
|
- "coins/monero/**"
|
||||||
|
- "processor/**"
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Only run these once since they will be consistent regardless of any node
|
# Only run these once since they will be consistent regardless of any node
|
||||||
unit-tests:
|
unit-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
- name: Test Dependencies
|
- name: Test Dependencies
|
||||||
uses: ./.github/actions/test-dependencies
|
uses: ./.github/actions/test-dependencies
|
||||||
@@ -24,7 +28,7 @@ jobs:
|
|||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Run Unit Tests Without Features
|
- name: Run Unit Tests Without Features
|
||||||
run: cargo test --package monero-serai --lib
|
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
|
# Doesn't run unit tests with features as the tests workflow will
|
||||||
|
|
||||||
@@ -33,10 +37,10 @@ jobs:
|
|||||||
# Test against all supported protocol versions
|
# Test against all supported protocol versions
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
version: [v0.17.3.2, v0.18.1.2]
|
version: [v0.17.3.2, v0.18.2.0]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
- name: Test Dependencies
|
- name: Test Dependencies
|
||||||
uses: ./.github/actions/test-dependencies
|
uses: ./.github/actions/test-dependencies
|
||||||
@@ -45,12 +49,11 @@ jobs:
|
|||||||
monero-version: ${{ matrix.version }}
|
monero-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: Run Integration Tests Without Features
|
- name: Run Integration Tests Without Features
|
||||||
|
# Runs with the binaries feature so the binaries build
|
||||||
# https://github.com/rust-lang/cargo/issues/8396
|
# https://github.com/rust-lang/cargo/issues/8396
|
||||||
run: cargo test --package monero-serai --test '*'
|
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --features binaries --test '*'
|
||||||
|
|
||||||
- name: Run Integration Tests
|
- name: Run Integration Tests
|
||||||
# Don't run if the the tests workflow also will
|
# Don't run if the the tests workflow also will
|
||||||
if: ${{ matrix.version != 'v0.18.1.2' }}
|
if: ${{ matrix.version != 'v0.18.2.0' }}
|
||||||
run: |
|
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
|
||||||
cargo test --package monero-serai --all-features --test '*'
|
|
||||||
cargo test --package serai-processor monero
|
|
||||||
|
|||||||
4
.github/workflows/monthly-nightly-update.yml
vendored
4
.github/workflows/monthly-nightly-update.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
name: Update nightly
|
name: Update nightly
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
git push -u origin $(date +"nightly-%Y-%m")
|
git push -u origin $(date +"nightly-%Y-%m")
|
||||||
|
|
||||||
- name: Pull Request
|
- name: Pull Request
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { repo, owner } = context.repo;
|
const { repo, owner } = context.repo;
|
||||||
|
|||||||
37
.github/workflows/no-std.yml
vendored
Normal file
37
.github/workflows/no-std.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- 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
|
||||||
44
.github/workflows/processor-tests.yml
vendored
Normal file
44
.github/workflows/processor-tests.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Processor Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "coins/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "orchestration/message-queue/**"
|
||||||
|
- "processor/**"
|
||||||
|
- "orchestration/processor/**"
|
||||||
|
- "tests/docker/**"
|
||||||
|
- "tests/processor/**"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "coins/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "orchestration/message-queue/**"
|
||||||
|
- "processor/**"
|
||||||
|
- "orchestration/processor/**"
|
||||||
|
- "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
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- name: Run processor Docker tests
|
||||||
|
run: cd tests/processor && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
||||||
38
.github/workflows/reproducible-runtime.yml
vendored
Normal file
38
.github/workflows/reproducible-runtime.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- name: Run Reproducible Runtime tests
|
||||||
|
run: cd tests/reproducible-runtime && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
||||||
102
.github/workflows/tests.yml
vendored
102
.github/workflows/tests.yml
vendored
@@ -4,77 +4,83 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "coins/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "processor/**"
|
||||||
|
- "coordinator/**"
|
||||||
|
- "substrate/**"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "common/**"
|
||||||
|
- "crypto/**"
|
||||||
|
- "coins/**"
|
||||||
|
- "message-queue/**"
|
||||||
|
- "processor/**"
|
||||||
|
- "coordinator/**"
|
||||||
|
- "substrate/**"
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
clippy:
|
test-infra:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
- name: Build Dependencies
|
||||||
uses: ./.github/actions/build-dependencies
|
uses: ./.github/actions/build-dependencies
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# Clippy requires nightly due to serai-runtime requiring it
|
|
||||||
rust-toolchain: ${{ steps.nightly.outputs.version }}
|
|
||||||
rust-components: clippy
|
|
||||||
|
|
||||||
- name: Run Clippy
|
- name: Run Tests
|
||||||
run: cargo clippy --all-features --tests -- -D warnings -A dead_code
|
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-docker-tests
|
||||||
|
|
||||||
deny:
|
test-substrate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
- name: Advisory Cache
|
- name: Build Dependencies
|
||||||
uses: actions/cache@v3
|
uses: ./.github/actions/build-dependencies
|
||||||
with:
|
|
||||||
path: ~/.cargo/advisory-db
|
|
||||||
key: rust-advisory-db
|
|
||||||
|
|
||||||
- name: Install cargo
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install cargo deny
|
|
||||||
run: cargo install --locked cargo-deny
|
|
||||||
|
|
||||||
- name: Run cargo deny
|
|
||||||
run: cargo deny -L error --all-features check
|
|
||||||
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: cargo test --all-features
|
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-pallet \
|
||||||
|
-p serai-runtime \
|
||||||
|
-p serai-node
|
||||||
|
|
||||||
fmt:
|
test-serai-client:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
||||||
|
|
||||||
- name: Get nightly version to use
|
- name: Build Dependencies
|
||||||
id: nightly
|
uses: ./.github/actions/build-dependencies
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Install rustfmt
|
|
||||||
uses: dtolnay/rust-toolchain@master
|
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ steps.nightly.outputs.version }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
components: rustfmt
|
|
||||||
|
|
||||||
- name: Run rustfmt
|
- name: Run Tests
|
||||||
run: cargo +${{ steps.nightly.outputs.version }} fmt -- --check
|
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
target
|
target
|
||||||
.vscode
|
.vscode
|
||||||
|
.test-logs
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
edition = "2021"
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
|
|
||||||
max_width = 100
|
max_width = 100
|
||||||
|
|||||||
8435
Cargo.lock
generated
8435
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
136
Cargo.toml
136
Cargo.toml
@@ -1,6 +1,11 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
|
"common/std-shims",
|
||||||
"common/zalloc",
|
"common/zalloc",
|
||||||
|
"common/db",
|
||||||
|
"common/env",
|
||||||
|
"common/request",
|
||||||
|
|
||||||
"crypto/transcript",
|
"crypto/transcript",
|
||||||
|
|
||||||
@@ -15,29 +20,63 @@ members = [
|
|||||||
"crypto/dleq",
|
"crypto/dleq",
|
||||||
"crypto/dkg",
|
"crypto/dkg",
|
||||||
"crypto/frost",
|
"crypto/frost",
|
||||||
|
"crypto/schnorrkel",
|
||||||
|
|
||||||
|
"coins/bitcoin",
|
||||||
"coins/ethereum",
|
"coins/ethereum",
|
||||||
"coins/monero/generators",
|
"coins/monero/generators",
|
||||||
"coins/monero",
|
"coins/monero",
|
||||||
|
|
||||||
|
"message-queue",
|
||||||
|
|
||||||
|
"processor/messages",
|
||||||
"processor",
|
"processor",
|
||||||
|
|
||||||
"substrate/serai/primitives",
|
"coordinator/tributary/tendermint",
|
||||||
|
"coordinator/tributary",
|
||||||
|
"coordinator",
|
||||||
|
|
||||||
|
"substrate/tree-cleanup/is-terminal",
|
||||||
|
"substrate/tree-cleanup/option-ext",
|
||||||
|
"substrate/tree-cleanup/directories-next",
|
||||||
|
"substrate/tree-cleanup/bandersnatch_vrfs",
|
||||||
|
"substrate/tree-cleanup/w3f-bls",
|
||||||
|
|
||||||
|
"substrate/primitives",
|
||||||
|
|
||||||
|
"substrate/coins/primitives",
|
||||||
|
"substrate/coins/pallet",
|
||||||
|
|
||||||
|
"substrate/in-instructions/primitives",
|
||||||
|
"substrate/in-instructions/pallet",
|
||||||
|
|
||||||
"substrate/validator-sets/primitives",
|
"substrate/validator-sets/primitives",
|
||||||
"substrate/validator-sets/pallet",
|
"substrate/validator-sets/pallet",
|
||||||
|
|
||||||
"substrate/tendermint/machine",
|
"substrate/signals/primitives",
|
||||||
"substrate/tendermint/primitives",
|
"substrate/signals/pallet",
|
||||||
"substrate/tendermint/client",
|
|
||||||
"substrate/tendermint/pallet",
|
"substrate/abi",
|
||||||
|
|
||||||
"substrate/runtime",
|
"substrate/runtime",
|
||||||
"substrate/node",
|
"substrate/node",
|
||||||
|
|
||||||
|
"substrate/client",
|
||||||
|
|
||||||
|
"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
|
# Always compile Monero (and a variety of dependencies) with optimizations due
|
||||||
# to the unoptimized performance of Bulletproofs
|
# to the extensive operations required for Bulletproofs
|
||||||
[profile.dev.package]
|
[profile.dev.package]
|
||||||
subtle = { opt-level = 3 }
|
subtle = { opt-level = 3 }
|
||||||
curve25519-dalek = { opt-level = 3 }
|
curve25519-dalek = { opt-level = 3 }
|
||||||
@@ -55,3 +94,88 @@ monero-serai = { opt-level = 3 }
|
|||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "unwind"
|
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" }
|
||||||
|
|
||||||
|
# subxt *can* pull these off crates.io yet there's no benefit to this
|
||||||
|
sp-core-hashing = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "experimental" }
|
||||||
|
sp-std = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "experimental" }
|
||||||
|
|
||||||
|
# is-terminal now has an std-based solution with an equivalent API
|
||||||
|
is-terminal = { path = "substrate/tree-cleanup/is-terminal" }
|
||||||
|
# So does matches
|
||||||
|
matches = { path = "substrate/tree-cleanup/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
|
||||||
|
# Serai's polkadot-sdk consolidated to directories-next, as directories-next is
|
||||||
|
# acceptable and because we couldn't consolidate to directories without forking
|
||||||
|
# wasmtime
|
||||||
|
# The following two patches resolve everything
|
||||||
|
option-ext = { path = "substrate/tree-cleanup/option-ext" }
|
||||||
|
directories-next = { path = "substrate/tree-cleanup/directories-next" }
|
||||||
|
|
||||||
|
# mach is unmaintained, so this wraps mach2 as mach
|
||||||
|
mach = { path = "substrate/tree-cleanup/mach" }
|
||||||
|
|
||||||
|
# cargo believes the following are in-tree despite no features activating them
|
||||||
|
# We provide empty crates to not only prove they're unused, yet also clean up
|
||||||
|
# our Cargo.lock
|
||||||
|
w3f-bls = { path = "substrate/tree-cleanup/w3f-bls" }
|
||||||
|
[patch."https://github.com/w3f/ring-vrf"]
|
||||||
|
bandersnatch_vrfs = { path = "substrate/tree-cleanup/bandersnatch_vrfs" }
|
||||||
|
|
||||||
|
[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"
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -9,13 +9,15 @@ wallet.
|
|||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
|
- `audits`: Audits for various parts of Serai.
|
||||||
|
|
||||||
- `docs`: Documentation on the Serai protocol.
|
- `docs`: Documentation on the Serai protocol.
|
||||||
|
|
||||||
- `common`: Crates containing utilities common to a variety of areas under
|
- `common`: Crates containing utilities common to a variety of areas under
|
||||||
Serai, none neatly fitting under another category.
|
Serai, none neatly fitting under another category.
|
||||||
|
|
||||||
- `crypto`: A series of composable cryptographic libraries built around the
|
- `crypto`: A series of composable cryptographic libraries built around the
|
||||||
`ff`/`group` APIs achieving a variety of tasks. These range from generic
|
`ff`/`group` APIs, achieving a variety of tasks. These range from generic
|
||||||
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
||||||
needed for Bitcoin-Monero atomic swaps.
|
needed for Bitcoin-Monero atomic swaps.
|
||||||
|
|
||||||
@@ -23,17 +25,39 @@ wallet.
|
|||||||
wider community. This means they will always support the functionality Serai
|
wider community. This means they will always support the functionality Serai
|
||||||
needs, yet won't disadvantage other use cases when possible.
|
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
|
- `processor`: A generic chain processor to process data for Serai and process
|
||||||
events from Serai, executing transactions as expected and needed.
|
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.
|
- `substrate`: Substrate crates used to instantiate the Serai network.
|
||||||
|
|
||||||
- `deploy`: Scripts to deploy a Serai node/test environment.
|
- `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
|
### Links
|
||||||
|
|
||||||
- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX
|
- [Website](https://serai.exchange/): https://serai.exchange/
|
||||||
- [Mastodon](https://cryptodon.lol/@serai): https://cryptodon.lol/@serai
|
- [Immunefi](https://immunefi.com/bounty/serai/): https://immunefi.com/bounty/serai/
|
||||||
- [Discord](https://discord.gg/mpEUtJR3vz): https://discord.gg/mpEUtJR3vz
|
- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX
|
||||||
- [Matrix](https://matrix.to/#/#serai:matrix.org):
|
- [Mastodon](https://cryptodon.lol/@serai): https://cryptodon.lol/@serai
|
||||||
https://matrix.to/#/#serai:matrix.org
|
- [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"]
|
||||||
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 TransactionMachine 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:18443".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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,31 +7,36 @@ repository = "https://github.com/serai-dex/serai/tree/develop/coins/ethereum"
|
|||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
rust-version = "1.74"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hex-literal = "0.3"
|
thiserror = { version = "1", default-features = false }
|
||||||
thiserror = "1"
|
eyre = { version = "0.6", default-features = false }
|
||||||
rand_core = "0.6"
|
|
||||||
|
|
||||||
serde_json = "1.0"
|
sha3 = { version = "0.10", default-features = false, features = ["std"] }
|
||||||
serde = "1.0"
|
|
||||||
|
|
||||||
sha3 = "0.10"
|
group = { version = "0.13", default-features = false }
|
||||||
|
k256 = { version = "^0.13.1", default-features = false, features = ["std", "ecdsa"] }
|
||||||
group = "0.12"
|
|
||||||
k256 = { version = "0.11", features = ["arithmetic", "keccak256", "ecdsa"] }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["secp256k1", "tests"] }
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["secp256k1", "tests"] }
|
||||||
|
|
||||||
eyre = "0.6"
|
ethers-core = { version = "2", default-features = false }
|
||||||
|
ethers-providers = { version = "2", default-features = false }
|
||||||
ethers = { version = "1", features = ["abigen", "ethers-solc"] }
|
ethers-contract = { version = "2", default-features = false, features = ["abigen", "providers"] }
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
ethers-solc = "1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
|
serde = { version = "1", default-features = false, features = ["std"] }
|
||||||
|
serde_json = { version = "1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
sha2 = { version = "0.10", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
tokio = { version = "1", features = ["macros"] }
|
tokio = { version = "1", features = ["macros"] }
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use ethers_solc::{Project, ProjectPathsConfig};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-changed=contracts");
|
println!("cargo:rerun-if-changed=contracts");
|
||||||
println!("cargo:rerun-if-changed=artifacts");
|
println!("cargo:rerun-if-changed=artifacts");
|
||||||
|
|
||||||
// configure the project with all its paths, solc, cache etc.
|
#[rustfmt::skip]
|
||||||
let project = Project::builder()
|
let args = [
|
||||||
.paths(ProjectPathsConfig::hardhat(env!("CARGO_MANIFEST_DIR")).unwrap())
|
"--base-path", ".",
|
||||||
.build()
|
"-o", "./artifacts", "--overwrite",
|
||||||
.unwrap();
|
"--bin", "--abi",
|
||||||
project.compile().unwrap();
|
"--optimize",
|
||||||
|
"./contracts/Schnorr.sol"
|
||||||
|
];
|
||||||
|
|
||||||
// Tell Cargo that if a source file changes, to rerun this build script.
|
assert!(std::process::Command::new("solc").args(args).status().unwrap().success());
|
||||||
project.rerun_if_sources_changed();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use crate::crypto::ProcessedSignature;
|
|
||||||
use ethers::{contract::ContractFactory, prelude::*, solc::artifacts::contract::ContractBytecode};
|
|
||||||
use eyre::{eyre, Result};
|
|
||||||
use std::fs::File;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use eyre::{eyre, Result};
|
||||||
|
|
||||||
|
use ethers_providers::{Provider, Http};
|
||||||
|
use ethers_contract::abigen;
|
||||||
|
|
||||||
|
use crate::crypto::ProcessedSignature;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum EthereumError {
|
pub enum EthereumError {
|
||||||
@@ -11,27 +12,10 @@ pub enum EthereumError {
|
|||||||
VerificationError,
|
VerificationError,
|
||||||
}
|
}
|
||||||
|
|
||||||
abigen!(
|
abigen!(Schnorr, "./artifacts/Schnorr.abi");
|
||||||
Schnorr,
|
|
||||||
"./artifacts/Schnorr.sol/Schnorr.json",
|
|
||||||
event_derives(serde::Deserialize, serde::Serialize),
|
|
||||||
);
|
|
||||||
|
|
||||||
pub async fn deploy_schnorr_verifier_contract(
|
|
||||||
client: Arc<SignerMiddleware<Provider<Http>, LocalWallet>>,
|
|
||||||
) -> Result<Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>> {
|
|
||||||
let path = "./artifacts/Schnorr.sol/Schnorr.json";
|
|
||||||
let artifact: ContractBytecode = serde_json::from_reader(File::open(path).unwrap()).unwrap();
|
|
||||||
let abi = artifact.abi.unwrap();
|
|
||||||
let bin = artifact.bytecode.unwrap().object;
|
|
||||||
let factory = ContractFactory::new(abi, bin.into_bytes().unwrap(), client.clone());
|
|
||||||
let contract = factory.deploy(())?.send().await?;
|
|
||||||
let contract = Schnorr::new(contract.address(), client);
|
|
||||||
Ok(contract)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn call_verify(
|
pub async fn call_verify(
|
||||||
contract: &Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>,
|
contract: &Schnorr<Provider<Http>>,
|
||||||
params: &ProcessedSignature,
|
params: &ProcessedSignature,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if contract
|
if contract
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ use sha3::{Digest, Keccak256};
|
|||||||
|
|
||||||
use group::Group;
|
use group::Group;
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, sec1::ToEncodedPoint, DecompressPoint},
|
elliptic_curve::{
|
||||||
|
bigint::ArrayEncoding, ops::Reduce, point::DecompressPoint, sec1::ToEncodedPoint,
|
||||||
|
},
|
||||||
AffinePoint, ProjectivePoint, Scalar, U256,
|
AffinePoint, ProjectivePoint, Scalar, U256,
|
||||||
};
|
};
|
||||||
|
|
||||||
use frost::{algorithm::Hram, curve::Secp256k1};
|
use frost::{algorithm::Hram, curve::Secp256k1};
|
||||||
|
|
||||||
pub fn keccak256(data: &[u8]) -> [u8; 32] {
|
pub fn keccak256(data: &[u8]) -> [u8; 32] {
|
||||||
Keccak256::digest(data).try_into().unwrap()
|
Keccak256::digest(data).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||||
Scalar::from_uint_reduced(U256::from_be_slice(&keccak256(data)))
|
Scalar::reduce(U256::from_be_slice(&keccak256(data)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn address(point: &ProjectivePoint) -> [u8; 20] {
|
pub fn address(point: &ProjectivePoint) -> [u8; 20] {
|
||||||
@@ -56,7 +58,7 @@ impl Hram<Secp256k1> for EthereumHram {
|
|||||||
let mut data = address(R).to_vec();
|
let mut data = address(R).to_vec();
|
||||||
data.append(&mut a_encoded);
|
data.append(&mut a_encoded);
|
||||||
data.append(&mut m.to_vec());
|
data.append(&mut m.to_vec());
|
||||||
Scalar::from_uint_reduced(U256::from_be_slice(&keccak256(&data)))
|
Scalar::reduce(U256::from_be_slice(&keccak256(&data)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ pub fn process_signature_for_contract(
|
|||||||
) -> ProcessedSignature {
|
) -> ProcessedSignature {
|
||||||
let encoded_pk = A.to_encoded_point(true);
|
let encoded_pk = A.to_encoded_point(true);
|
||||||
let px = &encoded_pk.as_ref()[1 .. 33];
|
let px = &encoded_pk.as_ref()[1 .. 33];
|
||||||
let px_scalar = Scalar::from_uint_reduced(U256::from_be_slice(px));
|
let px_scalar = Scalar::reduce(U256::from_be_slice(px));
|
||||||
let e = EthereumHram::hram(R, A, &[chain_id.to_be_byte_array().as_slice(), &m].concat());
|
let e = EthereumHram::hram(R, A, &[chain_id.to_be_byte_array().as_slice(), &m].concat());
|
||||||
ProcessedSignature {
|
ProcessedSignature {
|
||||||
s,
|
s,
|
||||||
|
|||||||
@@ -1,36 +1,94 @@
|
|||||||
use std::{convert::TryFrom, sync::Arc, time::Duration};
|
use std::{convert::TryFrom, sync::Arc, time::Duration, fs::File};
|
||||||
|
|
||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
|
|
||||||
use k256::{elliptic_curve::bigint::ArrayEncoding, U256};
|
use ::k256::{
|
||||||
|
elliptic_curve::{bigint::ArrayEncoding, PrimeField},
|
||||||
|
U256,
|
||||||
|
};
|
||||||
|
|
||||||
use ethers::{
|
use ethers_core::{
|
||||||
prelude::*,
|
types::Signature,
|
||||||
|
abi::Abi,
|
||||||
utils::{keccak256, Anvil, AnvilInstance},
|
utils::{keccak256, Anvil, AnvilInstance},
|
||||||
};
|
};
|
||||||
|
use ethers_contract::ContractFactory;
|
||||||
|
use ethers_providers::{Middleware, Provider, Http};
|
||||||
|
|
||||||
use frost::{
|
use frost::{
|
||||||
curve::Secp256k1,
|
curve::Secp256k1,
|
||||||
algorithm::Schnorr as Algo,
|
Participant,
|
||||||
|
algorithm::IetfSchnorr,
|
||||||
tests::{key_gen, algorithm_machines, sign},
|
tests::{key_gen, algorithm_machines, sign},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ethereum_serai::{
|
use ethereum_serai::{
|
||||||
crypto,
|
crypto,
|
||||||
contract::{Schnorr, call_verify, deploy_schnorr_verifier_contract},
|
contract::{Schnorr, call_verify},
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn deploy_test_contract(
|
// TODO: Replace with a contract deployment from an unknown account, so the environment solely has
|
||||||
) -> (u32, AnvilInstance, Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>) {
|
// to fund the deployer, not create/pass a wallet
|
||||||
|
pub async fn deploy_schnorr_verifier_contract(
|
||||||
|
chain_id: u32,
|
||||||
|
client: Arc<Provider<Http>>,
|
||||||
|
wallet: &k256::ecdsa::SigningKey,
|
||||||
|
) -> eyre::Result<Schnorr<Provider<Http>>> {
|
||||||
|
let abi: Abi = serde_json::from_reader(File::open("./artifacts/Schnorr.abi").unwrap()).unwrap();
|
||||||
|
|
||||||
|
let hex_bin_buf = std::fs::read_to_string("./artifacts/Schnorr.bin").unwrap();
|
||||||
|
let hex_bin =
|
||||||
|
if let Some(stripped) = hex_bin_buf.strip_prefix("0x") { stripped } else { &hex_bin_buf };
|
||||||
|
let bin = hex::decode(hex_bin).unwrap();
|
||||||
|
let factory = ContractFactory::new(abi, bin.into(), client.clone());
|
||||||
|
|
||||||
|
let mut deployment_tx = factory.deploy(())?.tx;
|
||||||
|
deployment_tx.set_chain_id(chain_id);
|
||||||
|
deployment_tx.set_gas(500_000);
|
||||||
|
let (max_fee_per_gas, max_priority_fee_per_gas) = client.estimate_eip1559_fees(None).await?;
|
||||||
|
deployment_tx.as_eip1559_mut().unwrap().max_fee_per_gas = Some(max_fee_per_gas);
|
||||||
|
deployment_tx.as_eip1559_mut().unwrap().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
||||||
|
|
||||||
|
let sig_hash = deployment_tx.sighash();
|
||||||
|
let (sig, rid) = wallet.sign_prehash_recoverable(sig_hash.as_ref()).unwrap();
|
||||||
|
|
||||||
|
// EIP-155 v
|
||||||
|
let mut v = u64::from(rid.to_byte());
|
||||||
|
assert!((v == 0) || (v == 1));
|
||||||
|
v += u64::from((chain_id * 2) + 35);
|
||||||
|
|
||||||
|
let r = sig.r().to_repr();
|
||||||
|
let r_ref: &[u8] = r.as_ref();
|
||||||
|
let s = sig.s().to_repr();
|
||||||
|
let s_ref: &[u8] = s.as_ref();
|
||||||
|
let deployment_tx = deployment_tx.rlp_signed(&Signature { r: r_ref.into(), s: s_ref.into(), v });
|
||||||
|
|
||||||
|
let pending_tx = client.send_raw_transaction(deployment_tx).await?;
|
||||||
|
|
||||||
|
let mut receipt;
|
||||||
|
while {
|
||||||
|
receipt = client.get_transaction_receipt(pending_tx.tx_hash()).await?;
|
||||||
|
receipt.is_none()
|
||||||
|
} {
|
||||||
|
tokio::time::sleep(Duration::from_secs(6)).await;
|
||||||
|
}
|
||||||
|
let receipt = receipt.unwrap();
|
||||||
|
assert!(receipt.status == Some(1.into()));
|
||||||
|
|
||||||
|
let contract = Schnorr::new(receipt.contract_address.unwrap(), client.clone());
|
||||||
|
Ok(contract)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deploy_test_contract() -> (u32, AnvilInstance, Schnorr<Provider<Http>>) {
|
||||||
let anvil = Anvil::new().spawn();
|
let anvil = Anvil::new().spawn();
|
||||||
|
|
||||||
let wallet: LocalWallet = anvil.keys()[0].clone().into();
|
|
||||||
let provider =
|
let provider =
|
||||||
Provider::<Http>::try_from(anvil.endpoint()).unwrap().interval(Duration::from_millis(10u64));
|
Provider::<Http>::try_from(anvil.endpoint()).unwrap().interval(Duration::from_millis(10u64));
|
||||||
let chain_id = provider.get_chainid().await.unwrap().as_u32();
|
let chain_id = provider.get_chainid().await.unwrap().as_u32();
|
||||||
let client = Arc::new(SignerMiddleware::new_with_provider_chain(provider, wallet).await.unwrap());
|
let wallet = anvil.keys()[0].clone().into();
|
||||||
|
let client = Arc::new(provider);
|
||||||
|
|
||||||
(chain_id, anvil, deploy_schnorr_verifier_contract(client).await.unwrap())
|
(chain_id, anvil, deploy_schnorr_verifier_contract(chain_id, client, &wallet).await.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -44,19 +102,19 @@ async fn test_ecrecover_hack() {
|
|||||||
let chain_id = U256::from(chain_id);
|
let chain_id = U256::from(chain_id);
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||||
let group_key = keys[&1].group_key();
|
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
let hashed_message = keccak256(MESSAGE);
|
let hashed_message = keccak256(MESSAGE);
|
||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||||
|
|
||||||
let algo = Algo::<Secp256k1, crypto::EthereumHram>::new();
|
let algo = IetfSchnorr::<Secp256k1, crypto::EthereumHram>::ietf();
|
||||||
let sig = sign(
|
let sig = sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
algo.clone(),
|
&algo,
|
||||||
keys.clone(),
|
keys.clone(),
|
||||||
algorithm_machines(&mut OsRng, algo, &keys),
|
algorithm_machines(&mut OsRng, &algo, &keys),
|
||||||
full_message,
|
full_message,
|
||||||
);
|
);
|
||||||
let mut processed_sig =
|
let mut processed_sig =
|
||||||
|
|||||||
@@ -1,68 +1,69 @@
|
|||||||
use ethereum_serai::crypto::*;
|
|
||||||
use frost::curve::Secp256k1;
|
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, sec1::ToEncodedPoint},
|
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, sec1::ToEncodedPoint},
|
||||||
ProjectivePoint, Scalar, U256,
|
ProjectivePoint, Scalar, U256,
|
||||||
};
|
};
|
||||||
|
use frost::{curve::Secp256k1, Participant};
|
||||||
|
|
||||||
|
use ethereum_serai::crypto::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ecrecover() {
|
fn test_ecrecover() {
|
||||||
use k256::ecdsa::{
|
|
||||||
recoverable::Signature,
|
|
||||||
signature::{Signer, Verifier},
|
|
||||||
SigningKey, VerifyingKey,
|
|
||||||
};
|
|
||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
|
use sha2::Sha256;
|
||||||
|
use sha3::{Digest, Keccak256};
|
||||||
|
use k256::ecdsa::{hazmat::SignPrimitive, signature::DigestVerifier, SigningKey, VerifyingKey};
|
||||||
|
|
||||||
let private = SigningKey::random(&mut OsRng);
|
let private = SigningKey::random(&mut OsRng);
|
||||||
let public = VerifyingKey::from(&private);
|
let public = VerifyingKey::from(&private);
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
let sig: Signature = private.sign(MESSAGE);
|
let (sig, recovery_id) = private
|
||||||
public.verify(MESSAGE, &sig).unwrap();
|
.as_nonzero_scalar()
|
||||||
|
.try_sign_prehashed_rfc6979::<Sha256>(&Keccak256::digest(MESSAGE), b"")
|
||||||
|
.unwrap();
|
||||||
|
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
||||||
|
{
|
||||||
|
assert_eq!(public.verify_digest(Keccak256::new_with_prefix(MESSAGE), &sig).unwrap(), ());
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ecrecover(hash_to_scalar(MESSAGE), sig.as_ref()[64], *sig.r(), *sig.s()).unwrap(),
|
ecrecover(hash_to_scalar(MESSAGE), recovery_id.unwrap().is_y_odd().into(), *sig.r(), *sig.s())
|
||||||
address(&ProjectivePoint::from(public))
|
.unwrap(),
|
||||||
|
address(&ProjectivePoint::from(public.as_affine()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_signing() {
|
fn test_signing() {
|
||||||
use frost::{
|
use frost::{
|
||||||
algorithm::Schnorr,
|
algorithm::IetfSchnorr,
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
tests::{algorithm_machines, key_gen, sign},
|
||||||
};
|
};
|
||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||||
let _group_key = keys[&1].group_key();
|
let _group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
|
|
||||||
let algo = Schnorr::<Secp256k1, EthereumHram>::new();
|
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||||
let _sig = sign(
|
let _sig =
|
||||||
&mut OsRng,
|
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
||||||
algo,
|
|
||||||
keys.clone(),
|
|
||||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
|
||||||
MESSAGE,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ecrecover_hack() {
|
fn test_ecrecover_hack() {
|
||||||
use frost::{
|
use frost::{
|
||||||
algorithm::Schnorr,
|
algorithm::IetfSchnorr,
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
tests::{algorithm_machines, key_gen, sign},
|
||||||
};
|
};
|
||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||||
let group_key = keys[&1].group_key();
|
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||||
let group_key_encoded = group_key.to_encoded_point(true);
|
let group_key_encoded = group_key.to_encoded_point(true);
|
||||||
let group_key_compressed = group_key_encoded.as_ref();
|
let group_key_compressed = group_key_encoded.as_ref();
|
||||||
let group_key_x = Scalar::from_uint_reduced(U256::from_be_slice(&group_key_compressed[1 .. 33]));
|
let group_key_x = Scalar::reduce(U256::from_be_slice(&group_key_compressed[1 .. 33]));
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
let hashed_message = keccak256(MESSAGE);
|
let hashed_message = keccak256(MESSAGE);
|
||||||
@@ -70,12 +71,12 @@ fn test_ecrecover_hack() {
|
|||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||||
|
|
||||||
let algo = Schnorr::<Secp256k1, EthereumHram>::new();
|
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
||||||
let sig = sign(
|
let sig = sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
algo.clone(),
|
&algo,
|
||||||
keys.clone(),
|
keys.clone(),
|
||||||
algorithm_machines(&mut OsRng, algo, &keys),
|
algorithm_machines(&mut OsRng, &algo, &keys),
|
||||||
full_message,
|
full_message,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +1,113 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "monero-serai"
|
name = "monero-serai"
|
||||||
version = "0.1.2-alpha"
|
version = "0.1.4-alpha"
|
||||||
description = "A modern Monero transaction library"
|
description = "A modern Monero transaction library"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero"
|
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero"
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
rust-version = "1.74"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hex-literal = "0.3"
|
std-shims = { path = "../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||||
lazy_static = "1"
|
|
||||||
thiserror = "1"
|
|
||||||
|
|
||||||
rand_core = "0.6"
|
async-trait = { version = "0.1", default-features = false }
|
||||||
rand_chacha = { version = "0.3", optional = true }
|
thiserror = { version = "1", default-features = false, optional = true }
|
||||||
rand = "0.8"
|
|
||||||
rand_distr = "0.4"
|
|
||||||
|
|
||||||
zeroize = { version = "1.5", features = ["zeroize_derive"] }
|
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||||
subtle = "2.4"
|
subtle = { version = "^2.4", default-features = false }
|
||||||
|
|
||||||
sha3 = "0.10"
|
rand_core = { version = "0.6", default-features = false }
|
||||||
blake2 = { version = "0.10", optional = true }
|
# 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" }
|
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.1" }
|
|
||||||
multiexp = { path = "../../crypto/multiexp", version = "0.2", features = ["batch"] }
|
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.2", features = ["recommended"], optional = true }
|
# Used for the hash to curve, along with the more complicated proofs
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.5", features = ["ed25519"], optional = true }
|
group = { version = "0.13", default-features = false }
|
||||||
dleq = { path = "../../crypto/dleq", version = "0.2", features = ["serialize"], optional = true }
|
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"] }
|
||||||
|
|
||||||
monero-generators = { path = "generators", version = "0.1" }
|
# Needed for multisig
|
||||||
|
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
||||||
|
dleq = { path = "../../crypto/dleq", version = "0.4", default-features = false, features = ["serialize"], optional = true }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["ed25519"], optional = true }
|
||||||
|
|
||||||
hex = "0.4"
|
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
|
|
||||||
base58-monero = "1"
|
async-lock = { version = "3", default-features = false, optional = true }
|
||||||
monero-epee-bin-serde = "1.0"
|
|
||||||
|
|
||||||
digest_auth = "0.3"
|
hex-literal = "0.4"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
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"] }
|
||||||
|
|
||||||
|
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]
|
[build-dependencies]
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.1" }
|
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
||||||
monero-generators = { path = "generators", version = "0.1" }
|
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["sync", "macros"] }
|
||||||
monero-rpc = "0.3"
|
|
||||||
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.5", features = ["ed25519", "tests"] }
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]
|
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",
|
||||||
|
"dleq/std",
|
||||||
|
|
||||||
|
"monero-generators/std",
|
||||||
|
|
||||||
|
"async-lock?/std",
|
||||||
|
|
||||||
|
"hex/std",
|
||||||
|
"serde/std",
|
||||||
|
"serde_json/std",
|
||||||
|
|
||||||
|
"base58-monero/std",
|
||||||
|
]
|
||||||
|
|
||||||
|
cache-distribution = ["async-lock"]
|
||||||
|
http-rpc = ["digest_auth", "simple-request", "tokio"]
|
||||||
|
multisig = ["transcript", "frost", "dleq", "std"]
|
||||||
|
binaries = ["tokio/rt-multi-thread", "tokio/macros", "http-rpc"]
|
||||||
|
experimental = []
|
||||||
|
|
||||||
|
default = ["std", "http-rpc"]
|
||||||
|
|||||||
@@ -4,16 +4,46 @@ A modern Monero transaction library intended for usage in wallets. It prides
|
|||||||
itself on accuracy, correctness, and removing common pit falls developers may
|
itself on accuracy, correctness, and removing common pit falls developers may
|
||||||
face.
|
face.
|
||||||
|
|
||||||
monero-serai contains safety features, such as first-class acknowledgement of
|
monero-serai also offers the following features:
|
||||||
the burning bug, yet also a high level API around creating transactions.
|
|
||||||
monero-serai also offers a FROST-based multisig, which is orders of magnitude
|
- Featured Addresses
|
||||||
more performant than Monero's.
|
- 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-serai was written for Serai, a decentralized exchange aiming to support
|
||||||
Monero. Despite this, monero-serai is intended to be a widely usable library,
|
Monero. Despite this, monero-serai is intended to be a widely usable library,
|
||||||
accurate to Monero. monero-serai guarantees the functionality needed for Serai,
|
accurate to Monero. monero-serai guarantees the functionality needed for Serai,
|
||||||
yet will not deprive functionality from other users, and may potentially leave
|
yet will not deprive functionality from other users.
|
||||||
Serai's umbrella at some point.
|
|
||||||
|
|
||||||
Various legacy transaction formats are not currently implemented, yet
|
Various legacy transaction formats are not currently implemented, yet we are
|
||||||
monero-serai is still increasing its support for various transaction types.
|
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.
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) {
|
|||||||
fn generators(prefix: &'static str, path: &str) {
|
fn generators(prefix: &'static str, path: &str) {
|
||||||
let generators = bulletproofs_generators(prefix.as_bytes());
|
let generators = bulletproofs_generators(prefix.as_bytes());
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
let mut G_str = "".to_string();
|
let mut G_str = String::new();
|
||||||
serialize(&mut G_str, &generators.G);
|
serialize(&mut G_str, &generators.G);
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
let mut H_str = "".to_string();
|
let mut H_str = String::new();
|
||||||
serialize(&mut H_str, &generators.H);
|
serialize(&mut H_str, &generators.H);
|
||||||
|
|
||||||
let path = Path::new(&env::var("OUT_DIR").unwrap()).join(path);
|
let path = Path::new(&env::var("OUT_DIR").unwrap()).join(path);
|
||||||
@@ -41,15 +41,16 @@ fn generators(prefix: &'static str, path: &str) {
|
|||||||
.write_all(
|
.write_all(
|
||||||
format!(
|
format!(
|
||||||
"
|
"
|
||||||
lazy_static! {{
|
pub(crate) static GENERATORS_CELL: OnceLock<Generators> = OnceLock::new();
|
||||||
pub static ref GENERATORS: Generators = Generators {{
|
pub fn GENERATORS() -> &'static Generators {{
|
||||||
G: [
|
GENERATORS_CELL.get_or_init(|| Generators {{
|
||||||
|
G: vec![
|
||||||
{G_str}
|
{G_str}
|
||||||
],
|
],
|
||||||
H: [
|
H: vec![
|
||||||
{H_str}
|
{H_str}
|
||||||
],
|
],
|
||||||
}};
|
}})
|
||||||
}}
|
}}
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "monero-generators"
|
name = "monero-generators"
|
||||||
version = "0.1.1"
|
version = "0.4.0"
|
||||||
description = "Monero's hash_to_point and generators"
|
description = "Monero's hash_to_point and generators"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/generators"
|
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/generators"
|
||||||
@@ -11,14 +11,21 @@ edition = "2021"
|
|||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lazy_static = "1"
|
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||||
|
|
||||||
subtle = "2.4"
|
subtle = { version = "^2.4", default-features = false }
|
||||||
|
|
||||||
sha3 = "0.10"
|
sha3 = { version = "0.10", default-features = false }
|
||||||
|
|
||||||
curve25519-dalek = { version = "3", features = ["std"] }
|
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
||||||
|
|
||||||
group = "0.12"
|
group = { version = "0.13", default-features = false }
|
||||||
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.1.4" }
|
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
std = ["std-shims/std", "subtle/std", "sha3/std", "dalek-ff-group/std"]
|
||||||
|
default = ["std"]
|
||||||
|
|||||||
@@ -3,3 +3,5 @@
|
|||||||
Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
||||||
An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
||||||
`hash_to_point` here, is included, as needed to generate generators.
|
`hash_to_point` here, is included, as needed to generate generators.
|
||||||
|
|
||||||
|
This library is usable under no-std when the `std` feature is disabled.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use subtle::ConditionallySelectable;
|
|||||||
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||||
|
|
||||||
use group::ff::{Field, PrimeField};
|
use group::ff::{Field, PrimeField};
|
||||||
use dalek_ff_group::field::FieldElement;
|
use dalek_ff_group::FieldElement;
|
||||||
|
|
||||||
use crate::hash;
|
use crate::hash;
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
|
|||||||
let A = FieldElement::from(486662u64);
|
let A = FieldElement::from(486662u64);
|
||||||
|
|
||||||
let v = FieldElement::from_square(hash(&bytes)).double();
|
let v = FieldElement::from_square(hash(&bytes)).double();
|
||||||
let w = v + FieldElement::one();
|
let w = v + FieldElement::ONE;
|
||||||
let x = w.square() + (-A.square() * v);
|
let x = w.square() + (-A.square() * v);
|
||||||
|
|
||||||
// This isn't the complete X, yet its initial value
|
// This isn't the complete X, yet its initial value
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
//! Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
//! Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
||||||
|
//!
|
||||||
//! An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
//! An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
||||||
//! `hash_to_point` here, is included, as needed to generate generators.
|
//! `hash_to_point` here, is included, as needed to generate generators.
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
|
use std_shims::{sync::OnceLock, vec::Vec};
|
||||||
|
|
||||||
use sha3::{Digest, Keccak256};
|
use sha3::{Digest, Keccak256};
|
||||||
|
|
||||||
use curve25519_dalek::{
|
use curve25519_dalek::edwards::{EdwardsPoint as DalekPoint, CompressedEdwardsY};
|
||||||
constants::ED25519_BASEPOINT_POINT,
|
|
||||||
edwards::{EdwardsPoint as DalekPoint, CompressedEdwardsY},
|
|
||||||
};
|
|
||||||
|
|
||||||
use group::Group;
|
use group::{Group, GroupEncoding};
|
||||||
use dalek_ff_group::EdwardsPoint;
|
use dalek_ff_group::EdwardsPoint;
|
||||||
|
|
||||||
mod varint;
|
mod varint;
|
||||||
@@ -24,13 +24,29 @@ fn hash(data: &[u8]) -> [u8; 32] {
|
|||||||
Keccak256::digest(data).into()
|
Keccak256::digest(data).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
static H_CELL: OnceLock<DalekPoint> = OnceLock::new();
|
||||||
/// Monero alternate generator `H`, used for amounts in Pedersen commitments.
|
/// Monero's alternate generator `H`, used for amounts in Pedersen commitments.
|
||||||
pub static ref H: DalekPoint =
|
#[allow(non_snake_case)]
|
||||||
CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes()))
|
pub fn H() -> DalekPoint {
|
||||||
|
*H_CELL.get_or_init(|| {
|
||||||
|
CompressedEdwardsY(hash(&EdwardsPoint::generator().to_bytes()))
|
||||||
.decompress()
|
.decompress()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.mul_by_cofactor();
|
.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 MAX_M: usize = 16;
|
||||||
@@ -40,25 +56,24 @@ const MAX_MN: usize = MAX_M * N;
|
|||||||
/// Container struct for Bulletproofs(+) generators.
|
/// Container struct for Bulletproofs(+) generators.
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub struct Generators {
|
pub struct Generators {
|
||||||
pub G: [EdwardsPoint; MAX_MN],
|
pub G: Vec<EdwardsPoint>,
|
||||||
pub H: [EdwardsPoint; MAX_MN],
|
pub H: Vec<EdwardsPoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate generators as needed for Bulletproofs(+), as Monero does.
|
/// Generate generators as needed for Bulletproofs(+), as Monero does.
|
||||||
pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators {
|
pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators {
|
||||||
let mut res =
|
let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) };
|
||||||
Generators { G: [EdwardsPoint::identity(); MAX_MN], H: [EdwardsPoint::identity(); MAX_MN] };
|
|
||||||
for i in 0 .. MAX_MN {
|
for i in 0 .. MAX_MN {
|
||||||
let i = 2 * i;
|
let i = 2 * i;
|
||||||
|
|
||||||
let mut even = H.compress().to_bytes().to_vec();
|
let mut even = H().compress().to_bytes().to_vec();
|
||||||
even.extend(dst);
|
even.extend(dst);
|
||||||
let mut odd = even.clone();
|
let mut odd = even.clone();
|
||||||
|
|
||||||
write_varint(&i.try_into().unwrap(), &mut even).unwrap();
|
write_varint(&i.try_into().unwrap(), &mut even).unwrap();
|
||||||
write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap();
|
write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap();
|
||||||
res.H[i / 2] = EdwardsPoint(hash_to_point(hash(&even)));
|
res.H.push(EdwardsPoint(hash_to_point(hash(&even))));
|
||||||
res.G[i / 2] = EdwardsPoint(hash_to_point(hash(&odd)));
|
res.G.push(EdwardsPoint(hash_to_point(hash(&odd))));
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::io::{self, Write};
|
use std_shims::io::{self, Write};
|
||||||
|
|
||||||
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
||||||
pub(crate) fn write_varint<W: Write>(varint: &u64, w: &mut W) -> io::Result<()> {
|
pub(crate) fn write_varint<W: Write>(varint: &u64, w: &mut W) -> io::Result<()> {
|
||||||
|
|||||||
323
coins/monero/src/bin/reserialize_chain.rs
Normal file
323
coins/monero/src/bin/reserialize_chain.rs
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
#[cfg(feature = "binaries")]
|
||||||
|
mod binaries {
|
||||||
|
pub(crate) use std::sync::Arc;
|
||||||
|
|
||||||
|
pub(crate) use curve25519_dalek::{
|
||||||
|
scalar::Scalar,
|
||||||
|
edwards::{CompressedEdwardsY, 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 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| {
|
||||||
|
CompressedEdwardsY(
|
||||||
|
hex::decode(point)
|
||||||
|
.expect("invalid hex for ring member")
|
||||||
|
.try_into()
|
||||||
|
.expect("invalid point len for ring member"),
|
||||||
|
)
|
||||||
|
.decompress()
|
||||||
|
.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,11 +1,24 @@
|
|||||||
use std::io::{self, Read, Write};
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{serialize::*, transaction::Transaction};
|
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");
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct BlockHeader {
|
pub struct BlockHeader {
|
||||||
pub major_version: u64,
|
pub major_version: u8,
|
||||||
pub minor_version: u64,
|
pub minor_version: u8,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
pub previous: [u8; 32],
|
pub previous: [u8; 32],
|
||||||
pub nonce: u32,
|
pub nonce: u32,
|
||||||
@@ -45,16 +58,55 @@ pub struct Block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Block {
|
impl Block {
|
||||||
|
pub fn number(&self) -> usize {
|
||||||
|
match self.miner_tx.prefix.inputs.first() {
|
||||||
|
Some(Input::Gen(number)) => (*number).try_into().unwrap(),
|
||||||
|
_ => panic!("invalid block, miner TX didn't have a Input::Gen"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
self.header.write(w)?;
|
self.header.write(w)?;
|
||||||
self.miner_tx.write(w)?;
|
self.miner_tx.write(w)?;
|
||||||
write_varint(&self.txs.len().try_into().unwrap(), w)?;
|
write_varint(&self.txs.len(), w)?;
|
||||||
for tx in &self.txs {
|
for tx in &self.txs {
|
||||||
w.write_all(tx)?;
|
w.write_all(tx)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
let mut serialized = vec![];
|
let mut serialized = vec![];
|
||||||
self.write(&mut serialized).unwrap();
|
self.write(&mut serialized).unwrap();
|
||||||
@@ -65,7 +117,7 @@ impl Block {
|
|||||||
Ok(Block {
|
Ok(Block {
|
||||||
header: BlockHeader::read(r)?,
|
header: BlockHeader::read(r)?,
|
||||||
miner_tx: Transaction::read(r)?,
|
miner_tx: Transaction::read(r)?,
|
||||||
txs: (0 .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
|
txs: (0_usize .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,37 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
//! A modern Monero transaction library intended for usage in wallets. It prides
|
#[cfg(not(feature = "std"))]
|
||||||
//! itself on accuracy, correctness, and removing common pit falls developers may
|
#[macro_use]
|
||||||
//! face.
|
extern crate alloc;
|
||||||
|
|
||||||
//! monero-serai contains safety features, such as first-class acknowledgement of
|
use std_shims::{sync::OnceLock, io};
|
||||||
//! the burning bug, yet also a high level API around creating transactions.
|
|
||||||
//! monero-serai also offers a FROST-based multisig, which is orders of magnitude
|
|
||||||
//! more performant than Monero's.
|
|
||||||
|
|
||||||
//! 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, and may potentially leave
|
|
||||||
//! Serai's umbrella at some point.
|
|
||||||
|
|
||||||
//! Various legacy transaction formats are not currently implemented, yet
|
|
||||||
//! monero-serai is still increasing its support for various transaction types.
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
use sha3::{Digest, Keccak256};
|
use sha3::{Digest, Keccak256};
|
||||||
|
|
||||||
use curve25519_dalek::{
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
||||||
constants::ED25519_BASEPOINT_TABLE,
|
|
||||||
scalar::Scalar,
|
|
||||||
edwards::{EdwardsPoint, EdwardsBasepointTable},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use monero_generators::H;
|
pub use monero_generators::H;
|
||||||
|
|
||||||
|
mod merkle;
|
||||||
|
|
||||||
mod serialize;
|
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.
|
/// RingCT structs and functionality.
|
||||||
pub mod ringct;
|
pub mod ringct;
|
||||||
|
use ringct::RctType;
|
||||||
|
|
||||||
/// Transaction structs.
|
/// Transaction structs.
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
@@ -51,22 +46,34 @@ pub mod wallet;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
/// Monero protocol version. v15 is omitted as v15 was simply v14 and v16 being active at the same
|
static INV_EIGHT_CELL: OnceLock<Scalar> = OnceLock::new();
|
||||||
/// time, with regards to the transactions supported. Accordingly, v16 should be used during v15.
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn INV_EIGHT() -> Scalar {
|
||||||
|
*INV_EIGHT_CELL.get_or_init(|| Scalar::from(8u8).invert())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum Protocol {
|
pub enum Protocol {
|
||||||
Unsupported(usize),
|
|
||||||
v14,
|
v14,
|
||||||
v16,
|
v16,
|
||||||
Custom { ring_len: usize, bp_plus: bool },
|
Custom {
|
||||||
|
ring_len: usize,
|
||||||
|
bp_plus: bool,
|
||||||
|
optimal_rct_type: RctType,
|
||||||
|
view_tags: bool,
|
||||||
|
v16_fee: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Protocol {
|
impl Protocol {
|
||||||
/// Amount of ring members under this protocol version.
|
/// Amount of ring members under this protocol version.
|
||||||
pub fn ring_len(&self) -> usize {
|
pub fn ring_len(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Protocol::Unsupported(_) => panic!("Unsupported protocol version"),
|
|
||||||
Protocol::v14 => 11,
|
Protocol::v14 => 11,
|
||||||
Protocol::v16 => 16,
|
Protocol::v16 => 16,
|
||||||
Protocol::Custom { ring_len, .. } => *ring_len,
|
Protocol::Custom { ring_len, .. } => *ring_len,
|
||||||
@@ -74,33 +81,115 @@ impl Protocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether or not the specified version uses Bulletproofs or Bulletproofs+.
|
/// 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.
|
/// This method will likely be reworked when versions not using Bulletproofs at all are added.
|
||||||
pub fn bp_plus(&self) -> bool {
|
pub fn bp_plus(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Protocol::Unsupported(_) => panic!("Unsupported protocol version"),
|
|
||||||
Protocol::v14 => false,
|
Protocol::v14 => false,
|
||||||
Protocol::v16 => true,
|
Protocol::v16 => true,
|
||||||
Protocol::Custom { bp_plus, .. } => *bp_plus,
|
Protocol::Custom { bp_plus, .. } => *bp_plus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
// TODO: Make this an Option when we support pre-RCT protocols
|
||||||
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&H);
|
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.
|
||||||
|
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).
|
||||||
|
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 Pedersen commitment's contents.
|
/// Transparent structure representing a Pedersen commitment's contents.
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
||||||
pub struct Commitment {
|
pub struct Commitment {
|
||||||
pub mask: Scalar,
|
pub mask: Scalar,
|
||||||
pub amount: u64,
|
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 {
|
impl Commitment {
|
||||||
/// The zero commitment, defined as a mask of 1 (as to not be the identity) and a 0 amount.
|
/// A commitment to zero, defined with a mask of 1 (as to not be the identity).
|
||||||
pub fn zero() -> Commitment {
|
pub fn zero() -> Commitment {
|
||||||
Commitment { mask: Scalar::one(), amount: 0 }
|
Commitment { mask: Scalar::ONE, amount: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(mask: Scalar, amount: u64) -> Commitment {
|
pub fn new(mask: Scalar, amount: u64) -> Commitment {
|
||||||
@@ -109,7 +198,7 @@ impl Commitment {
|
|||||||
|
|
||||||
/// Calculate a Pedersen commitment, as a point, from the transparent structure.
|
/// Calculate a Pedersen commitment, as a point, from the transparent structure.
|
||||||
pub fn calculate(&self) -> EdwardsPoint {
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +220,6 @@ pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
|||||||
// This library acknowledges its practical impossibility of it occurring, and doesn't bother to
|
// 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
|
// 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
|
// not generate/verify a proof we believe to be valid when it isn't
|
||||||
assert!(scalar != Scalar::zero(), "ZERO HASH: {data:?}");
|
assert!(scalar != Scalar::ZERO, "ZERO HASH: {data:?}");
|
||||||
scalar
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
coins/monero/src/ring_signatures.rs
Normal file
72
coins/monero/src/ring_signatures.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use std_shims::{
|
||||||
|
io::{self, *},
|
||||||
|
vec::Vec,
|
||||||
|
};
|
||||||
|
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
use curve25519_dalek::{EdwardsPoint, Scalar};
|
||||||
|
|
||||||
|
use monero_generators::hash_to_point;
|
||||||
|
|
||||||
|
use crate::{serialize::*, hash_to_scalar};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub struct Signature {
|
||||||
|
c: Scalar,
|
||||||
|
r: Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Signature {
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
write_scalar(&self.c, w)?;
|
||||||
|
write_scalar(&self.r, w)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
|
||||||
|
Ok(Signature { c: read_scalar(r)?, r: read_scalar(r)? })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub struct RingSignature {
|
||||||
|
sigs: Vec<Signature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RingSignature {
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
for sig in &self.sigs {
|
||||||
|
sig.write(w)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> {
|
||||||
|
Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, msg: &[u8; 32], ring: &[EdwardsPoint], key_image: &EdwardsPoint) -> bool {
|
||||||
|
if ring.len() != self.sigs.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(32 + (32 * 2 * ring.len()));
|
||||||
|
buf.extend_from_slice(msg);
|
||||||
|
|
||||||
|
let mut sum = Scalar::ZERO;
|
||||||
|
|
||||||
|
for (ring_member, sig) in ring.iter().zip(&self.sigs) {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.r);
|
||||||
|
buf.extend_from_slice(Li.compress().as_bytes());
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let Ri = (sig.r * hash_to_point(ring_member.compress().to_bytes())) + (sig.c * key_image);
|
||||||
|
buf.extend_from_slice(Ri.compress().as_bytes());
|
||||||
|
|
||||||
|
sum += sig.c;
|
||||||
|
}
|
||||||
|
|
||||||
|
sum == hash_to_scalar(&buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
97
coins/monero/src/ringct/borromean.rs
Normal file
97
coins/monero/src/ringct/borromean.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
use std_shims::io::{self, Read, Write};
|
||||||
|
|
||||||
|
use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use monero_generators::H_pow_2;
|
||||||
|
|
||||||
|
use crate::{hash_to_scalar, unreduced_scalar::UnreducedScalar, serialize::*};
|
||||||
|
|
||||||
|
/// 64 Borromean ring signatures.
|
||||||
|
///
|
||||||
|
/// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced.
|
||||||
|
/// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction
|
||||||
|
/// algorithm which was in use.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct BorromeanSignatures {
|
||||||
|
pub s0: [UnreducedScalar; 64],
|
||||||
|
pub s1: [UnreducedScalar; 64],
|
||||||
|
pub ee: Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorromeanSignatures {
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanSignatures> {
|
||||||
|
Ok(BorromeanSignatures {
|
||||||
|
s0: read_array(UnreducedScalar::read, r)?,
|
||||||
|
s1: read_array(UnreducedScalar::read, r)?,
|
||||||
|
ee: read_scalar(r)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
for s0 in &self.s0 {
|
||||||
|
s0.write(w)?;
|
||||||
|
}
|
||||||
|
for s1 in &self.s1 {
|
||||||
|
s1.write(w)?;
|
||||||
|
}
|
||||||
|
write_scalar(&self.ee, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(&self, keys_a: &[EdwardsPoint], keys_b: &[EdwardsPoint]) -> bool {
|
||||||
|
let mut transcript = [0; 2048];
|
||||||
|
|
||||||
|
for i in 0 .. 64 {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let LL = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
||||||
|
&self.ee,
|
||||||
|
&keys_a[i],
|
||||||
|
&self.s0[i].recover_monero_slide_scalar(),
|
||||||
|
);
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
||||||
|
&hash_to_scalar(LL.compress().as_bytes()),
|
||||||
|
&keys_b[i],
|
||||||
|
&self.s1[i].recover_monero_slide_scalar(),
|
||||||
|
);
|
||||||
|
transcript[(i * 32) .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
hash_to_scalar(&transcript) == self.ee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A range proof premised on Borromean ring signatures.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct BorromeanRange {
|
||||||
|
pub sigs: BorromeanSignatures,
|
||||||
|
pub bit_commitments: [EdwardsPoint; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorromeanRange {
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanRange> {
|
||||||
|
Ok(BorromeanRange {
|
||||||
|
sigs: BorromeanSignatures::read(r)?,
|
||||||
|
bit_commitments: read_array(read_point, r)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
self.sigs.write(w)?;
|
||||||
|
write_raw_vec(write_point, &self.bit_commitments, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, commitment: &EdwardsPoint) -> bool {
|
||||||
|
if &self.bit_commitments.iter().sum::<EdwardsPoint>() != commitment {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let H_pow_2 = H_pow_2();
|
||||||
|
let mut commitments_sub_one = [EdwardsPoint::identity(); 64];
|
||||||
|
for i in 0 .. 64 {
|
||||||
|
commitments_sub_one[i] = self.bit_commitments[i] - H_pow_2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sigs.verify(&self.bit_commitments, &commitments_sub_one)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
// Required to be for this entire file, which isn't an issue, as it wouldn't bind to the static
|
use std_shims::{vec::Vec, sync::OnceLock};
|
||||||
#![allow(non_upper_case_globals)]
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use subtle::{Choice, ConditionallySelectable};
|
use subtle::{Choice, ConditionallySelectable};
|
||||||
@@ -15,13 +13,17 @@ use multiexp::multiexp as multiexp_const;
|
|||||||
|
|
||||||
pub(crate) use monero_generators::Generators;
|
pub(crate) use monero_generators::Generators;
|
||||||
|
|
||||||
use crate::{H as DALEK_H, Commitment, hash_to_scalar as dalek_hash};
|
use crate::{INV_EIGHT as DALEK_INV_EIGHT, H as DALEK_H, Commitment, hash_to_scalar as dalek_hash};
|
||||||
pub(crate) use crate::ringct::bulletproofs::scalar_vector::*;
|
pub(crate) use crate::ringct::bulletproofs::scalar_vector::*;
|
||||||
|
|
||||||
// Bring things into ff/group
|
#[inline]
|
||||||
lazy_static! {
|
pub(crate) fn INV_EIGHT() -> Scalar {
|
||||||
pub(crate) static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert().unwrap();
|
Scalar(DALEK_INV_EIGHT())
|
||||||
pub(crate) static ref H: EdwardsPoint = EdwardsPoint(*DALEK_H);
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn H() -> EdwardsPoint {
|
||||||
|
EdwardsPoint(DALEK_H())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
|
pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||||
@@ -34,7 +36,7 @@ pub(crate) const LOG_N: usize = 6; // 2 << 6 == N
|
|||||||
pub(crate) const N: usize = 64;
|
pub(crate) const N: usize = 64;
|
||||||
|
|
||||||
pub(crate) fn prove_multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
|
pub(crate) fn prove_multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
|
||||||
multiexp_const(pairs) * *INV_EIGHT
|
multiexp_const(pairs) * INV_EIGHT()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn vector_exponent(
|
pub(crate) fn vector_exponent(
|
||||||
@@ -48,7 +50,7 @@ pub(crate) fn vector_exponent(
|
|||||||
|
|
||||||
pub(crate) fn hash_cache(cache: &mut Scalar, mash: &[[u8; 32]]) -> Scalar {
|
pub(crate) fn hash_cache(cache: &mut Scalar, mash: &[[u8; 32]]) -> Scalar {
|
||||||
let slice =
|
let slice =
|
||||||
&[cache.to_bytes().as_ref(), mash.iter().cloned().flatten().collect::<Vec<_>>().as_ref()]
|
&[cache.to_bytes().as_ref(), mash.iter().copied().flatten().collect::<Vec<_>>().as_ref()]
|
||||||
.concat();
|
.concat();
|
||||||
*cache = hash_to_scalar(slice);
|
*cache = hash_to_scalar(slice);
|
||||||
*cache
|
*cache
|
||||||
@@ -76,12 +78,10 @@ pub(crate) fn bit_decompose(commitments: &[Commitment]) -> (ScalarVector, Scalar
|
|||||||
|
|
||||||
for j in 0 .. M {
|
for j in 0 .. M {
|
||||||
for i in (0 .. N).rev() {
|
for i in (0 .. N).rev() {
|
||||||
let mut bit = Choice::from(0);
|
let bit =
|
||||||
if j < sv.len() {
|
if j < sv.len() { Choice::from((sv[j][i / 8] >> (i % 8)) & 1) } else { Choice::from(0) };
|
||||||
bit = Choice::from((sv[j][i / 8] >> (i % 8)) & 1);
|
aL.0[(j * N) + i] = Scalar::conditional_select(&Scalar::ZERO, &Scalar::ONE, bit);
|
||||||
}
|
aR.0[(j * N) + i] = Scalar::conditional_select(&-Scalar::ONE, &Scalar::ZERO, bit);
|
||||||
aL.0[(j * N) + i] = Scalar::conditional_select(&Scalar::zero(), &Scalar::one(), bit);
|
|
||||||
aR.0[(j * N) + i] = Scalar::conditional_select(&-Scalar::one(), &Scalar::zero(), bit);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ pub(crate) fn bit_decompose(commitments: &[Commitment]) -> (ScalarVector, Scalar
|
|||||||
pub(crate) fn hash_commitments<C: IntoIterator<Item = DalekPoint>>(
|
pub(crate) fn hash_commitments<C: IntoIterator<Item = DalekPoint>>(
|
||||||
commitments: C,
|
commitments: C,
|
||||||
) -> (Scalar, Vec<EdwardsPoint>) {
|
) -> (Scalar, Vec<EdwardsPoint>) {
|
||||||
let V = commitments.into_iter().map(|c| EdwardsPoint(c) * *INV_EIGHT).collect::<Vec<_>>();
|
let V = commitments.into_iter().map(|c| EdwardsPoint(c) * INV_EIGHT()).collect::<Vec<_>>();
|
||||||
(hash_to_scalar(&V.iter().flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>()), V)
|
(hash_to_scalar(&V.iter().flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>()), V)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ pub(crate) fn alpha_rho<R: RngCore + CryptoRng>(
|
|||||||
aR: &ScalarVector,
|
aR: &ScalarVector,
|
||||||
) -> (Scalar, EdwardsPoint) {
|
) -> (Scalar, EdwardsPoint) {
|
||||||
let ar = Scalar::random(rng);
|
let ar = Scalar::random(rng);
|
||||||
(ar, (vector_exponent(generators, aL, aR) + (EdwardsPoint::generator() * ar)) * *INV_EIGHT)
|
(ar, (vector_exponent(generators, aL, aR) + (EdwardsPoint::generator() * ar)) * INV_EIGHT())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn LR_statements(
|
pub(crate) fn LR_statements(
|
||||||
@@ -116,20 +116,21 @@ pub(crate) fn LR_statements(
|
|||||||
let mut res = a
|
let mut res = a
|
||||||
.0
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.copied()
|
||||||
.zip(G_i.iter().cloned())
|
.zip(G_i.iter().copied())
|
||||||
.chain(b.0.iter().cloned().zip(H_i.iter().cloned()))
|
.chain(b.0.iter().copied().zip(H_i.iter().copied()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
res.push((cL, U));
|
res.push((cL, U));
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
static TWO_N_CELL: OnceLock<ScalarVector> = OnceLock::new();
|
||||||
pub(crate) static ref TWO_N: ScalarVector = ScalarVector::powers(Scalar::from(2u8), N);
|
pub(crate) fn TWO_N() -> &'static ScalarVector {
|
||||||
|
TWO_N_CELL.get_or_init(|| ScalarVector::powers(Scalar::from(2u8), N))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn challenge_products(w: &[Scalar], winv: &[Scalar]) -> Vec<Scalar> {
|
pub(crate) fn challenge_products(w: &[Scalar], winv: &[Scalar]) -> Vec<Scalar> {
|
||||||
let mut products = vec![Scalar::zero(); 1 << w.len()];
|
let mut products = vec![Scalar::ZERO; 1 << w.len()];
|
||||||
products[0] = winv[0];
|
products[0] = winv[0];
|
||||||
products[1] = w[0];
|
products[1] = w[0];
|
||||||
for j in 1 .. w.len() {
|
for j in 1 .. w.len() {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use std::io::{self, Read, Write};
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
use curve25519_dalek::edwards::EdwardsPoint;
|
||||||
use multiexp::BatchVerifier;
|
use multiexp::BatchVerifier;
|
||||||
@@ -16,12 +19,10 @@ pub(crate) mod core;
|
|||||||
use self::core::LOG_N;
|
use self::core::LOG_N;
|
||||||
|
|
||||||
pub(crate) mod original;
|
pub(crate) mod original;
|
||||||
pub use original::GENERATORS as BULLETPROOFS_GENERATORS;
|
use self::original::OriginalStruct;
|
||||||
pub(crate) mod plus;
|
|
||||||
pub use plus::GENERATORS as BULLETPROOFS_PLUS_GENERATORS;
|
|
||||||
|
|
||||||
pub(crate) use self::original::OriginalStruct;
|
pub(crate) mod plus;
|
||||||
pub(crate) use self::plus::PlusStruct;
|
use self::plus::*;
|
||||||
|
|
||||||
pub(crate) const MAX_OUTPUTS: usize = self::core::MAX_M;
|
pub(crate) const MAX_OUTPUTS: usize = self::core::MAX_M;
|
||||||
|
|
||||||
@@ -30,27 +31,45 @@ pub(crate) const MAX_OUTPUTS: usize = self::core::MAX_M;
|
|||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum Bulletproofs {
|
pub enum Bulletproofs {
|
||||||
Original(OriginalStruct),
|
Original(OriginalStruct),
|
||||||
Plus(PlusStruct),
|
Plus(AggregateRangeProof),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bulletproofs {
|
impl Bulletproofs {
|
||||||
pub(crate) fn fee_weight(plus: bool, outputs: usize) -> usize {
|
fn bp_fields(plus: bool) -> usize {
|
||||||
let fields = if plus { 6 } else { 9 };
|
if plus {
|
||||||
|
6
|
||||||
|
} else {
|
||||||
|
9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
||||||
|
// src/cryptonote_basic/cryptonote_format_utils.cpp#L106-L124
|
||||||
|
pub(crate) fn calculate_bp_clawback(plus: bool, n_outputs: usize) -> (usize, usize) {
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
let mut LR_len = usize::try_from(usize::BITS - (outputs - 1).leading_zeros()).unwrap();
|
let mut LR_len = 0;
|
||||||
let padded_outputs = 1 << LR_len;
|
let mut n_padded_outputs = 1;
|
||||||
|
while n_padded_outputs < n_outputs {
|
||||||
|
LR_len += 1;
|
||||||
|
n_padded_outputs = 1 << LR_len;
|
||||||
|
}
|
||||||
LR_len += LOG_N;
|
LR_len += LOG_N;
|
||||||
|
|
||||||
let len = (fields + (2 * LR_len)) * 32;
|
let mut bp_clawback = 0;
|
||||||
len +
|
if n_padded_outputs > 2 {
|
||||||
if padded_outputs <= 2 {
|
let fields = Bulletproofs::bp_fields(plus);
|
||||||
0
|
let base = ((fields + (2 * (LOG_N + 1))) * 32) / 2;
|
||||||
} else {
|
let size = (fields + (2 * LR_len)) * 32;
|
||||||
let base = ((fields + (2 * (LOG_N + 1))) * 32) / 2;
|
bp_clawback = ((base * n_padded_outputs) - size) * 4 / 5;
|
||||||
let size = (fields + (2 * LR_len)) * 32;
|
}
|
||||||
((base * padded_outputs) - size) * 4 / 5
|
|
||||||
}
|
(bp_clawback, LR_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fee_weight(plus: bool, outputs: usize) -> usize {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let (bp_clawback, LR_len) = Bulletproofs::calculate_bp_clawback(plus, outputs);
|
||||||
|
32 * (Bulletproofs::bp_fields(plus) + (2 * LR_len)) + 2 + bp_clawback
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prove the list of commitments are within [0 .. 2^64).
|
/// Prove the list of commitments are within [0 .. 2^64).
|
||||||
@@ -59,13 +78,22 @@ impl Bulletproofs {
|
|||||||
outputs: &[Commitment],
|
outputs: &[Commitment],
|
||||||
plus: bool,
|
plus: bool,
|
||||||
) -> Result<Bulletproofs, TransactionError> {
|
) -> Result<Bulletproofs, TransactionError> {
|
||||||
|
if outputs.is_empty() {
|
||||||
|
Err(TransactionError::NoOutputs)?;
|
||||||
|
}
|
||||||
if outputs.len() > MAX_OUTPUTS {
|
if outputs.len() > MAX_OUTPUTS {
|
||||||
return Err(TransactionError::TooManyOutputs)?;
|
Err(TransactionError::TooManyOutputs)?;
|
||||||
}
|
}
|
||||||
Ok(if !plus {
|
Ok(if !plus {
|
||||||
Bulletproofs::Original(OriginalStruct::prove(rng, outputs))
|
Bulletproofs::Original(OriginalStruct::prove(rng, outputs))
|
||||||
} else {
|
} else {
|
||||||
Bulletproofs::Plus(PlusStruct::prove(rng, outputs))
|
use dalek_ff_group::EdwardsPoint as DfgPoint;
|
||||||
|
Bulletproofs::Plus(
|
||||||
|
AggregateRangeStatement::new(outputs.iter().map(|com| DfgPoint(com.calculate())).collect())
|
||||||
|
.unwrap()
|
||||||
|
.prove(rng, &Zeroizing::new(AggregateRangeWitness::new(outputs).unwrap()))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +102,22 @@ impl Bulletproofs {
|
|||||||
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
|
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Bulletproofs::Original(bp) => bp.verify(rng, commitments),
|
Bulletproofs::Original(bp) => bp.verify(rng, commitments),
|
||||||
Bulletproofs::Plus(bp) => bp.verify(rng, commitments),
|
Bulletproofs::Plus(bp) => {
|
||||||
|
let mut verifier = BatchVerifier::new(1);
|
||||||
|
// If this commitment is torsioned (which is allowed), this won't be a well-formed
|
||||||
|
// dfg::EdwardsPoint (expected to be of prime-order)
|
||||||
|
// The actual BP+ impl will perform a torsion clear though, making this safe
|
||||||
|
// TODO: Have AggregateRangeStatement take in dalek EdwardsPoint for clarity on this
|
||||||
|
let Some(statement) = AggregateRangeStatement::new(
|
||||||
|
commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(),
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !statement.verify(rng, &mut verifier, (), bp.clone()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
verifier.verify_vartime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +134,14 @@ impl Bulletproofs {
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Bulletproofs::Original(bp) => bp.batch_verify(rng, verifier, id, commitments),
|
Bulletproofs::Original(bp) => bp.batch_verify(rng, verifier, id, commitments),
|
||||||
Bulletproofs::Plus(bp) => bp.batch_verify(rng, verifier, id, commitments),
|
Bulletproofs::Plus(bp) => {
|
||||||
|
let Some(statement) = AggregateRangeStatement::new(
|
||||||
|
commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(),
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
statement.verify(rng, verifier, id, bp.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,14 +166,14 @@ impl Bulletproofs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bulletproofs::Plus(bp) => {
|
Bulletproofs::Plus(bp) => {
|
||||||
write_point(&bp.A, w)?;
|
write_point(&bp.A.0, w)?;
|
||||||
write_point(&bp.A1, w)?;
|
write_point(&bp.wip.A.0, w)?;
|
||||||
write_point(&bp.B, w)?;
|
write_point(&bp.wip.B.0, w)?;
|
||||||
write_scalar(&bp.r1, w)?;
|
write_scalar(&bp.wip.r_answer.0, w)?;
|
||||||
write_scalar(&bp.s1, w)?;
|
write_scalar(&bp.wip.s_answer.0, w)?;
|
||||||
write_scalar(&bp.d1, w)?;
|
write_scalar(&bp.wip.delta_answer.0, w)?;
|
||||||
specific_write_vec(&bp.L, w)?;
|
specific_write_vec(&bp.wip.L.iter().copied().map(|L| L.0).collect::<Vec<_>>(), w)?;
|
||||||
specific_write_vec(&bp.R, w)
|
specific_write_vec(&bp.wip.R.iter().copied().map(|R| R.0).collect::<Vec<_>>(), w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,15 +211,19 @@ impl Bulletproofs {
|
|||||||
|
|
||||||
/// Read Bulletproofs+.
|
/// Read Bulletproofs+.
|
||||||
pub fn read_plus<R: Read>(r: &mut R) -> io::Result<Bulletproofs> {
|
pub fn read_plus<R: Read>(r: &mut R) -> io::Result<Bulletproofs> {
|
||||||
Ok(Bulletproofs::Plus(PlusStruct {
|
use dalek_ff_group::{Scalar as DfgScalar, EdwardsPoint as DfgPoint};
|
||||||
A: read_point(r)?,
|
|
||||||
A1: read_point(r)?,
|
Ok(Bulletproofs::Plus(AggregateRangeProof {
|
||||||
B: read_point(r)?,
|
A: DfgPoint(read_point(r)?),
|
||||||
r1: read_scalar(r)?,
|
wip: WipProof {
|
||||||
s1: read_scalar(r)?,
|
A: DfgPoint(read_point(r)?),
|
||||||
d1: read_scalar(r)?,
|
B: DfgPoint(read_point(r)?),
|
||||||
L: read_vec(read_point, r)?,
|
r_answer: DfgScalar(read_scalar(r)?),
|
||||||
R: read_vec(read_point, r)?,
|
s_answer: DfgScalar(read_scalar(r)?),
|
||||||
|
delta_answer: DfgScalar(read_scalar(r)?),
|
||||||
|
L: read_vec(read_point, r)?.into_iter().map(DfgPoint).collect(),
|
||||||
|
R: read_vec(read_point, r)?.into_iter().map(DfgPoint).collect(),
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use lazy_static::lazy_static;
|
use std_shims::{vec::Vec, sync::OnceLock};
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
@@ -14,9 +15,9 @@ use crate::{Commitment, ringct::bulletproofs::core::*};
|
|||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/generators.rs"));
|
include!(concat!(env!("OUT_DIR"), "/generators.rs"));
|
||||||
|
|
||||||
lazy_static! {
|
static IP12_CELL: OnceLock<Scalar> = OnceLock::new();
|
||||||
static ref ONE_N: ScalarVector = ScalarVector(vec![Scalar::one(); N]);
|
pub(crate) fn IP12() -> Scalar {
|
||||||
static ref IP12: Scalar = inner_product(&ONE_N, &TWO_N);
|
*IP12_CELL.get_or_init(|| inner_product(&ScalarVector(vec![Scalar::ONE; N]), TWO_N()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
@@ -48,8 +49,9 @@ impl OriginalStruct {
|
|||||||
let (sL, sR) =
|
let (sL, sR) =
|
||||||
ScalarVector((0 .. (MN * 2)).map(|_| Scalar::random(&mut *rng)).collect::<Vec<_>>()).split();
|
ScalarVector((0 .. (MN * 2)).map(|_| Scalar::random(&mut *rng)).collect::<Vec<_>>()).split();
|
||||||
|
|
||||||
let (mut alpha, A) = alpha_rho(&mut *rng, &GENERATORS, &aL, &aR);
|
let generators = GENERATORS();
|
||||||
let (mut rho, S) = alpha_rho(&mut *rng, &GENERATORS, &sL, &sR);
|
let (mut alpha, A) = alpha_rho(&mut *rng, generators, &aL, &aR);
|
||||||
|
let (mut rho, S) = alpha_rho(&mut *rng, generators, &sL, &sR);
|
||||||
|
|
||||||
let y = hash_cache(&mut cache, &[A.compress().to_bytes(), S.compress().to_bytes()]);
|
let y = hash_cache(&mut cache, &[A.compress().to_bytes(), S.compress().to_bytes()]);
|
||||||
let mut cache = hash_to_scalar(&y.to_bytes());
|
let mut cache = hash_to_scalar(&y.to_bytes());
|
||||||
@@ -62,7 +64,7 @@ impl OriginalStruct {
|
|||||||
let zpow = ScalarVector::powers(z, M + 2);
|
let zpow = ScalarVector::powers(z, M + 2);
|
||||||
for j in 0 .. M {
|
for j in 0 .. M {
|
||||||
for i in 0 .. N {
|
for i in 0 .. N {
|
||||||
zero_twos.push(zpow[j + 2] * TWO_N[i]);
|
zero_twos.push(zpow[j + 2] * TWO_N()[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +79,8 @@ impl OriginalStruct {
|
|||||||
let mut tau1 = Scalar::random(&mut *rng);
|
let mut tau1 = Scalar::random(&mut *rng);
|
||||||
let mut tau2 = Scalar::random(&mut *rng);
|
let mut tau2 = Scalar::random(&mut *rng);
|
||||||
|
|
||||||
let T1 = prove_multiexp(&[(t1, *H), (tau1, EdwardsPoint::generator())]);
|
let T1 = prove_multiexp(&[(t1, H()), (tau1, EdwardsPoint::generator())]);
|
||||||
let T2 = prove_multiexp(&[(t2, *H), (tau2, EdwardsPoint::generator())]);
|
let T2 = prove_multiexp(&[(t2, H()), (tau2, EdwardsPoint::generator())]);
|
||||||
|
|
||||||
let x =
|
let x =
|
||||||
hash_cache(&mut cache, &[z.to_bytes(), T1.compress().to_bytes(), T2.compress().to_bytes()]);
|
hash_cache(&mut cache, &[z.to_bytes(), T1.compress().to_bytes(), T2.compress().to_bytes()]);
|
||||||
@@ -112,10 +114,10 @@ impl OriginalStruct {
|
|||||||
let yinv = y.invert().unwrap();
|
let yinv = y.invert().unwrap();
|
||||||
let yinvpow = ScalarVector::powers(yinv, MN);
|
let yinvpow = ScalarVector::powers(yinv, MN);
|
||||||
|
|
||||||
let mut G_proof = GENERATORS.G[.. a.len()].to_vec();
|
let mut G_proof = generators.G[.. a.len()].to_vec();
|
||||||
let mut H_proof = GENERATORS.H[.. a.len()].to_vec();
|
let mut H_proof = generators.H[.. a.len()].to_vec();
|
||||||
H_proof.iter_mut().zip(yinvpow.0.iter()).for_each(|(this_H, yinvpow)| *this_H *= yinvpow);
|
H_proof.iter_mut().zip(yinvpow.0.iter()).for_each(|(this_H, yinvpow)| *this_H *= yinvpow);
|
||||||
let U = *H * x_ip;
|
let U = H() * x_ip;
|
||||||
|
|
||||||
let mut L = Vec::with_capacity(logMN);
|
let mut L = Vec::with_capacity(logMN);
|
||||||
let mut R = Vec::with_capacity(logMN);
|
let mut R = Vec::with_capacity(logMN);
|
||||||
@@ -188,7 +190,7 @@ impl OriginalStruct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild all challenges
|
// Rebuild all challenges
|
||||||
let (mut cache, commitments) = hash_commitments(commitments.iter().cloned());
|
let (mut cache, commitments) = hash_commitments(commitments.iter().copied());
|
||||||
let y = hash_cache(&mut cache, &[self.A.compress().to_bytes(), self.S.compress().to_bytes()]);
|
let y = hash_cache(&mut cache, &[self.A.compress().to_bytes(), self.S.compress().to_bytes()]);
|
||||||
|
|
||||||
let z = hash_to_scalar(&y.to_bytes());
|
let z = hash_to_scalar(&y.to_bytes());
|
||||||
@@ -221,7 +223,7 @@ impl OriginalStruct {
|
|||||||
let A = normalize(&self.A);
|
let A = normalize(&self.A);
|
||||||
let S = normalize(&self.S);
|
let S = normalize(&self.S);
|
||||||
|
|
||||||
let commitments = commitments.iter().map(|c| c.mul_by_cofactor()).collect::<Vec<_>>();
|
let commitments = commitments.iter().map(EdwardsPoint::mul_by_cofactor).collect::<Vec<_>>();
|
||||||
|
|
||||||
// Verify it
|
// Verify it
|
||||||
let mut proof = Vec::with_capacity(4 + commitments.len());
|
let mut proof = Vec::with_capacity(4 + commitments.len());
|
||||||
@@ -230,10 +232,10 @@ impl OriginalStruct {
|
|||||||
let ip1y = ScalarVector::powers(y, M * N).sum();
|
let ip1y = ScalarVector::powers(y, M * N).sum();
|
||||||
let mut k = -(zpow[2] * ip1y);
|
let mut k = -(zpow[2] * ip1y);
|
||||||
for j in 1 ..= M {
|
for j in 1 ..= M {
|
||||||
k -= zpow[j + 2] * *IP12;
|
k -= zpow[j + 2] * IP12();
|
||||||
}
|
}
|
||||||
let y1 = Scalar(self.t) - ((z * ip1y) + k);
|
let y1 = Scalar(self.t) - ((z * ip1y) + k);
|
||||||
proof.push((-y1, *H));
|
proof.push((-y1, H()));
|
||||||
|
|
||||||
proof.push((-Scalar(self.taux), G));
|
proof.push((-Scalar(self.taux), G));
|
||||||
|
|
||||||
@@ -247,10 +249,10 @@ impl OriginalStruct {
|
|||||||
|
|
||||||
proof = Vec::with_capacity(4 + (2 * (MN + logMN)));
|
proof = Vec::with_capacity(4 + (2 * (MN + logMN)));
|
||||||
let z3 = (Scalar(self.t) - (Scalar(self.a) * Scalar(self.b))) * x_ip;
|
let z3 = (Scalar(self.t) - (Scalar(self.a) * Scalar(self.b))) * x_ip;
|
||||||
proof.push((z3, *H));
|
proof.push((z3, H()));
|
||||||
proof.push((-Scalar(self.mu), G));
|
proof.push((-Scalar(self.mu), G));
|
||||||
|
|
||||||
proof.push((Scalar::one(), A));
|
proof.push((Scalar::ONE, A));
|
||||||
proof.push((x, S));
|
proof.push((x, S));
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -260,13 +262,14 @@ impl OriginalStruct {
|
|||||||
|
|
||||||
let w_cache = challenge_products(&w, &winv);
|
let w_cache = challenge_products(&w, &winv);
|
||||||
|
|
||||||
|
let generators = GENERATORS();
|
||||||
for i in 0 .. MN {
|
for i in 0 .. MN {
|
||||||
let g = (Scalar(self.a) * w_cache[i]) + z;
|
let g = (Scalar(self.a) * w_cache[i]) + z;
|
||||||
proof.push((-g, GENERATORS.G[i]));
|
proof.push((-g, generators.G[i]));
|
||||||
|
|
||||||
let mut h = Scalar(self.b) * yinvpow[i] * w_cache[(!i) & (MN - 1)];
|
let mut h = Scalar(self.b) * yinvpow[i] * w_cache[(!i) & (MN - 1)];
|
||||||
h -= ((zpow[(i / N) + 2] * TWO_N[i % N]) + (z * ypow[i])) * yinvpow[i];
|
h -= ((zpow[(i / N) + 2] * TWO_N()[i % N]) + (z * ypow[i])) * yinvpow[i];
|
||||||
proof.push((-h, GENERATORS.H[i]));
|
proof.push((-h, generators.H[i]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,306 +0,0 @@
|
|||||||
use lazy_static::lazy_static;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar as DalekScalar, edwards::EdwardsPoint as DalekPoint};
|
|
||||||
|
|
||||||
use group::ff::Field;
|
|
||||||
use dalek_ff_group::{ED25519_BASEPOINT_POINT as G, Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Commitment, hash,
|
|
||||||
ringct::{hash_to_point::raw_hash_to_point, bulletproofs::core::*},
|
|
||||||
};
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/generators_plus.rs"));
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref TRANSCRIPT: [u8; 32] =
|
|
||||||
EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))).compress().to_bytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TRANSCRIPT isn't a Scalar, so we need this alternative for the first hash
|
|
||||||
fn hash_plus<C: IntoIterator<Item = DalekPoint>>(commitments: C) -> (Scalar, Vec<EdwardsPoint>) {
|
|
||||||
let (cache, commitments) = hash_commitments(commitments);
|
|
||||||
(hash_to_scalar(&[&*TRANSCRIPT as &[u8], &cache.to_bytes()].concat()), commitments)
|
|
||||||
}
|
|
||||||
|
|
||||||
// d[j*N+i] = z**(2*(j+1)) * 2**i
|
|
||||||
fn d(z: Scalar, M: usize, MN: usize) -> (ScalarVector, ScalarVector) {
|
|
||||||
let zpow = ScalarVector::even_powers(z, 2 * M);
|
|
||||||
let mut d = vec![Scalar::zero(); MN];
|
|
||||||
for j in 0 .. M {
|
|
||||||
for i in 0 .. N {
|
|
||||||
d[(j * N) + i] = zpow[j] * TWO_N[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(zpow, ScalarVector(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct PlusStruct {
|
|
||||||
pub(crate) A: DalekPoint,
|
|
||||||
pub(crate) A1: DalekPoint,
|
|
||||||
pub(crate) B: DalekPoint,
|
|
||||||
pub(crate) r1: DalekScalar,
|
|
||||||
pub(crate) s1: DalekScalar,
|
|
||||||
pub(crate) d1: DalekScalar,
|
|
||||||
pub(crate) L: Vec<DalekPoint>,
|
|
||||||
pub(crate) R: Vec<DalekPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlusStruct {
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
commitments: &[Commitment],
|
|
||||||
) -> PlusStruct {
|
|
||||||
let (logMN, M, MN) = MN(commitments.len());
|
|
||||||
|
|
||||||
let (aL, aR) = bit_decompose(commitments);
|
|
||||||
let commitments_points = commitments.iter().map(Commitment::calculate).collect::<Vec<_>>();
|
|
||||||
let (mut cache, _) = hash_plus(commitments_points.clone());
|
|
||||||
let (mut alpha1, A) = alpha_rho(&mut *rng, &GENERATORS, &aL, &aR);
|
|
||||||
|
|
||||||
let y = hash_cache(&mut cache, &[A.compress().to_bytes()]);
|
|
||||||
let mut cache = hash_to_scalar(&y.to_bytes());
|
|
||||||
let z = cache;
|
|
||||||
|
|
||||||
let (zpow, d) = d(z, M, MN);
|
|
||||||
|
|
||||||
let aL1 = aL - z;
|
|
||||||
|
|
||||||
let ypow = ScalarVector::powers(y, MN + 2);
|
|
||||||
let mut y_for_d = ScalarVector(ypow.0[1 ..= MN].to_vec());
|
|
||||||
y_for_d.0.reverse();
|
|
||||||
let aR1 = (aR + z) + (y_for_d * d);
|
|
||||||
|
|
||||||
for (j, gamma) in commitments.iter().map(|c| Scalar(c.mask)).enumerate() {
|
|
||||||
alpha1 += zpow[j] * ypow[MN + 1] * gamma;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut a = aL1;
|
|
||||||
let mut b = aR1;
|
|
||||||
|
|
||||||
let yinv = y.invert().unwrap();
|
|
||||||
let yinvpow = ScalarVector::powers(yinv, MN);
|
|
||||||
|
|
||||||
let mut G_proof = GENERATORS.G[.. a.len()].to_vec();
|
|
||||||
let mut H_proof = GENERATORS.H[.. a.len()].to_vec();
|
|
||||||
|
|
||||||
let mut L = Vec::with_capacity(logMN);
|
|
||||||
let mut R = Vec::with_capacity(logMN);
|
|
||||||
|
|
||||||
while a.len() != 1 {
|
|
||||||
let (aL, aR) = a.split();
|
|
||||||
let (bL, bR) = b.split();
|
|
||||||
|
|
||||||
let cL = weighted_inner_product(&aL, &bR, y);
|
|
||||||
let cR = weighted_inner_product(&(&aR * ypow[aR.len()]), &bL, y);
|
|
||||||
|
|
||||||
let (mut dL, mut dR) = (Scalar::random(&mut *rng), Scalar::random(&mut *rng));
|
|
||||||
|
|
||||||
let (G_L, G_R) = G_proof.split_at(aL.len());
|
|
||||||
let (H_L, H_R) = H_proof.split_at(aL.len());
|
|
||||||
|
|
||||||
let mut L_i = LR_statements(&(&aL * yinvpow[aL.len()]), G_R, &bR, H_L, cL, *H);
|
|
||||||
L_i.push((dL, G));
|
|
||||||
let L_i = prove_multiexp(&L_i);
|
|
||||||
L.push(L_i);
|
|
||||||
|
|
||||||
let mut R_i = LR_statements(&(&aR * ypow[aR.len()]), G_L, &bL, H_R, cR, *H);
|
|
||||||
R_i.push((dR, G));
|
|
||||||
let R_i = prove_multiexp(&R_i);
|
|
||||||
R.push(R_i);
|
|
||||||
|
|
||||||
let w = hash_cache(&mut cache, &[L_i.compress().to_bytes(), R_i.compress().to_bytes()]);
|
|
||||||
let winv = w.invert().unwrap();
|
|
||||||
|
|
||||||
G_proof = hadamard_fold(G_L, G_R, winv, w * yinvpow[aL.len()]);
|
|
||||||
H_proof = hadamard_fold(H_L, H_R, w, winv);
|
|
||||||
|
|
||||||
a = (&aL * w) + (aR * (winv * ypow[aL.len()]));
|
|
||||||
b = (bL * winv) + (bR * w);
|
|
||||||
|
|
||||||
alpha1 += (dL * (w * w)) + (dR * (winv * winv));
|
|
||||||
|
|
||||||
dL.zeroize();
|
|
||||||
dR.zeroize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut r = Scalar::random(&mut *rng);
|
|
||||||
let mut s = Scalar::random(&mut *rng);
|
|
||||||
let mut d = Scalar::random(&mut *rng);
|
|
||||||
let mut eta = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let A1 = prove_multiexp(&[
|
|
||||||
(r, G_proof[0]),
|
|
||||||
(s, H_proof[0]),
|
|
||||||
(d, G),
|
|
||||||
((r * y * b[0]) + (s * y * a[0]), *H),
|
|
||||||
]);
|
|
||||||
let B = prove_multiexp(&[(r * y * s, *H), (eta, G)]);
|
|
||||||
let e = hash_cache(&mut cache, &[A1.compress().to_bytes(), B.compress().to_bytes()]);
|
|
||||||
|
|
||||||
let r1 = (a[0] * e) + r;
|
|
||||||
r.zeroize();
|
|
||||||
let s1 = (b[0] * e) + s;
|
|
||||||
s.zeroize();
|
|
||||||
let d1 = ((d * e) + eta) + (alpha1 * (e * e));
|
|
||||||
d.zeroize();
|
|
||||||
eta.zeroize();
|
|
||||||
alpha1.zeroize();
|
|
||||||
|
|
||||||
let res = PlusStruct {
|
|
||||||
A: *A,
|
|
||||||
A1: *A1,
|
|
||||||
B: *B,
|
|
||||||
r1: *r1,
|
|
||||||
s1: *s1,
|
|
||||||
d1: *d1,
|
|
||||||
L: L.drain(..).map(|L| *L).collect(),
|
|
||||||
R: R.drain(..).map(|R| *R).collect(),
|
|
||||||
};
|
|
||||||
debug_assert!(res.verify(rng, &commitments_points));
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn verify_core<ID: Copy + Zeroize, R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier<ID, EdwardsPoint>,
|
|
||||||
id: ID,
|
|
||||||
commitments: &[DalekPoint],
|
|
||||||
) -> bool {
|
|
||||||
// Verify commitments are valid
|
|
||||||
if commitments.is_empty() || (commitments.len() > MAX_M) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify L and R are properly sized
|
|
||||||
if self.L.len() != self.R.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (logMN, M, MN) = MN(commitments.len());
|
|
||||||
if self.L.len() != logMN {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild all challenges
|
|
||||||
let (mut cache, commitments) = hash_plus(commitments.iter().cloned());
|
|
||||||
let y = hash_cache(&mut cache, &[self.A.compress().to_bytes()]);
|
|
||||||
let yinv = y.invert().unwrap();
|
|
||||||
let z = hash_to_scalar(&y.to_bytes());
|
|
||||||
cache = z;
|
|
||||||
|
|
||||||
let mut w = Vec::with_capacity(logMN);
|
|
||||||
let mut winv = Vec::with_capacity(logMN);
|
|
||||||
for (L, R) in self.L.iter().zip(&self.R) {
|
|
||||||
w.push(hash_cache(&mut cache, &[L.compress().to_bytes(), R.compress().to_bytes()]));
|
|
||||||
winv.push(cache.invert().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
let e = hash_cache(&mut cache, &[self.A1.compress().to_bytes(), self.B.compress().to_bytes()]);
|
|
||||||
|
|
||||||
// Convert the proof from * INV_EIGHT to its actual form
|
|
||||||
let normalize = |point: &DalekPoint| EdwardsPoint(point.mul_by_cofactor());
|
|
||||||
|
|
||||||
let L = self.L.iter().map(normalize).collect::<Vec<_>>();
|
|
||||||
let R = self.R.iter().map(normalize).collect::<Vec<_>>();
|
|
||||||
let A = normalize(&self.A);
|
|
||||||
let A1 = normalize(&self.A1);
|
|
||||||
let B = normalize(&self.B);
|
|
||||||
|
|
||||||
let mut commitments = commitments.iter().map(|c| c.mul_by_cofactor()).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Verify it
|
|
||||||
let mut proof = Vec::with_capacity(logMN + 5 + (2 * (MN + logMN)));
|
|
||||||
|
|
||||||
let mut yMN = y;
|
|
||||||
for _ in 0 .. logMN {
|
|
||||||
yMN *= yMN;
|
|
||||||
}
|
|
||||||
let yMNy = yMN * y;
|
|
||||||
|
|
||||||
let (zpow, d) = d(z, M, MN);
|
|
||||||
let zsq = zpow[0];
|
|
||||||
|
|
||||||
let esq = e * e;
|
|
||||||
let minus_esq = -esq;
|
|
||||||
let commitment_weight = minus_esq * yMNy;
|
|
||||||
for (i, commitment) in commitments.drain(..).enumerate() {
|
|
||||||
proof.push((commitment_weight * zpow[i], commitment));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invert B, instead of the Scalar, as the latter is only 2x as expensive yet enables reduction
|
|
||||||
// to a single addition under vartime for the first BP verified in the batch, which is expected
|
|
||||||
// to be much more significant
|
|
||||||
proof.push((Scalar::one(), -B));
|
|
||||||
proof.push((-e, A1));
|
|
||||||
proof.push((minus_esq, A));
|
|
||||||
proof.push((Scalar(self.d1), G));
|
|
||||||
|
|
||||||
let d_sum = zpow.sum() * Scalar::from(u64::MAX);
|
|
||||||
let y_sum = weighted_powers(y, MN).sum();
|
|
||||||
proof.push((
|
|
||||||
Scalar(self.r1 * y.0 * self.s1) + (esq * ((yMNy * z * d_sum) + ((zsq - z) * y_sum))),
|
|
||||||
*H,
|
|
||||||
));
|
|
||||||
|
|
||||||
let w_cache = challenge_products(&w, &winv);
|
|
||||||
|
|
||||||
let mut e_r1_y = e * Scalar(self.r1);
|
|
||||||
let e_s1 = e * Scalar(self.s1);
|
|
||||||
let esq_z = esq * z;
|
|
||||||
let minus_esq_z = -esq_z;
|
|
||||||
let mut minus_esq_y = minus_esq * yMN;
|
|
||||||
|
|
||||||
for i in 0 .. MN {
|
|
||||||
proof.push((e_r1_y * w_cache[i] + esq_z, GENERATORS.G[i]));
|
|
||||||
proof.push((
|
|
||||||
(e_s1 * w_cache[(!i) & (MN - 1)]) + minus_esq_z + (minus_esq_y * d[i]),
|
|
||||||
GENERATORS.H[i],
|
|
||||||
));
|
|
||||||
|
|
||||||
e_r1_y *= yinv;
|
|
||||||
minus_esq_y *= yinv;
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0 .. logMN {
|
|
||||||
proof.push((minus_esq * w[i] * w[i], L[i]));
|
|
||||||
proof.push((minus_esq * winv[i] * winv[i], R[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
verifier.queue(rng, id, proof);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn verify<R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
commitments: &[DalekPoint],
|
|
||||||
) -> bool {
|
|
||||||
let mut verifier = BatchVerifier::new(1);
|
|
||||||
if self.verify_core(rng, &mut verifier, (), commitments) {
|
|
||||||
verifier.verify_vartime()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn batch_verify<ID: Copy + Zeroize, R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier<ID, EdwardsPoint>,
|
|
||||||
id: ID,
|
|
||||||
commitments: &[DalekPoint],
|
|
||||||
) -> bool {
|
|
||||||
self.verify_core(rng, verifier, id, commitments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
use std_shims::vec::Vec;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
|
||||||
|
use multiexp::{multiexp, multiexp_vartime, BatchVerifier};
|
||||||
|
use group::{
|
||||||
|
ff::{Field, PrimeField},
|
||||||
|
Group, GroupEncoding,
|
||||||
|
};
|
||||||
|
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Commitment,
|
||||||
|
ringct::{
|
||||||
|
bulletproofs::core::{MAX_M, N},
|
||||||
|
bulletproofs::plus::{
|
||||||
|
ScalarVector, PointVector, GeneratorsList, Generators,
|
||||||
|
transcript::*,
|
||||||
|
weighted_inner_product::{WipStatement, WipWitness, WipProof},
|
||||||
|
padded_pow_of_2, u64_decompose,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Figure 3
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct AggregateRangeStatement {
|
||||||
|
generators: Generators,
|
||||||
|
V: Vec<EdwardsPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Zeroize for AggregateRangeStatement {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.V.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub(crate) struct AggregateRangeWitness {
|
||||||
|
values: Vec<u64>,
|
||||||
|
gammas: Vec<Scalar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AggregateRangeWitness {
|
||||||
|
pub(crate) fn new(commitments: &[Commitment]) -> Option<Self> {
|
||||||
|
if commitments.is_empty() || (commitments.len() > MAX_M) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values = Vec::with_capacity(commitments.len());
|
||||||
|
let mut gammas = Vec::with_capacity(commitments.len());
|
||||||
|
for commitment in commitments {
|
||||||
|
values.push(commitment.amount);
|
||||||
|
gammas.push(Scalar(commitment.mask));
|
||||||
|
}
|
||||||
|
Some(AggregateRangeWitness { values, gammas })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub struct AggregateRangeProof {
|
||||||
|
pub(crate) A: EdwardsPoint,
|
||||||
|
pub(crate) wip: WipProof,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AggregateRangeStatement {
|
||||||
|
pub(crate) fn new(V: Vec<EdwardsPoint>) -> Option<Self> {
|
||||||
|
if V.is_empty() || (V.len() > MAX_M) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self { generators: Generators::new(), V })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transcript_A(transcript: &mut Scalar, A: EdwardsPoint) -> (Scalar, Scalar) {
|
||||||
|
let y = hash_to_scalar(&[transcript.to_repr().as_ref(), A.to_bytes().as_ref()].concat());
|
||||||
|
let z = hash_to_scalar(y.to_bytes().as_ref());
|
||||||
|
*transcript = z;
|
||||||
|
(y, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn d_j(j: usize, m: usize) -> ScalarVector {
|
||||||
|
let mut d_j = Vec::with_capacity(m * N);
|
||||||
|
for _ in 0 .. (j - 1) * N {
|
||||||
|
d_j.push(Scalar::ZERO);
|
||||||
|
}
|
||||||
|
d_j.append(&mut ScalarVector::powers(Scalar::from(2u8), N).0);
|
||||||
|
for _ in 0 .. (m - j) * N {
|
||||||
|
d_j.push(Scalar::ZERO);
|
||||||
|
}
|
||||||
|
ScalarVector(d_j)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_A_hat(
|
||||||
|
mut V: PointVector,
|
||||||
|
generators: &Generators,
|
||||||
|
transcript: &mut Scalar,
|
||||||
|
mut A: EdwardsPoint,
|
||||||
|
) -> (Scalar, ScalarVector, Scalar, Scalar, ScalarVector, EdwardsPoint) {
|
||||||
|
let (y, z) = Self::transcript_A(transcript, A);
|
||||||
|
A = A.mul_by_cofactor();
|
||||||
|
|
||||||
|
while V.len() < padded_pow_of_2(V.len()) {
|
||||||
|
V.0.push(EdwardsPoint::identity());
|
||||||
|
}
|
||||||
|
let mn = V.len() * N;
|
||||||
|
|
||||||
|
let mut z_pow = Vec::with_capacity(V.len());
|
||||||
|
|
||||||
|
let mut d = ScalarVector::new(mn);
|
||||||
|
for j in 1 ..= V.len() {
|
||||||
|
z_pow.push(z.pow(Scalar::from(2 * u64::try_from(j).unwrap()))); // TODO: Optimize this
|
||||||
|
d = d.add_vec(&Self::d_j(j, V.len()).mul(z_pow[j - 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ascending_y = ScalarVector(vec![y]);
|
||||||
|
for i in 1 .. d.len() {
|
||||||
|
ascending_y.0.push(ascending_y[i - 1] * y);
|
||||||
|
}
|
||||||
|
let y_pows = ascending_y.clone().sum();
|
||||||
|
|
||||||
|
let mut descending_y = ascending_y.clone();
|
||||||
|
descending_y.0.reverse();
|
||||||
|
|
||||||
|
let d_descending_y = d.mul_vec(&descending_y);
|
||||||
|
|
||||||
|
let y_mn_plus_one = descending_y[0] * y;
|
||||||
|
|
||||||
|
let mut commitment_accum = EdwardsPoint::identity();
|
||||||
|
for (j, commitment) in V.0.iter().enumerate() {
|
||||||
|
commitment_accum += *commitment * z_pow[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
let neg_z = -z;
|
||||||
|
let mut A_terms = Vec::with_capacity((generators.len() * 2) + 2);
|
||||||
|
for (i, d_y_z) in d_descending_y.add(z).0.drain(..).enumerate() {
|
||||||
|
A_terms.push((neg_z, generators.generator(GeneratorsList::GBold1, i)));
|
||||||
|
A_terms.push((d_y_z, generators.generator(GeneratorsList::HBold1, i)));
|
||||||
|
}
|
||||||
|
A_terms.push((y_mn_plus_one, commitment_accum));
|
||||||
|
A_terms.push((
|
||||||
|
((y_pows * z) - (d.sum() * y_mn_plus_one * z) - (y_pows * z.square())),
|
||||||
|
Generators::g(),
|
||||||
|
));
|
||||||
|
|
||||||
|
(y, d_descending_y, y_mn_plus_one, z, ScalarVector(z_pow), A + multiexp_vartime(&A_terms))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
||||||
|
self,
|
||||||
|
rng: &mut R,
|
||||||
|
witness: &AggregateRangeWitness,
|
||||||
|
) -> Option<AggregateRangeProof> {
|
||||||
|
// Check for consistency with the witness
|
||||||
|
if self.V.len() != witness.values.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for (commitment, (value, gamma)) in
|
||||||
|
self.V.iter().zip(witness.values.iter().zip(witness.gammas.iter()))
|
||||||
|
{
|
||||||
|
if Commitment::new(**gamma, *value).calculate() != **commitment {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Self { generators, V } = self;
|
||||||
|
// Monero expects all of these points to be torsion-free
|
||||||
|
// Generally, for Bulletproofs, it sends points * INV_EIGHT and then performs a torsion clear
|
||||||
|
// by multiplying by 8
|
||||||
|
// This also restores the original value due to the preprocessing
|
||||||
|
// Commitments aren't transmitted INV_EIGHT though, so this multiplies by INV_EIGHT to enable
|
||||||
|
// clearing its cofactor without mutating the value
|
||||||
|
// For some reason, these values are transcripted * INV_EIGHT, not as transmitted
|
||||||
|
let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::<Vec<_>>();
|
||||||
|
let mut transcript = initial_transcript(V.iter());
|
||||||
|
V.iter_mut().for_each(|V| *V = V.mul_by_cofactor());
|
||||||
|
|
||||||
|
// Pad V
|
||||||
|
while V.len() < padded_pow_of_2(V.len()) {
|
||||||
|
V.push(EdwardsPoint::identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
let generators = generators.reduce(V.len() * N);
|
||||||
|
|
||||||
|
let mut d_js = Vec::with_capacity(V.len());
|
||||||
|
let mut a_l = ScalarVector(Vec::with_capacity(V.len() * N));
|
||||||
|
for j in 1 ..= V.len() {
|
||||||
|
d_js.push(Self::d_j(j, V.len()));
|
||||||
|
a_l.0.append(&mut u64_decompose(*witness.values.get(j - 1).unwrap_or(&0)).0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let a_r = a_l.sub(Scalar::ONE);
|
||||||
|
|
||||||
|
let alpha = Scalar::random(&mut *rng);
|
||||||
|
|
||||||
|
let mut A_terms = Vec::with_capacity((generators.len() * 2) + 1);
|
||||||
|
for (i, a_l) in a_l.0.iter().enumerate() {
|
||||||
|
A_terms.push((*a_l, generators.generator(GeneratorsList::GBold1, i)));
|
||||||
|
}
|
||||||
|
for (i, a_r) in a_r.0.iter().enumerate() {
|
||||||
|
A_terms.push((*a_r, generators.generator(GeneratorsList::HBold1, i)));
|
||||||
|
}
|
||||||
|
A_terms.push((alpha, Generators::h()));
|
||||||
|
let mut A = multiexp(&A_terms);
|
||||||
|
A_terms.zeroize();
|
||||||
|
|
||||||
|
// Multiply by INV_EIGHT per earlier commentary
|
||||||
|
A.0 *= crate::INV_EIGHT();
|
||||||
|
|
||||||
|
let (y, d_descending_y, y_mn_plus_one, z, z_pow, A_hat) =
|
||||||
|
Self::compute_A_hat(PointVector(V), &generators, &mut transcript, A);
|
||||||
|
|
||||||
|
let a_l = a_l.sub(z);
|
||||||
|
let a_r = a_r.add_vec(&d_descending_y).add(z);
|
||||||
|
let mut alpha = alpha;
|
||||||
|
for j in 1 ..= witness.gammas.len() {
|
||||||
|
alpha += z_pow[j - 1] * witness.gammas[j - 1] * y_mn_plus_one;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(AggregateRangeProof {
|
||||||
|
A,
|
||||||
|
wip: WipStatement::new(generators, A_hat, y)
|
||||||
|
.prove(rng, transcript, &Zeroizing::new(WipWitness::new(a_l, a_r, alpha).unwrap()))
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify<Id: Copy + Zeroize, R: RngCore + CryptoRng>(
|
||||||
|
self,
|
||||||
|
rng: &mut R,
|
||||||
|
verifier: &mut BatchVerifier<Id, EdwardsPoint>,
|
||||||
|
id: Id,
|
||||||
|
proof: AggregateRangeProof,
|
||||||
|
) -> bool {
|
||||||
|
let Self { generators, V } = self;
|
||||||
|
|
||||||
|
let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::<Vec<_>>();
|
||||||
|
let mut transcript = initial_transcript(V.iter());
|
||||||
|
V.iter_mut().for_each(|V| *V = V.mul_by_cofactor());
|
||||||
|
|
||||||
|
let generators = generators.reduce(V.len() * N);
|
||||||
|
|
||||||
|
let (y, _, _, _, _, A_hat) =
|
||||||
|
Self::compute_A_hat(PointVector(V), &generators, &mut transcript, proof.A);
|
||||||
|
WipStatement::new(generators, A_hat, y).verify(rng, verifier, id, transcript, proof.wip)
|
||||||
|
}
|
||||||
|
}
|
||||||
86
coins/monero/src/ringct/bulletproofs/plus/mod.rs
Normal file
86
coins/monero/src/ringct/bulletproofs/plus/mod.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use group::Group;
|
||||||
|
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
mod scalar_vector;
|
||||||
|
pub(crate) use scalar_vector::{ScalarVector, weighted_inner_product};
|
||||||
|
mod point_vector;
|
||||||
|
pub(crate) use point_vector::PointVector;
|
||||||
|
|
||||||
|
pub(crate) mod transcript;
|
||||||
|
pub(crate) mod weighted_inner_product;
|
||||||
|
pub(crate) use weighted_inner_product::*;
|
||||||
|
pub(crate) mod aggregate_range_proof;
|
||||||
|
pub(crate) use aggregate_range_proof::*;
|
||||||
|
|
||||||
|
pub(crate) fn padded_pow_of_2(i: usize) -> usize {
|
||||||
|
let mut next_pow_of_2 = 1;
|
||||||
|
while next_pow_of_2 < i {
|
||||||
|
next_pow_of_2 <<= 1;
|
||||||
|
}
|
||||||
|
next_pow_of_2
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub(crate) enum GeneratorsList {
|
||||||
|
GBold1,
|
||||||
|
HBold1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Table these
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Generators {
|
||||||
|
g_bold1: &'static [EdwardsPoint],
|
||||||
|
h_bold1: &'static [EdwardsPoint],
|
||||||
|
}
|
||||||
|
|
||||||
|
mod generators {
|
||||||
|
use std_shims::sync::OnceLock;
|
||||||
|
use monero_generators::Generators;
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/generators_plus.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Generators {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let gens = generators::GENERATORS();
|
||||||
|
Generators { g_bold1: &gens.G, h_bold1: &gens.H }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.g_bold1.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn g() -> EdwardsPoint {
|
||||||
|
dalek_ff_group::EdwardsPoint(crate::H())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn h() -> EdwardsPoint {
|
||||||
|
EdwardsPoint::generator()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn generator(&self, list: GeneratorsList, i: usize) -> EdwardsPoint {
|
||||||
|
match list {
|
||||||
|
GeneratorsList::GBold1 => self.g_bold1[i],
|
||||||
|
GeneratorsList::HBold1 => self.h_bold1[i],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reduce(&self, generators: usize) -> Self {
|
||||||
|
// Round to the nearest power of 2
|
||||||
|
let generators = padded_pow_of_2(generators);
|
||||||
|
assert!(generators <= self.g_bold1.len());
|
||||||
|
|
||||||
|
Generators { g_bold1: &self.g_bold1[.. generators], h_bold1: &self.h_bold1[.. generators] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the little-endian decomposition.
|
||||||
|
fn u64_decompose(value: u64) -> ScalarVector {
|
||||||
|
let mut bits = ScalarVector::new(64);
|
||||||
|
for bit in 0 .. 64 {
|
||||||
|
bits[bit] = Scalar::from((value >> bit) & 1);
|
||||||
|
}
|
||||||
|
bits
|
||||||
|
}
|
||||||
50
coins/monero/src/ringct/bulletproofs/plus/point_vector.rs
Normal file
50
coins/monero/src/ringct/bulletproofs/plus/point_vector.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use core::ops::{Index, IndexMut};
|
||||||
|
use std_shims::vec::Vec;
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
use dalek_ff_group::EdwardsPoint;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use multiexp::multiexp;
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::ringct::bulletproofs::plus::ScalarVector;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub(crate) struct PointVector(pub(crate) Vec<EdwardsPoint>);
|
||||||
|
|
||||||
|
impl Index<usize> for PointVector {
|
||||||
|
type Output = EdwardsPoint;
|
||||||
|
fn index(&self, index: usize) -> &EdwardsPoint {
|
||||||
|
&self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<usize> for PointVector {
|
||||||
|
fn index_mut(&mut self, index: usize) -> &mut EdwardsPoint {
|
||||||
|
&mut self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointVector {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn multiexp(&self, vector: &ScalarVector) -> EdwardsPoint {
|
||||||
|
debug_assert_eq!(self.len(), vector.len());
|
||||||
|
let mut res = Vec::with_capacity(self.len());
|
||||||
|
for (point, scalar) in self.0.iter().copied().zip(vector.0.iter().copied()) {
|
||||||
|
res.push((scalar, point));
|
||||||
|
}
|
||||||
|
multiexp(&res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn split(mut self) -> (Self, Self) {
|
||||||
|
debug_assert!(self.len() > 1);
|
||||||
|
let r = self.0.split_off(self.0.len() / 2);
|
||||||
|
debug_assert_eq!(self.len(), r.len());
|
||||||
|
(self, PointVector(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
114
coins/monero/src/ringct/bulletproofs/plus/scalar_vector.rs
Normal file
114
coins/monero/src/ringct/bulletproofs/plus/scalar_vector.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use core::{
|
||||||
|
borrow::Borrow,
|
||||||
|
ops::{Index, IndexMut},
|
||||||
|
};
|
||||||
|
use std_shims::vec::Vec;
|
||||||
|
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
use group::ff::Field;
|
||||||
|
use dalek_ff_group::Scalar;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub(crate) struct ScalarVector(pub(crate) Vec<Scalar>);
|
||||||
|
|
||||||
|
impl Index<usize> for ScalarVector {
|
||||||
|
type Output = Scalar;
|
||||||
|
fn index(&self, index: usize) -> &Scalar {
|
||||||
|
&self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<usize> for ScalarVector {
|
||||||
|
fn index_mut(&mut self, index: usize) -> &mut Scalar {
|
||||||
|
&mut self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScalarVector {
|
||||||
|
pub(crate) fn new(len: usize) -> Self {
|
||||||
|
ScalarVector(vec![Scalar::ZERO; len])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add(&self, scalar: impl Borrow<Scalar>) -> Self {
|
||||||
|
let mut res = self.clone();
|
||||||
|
for val in &mut res.0 {
|
||||||
|
*val += scalar.borrow();
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sub(&self, scalar: impl Borrow<Scalar>) -> Self {
|
||||||
|
let mut res = self.clone();
|
||||||
|
for val in &mut res.0 {
|
||||||
|
*val -= scalar.borrow();
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mul(&self, scalar: impl Borrow<Scalar>) -> Self {
|
||||||
|
let mut res = self.clone();
|
||||||
|
for val in &mut res.0 {
|
||||||
|
*val *= scalar.borrow();
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_vec(&self, vector: &Self) -> Self {
|
||||||
|
debug_assert_eq!(self.len(), vector.len());
|
||||||
|
let mut res = self.clone();
|
||||||
|
for (i, val) in res.0.iter_mut().enumerate() {
|
||||||
|
*val += vector.0[i];
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mul_vec(&self, vector: &Self) -> Self {
|
||||||
|
debug_assert_eq!(self.len(), vector.len());
|
||||||
|
let mut res = self.clone();
|
||||||
|
for (i, val) in res.0.iter_mut().enumerate() {
|
||||||
|
*val *= vector.0[i];
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inner_product(&self, vector: &Self) -> Scalar {
|
||||||
|
self.mul_vec(vector).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn powers(x: Scalar, len: usize) -> Self {
|
||||||
|
debug_assert!(len != 0);
|
||||||
|
|
||||||
|
let mut res = Vec::with_capacity(len);
|
||||||
|
res.push(Scalar::ONE);
|
||||||
|
res.push(x);
|
||||||
|
for i in 2 .. len {
|
||||||
|
res.push(res[i - 1] * x);
|
||||||
|
}
|
||||||
|
res.truncate(len);
|
||||||
|
ScalarVector(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sum(mut self) -> Scalar {
|
||||||
|
self.0.drain(..).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn split(mut self) -> (Self, Self) {
|
||||||
|
debug_assert!(self.len() > 1);
|
||||||
|
let r = self.0.split_off(self.0.len() / 2);
|
||||||
|
debug_assert_eq!(self.len(), r.len());
|
||||||
|
(self, ScalarVector(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn weighted_inner_product(
|
||||||
|
a: &ScalarVector,
|
||||||
|
b: &ScalarVector,
|
||||||
|
y: &ScalarVector,
|
||||||
|
) -> Scalar {
|
||||||
|
a.inner_product(&b.mul_vec(y))
|
||||||
|
}
|
||||||
24
coins/monero/src/ringct/bulletproofs/plus/transcript.rs
Normal file
24
coins/monero/src/ringct/bulletproofs/plus/transcript.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use std_shims::{sync::OnceLock, vec::Vec};
|
||||||
|
|
||||||
|
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use monero_generators::{hash_to_point as raw_hash_to_point};
|
||||||
|
use crate::{hash, hash_to_scalar as dalek_hash};
|
||||||
|
|
||||||
|
// Monero starts BP+ transcripts with the following constant.
|
||||||
|
static TRANSCRIPT_CELL: OnceLock<[u8; 32]> = OnceLock::new();
|
||||||
|
pub(crate) fn TRANSCRIPT() -> [u8; 32] {
|
||||||
|
// Why this uses a hash_to_point is completely unknown.
|
||||||
|
*TRANSCRIPT_CELL
|
||||||
|
.get_or_init(|| raw_hash_to_point(hash(b"bulletproof_plus_transcript")).compress().to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||||
|
Scalar(dalek_hash(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn initial_transcript(commitments: core::slice::Iter<'_, EdwardsPoint>) -> Scalar {
|
||||||
|
let commitments_hash =
|
||||||
|
hash_to_scalar(&commitments.flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>());
|
||||||
|
hash_to_scalar(&[TRANSCRIPT().as_ref(), &commitments_hash.to_bytes()].concat())
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
use std_shims::vec::Vec;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
use multiexp::{multiexp, multiexp_vartime, BatchVerifier};
|
||||||
|
use group::{
|
||||||
|
ff::{Field, PrimeField},
|
||||||
|
GroupEncoding,
|
||||||
|
};
|
||||||
|
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use crate::ringct::bulletproofs::plus::{
|
||||||
|
ScalarVector, PointVector, GeneratorsList, Generators, padded_pow_of_2, weighted_inner_product,
|
||||||
|
transcript::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Figure 1
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct WipStatement {
|
||||||
|
generators: Generators,
|
||||||
|
P: EdwardsPoint,
|
||||||
|
y: ScalarVector,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Zeroize for WipStatement {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.P.zeroize();
|
||||||
|
self.y.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub(crate) struct WipWitness {
|
||||||
|
a: ScalarVector,
|
||||||
|
b: ScalarVector,
|
||||||
|
alpha: Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WipWitness {
|
||||||
|
pub(crate) fn new(mut a: ScalarVector, mut b: ScalarVector, alpha: Scalar) -> Option<Self> {
|
||||||
|
if a.0.is_empty() || (a.len() != b.len()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad to the nearest power of 2
|
||||||
|
let missing = padded_pow_of_2(a.len()) - a.len();
|
||||||
|
a.0.reserve(missing);
|
||||||
|
b.0.reserve(missing);
|
||||||
|
for _ in 0 .. missing {
|
||||||
|
a.0.push(Scalar::ZERO);
|
||||||
|
b.0.push(Scalar::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self { a, b, alpha })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub(crate) struct WipProof {
|
||||||
|
pub(crate) L: Vec<EdwardsPoint>,
|
||||||
|
pub(crate) R: Vec<EdwardsPoint>,
|
||||||
|
pub(crate) A: EdwardsPoint,
|
||||||
|
pub(crate) B: EdwardsPoint,
|
||||||
|
pub(crate) r_answer: Scalar,
|
||||||
|
pub(crate) s_answer: Scalar,
|
||||||
|
pub(crate) delta_answer: Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WipStatement {
|
||||||
|
pub(crate) fn new(generators: Generators, P: EdwardsPoint, y: Scalar) -> Self {
|
||||||
|
debug_assert_eq!(generators.len(), padded_pow_of_2(generators.len()));
|
||||||
|
|
||||||
|
// y ** n
|
||||||
|
let mut y_vec = ScalarVector::new(generators.len());
|
||||||
|
y_vec[0] = y;
|
||||||
|
for i in 1 .. y_vec.len() {
|
||||||
|
y_vec[i] = y_vec[i - 1] * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { generators, P, y: y_vec }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transcript_L_R(transcript: &mut Scalar, L: EdwardsPoint, R: EdwardsPoint) -> Scalar {
|
||||||
|
let e = hash_to_scalar(
|
||||||
|
&[transcript.to_repr().as_ref(), L.to_bytes().as_ref(), R.to_bytes().as_ref()].concat(),
|
||||||
|
);
|
||||||
|
*transcript = e;
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transcript_A_B(transcript: &mut Scalar, A: EdwardsPoint, B: EdwardsPoint) -> Scalar {
|
||||||
|
let e = hash_to_scalar(
|
||||||
|
&[transcript.to_repr().as_ref(), A.to_bytes().as_ref(), B.to_bytes().as_ref()].concat(),
|
||||||
|
);
|
||||||
|
*transcript = e;
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prover's variant of the shared code block to calculate G/H/P when n > 1
|
||||||
|
// Returns each permutation of G/H since the prover needs to do operation on each permutation
|
||||||
|
// P is dropped as it's unused in the prover's path
|
||||||
|
// TODO: It'd still probably be faster to keep in terms of the original generators, both between
|
||||||
|
// the reduced amount of group operations and the potential tabling of the generators under
|
||||||
|
// multiexp
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn next_G_H(
|
||||||
|
transcript: &mut Scalar,
|
||||||
|
mut g_bold1: PointVector,
|
||||||
|
mut g_bold2: PointVector,
|
||||||
|
mut h_bold1: PointVector,
|
||||||
|
mut h_bold2: PointVector,
|
||||||
|
L: EdwardsPoint,
|
||||||
|
R: EdwardsPoint,
|
||||||
|
y_inv_n_hat: Scalar,
|
||||||
|
) -> (Scalar, Scalar, Scalar, Scalar, PointVector, PointVector) {
|
||||||
|
debug_assert_eq!(g_bold1.len(), g_bold2.len());
|
||||||
|
debug_assert_eq!(h_bold1.len(), h_bold2.len());
|
||||||
|
debug_assert_eq!(g_bold1.len(), h_bold1.len());
|
||||||
|
|
||||||
|
let e = Self::transcript_L_R(transcript, L, R);
|
||||||
|
let inv_e = e.invert().unwrap();
|
||||||
|
|
||||||
|
// This vartime is safe as all of these arguments are public
|
||||||
|
let mut new_g_bold = Vec::with_capacity(g_bold1.len());
|
||||||
|
let e_y_inv = e * y_inv_n_hat;
|
||||||
|
for g_bold in g_bold1.0.drain(..).zip(g_bold2.0.drain(..)) {
|
||||||
|
new_g_bold.push(multiexp_vartime(&[(inv_e, g_bold.0), (e_y_inv, g_bold.1)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_h_bold = Vec::with_capacity(h_bold1.len());
|
||||||
|
for h_bold in h_bold1.0.drain(..).zip(h_bold2.0.drain(..)) {
|
||||||
|
new_h_bold.push(multiexp_vartime(&[(e, h_bold.0), (inv_e, h_bold.1)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let e_square = e.square();
|
||||||
|
let inv_e_square = inv_e.square();
|
||||||
|
|
||||||
|
(e, inv_e, e_square, inv_e_square, PointVector(new_g_bold), PointVector(new_h_bold))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This has room for optimization worth investigating further. It currently takes
|
||||||
|
an iterative approach. It can be optimized further via divide and conquer.
|
||||||
|
|
||||||
|
Assume there are 4 challenges.
|
||||||
|
|
||||||
|
Iterative approach (current):
|
||||||
|
1. Do the optimal multiplications across challenge column 0 and 1.
|
||||||
|
2. Do the optimal multiplications across that result and column 2.
|
||||||
|
3. Do the optimal multiplications across that result and column 3.
|
||||||
|
|
||||||
|
Divide and conquer (worth investigating further):
|
||||||
|
1. Do the optimal multiplications across challenge column 0 and 1.
|
||||||
|
2. Do the optimal multiplications across challenge column 2 and 3.
|
||||||
|
3. Multiply both results together.
|
||||||
|
|
||||||
|
When there are 4 challenges (n=16), the iterative approach does 28 multiplications
|
||||||
|
versus divide and conquer's 24.
|
||||||
|
*/
|
||||||
|
fn challenge_products(challenges: &[(Scalar, Scalar)]) -> Vec<Scalar> {
|
||||||
|
let mut products = vec![Scalar::ONE; 1 << challenges.len()];
|
||||||
|
|
||||||
|
if !challenges.is_empty() {
|
||||||
|
products[0] = challenges[0].1;
|
||||||
|
products[1] = challenges[0].0;
|
||||||
|
|
||||||
|
for (j, challenge) in challenges.iter().enumerate().skip(1) {
|
||||||
|
let mut slots = (1 << (j + 1)) - 1;
|
||||||
|
while slots > 0 {
|
||||||
|
products[slots] = products[slots / 2] * challenge.0;
|
||||||
|
products[slots - 1] = products[slots / 2] * challenge.1;
|
||||||
|
|
||||||
|
slots = slots.saturating_sub(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check since if the above failed to populate, it'd be critical
|
||||||
|
for product in &products {
|
||||||
|
debug_assert!(!bool::from(product.is_zero()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
products
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
||||||
|
self,
|
||||||
|
rng: &mut R,
|
||||||
|
mut transcript: Scalar,
|
||||||
|
witness: &WipWitness,
|
||||||
|
) -> Option<WipProof> {
|
||||||
|
let WipStatement { generators, P, mut y } = self;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let _ = P;
|
||||||
|
|
||||||
|
if generators.len() != witness.a.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (g, h) = (Generators::g(), Generators::h());
|
||||||
|
let mut g_bold = vec![];
|
||||||
|
let mut h_bold = vec![];
|
||||||
|
for i in 0 .. generators.len() {
|
||||||
|
g_bold.push(generators.generator(GeneratorsList::GBold1, i));
|
||||||
|
h_bold.push(generators.generator(GeneratorsList::HBold1, i));
|
||||||
|
}
|
||||||
|
let mut g_bold = PointVector(g_bold);
|
||||||
|
let mut h_bold = PointVector(h_bold);
|
||||||
|
|
||||||
|
// Check P has the expected relationship
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
let mut P_terms = witness
|
||||||
|
.a
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.zip(g_bold.0.iter().copied())
|
||||||
|
.chain(witness.b.0.iter().copied().zip(h_bold.0.iter().copied()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
P_terms.push((weighted_inner_product(&witness.a, &witness.b, &y), g));
|
||||||
|
P_terms.push((witness.alpha, h));
|
||||||
|
debug_assert_eq!(multiexp(&P_terms), P);
|
||||||
|
P_terms.zeroize();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut a = witness.a.clone();
|
||||||
|
let mut b = witness.b.clone();
|
||||||
|
let mut alpha = witness.alpha;
|
||||||
|
|
||||||
|
// From here on, g_bold.len() is used as n
|
||||||
|
debug_assert_eq!(g_bold.len(), a.len());
|
||||||
|
|
||||||
|
let mut L_vec = vec![];
|
||||||
|
let mut R_vec = vec![];
|
||||||
|
|
||||||
|
// else n > 1 case from figure 1
|
||||||
|
while g_bold.len() > 1 {
|
||||||
|
let (a1, a2) = a.clone().split();
|
||||||
|
let (b1, b2) = b.clone().split();
|
||||||
|
let (g_bold1, g_bold2) = g_bold.split();
|
||||||
|
let (h_bold1, h_bold2) = h_bold.split();
|
||||||
|
|
||||||
|
let n_hat = g_bold1.len();
|
||||||
|
debug_assert_eq!(a1.len(), n_hat);
|
||||||
|
debug_assert_eq!(a2.len(), n_hat);
|
||||||
|
debug_assert_eq!(b1.len(), n_hat);
|
||||||
|
debug_assert_eq!(b2.len(), n_hat);
|
||||||
|
debug_assert_eq!(g_bold1.len(), n_hat);
|
||||||
|
debug_assert_eq!(g_bold2.len(), n_hat);
|
||||||
|
debug_assert_eq!(h_bold1.len(), n_hat);
|
||||||
|
debug_assert_eq!(h_bold2.len(), n_hat);
|
||||||
|
|
||||||
|
let y_n_hat = y[n_hat - 1];
|
||||||
|
y.0.truncate(n_hat);
|
||||||
|
|
||||||
|
let d_l = Scalar::random(&mut *rng);
|
||||||
|
let d_r = Scalar::random(&mut *rng);
|
||||||
|
|
||||||
|
let c_l = weighted_inner_product(&a1, &b2, &y);
|
||||||
|
let c_r = weighted_inner_product(&(a2.mul(y_n_hat)), &b1, &y);
|
||||||
|
|
||||||
|
// TODO: Calculate these with a batch inversion
|
||||||
|
let y_inv_n_hat = y_n_hat.invert().unwrap();
|
||||||
|
|
||||||
|
let mut L_terms = a1
|
||||||
|
.mul(y_inv_n_hat)
|
||||||
|
.0
|
||||||
|
.drain(..)
|
||||||
|
.zip(g_bold2.0.iter().copied())
|
||||||
|
.chain(b2.0.iter().copied().zip(h_bold1.0.iter().copied()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
L_terms.push((c_l, g));
|
||||||
|
L_terms.push((d_l, h));
|
||||||
|
let L = multiexp(&L_terms) * Scalar(crate::INV_EIGHT());
|
||||||
|
L_vec.push(L);
|
||||||
|
L_terms.zeroize();
|
||||||
|
|
||||||
|
let mut R_terms = a2
|
||||||
|
.mul(y_n_hat)
|
||||||
|
.0
|
||||||
|
.drain(..)
|
||||||
|
.zip(g_bold1.0.iter().copied())
|
||||||
|
.chain(b1.0.iter().copied().zip(h_bold2.0.iter().copied()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
R_terms.push((c_r, g));
|
||||||
|
R_terms.push((d_r, h));
|
||||||
|
let R = multiexp(&R_terms) * Scalar(crate::INV_EIGHT());
|
||||||
|
R_vec.push(R);
|
||||||
|
R_terms.zeroize();
|
||||||
|
|
||||||
|
let (e, inv_e, e_square, inv_e_square);
|
||||||
|
(e, inv_e, e_square, inv_e_square, g_bold, h_bold) =
|
||||||
|
Self::next_G_H(&mut transcript, g_bold1, g_bold2, h_bold1, h_bold2, L, R, y_inv_n_hat);
|
||||||
|
|
||||||
|
a = a1.mul(e).add_vec(&a2.mul(y_n_hat * inv_e));
|
||||||
|
b = b1.mul(inv_e).add_vec(&b2.mul(e));
|
||||||
|
alpha += (d_l * e_square) + (d_r * inv_e_square);
|
||||||
|
|
||||||
|
debug_assert_eq!(g_bold.len(), a.len());
|
||||||
|
debug_assert_eq!(g_bold.len(), h_bold.len());
|
||||||
|
debug_assert_eq!(g_bold.len(), b.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// n == 1 case from figure 1
|
||||||
|
debug_assert_eq!(g_bold.len(), 1);
|
||||||
|
debug_assert_eq!(h_bold.len(), 1);
|
||||||
|
|
||||||
|
debug_assert_eq!(a.len(), 1);
|
||||||
|
debug_assert_eq!(b.len(), 1);
|
||||||
|
|
||||||
|
let r = Scalar::random(&mut *rng);
|
||||||
|
let s = Scalar::random(&mut *rng);
|
||||||
|
let delta = Scalar::random(&mut *rng);
|
||||||
|
let eta = Scalar::random(&mut *rng);
|
||||||
|
|
||||||
|
let ry = r * y[0];
|
||||||
|
|
||||||
|
let mut A_terms =
|
||||||
|
vec![(r, g_bold[0]), (s, h_bold[0]), ((ry * b[0]) + (s * y[0] * a[0]), g), (delta, h)];
|
||||||
|
let A = multiexp(&A_terms) * Scalar(crate::INV_EIGHT());
|
||||||
|
A_terms.zeroize();
|
||||||
|
|
||||||
|
let mut B_terms = vec![(ry * s, g), (eta, h)];
|
||||||
|
let B = multiexp(&B_terms) * Scalar(crate::INV_EIGHT());
|
||||||
|
B_terms.zeroize();
|
||||||
|
|
||||||
|
let e = Self::transcript_A_B(&mut transcript, A, B);
|
||||||
|
|
||||||
|
let r_answer = r + (a[0] * e);
|
||||||
|
let s_answer = s + (b[0] * e);
|
||||||
|
let delta_answer = eta + (delta * e) + (alpha * e.square());
|
||||||
|
|
||||||
|
Some(WipProof { L: L_vec, R: R_vec, A, B, r_answer, s_answer, delta_answer })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify<Id: Copy + Zeroize, R: RngCore + CryptoRng>(
|
||||||
|
self,
|
||||||
|
rng: &mut R,
|
||||||
|
verifier: &mut BatchVerifier<Id, EdwardsPoint>,
|
||||||
|
id: Id,
|
||||||
|
mut transcript: Scalar,
|
||||||
|
mut proof: WipProof,
|
||||||
|
) -> bool {
|
||||||
|
let WipStatement { generators, P, y } = self;
|
||||||
|
|
||||||
|
let (g, h) = (Generators::g(), Generators::h());
|
||||||
|
|
||||||
|
// Verify the L/R lengths
|
||||||
|
{
|
||||||
|
let mut lr_len = 0;
|
||||||
|
while (1 << lr_len) < generators.len() {
|
||||||
|
lr_len += 1;
|
||||||
|
}
|
||||||
|
if (proof.L.len() != lr_len) ||
|
||||||
|
(proof.R.len() != lr_len) ||
|
||||||
|
(generators.len() != (1 << lr_len))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inv_y = {
|
||||||
|
let inv_y = y[0].invert().unwrap();
|
||||||
|
let mut res = Vec::with_capacity(y.len());
|
||||||
|
res.push(inv_y);
|
||||||
|
while res.len() < y.len() {
|
||||||
|
res.push(inv_y * res.last().unwrap());
|
||||||
|
}
|
||||||
|
res
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut P_terms = vec![(Scalar::ONE, P)];
|
||||||
|
P_terms.reserve(6 + (2 * generators.len()) + proof.L.len());
|
||||||
|
|
||||||
|
let mut challenges = Vec::with_capacity(proof.L.len());
|
||||||
|
let product_cache = {
|
||||||
|
let mut es = Vec::with_capacity(proof.L.len());
|
||||||
|
for (L, R) in proof.L.iter_mut().zip(proof.R.iter_mut()) {
|
||||||
|
es.push(Self::transcript_L_R(&mut transcript, *L, *R));
|
||||||
|
*L = L.mul_by_cofactor();
|
||||||
|
*R = R.mul_by_cofactor();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut inv_es = es.clone();
|
||||||
|
let mut scratch = vec![Scalar::ZERO; es.len()];
|
||||||
|
group::ff::BatchInverter::invert_with_external_scratch(&mut inv_es, &mut scratch);
|
||||||
|
drop(scratch);
|
||||||
|
|
||||||
|
debug_assert_eq!(es.len(), inv_es.len());
|
||||||
|
debug_assert_eq!(es.len(), proof.L.len());
|
||||||
|
debug_assert_eq!(es.len(), proof.R.len());
|
||||||
|
for ((e, inv_e), (L, R)) in
|
||||||
|
es.drain(..).zip(inv_es.drain(..)).zip(proof.L.iter().zip(proof.R.iter()))
|
||||||
|
{
|
||||||
|
debug_assert_eq!(e.invert().unwrap(), inv_e);
|
||||||
|
|
||||||
|
challenges.push((e, inv_e));
|
||||||
|
|
||||||
|
let e_square = e.square();
|
||||||
|
let inv_e_square = inv_e.square();
|
||||||
|
P_terms.push((e_square, *L));
|
||||||
|
P_terms.push((inv_e_square, *R));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::challenge_products(&challenges)
|
||||||
|
};
|
||||||
|
|
||||||
|
let e = Self::transcript_A_B(&mut transcript, proof.A, proof.B);
|
||||||
|
proof.A = proof.A.mul_by_cofactor();
|
||||||
|
proof.B = proof.B.mul_by_cofactor();
|
||||||
|
let neg_e_square = -e.square();
|
||||||
|
|
||||||
|
let mut multiexp = P_terms;
|
||||||
|
multiexp.reserve(4 + (2 * generators.len()));
|
||||||
|
for (scalar, _) in &mut multiexp {
|
||||||
|
*scalar *= neg_e_square;
|
||||||
|
}
|
||||||
|
|
||||||
|
let re = proof.r_answer * e;
|
||||||
|
for i in 0 .. generators.len() {
|
||||||
|
let mut scalar = product_cache[i] * re;
|
||||||
|
if i > 0 {
|
||||||
|
scalar *= inv_y[i - 1];
|
||||||
|
}
|
||||||
|
multiexp.push((scalar, generators.generator(GeneratorsList::GBold1, i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let se = proof.s_answer * e;
|
||||||
|
for i in 0 .. generators.len() {
|
||||||
|
multiexp.push((
|
||||||
|
se * product_cache[product_cache.len() - 1 - i],
|
||||||
|
generators.generator(GeneratorsList::HBold1, i),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
multiexp.push((-e, proof.A));
|
||||||
|
multiexp.push((proof.r_answer * y[0] * proof.s_answer, g));
|
||||||
|
multiexp.push((proof.delta_answer, h));
|
||||||
|
multiexp.push((-Scalar::ONE, proof.B));
|
||||||
|
|
||||||
|
verifier.queue(rng, id, multiexp);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use core::ops::{Add, Sub, Mul, Index};
|
use core::ops::{Add, Sub, Mul, Index};
|
||||||
|
use std_shims::vec::Vec;
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ use multiexp::multiexp;
|
|||||||
pub(crate) struct ScalarVector(pub(crate) Vec<Scalar>);
|
pub(crate) struct ScalarVector(pub(crate) Vec<Scalar>);
|
||||||
macro_rules! math_op {
|
macro_rules! math_op {
|
||||||
($Op: ident, $op: ident, $f: expr) => {
|
($Op: ident, $op: ident, $f: expr) => {
|
||||||
|
#[allow(clippy::redundant_closure_call)]
|
||||||
impl $Op<Scalar> for ScalarVector {
|
impl $Op<Scalar> for ScalarVector {
|
||||||
type Output = ScalarVector;
|
type Output = ScalarVector;
|
||||||
fn $op(self, b: Scalar) -> ScalarVector {
|
fn $op(self, b: Scalar) -> ScalarVector {
|
||||||
@@ -18,6 +20,7 @@ macro_rules! math_op {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure_call)]
|
||||||
impl $Op<Scalar> for &ScalarVector {
|
impl $Op<Scalar> for &ScalarVector {
|
||||||
type Output = ScalarVector;
|
type Output = ScalarVector;
|
||||||
fn $op(self, b: Scalar) -> ScalarVector {
|
fn $op(self, b: Scalar) -> ScalarVector {
|
||||||
@@ -25,6 +28,7 @@ macro_rules! math_op {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure_call)]
|
||||||
impl $Op<ScalarVector> for ScalarVector {
|
impl $Op<ScalarVector> for ScalarVector {
|
||||||
type Output = ScalarVector;
|
type Output = ScalarVector;
|
||||||
fn $op(self, b: ScalarVector) -> ScalarVector {
|
fn $op(self, b: ScalarVector) -> ScalarVector {
|
||||||
@@ -33,6 +37,7 @@ macro_rules! math_op {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure_call)]
|
||||||
impl $Op<&ScalarVector> for &ScalarVector {
|
impl $Op<&ScalarVector> for &ScalarVector {
|
||||||
type Output = ScalarVector;
|
type Output = ScalarVector;
|
||||||
fn $op(self, b: &ScalarVector) -> ScalarVector {
|
fn $op(self, b: &ScalarVector) -> ScalarVector {
|
||||||
@@ -48,38 +53,20 @@ math_op!(Mul, mul, |(a, b): (&Scalar, &Scalar)| *a * *b);
|
|||||||
|
|
||||||
impl ScalarVector {
|
impl ScalarVector {
|
||||||
pub(crate) fn new(len: usize) -> ScalarVector {
|
pub(crate) fn new(len: usize) -> ScalarVector {
|
||||||
ScalarVector(vec![Scalar::zero(); len])
|
ScalarVector(vec![Scalar::ZERO; len])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn powers(x: Scalar, len: usize) -> ScalarVector {
|
pub(crate) fn powers(x: Scalar, len: usize) -> ScalarVector {
|
||||||
debug_assert!(len != 0);
|
debug_assert!(len != 0);
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(len);
|
let mut res = Vec::with_capacity(len);
|
||||||
res.push(Scalar::one());
|
res.push(Scalar::ONE);
|
||||||
for i in 1 .. len {
|
for i in 1 .. len {
|
||||||
res.push(res[i - 1] * x);
|
res.push(res[i - 1] * x);
|
||||||
}
|
}
|
||||||
ScalarVector(res)
|
ScalarVector(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn even_powers(x: Scalar, pow: usize) -> ScalarVector {
|
|
||||||
debug_assert!(pow != 0);
|
|
||||||
// Verify pow is a power of two
|
|
||||||
debug_assert_eq!(((pow - 1) & pow), 0);
|
|
||||||
|
|
||||||
let xsq = x * x;
|
|
||||||
let mut res = ScalarVector(Vec::with_capacity(pow / 2));
|
|
||||||
res.0.push(xsq);
|
|
||||||
|
|
||||||
let mut prev = 2;
|
|
||||||
while prev < pow {
|
|
||||||
res.0.push(res[res.len() - 1] * xsq);
|
|
||||||
prev += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sum(mut self) -> Scalar {
|
pub(crate) fn sum(mut self) -> Scalar {
|
||||||
self.0.drain(..).sum()
|
self.0.drain(..).sum()
|
||||||
}
|
}
|
||||||
@@ -105,20 +92,11 @@ pub(crate) fn inner_product(a: &ScalarVector, b: &ScalarVector) -> Scalar {
|
|||||||
(a * b).sum()
|
(a * b).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn weighted_powers(x: Scalar, len: usize) -> ScalarVector {
|
|
||||||
ScalarVector(ScalarVector::powers(x, len + 1).0[1 ..].to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn weighted_inner_product(a: &ScalarVector, b: &ScalarVector, y: Scalar) -> Scalar {
|
|
||||||
// y ** 0 is not used as a power
|
|
||||||
(a * b * weighted_powers(y, a.len())).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mul<&[EdwardsPoint]> for &ScalarVector {
|
impl Mul<&[EdwardsPoint]> for &ScalarVector {
|
||||||
type Output = EdwardsPoint;
|
type Output = EdwardsPoint;
|
||||||
fn mul(self, b: &[EdwardsPoint]) -> EdwardsPoint {
|
fn mul(self, b: &[EdwardsPoint]) -> EdwardsPoint {
|
||||||
debug_assert_eq!(self.len(), b.len());
|
debug_assert_eq!(self.len(), b.len());
|
||||||
multiexp(&self.0.iter().cloned().zip(b.iter().cloned()).collect::<Vec<_>>())
|
multiexp(&self.0.iter().copied().zip(b.iter().copied()).collect::<Vec<_>>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use std::io::{self, Read, Write};
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use thiserror::Error;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
@@ -18,8 +19,8 @@ use curve25519_dalek::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Commitment, random_scalar, hash_to_scalar, wallet::decoys::Decoys, ringct::hash_to_point,
|
INV_EIGHT, Commitment, random_scalar, hash_to_scalar, wallet::decoys::Decoys,
|
||||||
serialize::*,
|
ringct::hash_to_point, serialize::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
@@ -29,28 +30,25 @@ pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
|
|||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
pub(crate) use multisig::add_key_image_share;
|
pub(crate) use multisig::add_key_image_share;
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors returned when CLSAG signing fails.
|
/// Errors returned when CLSAG signing fails.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||||
pub enum ClsagError {
|
pub enum ClsagError {
|
||||||
#[error("internal error ({0})")]
|
#[cfg_attr(feature = "std", error("internal error ({0})"))]
|
||||||
InternalError(&'static str),
|
InternalError(&'static str),
|
||||||
#[error("invalid ring")]
|
#[cfg_attr(feature = "std", error("invalid ring"))]
|
||||||
InvalidRing,
|
InvalidRing,
|
||||||
#[error("invalid ring member (member {0}, ring size {1})")]
|
#[cfg_attr(feature = "std", error("invalid ring member (member {0}, ring size {1})"))]
|
||||||
InvalidRingMember(u8, u8),
|
InvalidRingMember(u8, u8),
|
||||||
#[error("invalid commitment")]
|
#[cfg_attr(feature = "std", error("invalid commitment"))]
|
||||||
InvalidCommitment,
|
InvalidCommitment,
|
||||||
#[error("invalid key image")]
|
#[cfg_attr(feature = "std", error("invalid key image"))]
|
||||||
InvalidImage,
|
InvalidImage,
|
||||||
#[error("invalid D")]
|
#[cfg_attr(feature = "std", error("invalid D"))]
|
||||||
InvalidD,
|
InvalidD,
|
||||||
#[error("invalid s")]
|
#[cfg_attr(feature = "std", error("invalid s"))]
|
||||||
InvalidS,
|
InvalidS,
|
||||||
#[error("invalid c1")]
|
#[cfg_attr(feature = "std", error("invalid c1"))]
|
||||||
InvalidC1,
|
InvalidC1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +96,12 @@ fn core(
|
|||||||
msg: &[u8; 32],
|
msg: &[u8; 32],
|
||||||
D: &EdwardsPoint,
|
D: &EdwardsPoint,
|
||||||
s: &[Scalar],
|
s: &[Scalar],
|
||||||
A_c1: Mode,
|
A_c1: &Mode,
|
||||||
) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
|
) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
|
||||||
let n = ring.len();
|
let n = ring.len();
|
||||||
|
|
||||||
let images_precomp = VartimeEdwardsPrecomputation::new([I, D]);
|
let images_precomp = VartimeEdwardsPrecomputation::new([I, D]);
|
||||||
let D = D * *INV_EIGHT;
|
let D = D * INV_EIGHT();
|
||||||
|
|
||||||
// Generate the transcript
|
// Generate the transcript
|
||||||
// Instead of generating multiple, a single transcript is created and then edited as needed
|
// Instead of generating multiple, a single transcript is created and then edited as needed
|
||||||
@@ -166,12 +164,12 @@ fn core(
|
|||||||
Mode::Verify(c1) => {
|
Mode::Verify(c1) => {
|
||||||
start = 0;
|
start = 0;
|
||||||
end = n;
|
end = n;
|
||||||
c = c1;
|
c = *c1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the core loop
|
// Perform the core loop
|
||||||
let mut c1 = CtOption::new(Scalar::zero(), Choice::from(0));
|
let mut c1 = CtOption::new(Scalar::ZERO, Choice::from(0));
|
||||||
for i in (start .. end).map(|i| i % n) {
|
for i in (start .. end).map(|i| i % n) {
|
||||||
// This will only execute once and shouldn't need to be constant time. Making it constant time
|
// This will only execute once and shouldn't need to be constant time. Making it constant time
|
||||||
// removes the risk of branch prediction creating timing differences depending on ring index
|
// removes the risk of branch prediction creating timing differences depending on ring index
|
||||||
@@ -181,10 +179,10 @@ fn core(
|
|||||||
let c_p = mu_P * c;
|
let c_p = mu_P * c;
|
||||||
let c_c = mu_C * c;
|
let c_c = mu_C * c;
|
||||||
|
|
||||||
let L = (&s[i] * &ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]);
|
let L = (&s[i] * ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]);
|
||||||
let PH = hash_to_point(P[i]);
|
let PH = hash_to_point(&P[i]);
|
||||||
// Shouldn't be an issue as all of the variables in this vartime statement are public
|
// Shouldn't be an issue as all of the variables in this vartime statement are public
|
||||||
let R = (s[i] * PH) + images_precomp.vartime_multiscalar_mul(&[c_p, c_c]);
|
let R = (s[i] * PH) + images_precomp.vartime_multiscalar_mul([c_p, c_c]);
|
||||||
|
|
||||||
to_hash.truncate(((2 * n) + 3) * 32);
|
to_hash.truncate(((2 * n) + 3) * 32);
|
||||||
to_hash.extend(L.compress().to_bytes());
|
to_hash.extend(L.compress().to_bytes());
|
||||||
@@ -221,14 +219,14 @@ impl Clsag {
|
|||||||
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
|
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
|
||||||
let z = input.commitment.mask - mask;
|
let z = input.commitment.mask - mask;
|
||||||
|
|
||||||
let H = hash_to_point(input.decoys.ring[r][0]);
|
let H = hash_to_point(&input.decoys.ring[r][0]);
|
||||||
let D = H * z;
|
let D = H * z;
|
||||||
let mut s = Vec::with_capacity(input.decoys.ring.len());
|
let mut s = Vec::with_capacity(input.decoys.ring.len());
|
||||||
for _ in 0 .. input.decoys.ring.len() {
|
for _ in 0 .. input.decoys.ring.len() {
|
||||||
s.push(random_scalar(rng));
|
s.push(random_scalar(rng));
|
||||||
}
|
}
|
||||||
let ((D, p, c), c1) =
|
let ((D, p, c), c1) =
|
||||||
core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, Mode::Sign(r, A, AH));
|
core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, &Mode::Sign(r, A, AH));
|
||||||
|
|
||||||
(Clsag { D, s, c1 }, pseudo_out, p, c * z)
|
(Clsag { D, s, c1 }, pseudo_out, p, c * z)
|
||||||
}
|
}
|
||||||
@@ -243,7 +241,7 @@ impl Clsag {
|
|||||||
msg: [u8; 32],
|
msg: [u8; 32],
|
||||||
) -> Vec<(Clsag, EdwardsPoint)> {
|
) -> Vec<(Clsag, EdwardsPoint)> {
|
||||||
let mut res = Vec::with_capacity(inputs.len());
|
let mut res = Vec::with_capacity(inputs.len());
|
||||||
let mut sum_pseudo_outs = Scalar::zero();
|
let mut sum_pseudo_outs = Scalar::ZERO;
|
||||||
for i in 0 .. inputs.len() {
|
for i in 0 .. inputs.len() {
|
||||||
let mut mask = random_scalar(rng);
|
let mut mask = random_scalar(rng);
|
||||||
if i == (inputs.len() - 1) {
|
if i == (inputs.len() - 1) {
|
||||||
@@ -259,9 +257,9 @@ impl Clsag {
|
|||||||
&inputs[i].2,
|
&inputs[i].2,
|
||||||
mask,
|
mask,
|
||||||
&msg,
|
&msg,
|
||||||
nonce.deref() * &ED25519_BASEPOINT_TABLE,
|
nonce.deref() * ED25519_BASEPOINT_TABLE,
|
||||||
nonce.deref() *
|
nonce.deref() *
|
||||||
hash_to_point(inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0]),
|
hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0]),
|
||||||
);
|
);
|
||||||
clsag.s[usize::from(inputs[i].2.decoys.i)] =
|
clsag.s[usize::from(inputs[i].2.decoys.i)] =
|
||||||
(-((p * inputs[i].0.deref()) + c)) + nonce.deref();
|
(-((p * inputs[i].0.deref()) + c)) + nonce.deref();
|
||||||
@@ -303,7 +301,7 @@ impl Clsag {
|
|||||||
Err(ClsagError::InvalidD)?;
|
Err(ClsagError::InvalidD)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, c1) = core(ring, I, pseudo_out, msg, &D, &self.s, Mode::Verify(self.c1));
|
let (_, c1) = core(ring, I, pseudo_out, msg, &D, &self.s, &Mode::Verify(self.c1));
|
||||||
if c1 != self.c1 {
|
if c1 != self.c1 {
|
||||||
Err(ClsagError::InvalidC1)?;
|
Err(ClsagError::InvalidC1)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
use core::{ops::Deref, fmt::Debug};
|
use core::{ops::Deref, fmt::Debug};
|
||||||
use std::{
|
use std_shims::io::{self, Read, Write};
|
||||||
io::{self, Read, Write},
|
use std::sync::{Arc, RwLock};
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
|
||||||
use curve25519_dalek::{
|
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
||||||
traits::{Identity, IsIdentity},
|
|
||||||
scalar::Scalar,
|
|
||||||
edwards::EdwardsPoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use group::{ff::Field, Group, GroupEncoding};
|
use group::{ff::Field, Group, GroupEncoding};
|
||||||
|
|
||||||
@@ -23,7 +17,7 @@ use dleq::DLEqProof;
|
|||||||
use frost::{
|
use frost::{
|
||||||
dkg::lagrange,
|
dkg::lagrange,
|
||||||
curve::Ed25519,
|
curve::Ed25519,
|
||||||
FrostError, ThresholdKeys, ThresholdView,
|
Participant, FrostError, ThresholdKeys, ThresholdView,
|
||||||
algorithm::{WriteAddendum, Algorithm},
|
algorithm::{WriteAddendum, Algorithm},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,7 +116,7 @@ impl ClsagMultisig {
|
|||||||
ClsagMultisig {
|
ClsagMultisig {
|
||||||
transcript,
|
transcript,
|
||||||
|
|
||||||
H: hash_to_point(output_key),
|
H: hash_to_point(&output_key),
|
||||||
image: EdwardsPoint::identity(),
|
image: EdwardsPoint::identity(),
|
||||||
|
|
||||||
details,
|
details,
|
||||||
@@ -145,11 +139,11 @@ pub(crate) fn add_key_image_share(
|
|||||||
image: &mut EdwardsPoint,
|
image: &mut EdwardsPoint,
|
||||||
generator: EdwardsPoint,
|
generator: EdwardsPoint,
|
||||||
offset: Scalar,
|
offset: Scalar,
|
||||||
included: &[u16],
|
included: &[Participant],
|
||||||
participant: u16,
|
participant: Participant,
|
||||||
share: EdwardsPoint,
|
share: EdwardsPoint,
|
||||||
) {
|
) {
|
||||||
if image.is_identity() {
|
if image.is_identity().into() {
|
||||||
*image = generator * offset;
|
*image = generator * offset;
|
||||||
}
|
}
|
||||||
*image += share * lagrange::<dfg::Scalar>(participant, included).0;
|
*image += share * lagrange::<dfg::Scalar>(participant, included).0;
|
||||||
@@ -190,10 +184,10 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
reader.read_exact(&mut bytes)?;
|
reader.read_exact(&mut bytes)?;
|
||||||
// dfg ensures the point is torsion free
|
// dfg ensures the point is torsion free
|
||||||
let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
|
let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid key image"))?;
|
.ok_or_else(|| io::Error::other("invalid key image"))?;
|
||||||
// Ensure this is a canonical point
|
// Ensure this is a canonical point
|
||||||
if xH.to_bytes() != bytes {
|
if xH.to_bytes() != bytes {
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "non-canonical key image"))?;
|
Err(io::Error::other("non-canonical key image"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::read(reader)? })
|
Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::read(reader)? })
|
||||||
@@ -202,16 +196,16 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
fn process_addendum(
|
fn process_addendum(
|
||||||
&mut self,
|
&mut self,
|
||||||
view: &ThresholdView<Ed25519>,
|
view: &ThresholdView<Ed25519>,
|
||||||
l: u16,
|
l: Participant,
|
||||||
addendum: ClsagAddendum,
|
addendum: ClsagAddendum,
|
||||||
) -> Result<(), FrostError> {
|
) -> Result<(), FrostError> {
|
||||||
if self.image.is_identity() {
|
if self.image.is_identity().into() {
|
||||||
self.transcript.domain_separate(b"CLSAG");
|
self.transcript.domain_separate(b"CLSAG");
|
||||||
self.input().transcript(&mut self.transcript);
|
self.input().transcript(&mut self.transcript);
|
||||||
self.transcript.append_message(b"mask", self.mask().to_bytes());
|
self.transcript.append_message(b"mask", self.mask().to_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transcript.append_message(b"participant", l.to_be_bytes());
|
self.transcript.append_message(b"participant", l.to_bytes());
|
||||||
|
|
||||||
addendum
|
addendum
|
||||||
.dleq
|
.dleq
|
||||||
@@ -304,7 +298,7 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
Ok(vec![
|
Ok(vec![
|
||||||
(share, dfg::EdwardsPoint::generator()),
|
(share, dfg::EdwardsPoint::generator()),
|
||||||
(dfg::Scalar(interim.p), verification_share),
|
(dfg::Scalar(interim.p), verification_share),
|
||||||
(-dfg::Scalar::one(), nonces[0][0]),
|
(-dfg::Scalar::ONE, nonces[0][0]),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ use curve25519_dalek::edwards::EdwardsPoint;
|
|||||||
pub use monero_generators::{hash_to_point as raw_hash_to_point};
|
pub use monero_generators::{hash_to_point as raw_hash_to_point};
|
||||||
|
|
||||||
/// Monero's hash to point function, as named `ge_fromfe_frombytes_vartime`.
|
/// Monero's hash to point function, as named `ge_fromfe_frombytes_vartime`.
|
||||||
pub fn hash_to_point(key: EdwardsPoint) -> EdwardsPoint {
|
pub fn hash_to_point(key: &EdwardsPoint) -> EdwardsPoint {
|
||||||
raw_hash_to_point(key.compress().to_bytes())
|
raw_hash_to_point(key.compress().to_bytes())
|
||||||
}
|
}
|
||||||
|
|||||||
213
coins/monero/src/ringct/mlsag.rs
Normal file
213
coins/monero/src/ringct/mlsag.rs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
use curve25519_dalek::{traits::IsIdentity, Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use monero_generators::H;
|
||||||
|
|
||||||
|
use crate::{hash_to_scalar, ringct::hash_to_point, serialize::*};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||||
|
pub enum MlsagError {
|
||||||
|
#[cfg_attr(feature = "std", error("invalid ring"))]
|
||||||
|
InvalidRing,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid amount of key images"))]
|
||||||
|
InvalidAmountOfKeyImages,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid ss"))]
|
||||||
|
InvalidSs,
|
||||||
|
#[cfg_attr(feature = "std", error("key image was identity"))]
|
||||||
|
IdentityKeyImage,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid ci"))]
|
||||||
|
InvalidCi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub struct RingMatrix {
|
||||||
|
matrix: Vec<Vec<EdwardsPoint>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RingMatrix {
|
||||||
|
pub fn new(matrix: Vec<Vec<EdwardsPoint>>) -> Result<Self, MlsagError> {
|
||||||
|
if matrix.is_empty() {
|
||||||
|
Err(MlsagError::InvalidRing)?;
|
||||||
|
}
|
||||||
|
for member in &matrix {
|
||||||
|
if member.is_empty() || (member.len() != matrix[0].len()) {
|
||||||
|
Err(MlsagError::InvalidRing)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RingMatrix { matrix })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a ring matrix for an individual output.
|
||||||
|
pub fn individual(
|
||||||
|
ring: &[[EdwardsPoint; 2]],
|
||||||
|
pseudo_out: EdwardsPoint,
|
||||||
|
) -> Result<Self, MlsagError> {
|
||||||
|
let mut matrix = Vec::with_capacity(ring.len());
|
||||||
|
for ring_member in ring {
|
||||||
|
matrix.push(vec![ring_member[0], ring_member[1] - pseudo_out]);
|
||||||
|
}
|
||||||
|
RingMatrix::new(matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &[EdwardsPoint]> {
|
||||||
|
self.matrix.iter().map(AsRef::as_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the amount of members in the ring.
|
||||||
|
pub fn members(&self) -> usize {
|
||||||
|
self.matrix.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the length of a ring member.
|
||||||
|
///
|
||||||
|
/// A ring member is a vector of points for which the signer knows all of the discrete logarithms
|
||||||
|
/// of.
|
||||||
|
pub fn member_len(&self) -> usize {
|
||||||
|
// this is safe to do as the constructors don't allow empty rings
|
||||||
|
self.matrix[0].len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub struct Mlsag {
|
||||||
|
pub ss: Vec<Vec<Scalar>>,
|
||||||
|
pub cc: Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mlsag {
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
for ss in &self.ss {
|
||||||
|
write_raw_vec(write_scalar, ss, w)?;
|
||||||
|
}
|
||||||
|
write_scalar(&self.cc, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(mixins: usize, ss_2_elements: usize, r: &mut R) -> io::Result<Mlsag> {
|
||||||
|
Ok(Mlsag {
|
||||||
|
ss: (0 .. mixins)
|
||||||
|
.map(|_| read_raw_vec(read_scalar, ss_2_elements, r))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
cc: read_scalar(r)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(
|
||||||
|
&self,
|
||||||
|
msg: &[u8; 32],
|
||||||
|
ring: &RingMatrix,
|
||||||
|
key_images: &[EdwardsPoint],
|
||||||
|
) -> Result<(), MlsagError> {
|
||||||
|
// Mlsag allows for layers to not need linkability, hence they don't need key images
|
||||||
|
// Monero requires that there is always only 1 non-linkable layer - the amount commitments.
|
||||||
|
if ring.member_len() != (key_images.len() + 1) {
|
||||||
|
Err(MlsagError::InvalidAmountOfKeyImages)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(6 * 32);
|
||||||
|
buf.extend_from_slice(msg);
|
||||||
|
|
||||||
|
let mut ci = self.cc;
|
||||||
|
|
||||||
|
// This is an iterator over the key images as options with an added entry of `None` at the
|
||||||
|
// end for the non-linkable layer
|
||||||
|
let key_images_iter = key_images.iter().map(|ki| Some(*ki)).chain(core::iter::once(None));
|
||||||
|
|
||||||
|
if ring.matrix.len() != self.ss.len() {
|
||||||
|
Err(MlsagError::InvalidSs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ring_member, ss) in ring.iter().zip(&self.ss) {
|
||||||
|
if ring_member.len() != ss.len() {
|
||||||
|
Err(MlsagError::InvalidSs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((ring_member_entry, s), ki) in ring_member.iter().zip(ss).zip(key_images_iter.clone()) {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let L = EdwardsPoint::vartime_double_scalar_mul_basepoint(&ci, ring_member_entry, s);
|
||||||
|
|
||||||
|
buf.extend_from_slice(ring_member_entry.compress().as_bytes());
|
||||||
|
buf.extend_from_slice(L.compress().as_bytes());
|
||||||
|
|
||||||
|
// Not all dimensions need to be linkable, e.g. commitments, and only linkable layers need
|
||||||
|
// to have key images.
|
||||||
|
if let Some(ki) = ki {
|
||||||
|
if ki.is_identity() {
|
||||||
|
Err(MlsagError::IdentityKeyImage)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let R = (s * hash_to_point(ring_member_entry)) + (ci * ki);
|
||||||
|
buf.extend_from_slice(R.compress().as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ci = hash_to_scalar(&buf);
|
||||||
|
// keep the msg in the buffer.
|
||||||
|
buf.drain(msg.len() ..);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ci != self.cc {
|
||||||
|
Err(MlsagError::InvalidCi)?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An aggregate ring matrix builder, usable to set up the ring matrix to prove/verify an aggregate
|
||||||
|
/// MLSAG signature.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub struct AggregateRingMatrixBuilder {
|
||||||
|
key_ring: Vec<Vec<EdwardsPoint>>,
|
||||||
|
amounts_ring: Vec<EdwardsPoint>,
|
||||||
|
sum_out: EdwardsPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AggregateRingMatrixBuilder {
|
||||||
|
/// Create a new AggregateRingMatrixBuilder.
|
||||||
|
///
|
||||||
|
/// Takes in the transaction's outputs; commitments and fee.
|
||||||
|
pub fn new(commitments: &[EdwardsPoint], fee: u64) -> Self {
|
||||||
|
AggregateRingMatrixBuilder {
|
||||||
|
key_ring: vec![],
|
||||||
|
amounts_ring: vec![],
|
||||||
|
sum_out: commitments.iter().sum::<EdwardsPoint>() + (H() * Scalar::from(fee)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a ring of [output key, commitment] to the matrix.
|
||||||
|
pub fn push_ring(&mut self, ring: &[[EdwardsPoint; 2]]) -> Result<(), MlsagError> {
|
||||||
|
if self.key_ring.is_empty() {
|
||||||
|
self.key_ring = vec![vec![]; ring.len()];
|
||||||
|
// Now that we know the length of the ring, fill the `amounts_ring`.
|
||||||
|
self.amounts_ring = vec![-self.sum_out; ring.len()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.amounts_ring.len() != ring.len()) || ring.is_empty() {
|
||||||
|
// All the rings in an aggregate matrix must be the same length.
|
||||||
|
return Err(MlsagError::InvalidRing);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, ring_member) in ring.iter().enumerate() {
|
||||||
|
self.key_ring[i].push(ring_member[0]);
|
||||||
|
self.amounts_ring[i] += ring_member[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build and return the [`RingMatrix`]
|
||||||
|
pub fn build(mut self) -> Result<RingMatrix, MlsagError> {
|
||||||
|
for (i, amount_commitment) in self.amounts_ring.drain(..).enumerate() {
|
||||||
|
self.key_ring[i].push(amount_commitment);
|
||||||
|
}
|
||||||
|
RingMatrix::new(self.key_ring)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +1,184 @@
|
|||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use std::io::{self, Read, Write};
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
||||||
|
|
||||||
pub(crate) mod hash_to_point;
|
pub(crate) mod hash_to_point;
|
||||||
pub use hash_to_point::{raw_hash_to_point, hash_to_point};
|
pub use hash_to_point::{raw_hash_to_point, hash_to_point};
|
||||||
|
|
||||||
|
/// MLSAG struct, along with verifying functionality.
|
||||||
|
pub mod mlsag;
|
||||||
/// CLSAG struct, along with signing and verifying functionality.
|
/// CLSAG struct, along with signing and verifying functionality.
|
||||||
pub mod clsag;
|
pub mod clsag;
|
||||||
|
/// BorromeanRange struct, along with verifying functionality.
|
||||||
|
pub mod borromean;
|
||||||
/// Bulletproofs(+) structs, along with proving and verifying functionality.
|
/// Bulletproofs(+) structs, along with proving and verifying functionality.
|
||||||
pub mod bulletproofs;
|
pub mod bulletproofs;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Protocol,
|
Protocol,
|
||||||
serialize::*,
|
serialize::*,
|
||||||
ringct::{clsag::Clsag, bulletproofs::Bulletproofs},
|
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproofs},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`.
|
/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`.
|
||||||
pub fn generate_key_image(secret: &Zeroizing<Scalar>) -> EdwardsPoint {
|
pub fn generate_key_image(secret: &Zeroizing<Scalar>) -> EdwardsPoint {
|
||||||
hash_to_point(&ED25519_BASEPOINT_TABLE * secret.deref()) * secret.deref()
|
hash_to_point(&(ED25519_BASEPOINT_TABLE * secret.deref())) * secret.deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum EncryptedAmount {
|
||||||
|
Original { mask: [u8; 32], amount: [u8; 32] },
|
||||||
|
Compact { amount: [u8; 8] },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EncryptedAmount {
|
||||||
|
pub fn read<R: Read>(compact: bool, r: &mut R) -> io::Result<EncryptedAmount> {
|
||||||
|
Ok(if !compact {
|
||||||
|
EncryptedAmount::Original { mask: read_bytes(r)?, amount: read_bytes(r)? }
|
||||||
|
} else {
|
||||||
|
EncryptedAmount::Compact { amount: read_bytes(r)? }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
EncryptedAmount::Original { mask, amount } => {
|
||||||
|
w.write_all(mask)?;
|
||||||
|
w.write_all(amount)
|
||||||
|
}
|
||||||
|
EncryptedAmount::Compact { amount } => w.write_all(amount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub enum RctType {
|
||||||
|
/// No RCT proofs.
|
||||||
|
Null,
|
||||||
|
/// One MLSAG for multiple inputs and Borromean range proofs (RCTTypeFull).
|
||||||
|
MlsagAggregate,
|
||||||
|
// One MLSAG for each input and a Borromean range proof (RCTTypeSimple).
|
||||||
|
MlsagIndividual,
|
||||||
|
// One MLSAG for each input and a Bulletproof (RCTTypeBulletproof).
|
||||||
|
Bulletproofs,
|
||||||
|
/// One MLSAG for each input and a Bulletproof, yet starting to use EncryptedAmount::Compact
|
||||||
|
/// (RCTTypeBulletproof2).
|
||||||
|
BulletproofsCompactAmount,
|
||||||
|
/// One CLSAG for each input and a Bulletproof (RCTTypeCLSAG).
|
||||||
|
Clsag,
|
||||||
|
/// One CLSAG for each input and a Bulletproof+ (RCTTypeBulletproofPlus).
|
||||||
|
BulletproofsPlus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RctType {
|
||||||
|
pub fn to_byte(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
RctType::Null => 0,
|
||||||
|
RctType::MlsagAggregate => 1,
|
||||||
|
RctType::MlsagIndividual => 2,
|
||||||
|
RctType::Bulletproofs => 3,
|
||||||
|
RctType::BulletproofsCompactAmount => 4,
|
||||||
|
RctType::Clsag => 5,
|
||||||
|
RctType::BulletproofsPlus => 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_byte(byte: u8) -> Option<Self> {
|
||||||
|
Some(match byte {
|
||||||
|
0 => RctType::Null,
|
||||||
|
1 => RctType::MlsagAggregate,
|
||||||
|
2 => RctType::MlsagIndividual,
|
||||||
|
3 => RctType::Bulletproofs,
|
||||||
|
4 => RctType::BulletproofsCompactAmount,
|
||||||
|
5 => RctType::Clsag,
|
||||||
|
6 => RctType::BulletproofsPlus,
|
||||||
|
_ => None?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compact_encrypted_amounts(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
RctType::Null |
|
||||||
|
RctType::MlsagAggregate |
|
||||||
|
RctType::MlsagIndividual |
|
||||||
|
RctType::Bulletproofs => false,
|
||||||
|
RctType::BulletproofsCompactAmount | RctType::Clsag | RctType::BulletproofsPlus => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct RctBase {
|
pub struct RctBase {
|
||||||
pub fee: u64,
|
pub fee: u64,
|
||||||
pub ecdh_info: Vec<[u8; 8]>,
|
pub pseudo_outs: Vec<EdwardsPoint>,
|
||||||
|
pub encrypted_amounts: Vec<EncryptedAmount>,
|
||||||
pub commitments: Vec<EdwardsPoint>,
|
pub commitments: Vec<EdwardsPoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RctBase {
|
impl RctBase {
|
||||||
pub(crate) fn fee_weight(outputs: usize) -> usize {
|
pub(crate) fn fee_weight(outputs: usize, fee: u64) -> usize {
|
||||||
1 + 8 + (outputs * (8 + 32))
|
// 1 byte for the RCT signature type
|
||||||
|
1 + (outputs * (8 + 32)) + varint_len(fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W, rct_type: u8) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
||||||
w.write_all(&[rct_type])?;
|
w.write_all(&[rct_type.to_byte()])?;
|
||||||
match rct_type {
|
match rct_type {
|
||||||
0 => Ok(()),
|
RctType::Null => Ok(()),
|
||||||
5 | 6 => {
|
_ => {
|
||||||
write_varint(&self.fee, w)?;
|
write_varint(&self.fee, w)?;
|
||||||
for ecdh in &self.ecdh_info {
|
if rct_type == RctType::MlsagIndividual {
|
||||||
w.write_all(ecdh)?;
|
write_raw_vec(write_point, &self.pseudo_outs, w)?;
|
||||||
|
}
|
||||||
|
for encrypted_amount in &self.encrypted_amounts {
|
||||||
|
encrypted_amount.write(w)?;
|
||||||
}
|
}
|
||||||
write_raw_vec(write_point, &self.commitments, w)
|
write_raw_vec(write_point, &self.commitments, w)
|
||||||
}
|
}
|
||||||
_ => panic!("Serializing unknown RctType's Base"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(outputs: usize, r: &mut R) -> io::Result<(RctBase, u8)> {
|
pub fn read<R: Read>(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(RctBase, RctType)> {
|
||||||
let rct_type = read_byte(r)?;
|
let rct_type =
|
||||||
|
RctType::from_byte(read_byte(r)?).ok_or_else(|| io::Error::other("invalid RCT type"))?;
|
||||||
|
|
||||||
|
match rct_type {
|
||||||
|
RctType::Null | RctType::MlsagAggregate | RctType::MlsagIndividual => {}
|
||||||
|
RctType::Bulletproofs |
|
||||||
|
RctType::BulletproofsCompactAmount |
|
||||||
|
RctType::Clsag |
|
||||||
|
RctType::BulletproofsPlus => {
|
||||||
|
if outputs == 0 {
|
||||||
|
// Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if
|
||||||
|
// Bulletproofs are in use
|
||||||
|
// If there are Bulletproofs, there must be a matching amount of outputs, implicitly
|
||||||
|
// banning 0 outputs
|
||||||
|
// Since HF 12 (CLSAG being 13), a 2-output minimum has also been enforced
|
||||||
|
Err(io::Error::other("RCT with Bulletproofs(+) had 0 outputs"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
if rct_type == 0 {
|
if rct_type == RctType::Null {
|
||||||
RctBase { fee: 0, ecdh_info: vec![], commitments: vec![] }
|
RctBase { fee: 0, pseudo_outs: vec![], encrypted_amounts: vec![], commitments: vec![] }
|
||||||
} else {
|
} else {
|
||||||
RctBase {
|
RctBase {
|
||||||
fee: read_varint(r)?,
|
fee: read_varint(r)?,
|
||||||
ecdh_info: (0 .. outputs).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
|
pseudo_outs: if rct_type == RctType::MlsagIndividual {
|
||||||
|
read_raw_vec(read_point, inputs, r)?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
encrypted_amounts: (0 .. outputs)
|
||||||
|
.map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
commitments: read_raw_vec(read_point, outputs, r)?,
|
commitments: read_raw_vec(read_point, outputs, r)?,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -71,67 +190,139 @@ impl RctBase {
|
|||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum RctPrunable {
|
pub enum RctPrunable {
|
||||||
Null,
|
Null,
|
||||||
Clsag { bulletproofs: Vec<Bulletproofs>, clsags: Vec<Clsag>, pseudo_outs: Vec<EdwardsPoint> },
|
AggregateMlsagBorromean {
|
||||||
|
borromean: Vec<BorromeanRange>,
|
||||||
|
mlsag: Mlsag,
|
||||||
|
},
|
||||||
|
MlsagBorromean {
|
||||||
|
borromean: Vec<BorromeanRange>,
|
||||||
|
mlsags: Vec<Mlsag>,
|
||||||
|
},
|
||||||
|
MlsagBulletproofs {
|
||||||
|
bulletproofs: Bulletproofs,
|
||||||
|
mlsags: Vec<Mlsag>,
|
||||||
|
pseudo_outs: Vec<EdwardsPoint>,
|
||||||
|
},
|
||||||
|
Clsag {
|
||||||
|
bulletproofs: Bulletproofs,
|
||||||
|
clsags: Vec<Clsag>,
|
||||||
|
pseudo_outs: Vec<EdwardsPoint>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RctPrunable {
|
impl RctPrunable {
|
||||||
/// RCT Type byte for a given RctPrunable struct.
|
|
||||||
pub fn rct_type(&self) -> u8 {
|
|
||||||
match self {
|
|
||||||
RctPrunable::Null => 0,
|
|
||||||
RctPrunable::Clsag { bulletproofs, .. } => {
|
|
||||||
if matches!(bulletproofs[0], Bulletproofs::Original { .. }) {
|
|
||||||
5
|
|
||||||
} else {
|
|
||||||
6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize {
|
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize {
|
||||||
|
// 1 byte for number of BPs (technically a VarInt, yet there's always just zero or one)
|
||||||
1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) +
|
1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) +
|
||||||
(inputs * (Clsag::fee_weight(protocol.ring_len()) + 32))
|
(inputs * (Clsag::fee_weight(protocol.ring_len()) + 32))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
RctPrunable::Null => Ok(()),
|
RctPrunable::Null => Ok(()),
|
||||||
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs, .. } => {
|
RctPrunable::AggregateMlsagBorromean { borromean, mlsag } => {
|
||||||
write_vec(Bulletproofs::write, bulletproofs, w)?;
|
write_raw_vec(BorromeanRange::write, borromean, w)?;
|
||||||
|
mlsag.write(w)
|
||||||
|
}
|
||||||
|
RctPrunable::MlsagBorromean { borromean, mlsags } => {
|
||||||
|
write_raw_vec(BorromeanRange::write, borromean, w)?;
|
||||||
|
write_raw_vec(Mlsag::write, mlsags, w)
|
||||||
|
}
|
||||||
|
RctPrunable::MlsagBulletproofs { bulletproofs, mlsags, pseudo_outs } => {
|
||||||
|
if rct_type == RctType::Bulletproofs {
|
||||||
|
w.write_all(&1u32.to_le_bytes())?;
|
||||||
|
} else {
|
||||||
|
w.write_all(&[1])?;
|
||||||
|
}
|
||||||
|
bulletproofs.write(w)?;
|
||||||
|
|
||||||
|
write_raw_vec(Mlsag::write, mlsags, w)?;
|
||||||
|
write_raw_vec(write_point, pseudo_outs, w)
|
||||||
|
}
|
||||||
|
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => {
|
||||||
|
w.write_all(&[1])?;
|
||||||
|
bulletproofs.write(w)?;
|
||||||
|
|
||||||
write_raw_vec(Clsag::write, clsags, w)?;
|
write_raw_vec(Clsag::write, clsags, w)?;
|
||||||
write_raw_vec(write_point, pseudo_outs, w)
|
write_raw_vec(write_point, pseudo_outs, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn serialize(&self, rct_type: RctType) -> Vec<u8> {
|
||||||
let mut serialized = vec![];
|
let mut serialized = vec![];
|
||||||
self.write(&mut serialized).unwrap();
|
self.write(&mut serialized, rct_type).unwrap();
|
||||||
serialized
|
serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(rct_type: u8, decoys: &[usize], r: &mut R) -> io::Result<RctPrunable> {
|
pub fn read<R: Read>(
|
||||||
|
rct_type: RctType,
|
||||||
|
decoys: &[usize],
|
||||||
|
outputs: usize,
|
||||||
|
r: &mut R,
|
||||||
|
) -> io::Result<RctPrunable> {
|
||||||
|
// While we generally don't bother with misc consensus checks, this affects the safety of
|
||||||
|
// the below defined rct_type function
|
||||||
|
// The exact line preventing zero-input transactions is:
|
||||||
|
// https://github.com/monero-project/monero/blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
|
||||||
|
// src/ringct/rctSigs.cpp#L609
|
||||||
|
// And then for RctNull, that's only allowed for miner TXs which require one input of
|
||||||
|
// Input::Gen
|
||||||
|
if decoys.is_empty() {
|
||||||
|
Err(io::Error::other("transaction had no inputs"))?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(match rct_type {
|
Ok(match rct_type {
|
||||||
0 => RctPrunable::Null,
|
RctType::Null => RctPrunable::Null,
|
||||||
5 | 6 => RctPrunable::Clsag {
|
RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean {
|
||||||
bulletproofs: read_vec(
|
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
|
||||||
if rct_type == 5 { Bulletproofs::read } else { Bulletproofs::read_plus },
|
mlsag: Mlsag::read(decoys[0], decoys.len() + 1, r)?,
|
||||||
r,
|
},
|
||||||
)?,
|
RctType::MlsagIndividual => RctPrunable::MlsagBorromean {
|
||||||
|
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
|
||||||
|
mlsags: decoys.iter().map(|d| Mlsag::read(*d, 2, r)).collect::<Result<_, _>>()?,
|
||||||
|
},
|
||||||
|
RctType::Bulletproofs | RctType::BulletproofsCompactAmount => {
|
||||||
|
RctPrunable::MlsagBulletproofs {
|
||||||
|
bulletproofs: {
|
||||||
|
if (if rct_type == RctType::Bulletproofs {
|
||||||
|
u64::from(read_u32(r)?)
|
||||||
|
} else {
|
||||||
|
read_varint(r)?
|
||||||
|
}) != 1
|
||||||
|
{
|
||||||
|
Err(io::Error::other("n bulletproofs instead of one"))?;
|
||||||
|
}
|
||||||
|
Bulletproofs::read(r)?
|
||||||
|
},
|
||||||
|
mlsags: decoys.iter().map(|d| Mlsag::read(*d, 2, r)).collect::<Result<_, _>>()?,
|
||||||
|
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RctType::Clsag | RctType::BulletproofsPlus => RctPrunable::Clsag {
|
||||||
|
bulletproofs: {
|
||||||
|
if read_varint::<_, u64>(r)? != 1 {
|
||||||
|
Err(io::Error::other("n bulletproofs instead of one"))?;
|
||||||
|
}
|
||||||
|
(if rct_type == RctType::Clsag { Bulletproofs::read } else { Bulletproofs::read_plus })(
|
||||||
|
r,
|
||||||
|
)?
|
||||||
|
},
|
||||||
clsags: (0 .. decoys.len()).map(|o| Clsag::read(decoys[o], r)).collect::<Result<_, _>>()?,
|
clsags: (0 .. decoys.len()).map(|o| Clsag::read(decoys[o], r)).collect::<Result<_, _>>()?,
|
||||||
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?,
|
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?,
|
||||||
},
|
},
|
||||||
_ => Err(io::Error::new(io::ErrorKind::Other, "Tried to deserialize unknown RCT type"))?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"),
|
RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"),
|
||||||
RctPrunable::Clsag { bulletproofs, .. } => {
|
RctPrunable::AggregateMlsagBorromean { borromean, .. } |
|
||||||
bulletproofs.iter().try_for_each(|bp| bp.signature_write(w))
|
RctPrunable::MlsagBorromean { borromean, .. } => {
|
||||||
|
borromean.iter().try_for_each(|rs| rs.write(w))
|
||||||
}
|
}
|
||||||
|
RctPrunable::MlsagBulletproofs { bulletproofs, .. } |
|
||||||
|
RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,13 +334,46 @@ pub struct RctSignatures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RctSignatures {
|
impl RctSignatures {
|
||||||
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize {
|
/// RctType for a given RctSignatures struct.
|
||||||
RctBase::fee_weight(outputs) + RctPrunable::fee_weight(protocol, inputs, outputs)
|
pub fn rct_type(&self) -> RctType {
|
||||||
|
match &self.prunable {
|
||||||
|
RctPrunable::Null => RctType::Null,
|
||||||
|
RctPrunable::AggregateMlsagBorromean { .. } => RctType::MlsagAggregate,
|
||||||
|
RctPrunable::MlsagBorromean { .. } => RctType::MlsagIndividual,
|
||||||
|
// RctBase ensures there's at least one output, making the following
|
||||||
|
// inferences guaranteed/expects impossible on any valid RctSignatures
|
||||||
|
RctPrunable::MlsagBulletproofs { .. } => {
|
||||||
|
if matches!(
|
||||||
|
self
|
||||||
|
.base
|
||||||
|
.encrypted_amounts
|
||||||
|
.first()
|
||||||
|
.expect("MLSAG with Bulletproofs didn't have any outputs"),
|
||||||
|
EncryptedAmount::Original { .. }
|
||||||
|
) {
|
||||||
|
RctType::Bulletproofs
|
||||||
|
} else {
|
||||||
|
RctType::BulletproofsCompactAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RctPrunable::Clsag { bulletproofs, .. } => {
|
||||||
|
if matches!(bulletproofs, Bulletproofs::Original { .. }) {
|
||||||
|
RctType::Clsag
|
||||||
|
} else {
|
||||||
|
RctType::BulletproofsPlus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize, fee: u64) -> usize {
|
||||||
|
RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(protocol, inputs, outputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
self.base.write(w, self.prunable.rct_type())?;
|
let rct_type = self.rct_type();
|
||||||
self.prunable.write(w)
|
self.base.write(w, rct_type)?;
|
||||||
|
self.prunable.write(w, rct_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
@@ -158,8 +382,8 @@ impl RctSignatures {
|
|||||||
serialized
|
serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(decoys: Vec<usize>, outputs: usize, r: &mut R) -> io::Result<RctSignatures> {
|
pub fn read<R: Read>(decoys: &[usize], outputs: usize, r: &mut R) -> io::Result<RctSignatures> {
|
||||||
let base = RctBase::read(outputs, r)?;
|
let base = RctBase::read(decoys.len(), outputs, r)?;
|
||||||
Ok(RctSignatures { base: base.0, prunable: RctPrunable::read(base.1, &decoys, r)? })
|
Ok(RctSignatures { base: base.0, prunable: RctPrunable::read(base.1, decoys, outputs, r)? })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,517 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
|
|
||||||
use digest_auth::AuthContext;
|
|
||||||
use reqwest::{Client, RequestBuilder};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Protocol,
|
|
||||||
transaction::{Input, Timelock, Transaction},
|
|
||||||
block::Block,
|
|
||||||
wallet::Fee,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct EmptyResponse {}
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct JsonRpcResponse<T> {
|
|
||||||
result: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct TransactionResponse {
|
|
||||||
tx_hash: String,
|
|
||||||
block_height: Option<usize>,
|
|
||||||
as_hex: String,
|
|
||||||
pruned_as_hex: String,
|
|
||||||
}
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct TransactionsResponse {
|
|
||||||
#[serde(default)]
|
|
||||||
missed_tx: Vec<String>,
|
|
||||||
txs: Vec<TransactionResponse>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub enum RpcError {
|
|
||||||
#[error("internal error ({0})")]
|
|
||||||
InternalError(&'static str),
|
|
||||||
#[error("connection error")]
|
|
||||||
ConnectionError,
|
|
||||||
#[error("invalid node")]
|
|
||||||
InvalidNode,
|
|
||||||
#[error("transactions not found")]
|
|
||||||
TransactionsNotFound(Vec<[u8; 32]>),
|
|
||||||
#[error("invalid point ({0})")]
|
|
||||||
InvalidPoint(String),
|
|
||||||
#[error("pruned transaction")]
|
|
||||||
PrunedTransaction,
|
|
||||||
#[error("invalid transaction ({0:?})")]
|
|
||||||
InvalidTransaction([u8; 32]),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
|
|
||||||
hex::decode(value).map_err(|_| RpcError::InvalidNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash_hex(hash: &str) -> Result<[u8; 32], RpcError> {
|
|
||||||
rpc_hex(hash)?.try_into().map_err(|_| RpcError::InvalidNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
|
||||||
CompressedEdwardsY(
|
|
||||||
rpc_hex(point)?.try_into().map_err(|_| RpcError::InvalidPoint(point.to_string()))?,
|
|
||||||
)
|
|
||||||
.decompress()
|
|
||||||
.ok_or_else(|| RpcError::InvalidPoint(point.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Rpc {
|
|
||||||
client: Client,
|
|
||||||
userpass: Option<(String, String)>,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rpc {
|
|
||||||
/// Create a new RPC connection.
|
|
||||||
/// A daemon requiring authentication can be used via including the username and password in the
|
|
||||||
/// URL.
|
|
||||||
pub fn new(mut url: String) -> Result<Rpc, RpcError> {
|
|
||||||
// Parse out the username and password
|
|
||||||
let userpass = if url.contains('@') {
|
|
||||||
let url_clone = url.clone();
|
|
||||||
let split_url = url_clone.split('@').collect::<Vec<_>>();
|
|
||||||
if split_url.len() != 2 {
|
|
||||||
Err(RpcError::InvalidNode)?;
|
|
||||||
}
|
|
||||||
let mut userpass = split_url[0];
|
|
||||||
url = split_url[1].to_string();
|
|
||||||
|
|
||||||
// If there was additionally a protocol string, restore that to the daemon URL
|
|
||||||
if userpass.contains("://") {
|
|
||||||
let split_userpass = userpass.split("://").collect::<Vec<_>>();
|
|
||||||
if split_userpass.len() != 2 {
|
|
||||||
Err(RpcError::InvalidNode)?;
|
|
||||||
}
|
|
||||||
url = split_userpass[0].to_string() + "://" + &url;
|
|
||||||
userpass = split_userpass[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
let split_userpass = userpass.split(':').collect::<Vec<_>>();
|
|
||||||
if split_userpass.len() != 2 {
|
|
||||||
Err(RpcError::InvalidNode)?;
|
|
||||||
}
|
|
||||||
Some((split_userpass[0].to_string(), split_userpass[1].to_string()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Rpc { client: Client::new(), userpass, url })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform a RPC call to the specified method with the provided parameters.
|
|
||||||
/// This is NOT a JSON-RPC call, which use a method of "json_rpc" and are available via
|
|
||||||
/// `json_rpc_call`.
|
|
||||||
pub async fn rpc_call<Params: Serialize + Debug, Response: DeserializeOwned + Debug>(
|
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
params: Option<Params>,
|
|
||||||
) -> Result<Response, RpcError> {
|
|
||||||
let mut builder = self.client.post(self.url.clone() + "/" + method);
|
|
||||||
if let Some(params) = params.as_ref() {
|
|
||||||
builder = builder.json(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.call_tail(method, builder).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform a JSON-RPC call to the specified method with the provided parameters
|
|
||||||
pub async fn json_rpc_call<Response: DeserializeOwned + Debug>(
|
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
params: Option<Value>,
|
|
||||||
) -> Result<Response, RpcError> {
|
|
||||||
let mut req = json!({ "method": method });
|
|
||||||
if let Some(params) = params {
|
|
||||||
req.as_object_mut().unwrap().insert("params".into(), params);
|
|
||||||
}
|
|
||||||
Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform a binary call to the specified method with the provided parameters.
|
|
||||||
pub async fn bin_call<Response: DeserializeOwned + Debug>(
|
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
params: Vec<u8>,
|
|
||||||
) -> Result<Response, RpcError> {
|
|
||||||
let builder = self.client.post(self.url.clone() + "/" + method).body(params.clone());
|
|
||||||
self.call_tail(method, builder.header("Content-Type", "application/octet-stream")).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call_tail<Response: DeserializeOwned + Debug>(
|
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
mut builder: RequestBuilder,
|
|
||||||
) -> Result<Response, RpcError> {
|
|
||||||
if let Some((user, pass)) = &self.userpass {
|
|
||||||
let req = self.client.post(&self.url).send().await.map_err(|_| RpcError::InvalidNode)?;
|
|
||||||
// Only provide authentication if this daemon actually expects it
|
|
||||||
if let Some(header) = req.headers().get("www-authenticate") {
|
|
||||||
builder = builder.header(
|
|
||||||
"Authorization",
|
|
||||||
digest_auth::parse(header.to_str().map_err(|_| RpcError::InvalidNode)?)
|
|
||||||
.map_err(|_| RpcError::InvalidNode)?
|
|
||||||
.respond(&AuthContext::new_post::<_, _, _, &[u8]>(
|
|
||||||
user,
|
|
||||||
pass,
|
|
||||||
"/".to_string() + method,
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.map_err(|_| RpcError::InvalidNode)?
|
|
||||||
.to_header_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = builder.send().await.map_err(|_| RpcError::ConnectionError)?;
|
|
||||||
|
|
||||||
Ok(if !method.ends_with(".bin") {
|
|
||||||
serde_json::from_str(&res.text().await.map_err(|_| RpcError::ConnectionError)?)
|
|
||||||
.map_err(|_| RpcError::InternalError("Failed to parse JSON response"))?
|
|
||||||
} else {
|
|
||||||
monero_epee_bin_serde::from_bytes(&res.bytes().await.map_err(|_| RpcError::ConnectionError)?)
|
|
||||||
.map_err(|_| RpcError::InternalError("Failed to parse binary response"))?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the active blockchain protocol version.
|
|
||||||
pub async fn get_protocol(&self) -> Result<Protocol, RpcError> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct ProtocolResponse {
|
|
||||||
major_version: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct LastHeaderResponse {
|
|
||||||
block_header: ProtocolResponse,
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
match self
|
|
||||||
.json_rpc_call::<LastHeaderResponse>("get_last_block_header", None)
|
|
||||||
.await?
|
|
||||||
.block_header
|
|
||||||
.major_version
|
|
||||||
{
|
|
||||||
13 | 14 => Protocol::v14,
|
|
||||||
15 | 16 => Protocol::v16,
|
|
||||||
version => Protocol::Unsupported(version),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_height(&self) -> Result<usize, RpcError> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct HeightResponse {
|
|
||||||
height: usize,
|
|
||||||
}
|
|
||||||
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
|
|
||||||
if hashes.is_empty() {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let txs: TransactionsResponse = self
|
|
||||||
.rpc_call(
|
|
||||||
"get_transactions",
|
|
||||||
Some(json!({
|
|
||||||
"txs_hashes": hashes.iter().map(hex::encode).collect::<Vec<_>>()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !txs.missed_tx.is_empty() {
|
|
||||||
Err(RpcError::TransactionsNotFound(
|
|
||||||
txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
txs
|
|
||||||
.txs
|
|
||||||
.iter()
|
|
||||||
.map(|res| {
|
|
||||||
let tx = Transaction::read::<&[u8]>(
|
|
||||||
&mut rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.map_err(|_| match hash_hex(&res.tx_hash) {
|
|
||||||
Ok(hash) => RpcError::InvalidTransaction(hash),
|
|
||||||
Err(err) => err,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// https://github.com/monero-project/monero/issues/8311
|
|
||||||
if res.as_hex.is_empty() {
|
|
||||||
match tx.prefix.inputs.get(0) {
|
|
||||||
Some(Input::Gen { .. }) => (),
|
|
||||||
_ => Err(RpcError::PrunedTransaction)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tx)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_transaction(&self, tx: [u8; 32]) -> Result<Transaction, RpcError> {
|
|
||||||
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_transaction_block_number(&self, tx: &[u8]) -> Result<Option<usize>, RpcError> {
|
|
||||||
let txs: TransactionsResponse =
|
|
||||||
self.rpc_call("get_transactions", Some(json!({ "txs_hashes": [hex::encode(tx)] }))).await?;
|
|
||||||
|
|
||||||
if !txs.missed_tx.is_empty() {
|
|
||||||
Err(RpcError::TransactionsNotFound(
|
|
||||||
txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(txs.txs[0].block_height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct BlockHeaderResponse {
|
|
||||||
hash: String,
|
|
||||||
}
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct BlockHeaderByHeightResponse {
|
|
||||||
block_header: BlockHeaderResponse,
|
|
||||||
}
|
|
||||||
|
|
||||||
let header: BlockHeaderByHeightResponse =
|
|
||||||
self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?;
|
|
||||||
rpc_hex(&header.block_header.hash)?.try_into().map_err(|_| RpcError::InvalidNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_block(&self, hash: [u8; 32]) -> Result<Block, RpcError> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct BlockResponse {
|
|
||||||
blob: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let res: BlockResponse =
|
|
||||||
self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?;
|
|
||||||
|
|
||||||
Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()).map_err(|_| RpcError::InvalidNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_block_by_number(&self, number: usize) -> Result<Block, RpcError> {
|
|
||||||
self.get_block(self.get_block_hash(number).await?).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_block_transactions(&self, hash: [u8; 32]) -> Result<Vec<Transaction>, RpcError> {
|
|
||||||
let block = self.get_block(hash).await?;
|
|
||||||
let mut res = vec![block.miner_tx];
|
|
||||||
res.extend(self.get_transactions(&block.txs).await?);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_block_transactions_by_number(
|
|
||||||
&self,
|
|
||||||
number: usize,
|
|
||||||
) -> Result<Vec<Transaction>, RpcError> {
|
|
||||||
self.get_block_transactions(self.get_block_hash(number).await?).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the output indexes of the specified transaction.
|
|
||||||
pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
struct Request {
|
|
||||||
txid: [u8; 32],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct OIndexes {
|
|
||||||
o_indexes: Vec<u64>,
|
|
||||||
status: String,
|
|
||||||
untrusted: bool,
|
|
||||||
credits: usize,
|
|
||||||
top_hash: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let indexes: OIndexes = self
|
|
||||||
.bin_call(
|
|
||||||
"get_o_indexes.bin",
|
|
||||||
monero_epee_bin_serde::to_bytes(&Request { txid: hash }).unwrap(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(indexes.o_indexes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the output distribution, from the specified height to the specified height (both
|
|
||||||
/// inclusive).
|
|
||||||
pub async fn get_output_distribution(
|
|
||||||
&self,
|
|
||||||
from: usize,
|
|
||||||
to: usize,
|
|
||||||
) -> Result<Vec<u64>, RpcError> {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Distribution {
|
|
||||||
distribution: Vec<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Distributions {
|
|
||||||
distributions: Vec<Distribution>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut distributions: Distributions = self
|
|
||||||
.json_rpc_call(
|
|
||||||
"get_output_distribution",
|
|
||||||
Some(json!({
|
|
||||||
"binary": false,
|
|
||||||
"amounts": [0],
|
|
||||||
"cumulative": true,
|
|
||||||
"from_height": from,
|
|
||||||
"to_height": to,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(distributions.distributions.swap_remove(0).distribution)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if they're
|
|
||||||
/// unlocked.
|
|
||||||
pub async fn get_unlocked_outputs(
|
|
||||||
&self,
|
|
||||||
indexes: &[u64],
|
|
||||||
height: usize,
|
|
||||||
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Out {
|
|
||||||
key: String,
|
|
||||||
mask: String,
|
|
||||||
txid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Outs {
|
|
||||||
outs: Vec<Out>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let outs: Outs = self
|
|
||||||
.rpc_call(
|
|
||||||
"get_outs",
|
|
||||||
Some(json!({
|
|
||||||
"get_txid": true,
|
|
||||||
"outputs": indexes.iter().map(|o| json!({
|
|
||||||
"amount": 0,
|
|
||||||
"index": o
|
|
||||||
})).collect::<Vec<_>>()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let txs = self
|
|
||||||
.get_transactions(
|
|
||||||
&outs
|
|
||||||
.outs
|
|
||||||
.iter()
|
|
||||||
.map(|out| rpc_hex(&out.txid)?.try_into().map_err(|_| RpcError::InvalidNode))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: https://github.com/serai-dex/serai/issues/104
|
|
||||||
outs
|
|
||||||
.outs
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, out)| {
|
|
||||||
Ok(Some([rpc_point(&out.key)?, rpc_point(&out.mask)?]).filter(|_| {
|
|
||||||
match txs[i].prefix.timelock {
|
|
||||||
Timelock::Block(t_height) => t_height <= height,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the currently estimated fee from the node. This may be manipulated to unsafe levels and
|
|
||||||
/// MUST be sanity checked.
|
|
||||||
// TODO: Take a sanity check argument
|
|
||||||
pub async fn get_fee(&self) -> Result<Fee, RpcError> {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct FeeResponse {
|
|
||||||
fee: u64,
|
|
||||||
quantization_mask: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
let res: FeeResponse = self.json_rpc_call("get_fee_estimate", None).await?;
|
|
||||||
Ok(Fee { per_weight: res.fee, mask: res.quantization_mask })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct SendRawResponse {
|
|
||||||
status: String,
|
|
||||||
double_spend: bool,
|
|
||||||
fee_too_low: bool,
|
|
||||||
invalid_input: bool,
|
|
||||||
invalid_output: bool,
|
|
||||||
low_mixin: bool,
|
|
||||||
not_relayed: bool,
|
|
||||||
overspend: bool,
|
|
||||||
too_big: bool,
|
|
||||||
too_few_outputs: bool,
|
|
||||||
reason: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(2048);
|
|
||||||
tx.write(&mut buf).unwrap();
|
|
||||||
let res: SendRawResponse = self
|
|
||||||
.rpc_call("send_raw_transaction", Some(json!({ "tx_as_hex": hex::encode(&buf) })))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if res.status != "OK" {
|
|
||||||
Err(RpcError::InvalidTransaction(tx.hash()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_blocks(&self, address: &str, block_count: usize) -> Result<(), RpcError> {
|
|
||||||
self
|
|
||||||
.rpc_call::<_, EmptyResponse>(
|
|
||||||
"json_rpc",
|
|
||||||
Some(json!({
|
|
||||||
"method": "generateblocks",
|
|
||||||
"params": {
|
|
||||||
"wallet_address": address,
|
|
||||||
"amount_of_blocks": block_count
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
286
coins/monero/src/rpc/http.rs
Normal file
286
coins/monero/src/rpc/http.rs
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
use std::{sync::Arc, io::Read, time::Duration};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use digest_auth::{WwwAuthenticateHeader, AuthContext};
|
||||||
|
use simple_request::{
|
||||||
|
hyper::{StatusCode, header::HeaderValue, Request},
|
||||||
|
Response, Client,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::rpc::{RpcError, RpcConnection, Rpc};
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum Authentication {
|
||||||
|
// If unauthenticated, use a single client
|
||||||
|
Unauthenticated(Client),
|
||||||
|
// If authenticated, use a single client which supports being locked and tracks its nonce
|
||||||
|
// This ensures that if a nonce is requested, another caller doesn't make a request invalidating
|
||||||
|
// it
|
||||||
|
Authenticated {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
connection: Arc<Mutex<(Option<(WwwAuthenticateHeader, u64)>, Client)>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An HTTP(S) transport for the RPC.
|
||||||
|
///
|
||||||
|
/// Requires tokio.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct HttpRpc {
|
||||||
|
authentication: Authentication,
|
||||||
|
url: String,
|
||||||
|
request_timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpRpc {
|
||||||
|
fn digest_auth_challenge(
|
||||||
|
response: &Response,
|
||||||
|
) -> Result<Option<(WwwAuthenticateHeader, u64)>, RpcError> {
|
||||||
|
Ok(if let Some(header) = response.headers().get("www-authenticate") {
|
||||||
|
Some((
|
||||||
|
digest_auth::parse(header.to_str().map_err(|_| {
|
||||||
|
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
||||||
|
})?)
|
||||||
|
.map_err(|_| RpcError::InvalidNode("invalid digest-auth response".to_string()))?,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new HTTP(S) RPC connection.
|
||||||
|
///
|
||||||
|
/// A daemon requiring authentication can be used via including the username and password in the
|
||||||
|
/// URL.
|
||||||
|
pub async fn new(url: String) -> Result<Rpc<HttpRpc>, RpcError> {
|
||||||
|
Self::with_custom_timeout(url, DEFAULT_TIMEOUT).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new HTTP(S) RPC connection with a custom timeout.
|
||||||
|
///
|
||||||
|
/// A daemon requiring authentication can be used via including the username and password in the
|
||||||
|
/// URL.
|
||||||
|
pub async fn with_custom_timeout(
|
||||||
|
mut url: String,
|
||||||
|
request_timeout: Duration,
|
||||||
|
) -> Result<Rpc<HttpRpc>, RpcError> {
|
||||||
|
let authentication = if url.contains('@') {
|
||||||
|
// Parse out the username and password
|
||||||
|
let url_clone = url;
|
||||||
|
let split_url = url_clone.split('@').collect::<Vec<_>>();
|
||||||
|
if split_url.len() != 2 {
|
||||||
|
Err(RpcError::ConnectionError("invalid amount of login specifications".to_string()))?;
|
||||||
|
}
|
||||||
|
let mut userpass = split_url[0];
|
||||||
|
url = split_url[1].to_string();
|
||||||
|
|
||||||
|
// If there was additionally a protocol string, restore that to the daemon URL
|
||||||
|
if userpass.contains("://") {
|
||||||
|
let split_userpass = userpass.split("://").collect::<Vec<_>>();
|
||||||
|
if split_userpass.len() != 2 {
|
||||||
|
Err(RpcError::ConnectionError("invalid amount of protocol specifications".to_string()))?;
|
||||||
|
}
|
||||||
|
url = split_userpass[0].to_string() + "://" + &url;
|
||||||
|
userpass = split_userpass[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let split_userpass = userpass.split(':').collect::<Vec<_>>();
|
||||||
|
if split_userpass.len() > 2 {
|
||||||
|
Err(RpcError::ConnectionError("invalid amount of passwords".to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = Client::without_connection_pool(&url)
|
||||||
|
.map_err(|_| RpcError::ConnectionError("invalid URL".to_string()))?;
|
||||||
|
// Obtain the initial challenge, which also somewhat validates this connection
|
||||||
|
let challenge = Self::digest_auth_challenge(
|
||||||
|
&client
|
||||||
|
.request(
|
||||||
|
Request::post(url.clone())
|
||||||
|
.body(vec![].into())
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))?,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
||||||
|
)?;
|
||||||
|
Authentication::Authenticated {
|
||||||
|
username: split_userpass[0].to_string(),
|
||||||
|
password: (*split_userpass.get(1).unwrap_or(&"")).to_string(),
|
||||||
|
connection: Arc::new(Mutex::new((challenge, client))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Authentication::Unauthenticated(Client::with_connection_pool())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Rpc(HttpRpc { authentication, url, request_timeout }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpRpc {
|
||||||
|
async fn inner_post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
||||||
|
let request_fn = |uri| {
|
||||||
|
Request::post(uri)
|
||||||
|
.body(body.clone().into())
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn body_from_response(response: Response<'_>) -> Result<Vec<u8>, RpcError> {
|
||||||
|
/*
|
||||||
|
let length = usize::try_from(
|
||||||
|
response
|
||||||
|
.headers()
|
||||||
|
.get("content-length")
|
||||||
|
.ok_or(RpcError::InvalidNode("no content-length header"))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| RpcError::InvalidNode("non-ascii content-length value"))?
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|_| RpcError::InvalidNode("non-u32 content-length value"))?,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Only pre-allocate 1 MB so a malicious node which claims a content-length of 1 GB actually
|
||||||
|
// has to send 1 GB of data to cause a 1 GB allocation
|
||||||
|
let mut res = Vec::with_capacity(length.max(1024 * 1024));
|
||||||
|
let mut body = response.into_body();
|
||||||
|
while res.len() < length {
|
||||||
|
let Some(data) = body.data().await else { break };
|
||||||
|
res.extend(data.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?.as_ref());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let mut res = Vec::with_capacity(128);
|
||||||
|
response
|
||||||
|
.body()
|
||||||
|
.await
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
||||||
|
.read_to_end(&mut res)
|
||||||
|
.unwrap();
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
for attempt in 0 .. 2 {
|
||||||
|
return Ok(match &self.authentication {
|
||||||
|
Authentication::Unauthenticated(client) => {
|
||||||
|
body_from_response(
|
||||||
|
client
|
||||||
|
.request(request_fn(self.url.clone() + "/" + route)?)
|
||||||
|
.await
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
Authentication::Authenticated { username, password, connection } => {
|
||||||
|
let mut connection_lock = connection.lock().await;
|
||||||
|
|
||||||
|
let mut request = request_fn("/".to_string() + route)?;
|
||||||
|
|
||||||
|
// If we don't have an auth challenge, obtain one
|
||||||
|
if connection_lock.0.is_none() {
|
||||||
|
connection_lock.0 = Self::digest_auth_challenge(
|
||||||
|
&connection_lock
|
||||||
|
.1
|
||||||
|
.request(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
||||||
|
)?;
|
||||||
|
request = request_fn("/".to_string() + route)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the challenge response, if we have a challenge
|
||||||
|
if let Some((challenge, cnonce)) = connection_lock.0.as_mut() {
|
||||||
|
// Update the cnonce
|
||||||
|
// Overflow isn't a concern as this is a u64
|
||||||
|
*cnonce += 1;
|
||||||
|
|
||||||
|
let mut context = AuthContext::new_post::<_, _, _, &[u8]>(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
"/".to_string() + route,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
context.set_custom_cnonce(hex::encode(cnonce.to_le_bytes()));
|
||||||
|
|
||||||
|
request.headers_mut().insert(
|
||||||
|
"Authorization",
|
||||||
|
HeaderValue::from_str(
|
||||||
|
&challenge
|
||||||
|
.respond(&context)
|
||||||
|
.map_err(|_| {
|
||||||
|
RpcError::InvalidNode("couldn't respond to digest-auth challenge".to_string())
|
||||||
|
})?
|
||||||
|
.to_header_string(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = connection_lock
|
||||||
|
.1
|
||||||
|
.request(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")));
|
||||||
|
|
||||||
|
let (error, is_stale) = match &response {
|
||||||
|
Err(e) => (Some(e.clone()), false),
|
||||||
|
Ok(response) => (
|
||||||
|
None,
|
||||||
|
if response.status() == StatusCode::UNAUTHORIZED {
|
||||||
|
if let Some(header) = response.headers().get("www-authenticate") {
|
||||||
|
header
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| {
|
||||||
|
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
||||||
|
})?
|
||||||
|
.contains("stale")
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the connection entered an error state, drop the cached challenge as challenges are
|
||||||
|
// per-connection
|
||||||
|
// We don't need to create a new connection as simple-request will for us
|
||||||
|
if error.is_some() || is_stale {
|
||||||
|
connection_lock.0 = None;
|
||||||
|
// If we're not already on our second attempt, move to the next loop iteration
|
||||||
|
// (retrying all of this once)
|
||||||
|
if attempt == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(e) = error {
|
||||||
|
Err(e)?
|
||||||
|
} else {
|
||||||
|
debug_assert!(is_stale);
|
||||||
|
Err(RpcError::InvalidNode(
|
||||||
|
"node claimed fresh connection had stale authentication".to_string(),
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body_from_response(response.unwrap()).await?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RpcConnection for HttpRpc {
|
||||||
|
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
||||||
|
tokio::time::timeout(self.request_timeout, self.inner_post(route, body))
|
||||||
|
.await
|
||||||
|
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
||||||
|
}
|
||||||
|
}
|
||||||
739
coins/monero/src/rpc/mod.rs
Normal file
739
coins/monero/src/rpc/mod.rs
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io,
|
||||||
|
string::{String, ToString},
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Protocol,
|
||||||
|
serialize::*,
|
||||||
|
transaction::{Input, Timelock, Transaction},
|
||||||
|
block::Block,
|
||||||
|
wallet::{FeePriority, Fee},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "http-rpc")]
|
||||||
|
mod http;
|
||||||
|
#[cfg(feature = "http-rpc")]
|
||||||
|
pub use http::*;
|
||||||
|
|
||||||
|
// Number of blocks the fee estimate will be valid for
|
||||||
|
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
||||||
|
// src/wallet/wallet2.cpp#L121
|
||||||
|
const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct EmptyResponse {}
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct JsonRpcResponse<T> {
|
||||||
|
result: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct TransactionResponse {
|
||||||
|
tx_hash: String,
|
||||||
|
as_hex: String,
|
||||||
|
pruned_as_hex: String,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct TransactionsResponse {
|
||||||
|
#[serde(default)]
|
||||||
|
missed_tx: Vec<String>,
|
||||||
|
txs: Vec<TransactionResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||||
|
pub enum RpcError {
|
||||||
|
#[cfg_attr(feature = "std", error("internal error ({0})"))]
|
||||||
|
InternalError(&'static str),
|
||||||
|
#[cfg_attr(feature = "std", error("connection error ({0})"))]
|
||||||
|
ConnectionError(String),
|
||||||
|
#[cfg_attr(feature = "std", error("invalid node ({0})"))]
|
||||||
|
InvalidNode(String),
|
||||||
|
#[cfg_attr(feature = "std", error("unsupported protocol version ({0})"))]
|
||||||
|
UnsupportedProtocol(usize),
|
||||||
|
#[cfg_attr(feature = "std", error("transactions not found"))]
|
||||||
|
TransactionsNotFound(Vec<[u8; 32]>),
|
||||||
|
#[cfg_attr(feature = "std", error("invalid point ({0})"))]
|
||||||
|
InvalidPoint(String),
|
||||||
|
#[cfg_attr(feature = "std", error("pruned transaction"))]
|
||||||
|
PrunedTransaction,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))]
|
||||||
|
InvalidTransaction([u8; 32]),
|
||||||
|
#[cfg_attr(feature = "std", error("unexpected fee response"))]
|
||||||
|
InvalidFee,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid priority"))]
|
||||||
|
InvalidPriority,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
|
||||||
|
hex::decode(value).map_err(|_| RpcError::InvalidNode("expected hex wasn't hex".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_hex(hash: &str) -> Result<[u8; 32], RpcError> {
|
||||||
|
rpc_hex(hash)?.try_into().map_err(|_| RpcError::InvalidNode("hash wasn't 32-bytes".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
||||||
|
CompressedEdwardsY(
|
||||||
|
rpc_hex(point)?.try_into().map_err(|_| RpcError::InvalidPoint(point.to_string()))?,
|
||||||
|
)
|
||||||
|
.decompress()
|
||||||
|
.ok_or_else(|| RpcError::InvalidPoint(point.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read an EPEE VarInt, distinct from the VarInts used throughout the rest of the protocol
|
||||||
|
fn read_epee_vi<R: io::Read>(reader: &mut R) -> io::Result<u64> {
|
||||||
|
let vi_start = read_byte(reader)?;
|
||||||
|
let len = match vi_start & 0b11 {
|
||||||
|
0 => 1,
|
||||||
|
1 => 2,
|
||||||
|
2 => 4,
|
||||||
|
3 => 8,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let mut vi = u64::from(vi_start >> 2);
|
||||||
|
for i in 1 .. len {
|
||||||
|
vi |= u64::from(read_byte(reader)?) << (((i - 1) * 8) + 6);
|
||||||
|
}
|
||||||
|
Ok(vi)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RpcConnection: Clone + Debug {
|
||||||
|
/// Perform a POST request to the specified route with the specified body.
|
||||||
|
///
|
||||||
|
/// The implementor is left to handle anything such as authentication.
|
||||||
|
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this provided methods for RpcConnection?
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Rpc<R: RpcConnection>(R);
|
||||||
|
impl<R: RpcConnection> Rpc<R> {
|
||||||
|
/// Perform a RPC call to the specified route with the provided parameters.
|
||||||
|
///
|
||||||
|
/// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via
|
||||||
|
/// `json_rpc_call`.
|
||||||
|
pub async fn rpc_call<Params: Serialize + Debug, Response: DeserializeOwned + Debug>(
|
||||||
|
&self,
|
||||||
|
route: &str,
|
||||||
|
params: Option<Params>,
|
||||||
|
) -> Result<Response, RpcError> {
|
||||||
|
let res = self
|
||||||
|
.0
|
||||||
|
.post(
|
||||||
|
route,
|
||||||
|
if let Some(params) = params {
|
||||||
|
serde_json::to_string(¶ms).unwrap().into_bytes()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let res_str = std_shims::str::from_utf8(&res)
|
||||||
|
.map_err(|_| RpcError::InvalidNode("response wasn't utf-8".to_string()))?;
|
||||||
|
serde_json::from_str(res_str)
|
||||||
|
.map_err(|_| RpcError::InvalidNode(format!("response wasn't json: {res_str}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a JSON-RPC call with the specified method with the provided parameters
|
||||||
|
pub async fn json_rpc_call<Response: DeserializeOwned + Debug>(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
params: Option<Value>,
|
||||||
|
) -> Result<Response, RpcError> {
|
||||||
|
let mut req = json!({ "method": method });
|
||||||
|
if let Some(params) = params {
|
||||||
|
req.as_object_mut().unwrap().insert("params".into(), params);
|
||||||
|
}
|
||||||
|
Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a binary call to the specified route with the provided parameters.
|
||||||
|
pub async fn bin_call(&self, route: &str, params: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
||||||
|
self.0.post(route, params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the active blockchain protocol version.
|
||||||
|
pub async fn get_protocol(&self) -> Result<Protocol, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ProtocolResponse {
|
||||||
|
major_version: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct LastHeaderResponse {
|
||||||
|
block_header: ProtocolResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
match self
|
||||||
|
.json_rpc_call::<LastHeaderResponse>("get_last_block_header", None)
|
||||||
|
.await?
|
||||||
|
.block_header
|
||||||
|
.major_version
|
||||||
|
{
|
||||||
|
13 | 14 => Protocol::v14,
|
||||||
|
15 | 16 => Protocol::v16,
|
||||||
|
protocol => Err(RpcError::UnsupportedProtocol(protocol))?,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_height(&self) -> Result<usize, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct HeightResponse {
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
if hashes.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
|
||||||
|
let mut all_txs = Vec::with_capacity(hashes.len());
|
||||||
|
while !hashes_hex.is_empty() {
|
||||||
|
// Monero errors if more than 100 is requested unless using a non-restricted RPC
|
||||||
|
const TXS_PER_REQUEST: usize = 100;
|
||||||
|
let this_count = TXS_PER_REQUEST.min(hashes_hex.len());
|
||||||
|
|
||||||
|
let txs: TransactionsResponse = self
|
||||||
|
.rpc_call(
|
||||||
|
"get_transactions",
|
||||||
|
Some(json!({
|
||||||
|
"txs_hashes": hashes_hex.drain(.. this_count).collect::<Vec<_>>(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !txs.missed_tx.is_empty() {
|
||||||
|
Err(RpcError::TransactionsNotFound(
|
||||||
|
txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_txs.extend(txs.txs);
|
||||||
|
}
|
||||||
|
|
||||||
|
all_txs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, res)| {
|
||||||
|
let tx = Transaction::read::<&[u8]>(
|
||||||
|
&mut rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.map_err(|_| match hash_hex(&res.tx_hash) {
|
||||||
|
Ok(hash) => RpcError::InvalidTransaction(hash),
|
||||||
|
Err(err) => err,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// https://github.com/monero-project/monero/issues/8311
|
||||||
|
if res.as_hex.is_empty() {
|
||||||
|
match tx.prefix.inputs.first() {
|
||||||
|
Some(Input::Gen { .. }) => (),
|
||||||
|
_ => Err(RpcError::PrunedTransaction)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This does run a few keccak256 hashes, which is pointless if the node is trusted
|
||||||
|
// In exchange, this provides resilience against invalid/malicious nodes
|
||||||
|
if tx.hash() != hashes[i] {
|
||||||
|
Err(RpcError::InvalidNode(
|
||||||
|
"replied with transaction wasn't the requested transaction".to_string(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_transaction(&self, tx: [u8; 32]) -> Result<Transaction, RpcError> {
|
||||||
|
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the hash of a block from the node by the block's numbers.
|
||||||
|
/// This function does not verify the returned block hash is actually for the number in question.
|
||||||
|
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockHeaderResponse {
|
||||||
|
hash: String,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockHeaderByHeightResponse {
|
||||||
|
block_header: BlockHeaderResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
let header: BlockHeaderByHeightResponse =
|
||||||
|
self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?;
|
||||||
|
hash_hex(&header.block_header.hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a block from the node by its hash.
|
||||||
|
/// This function does not verify the returned block actually has the hash in question.
|
||||||
|
pub async fn get_block(&self, hash: [u8; 32]) -> Result<Block, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockResponse {
|
||||||
|
blob: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: BlockResponse =
|
||||||
|
self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?;
|
||||||
|
|
||||||
|
let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref())
|
||||||
|
.map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
|
||||||
|
if block.hash() != hash {
|
||||||
|
Err(RpcError::InvalidNode("different block than requested (hash)".to_string()))?;
|
||||||
|
}
|
||||||
|
Ok(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_by_number(&self, number: usize) -> Result<Block, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockResponse {
|
||||||
|
blob: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: BlockResponse =
|
||||||
|
self.json_rpc_call("get_block", Some(json!({ "height": number }))).await?;
|
||||||
|
|
||||||
|
let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref())
|
||||||
|
.map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
|
||||||
|
|
||||||
|
// Make sure this is actually the block for this number
|
||||||
|
match block.miner_tx.prefix.inputs.first() {
|
||||||
|
Some(Input::Gen(actual)) => {
|
||||||
|
if usize::try_from(*actual).unwrap() == number {
|
||||||
|
Ok(block)
|
||||||
|
} else {
|
||||||
|
Err(RpcError::InvalidNode("different block than requested (number)".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(RpcError::InvalidNode(
|
||||||
|
"block's miner_tx didn't have an input of kind Input::Gen".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_transactions(&self, hash: [u8; 32]) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
let block = self.get_block(hash).await?;
|
||||||
|
let mut res = vec![block.miner_tx];
|
||||||
|
res.extend(self.get_transactions(&block.txs).await?);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_transactions_by_number(
|
||||||
|
&self,
|
||||||
|
number: usize,
|
||||||
|
) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
self.get_block_transactions(self.get_block_hash(number).await?).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the output indexes of the specified transaction.
|
||||||
|
pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
|
||||||
|
/*
|
||||||
|
TODO: Use these when a suitable epee serde lib exists
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct Request {
|
||||||
|
txid: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct OIndexes {
|
||||||
|
o_indexes: Vec<u64>,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated
|
||||||
|
// to work against this specific function
|
||||||
|
|
||||||
|
// Header for EPEE, an 8-byte magic and a version
|
||||||
|
const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01";
|
||||||
|
|
||||||
|
let mut request = EPEE_HEADER.to_vec();
|
||||||
|
// Number of fields (shifted over 2 bits as the 2 LSBs are reserved for metadata)
|
||||||
|
request.push(1 << 2);
|
||||||
|
// Length of field name
|
||||||
|
request.push(4);
|
||||||
|
// Field name
|
||||||
|
request.extend(b"txid");
|
||||||
|
// Type of field
|
||||||
|
request.push(10);
|
||||||
|
// Length of string, since this byte array is technically a string
|
||||||
|
request.push(32 << 2);
|
||||||
|
// The "string"
|
||||||
|
request.extend(hash);
|
||||||
|
|
||||||
|
let indexes_buf = self.bin_call("get_o_indexes.bin", request).await?;
|
||||||
|
let mut indexes: &[u8] = indexes_buf.as_ref();
|
||||||
|
|
||||||
|
(|| {
|
||||||
|
let mut res = None;
|
||||||
|
let mut is_okay = false;
|
||||||
|
|
||||||
|
if read_bytes::<_, { EPEE_HEADER.len() }>(&mut indexes)? != EPEE_HEADER {
|
||||||
|
Err(io::Error::other("invalid header"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let read_object = |reader: &mut &[u8]| -> io::Result<Vec<u64>> {
|
||||||
|
let fields = read_byte(reader)? >> 2;
|
||||||
|
|
||||||
|
for _ in 0 .. fields {
|
||||||
|
let name_len = read_byte(reader)?;
|
||||||
|
let name = read_raw_vec(read_byte, name_len.into(), reader)?;
|
||||||
|
|
||||||
|
let type_with_array_flag = read_byte(reader)?;
|
||||||
|
let kind = type_with_array_flag & (!0x80);
|
||||||
|
|
||||||
|
let iters = if type_with_array_flag != kind { read_epee_vi(reader)? } else { 1 };
|
||||||
|
|
||||||
|
if (&name == b"o_indexes") && (kind != 5) {
|
||||||
|
Err(io::Error::other("o_indexes weren't u64s"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let f = match kind {
|
||||||
|
// i64
|
||||||
|
1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
|
||||||
|
// i32
|
||||||
|
2 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
|
||||||
|
// i16
|
||||||
|
3 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
|
||||||
|
// i8
|
||||||
|
4 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
|
||||||
|
// u64
|
||||||
|
5 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
|
||||||
|
// u32
|
||||||
|
6 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
|
||||||
|
// u16
|
||||||
|
7 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
|
||||||
|
// u8
|
||||||
|
8 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
|
||||||
|
// double
|
||||||
|
9 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
|
||||||
|
// string, or any collection of bytes
|
||||||
|
10 => |reader: &mut &[u8]| {
|
||||||
|
let len = read_epee_vi(reader)?;
|
||||||
|
read_raw_vec(
|
||||||
|
read_byte,
|
||||||
|
len.try_into().map_err(|_| io::Error::other("u64 length exceeded usize"))?,
|
||||||
|
reader,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// bool
|
||||||
|
11 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
|
||||||
|
// object, errors here as it shouldn't be used on this call
|
||||||
|
12 => {
|
||||||
|
|_: &mut &[u8]| Err(io::Error::other("node used object in reply to get_o_indexes"))
|
||||||
|
}
|
||||||
|
// array, so far unused
|
||||||
|
13 => |_: &mut &[u8]| Err(io::Error::other("node used the unused array type")),
|
||||||
|
_ => |_: &mut &[u8]| Err(io::Error::other("node used an invalid type")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut bytes_res = vec![];
|
||||||
|
for _ in 0 .. iters {
|
||||||
|
bytes_res.push(f(reader)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut actual_res = Vec::with_capacity(bytes_res.len());
|
||||||
|
match name.as_slice() {
|
||||||
|
b"o_indexes" => {
|
||||||
|
for o_index in bytes_res {
|
||||||
|
actual_res.push(u64::from_le_bytes(
|
||||||
|
o_index
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| io::Error::other("node didn't provide 8 bytes for a u64"))?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
res = Some(actual_res);
|
||||||
|
}
|
||||||
|
b"status" => {
|
||||||
|
if bytes_res
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| io::Error::other("status wasn't a string"))?
|
||||||
|
.as_slice() !=
|
||||||
|
b"OK"
|
||||||
|
{
|
||||||
|
// TODO: Better handle non-OK responses
|
||||||
|
Err(io::Error::other("response wasn't OK"))?;
|
||||||
|
}
|
||||||
|
is_okay = true;
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_okay && res.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't return a response with a status
|
||||||
|
// (if the status wasn't okay, we would've already errored)
|
||||||
|
if !is_okay {
|
||||||
|
Err(io::Error::other("response didn't contain a status"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the Vec was empty, it would've been omitted, hence the unwrap_or
|
||||||
|
// TODO: Test against a 0-output TX, such as the ones found in block 202612
|
||||||
|
Ok(res.unwrap_or(vec![]))
|
||||||
|
};
|
||||||
|
|
||||||
|
read_object(&mut indexes)
|
||||||
|
})()
|
||||||
|
.map_err(|_| RpcError::InvalidNode("invalid binary response".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the output distribution, from the specified height to the specified height (both
|
||||||
|
/// inclusive).
|
||||||
|
pub async fn get_output_distribution(
|
||||||
|
&self,
|
||||||
|
from: usize,
|
||||||
|
to: usize,
|
||||||
|
) -> Result<Vec<u64>, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Distribution {
|
||||||
|
distribution: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Distributions {
|
||||||
|
distributions: Vec<Distribution>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut distributions: Distributions = self
|
||||||
|
.json_rpc_call(
|
||||||
|
"get_output_distribution",
|
||||||
|
Some(json!({
|
||||||
|
"binary": false,
|
||||||
|
"amounts": [0],
|
||||||
|
"cumulative": true,
|
||||||
|
"from_height": from,
|
||||||
|
"to_height": to,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(distributions.distributions.swap_remove(0).distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
|
||||||
|
/// timelock has been satisfied.
|
||||||
|
///
|
||||||
|
/// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
|
||||||
|
/// Monero transactions.
|
||||||
|
pub async fn get_unlocked_outputs(
|
||||||
|
&self,
|
||||||
|
indexes: &[u64],
|
||||||
|
height: usize,
|
||||||
|
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Out {
|
||||||
|
key: String,
|
||||||
|
mask: String,
|
||||||
|
txid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Outs {
|
||||||
|
outs: Vec<Out>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let outs: Outs = self
|
||||||
|
.rpc_call(
|
||||||
|
"get_outs",
|
||||||
|
Some(json!({
|
||||||
|
"get_txid": true,
|
||||||
|
"outputs": indexes.iter().map(|o| json!({
|
||||||
|
"amount": 0,
|
||||||
|
"index": o
|
||||||
|
})).collect::<Vec<_>>()
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let txs = self
|
||||||
|
.get_transactions(
|
||||||
|
&outs.outs.iter().map(|out| hash_hex(&out.txid)).collect::<Result<Vec<_>, _>>()?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO: https://github.com/serai-dex/serai/issues/104
|
||||||
|
outs
|
||||||
|
.outs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, out)| {
|
||||||
|
// Allow keys to be invalid, though if they are, return None to trigger selection of a new
|
||||||
|
// decoy
|
||||||
|
// Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet
|
||||||
|
// invalid keys may honestly exist on the blockchain
|
||||||
|
// Only a recent hard fork checked output keys were valid points
|
||||||
|
let Some(key) = CompressedEdwardsY(
|
||||||
|
rpc_hex(&out.key)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| RpcError::InvalidNode("non-32-byte point".to_string()))?,
|
||||||
|
)
|
||||||
|
.decompress() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(
|
||||||
|
Some([key, rpc_point(&out.mask)?])
|
||||||
|
.filter(|_| Timelock::Block(height) >= txs[i].prefix.timelock),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_fee_v14(&self, priority: FeePriority) -> Result<Fee, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct FeeResponseV14 {
|
||||||
|
status: String,
|
||||||
|
fee: u64,
|
||||||
|
quantization_mask: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
||||||
|
// src/wallet/wallet2.cpp#L7569-L7584
|
||||||
|
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
||||||
|
// src/wallet/wallet2.cpp#L7660-L7661
|
||||||
|
let priority_idx =
|
||||||
|
usize::try_from(if priority.fee_priority() == 0 { 1 } else { priority.fee_priority() - 1 })
|
||||||
|
.map_err(|_| RpcError::InvalidPriority)?;
|
||||||
|
let multipliers = [1, 5, 25, 1000];
|
||||||
|
if priority_idx >= multipliers.len() {
|
||||||
|
// though not an RPC error, it seems sensible to treat as such
|
||||||
|
Err(RpcError::InvalidPriority)?;
|
||||||
|
}
|
||||||
|
let fee_multiplier = multipliers[priority_idx];
|
||||||
|
|
||||||
|
let res: FeeResponseV14 = self
|
||||||
|
.json_rpc_call(
|
||||||
|
"get_fee_estimate",
|
||||||
|
Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if res.status != "OK" {
|
||||||
|
Err(RpcError::InvalidFee)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Fee { per_weight: res.fee * fee_multiplier, mask: res.quantization_mask })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the currently estimated fee from the node.
|
||||||
|
///
|
||||||
|
/// This may be manipulated to unsafe levels and MUST be sanity checked.
|
||||||
|
// TODO: Take a sanity check argument
|
||||||
|
pub async fn get_fee(&self, protocol: Protocol, priority: FeePriority) -> Result<Fee, RpcError> {
|
||||||
|
// TODO: Implement wallet2's adjust_priority which by default automatically uses a lower
|
||||||
|
// priority than provided depending on the backlog in the pool
|
||||||
|
if protocol.v16_fee() {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct FeeResponse {
|
||||||
|
status: String,
|
||||||
|
fees: Vec<u64>,
|
||||||
|
quantization_mask: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: FeeResponse = self
|
||||||
|
.json_rpc_call(
|
||||||
|
"get_fee_estimate",
|
||||||
|
Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
||||||
|
// src/wallet/wallet2.cpp#L7615-L7620
|
||||||
|
let priority_idx = usize::try_from(if priority.fee_priority() >= 4 {
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
priority.fee_priority().saturating_sub(1)
|
||||||
|
})
|
||||||
|
.map_err(|_| RpcError::InvalidPriority)?;
|
||||||
|
|
||||||
|
if res.status != "OK" {
|
||||||
|
Err(RpcError::InvalidFee)
|
||||||
|
} else if priority_idx >= res.fees.len() {
|
||||||
|
Err(RpcError::InvalidPriority)
|
||||||
|
} else {
|
||||||
|
Ok(Fee { per_weight: res.fees[priority_idx], mask: res.quantization_mask })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.get_fee_v14(priority).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct SendRawResponse {
|
||||||
|
status: String,
|
||||||
|
double_spend: bool,
|
||||||
|
fee_too_low: bool,
|
||||||
|
invalid_input: bool,
|
||||||
|
invalid_output: bool,
|
||||||
|
low_mixin: bool,
|
||||||
|
not_relayed: bool,
|
||||||
|
overspend: bool,
|
||||||
|
too_big: bool,
|
||||||
|
too_few_outputs: bool,
|
||||||
|
reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: SendRawResponse = self
|
||||||
|
.rpc_call("send_raw_transaction", Some(json!({ "tx_as_hex": hex::encode(tx.serialize()) })))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if res.status != "OK" {
|
||||||
|
Err(RpcError::InvalidTransaction(tx.hash()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Take &Address, not &str?
|
||||||
|
pub async fn generate_blocks(
|
||||||
|
&self,
|
||||||
|
address: &str,
|
||||||
|
block_count: usize,
|
||||||
|
) -> Result<Vec<[u8; 32]>, RpcError> {
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BlocksResponse {
|
||||||
|
blocks: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let block_strs = self
|
||||||
|
.json_rpc_call::<BlocksResponse>(
|
||||||
|
"generateblocks",
|
||||||
|
Some(json!({
|
||||||
|
"wallet_address": address,
|
||||||
|
"amount_of_blocks": block_count
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.blocks;
|
||||||
|
|
||||||
|
let mut blocks = Vec::with_capacity(block_strs.len());
|
||||||
|
for block in block_strs {
|
||||||
|
blocks.push(hash_hex(&block)?);
|
||||||
|
}
|
||||||
|
Ok(blocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
use std::io::{self, Read, Write};
|
use core::fmt::Debug;
|
||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use curve25519_dalek::{
|
use curve25519_dalek::{
|
||||||
scalar::Scalar,
|
scalar::Scalar,
|
||||||
@@ -7,16 +11,27 @@ use curve25519_dalek::{
|
|||||||
|
|
||||||
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
||||||
|
|
||||||
pub(crate) fn varint_len(varint: usize) -> usize {
|
mod sealed {
|
||||||
((usize::try_from(usize::BITS - varint.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1
|
pub trait VarInt: TryInto<u64> + TryFrom<u64> + Copy {}
|
||||||
|
impl VarInt for u8 {}
|
||||||
|
impl VarInt for u32 {}
|
||||||
|
impl VarInt for u64 {}
|
||||||
|
impl VarInt for usize {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will panic if the VarInt exceeds u64::MAX
|
||||||
|
pub(crate) fn varint_len<U: sealed::VarInt>(varint: U) -> usize {
|
||||||
|
let varint_u64: u64 = varint.try_into().map_err(|_| "varint exceeded u64").unwrap();
|
||||||
|
((usize::try_from(u64::BITS - varint_u64.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn write_byte<W: Write>(byte: &u8, w: &mut W) -> io::Result<()> {
|
pub(crate) fn write_byte<W: Write>(byte: &u8, w: &mut W) -> io::Result<()> {
|
||||||
w.write_all(&[*byte])
|
w.write_all(&[*byte])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn write_varint<W: Write>(varint: &u64, w: &mut W) -> io::Result<()> {
|
// This will panic if the VarInt exceeds u64::MAX
|
||||||
let mut varint = *varint;
|
pub(crate) fn write_varint<W: Write, U: sealed::VarInt>(varint: &U, w: &mut W) -> io::Result<()> {
|
||||||
|
let mut varint: u64 = (*varint).try_into().map_err(|_| "varint exceeded u64").unwrap();
|
||||||
while {
|
while {
|
||||||
let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap();
|
let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap();
|
||||||
varint >>= 7;
|
varint >>= 7;
|
||||||
@@ -53,7 +68,7 @@ pub(crate) fn write_vec<T, W: Write, F: Fn(&T, &mut W) -> io::Result<()>>(
|
|||||||
values: &[T],
|
values: &[T],
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
write_varint(&values.len().try_into().unwrap(), w)?;
|
write_varint(&values.len(), w)?;
|
||||||
write_raw_vec(f, values, w)
|
write_raw_vec(f, values, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,31 +82,35 @@ pub(crate) fn read_byte<R: Read>(r: &mut R) -> io::Result<u8> {
|
|||||||
Ok(read_bytes::<_, 1>(r)?[0])
|
Ok(read_bytes::<_, 1>(r)?[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_u64<R: Read>(r: &mut R) -> io::Result<u64> {
|
pub(crate) fn read_u16<R: Read>(r: &mut R) -> io::Result<u16> {
|
||||||
read_bytes(r).map(u64::from_le_bytes)
|
read_bytes(r).map(u16::from_le_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_u32<R: Read>(r: &mut R) -> io::Result<u32> {
|
pub(crate) fn read_u32<R: Read>(r: &mut R) -> io::Result<u32> {
|
||||||
read_bytes(r).map(u32::from_le_bytes)
|
read_bytes(r).map(u32::from_le_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_varint<R: Read>(r: &mut R) -> io::Result<u64> {
|
pub(crate) fn read_u64<R: Read>(r: &mut R) -> io::Result<u64> {
|
||||||
|
read_bytes(r).map(u64::from_le_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_varint<R: Read, U: sealed::VarInt>(r: &mut R) -> io::Result<U> {
|
||||||
let mut bits = 0;
|
let mut bits = 0;
|
||||||
let mut res = 0;
|
let mut res = 0;
|
||||||
while {
|
while {
|
||||||
let b = read_byte(r)?;
|
let b = read_byte(r)?;
|
||||||
if (bits != 0) && (b == 0) {
|
if (bits != 0) && (b == 0) {
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "non-canonical varint"))?;
|
Err(io::Error::other("non-canonical varint"))?;
|
||||||
}
|
}
|
||||||
if ((bits + 7) > 64) && (b >= (1 << (64 - bits))) {
|
if ((bits + 7) > 64) && (b >= (1 << (64 - bits))) {
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "varint overflow"))?;
|
Err(io::Error::other("varint overflow"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
res += u64::from(b & (!VARINT_CONTINUATION_MASK)) << bits;
|
res += u64::from(b & (!VARINT_CONTINUATION_MASK)) << bits;
|
||||||
bits += 7;
|
bits += 7;
|
||||||
b & VARINT_CONTINUATION_MASK == VARINT_CONTINUATION_MASK
|
b & VARINT_CONTINUATION_MASK == VARINT_CONTINUATION_MASK
|
||||||
} {}
|
} {}
|
||||||
Ok(res)
|
res.try_into().map_err(|_| io::Error::other("VarInt does not fit into integer type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// All scalar fields supported by monero-serai are checked to be canonical for valid transactions
|
// All scalar fields supported by monero-serai are checked to be canonical for valid transactions
|
||||||
@@ -101,8 +120,8 @@ pub(crate) fn read_varint<R: Read>(r: &mut R) -> io::Result<u64> {
|
|||||||
// https://github.com/monero-project/monero/issues/8438, where some scalars had an archaic
|
// https://github.com/monero-project/monero/issues/8438, where some scalars had an archaic
|
||||||
// reduction applied
|
// reduction applied
|
||||||
pub(crate) fn read_scalar<R: Read>(r: &mut R) -> io::Result<Scalar> {
|
pub(crate) fn read_scalar<R: Read>(r: &mut R) -> io::Result<Scalar> {
|
||||||
Scalar::from_canonical_bytes(read_bytes(r)?)
|
Option::from(Scalar::from_canonical_bytes(read_bytes(r)?))
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "unreduced scalar"))
|
.ok_or_else(|| io::Error::other("unreduced scalar"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
pub(crate) fn read_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
||||||
@@ -111,14 +130,14 @@ pub(crate) fn read_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
|||||||
.decompress()
|
.decompress()
|
||||||
// Ban points which are either unreduced or -0
|
// Ban points which are either unreduced or -0
|
||||||
.filter(|point| point.compress().to_bytes() == bytes)
|
.filter(|point| point.compress().to_bytes() == bytes)
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))
|
.ok_or_else(|| io::Error::other("invalid point"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_torsion_free_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
pub(crate) fn read_torsion_free_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
||||||
read_point(r)
|
read_point(r)
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|point| point.is_torsion_free())
|
.filter(EdwardsPoint::is_torsion_free)
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))
|
.ok_or_else(|| io::Error::other("invalid point"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_raw_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
pub(crate) fn read_raw_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
||||||
@@ -133,9 +152,16 @@ pub(crate) fn read_raw_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_array<R: Read, T: Debug, F: Fn(&mut R) -> io::Result<T>, const N: usize>(
|
||||||
|
f: F,
|
||||||
|
r: &mut R,
|
||||||
|
) -> io::Result<[T; N]> {
|
||||||
|
read_raw_vec(f, N, r).map(|vec| vec.try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn read_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
pub(crate) fn read_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
||||||
f: F,
|
f: F,
|
||||||
r: &mut R,
|
r: &mut R,
|
||||||
) -> io::Result<Vec<T>> {
|
) -> io::Result<Vec<T>> {
|
||||||
read_raw_vec(f, read_varint(r)?.try_into().unwrap(), r)
|
read_raw_vec(f, read_varint(r)?, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ fn standard_address() {
|
|||||||
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
|
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
|
||||||
assert_eq!(addr.meta.network, Network::Mainnet);
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
assert_eq!(addr.meta.kind, AddressType::Standard);
|
assert_eq!(addr.meta.kind, AddressType::Standard);
|
||||||
assert!(!addr.meta.kind.subaddress());
|
assert!(!addr.meta.kind.is_subaddress());
|
||||||
assert_eq!(addr.meta.kind.payment_id(), None);
|
assert_eq!(addr.meta.kind.payment_id(), None);
|
||||||
assert!(!addr.meta.kind.guaranteed());
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
assert_eq!(addr.to_string(), STANDARD);
|
assert_eq!(addr.to_string(), STANDARD);
|
||||||
@@ -46,9 +46,9 @@ fn integrated_address() {
|
|||||||
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
|
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
|
||||||
assert_eq!(addr.meta.network, Network::Mainnet);
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
|
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
|
||||||
assert!(!addr.meta.kind.subaddress());
|
assert!(!addr.meta.kind.is_subaddress());
|
||||||
assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID));
|
assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID));
|
||||||
assert!(!addr.meta.kind.guaranteed());
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
assert_eq!(addr.to_string(), INTEGRATED);
|
assert_eq!(addr.to_string(), INTEGRATED);
|
||||||
@@ -59,9 +59,9 @@ fn subaddress() {
|
|||||||
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
|
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
|
||||||
assert_eq!(addr.meta.network, Network::Mainnet);
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
assert_eq!(addr.meta.kind, AddressType::Subaddress);
|
assert_eq!(addr.meta.kind, AddressType::Subaddress);
|
||||||
assert!(addr.meta.kind.subaddress());
|
assert!(addr.meta.kind.is_subaddress());
|
||||||
assert_eq!(addr.meta.kind.payment_id(), None);
|
assert_eq!(addr.meta.kind.payment_id(), None);
|
||||||
assert!(!addr.meta.kind.guaranteed());
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
||||||
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
||||||
assert_eq!(addr.to_string(), SUBADDRESS);
|
assert_eq!(addr.to_string(), SUBADDRESS);
|
||||||
@@ -73,8 +73,8 @@ fn featured() {
|
|||||||
[(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')]
|
[(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')]
|
||||||
{
|
{
|
||||||
for _ in 0 .. 100 {
|
for _ in 0 .. 100 {
|
||||||
let spend = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
|
let spend = &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
||||||
let view = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
|
let view = &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
||||||
|
|
||||||
for features in 0 .. (1 << 3) {
|
for features in 0 .. (1 << 3) {
|
||||||
const SUBADDRESS_FEATURE_BIT: u8 = 1;
|
const SUBADDRESS_FEATURE_BIT: u8 = 1;
|
||||||
@@ -100,9 +100,9 @@ fn featured() {
|
|||||||
assert_eq!(addr.spend, spend);
|
assert_eq!(addr.spend, spend);
|
||||||
assert_eq!(addr.view, view);
|
assert_eq!(addr.view, view);
|
||||||
|
|
||||||
assert_eq!(addr.subaddress(), subaddress);
|
assert_eq!(addr.is_subaddress(), subaddress);
|
||||||
assert_eq!(addr.payment_id(), payment_id);
|
assert_eq!(addr.payment_id(), payment_id);
|
||||||
assert_eq!(addr.guaranteed(), guaranteed);
|
assert_eq!(addr.is_guaranteed(), guaranteed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,19 +142,23 @@ fn featured_vectors() {
|
|||||||
}
|
}
|
||||||
_ => panic!("Unknown network"),
|
_ => panic!("Unknown network"),
|
||||||
};
|
};
|
||||||
let spend =
|
let spend = CompressedEdwardsY::from_slice(&hex::decode(vector.spend).unwrap())
|
||||||
CompressedEdwardsY::from_slice(&hex::decode(vector.spend).unwrap()).decompress().unwrap();
|
.unwrap()
|
||||||
let view =
|
.decompress()
|
||||||
CompressedEdwardsY::from_slice(&hex::decode(vector.view).unwrap()).decompress().unwrap();
|
.unwrap();
|
||||||
|
let view = CompressedEdwardsY::from_slice(&hex::decode(vector.view).unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.decompress()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let addr = MoneroAddress::from_str(network, &vector.address).unwrap();
|
let addr = MoneroAddress::from_str(network, &vector.address).unwrap();
|
||||||
assert_eq!(addr.spend, spend);
|
assert_eq!(addr.spend, spend);
|
||||||
assert_eq!(addr.view, view);
|
assert_eq!(addr.view, view);
|
||||||
|
|
||||||
assert_eq!(addr.subaddress(), vector.subaddress);
|
assert_eq!(addr.is_subaddress(), vector.subaddress);
|
||||||
assert_eq!(vector.integrated, vector.payment_id.is_some());
|
assert_eq!(vector.integrated, vector.payment_id.is_some());
|
||||||
assert_eq!(addr.payment_id(), vector.payment_id);
|
assert_eq!(addr.payment_id(), vector.payment_id);
|
||||||
assert_eq!(addr.guaranteed(), vector.guaranteed);
|
assert_eq!(addr.is_guaranteed(), vector.guaranteed);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MoneroAddress::new(
|
MoneroAddress::new(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use hex_literal::hex;
|
use hex_literal::hex;
|
||||||
use rand::rngs::OsRng;
|
use rand_core::OsRng;
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar, edwards::CompressedEdwardsY};
|
use curve25519_dalek::{scalar::Scalar, edwards::CompressedEdwardsY};
|
||||||
use multiexp::BatchVerifier;
|
use multiexp::BatchVerifier;
|
||||||
@@ -9,6 +9,8 @@ use crate::{
|
|||||||
ringct::bulletproofs::{Bulletproofs, original::OriginalStruct},
|
ringct::bulletproofs::{Bulletproofs, original::OriginalStruct},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod plus;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bulletproofs_vector() {
|
fn bulletproofs_vector() {
|
||||||
let scalar = |scalar| Scalar::from_canonical_bytes(scalar).unwrap();
|
let scalar = |scalar| Scalar::from_canonical_bytes(scalar).unwrap();
|
||||||
@@ -62,7 +64,7 @@ macro_rules! bulletproofs_tests {
|
|||||||
fn $name() {
|
fn $name() {
|
||||||
// Create Bulletproofs for all possible output quantities
|
// Create Bulletproofs for all possible output quantities
|
||||||
let mut verifier = BatchVerifier::new(16);
|
let mut verifier = BatchVerifier::new(16);
|
||||||
for i in 1 .. 17 {
|
for i in 1 ..= 16 {
|
||||||
let commitments = (1 ..= i)
|
let commitments = (1 ..= i)
|
||||||
.map(|i| Commitment::new(random_scalar(&mut OsRng), u64::try_from(i).unwrap()))
|
.map(|i| Commitment::new(random_scalar(&mut OsRng), u64::try_from(i).unwrap()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -81,7 +83,7 @@ macro_rules! bulletproofs_tests {
|
|||||||
// Check Bulletproofs errors if we try to prove for too many outputs
|
// Check Bulletproofs errors if we try to prove for too many outputs
|
||||||
let mut commitments = vec![];
|
let mut commitments = vec![];
|
||||||
for _ in 0 .. 17 {
|
for _ in 0 .. 17 {
|
||||||
commitments.push(Commitment::new(Scalar::zero(), 0));
|
commitments.push(Commitment::new(Scalar::ZERO, 0));
|
||||||
}
|
}
|
||||||
assert!(Bulletproofs::prove(&mut OsRng, &commitments, $plus).is_err());
|
assert!(Bulletproofs::prove(&mut OsRng, &commitments, $plus).is_err());
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
|
use multiexp::BatchVerifier;
|
||||||
|
use group::ff::Field;
|
||||||
|
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Commitment,
|
||||||
|
ringct::bulletproofs::plus::aggregate_range_proof::{
|
||||||
|
AggregateRangeStatement, AggregateRangeWitness,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aggregate_range_proof() {
|
||||||
|
let mut verifier = BatchVerifier::new(16);
|
||||||
|
for m in 1 ..= 16 {
|
||||||
|
let mut commitments = vec![];
|
||||||
|
for _ in 0 .. m {
|
||||||
|
commitments.push(Commitment::new(*Scalar::random(&mut OsRng), OsRng.next_u64()));
|
||||||
|
}
|
||||||
|
let commitment_points = commitments.iter().map(|com| EdwardsPoint(com.calculate())).collect();
|
||||||
|
let statement = AggregateRangeStatement::new(commitment_points).unwrap();
|
||||||
|
let witness = AggregateRangeWitness::new(&commitments).unwrap();
|
||||||
|
|
||||||
|
let proof = statement.clone().prove(&mut OsRng, &witness).unwrap();
|
||||||
|
statement.verify(&mut OsRng, &mut verifier, (), proof);
|
||||||
|
}
|
||||||
|
assert!(verifier.verify_vartime());
|
||||||
|
}
|
||||||
4
coins/monero/src/tests/bulletproofs/plus/mod.rs
Normal file
4
coins/monero/src/tests/bulletproofs/plus/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod weighted_inner_product;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod aggregate_range_proof;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// The inner product relation is P = sum(g_bold * a, h_bold * b, g * (a * y * b), h * alpha)
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use multiexp::BatchVerifier;
|
||||||
|
use group::{ff::Field, Group};
|
||||||
|
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use crate::ringct::bulletproofs::plus::{
|
||||||
|
ScalarVector, PointVector, GeneratorsList, Generators,
|
||||||
|
weighted_inner_product::{WipStatement, WipWitness},
|
||||||
|
weighted_inner_product,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zero_weighted_inner_product() {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let P = EdwardsPoint::identity();
|
||||||
|
let y = Scalar::random(&mut OsRng);
|
||||||
|
|
||||||
|
let generators = Generators::new().reduce(1);
|
||||||
|
let statement = WipStatement::new(generators, P, y);
|
||||||
|
let witness = WipWitness::new(ScalarVector::new(1), ScalarVector::new(1), Scalar::ZERO).unwrap();
|
||||||
|
|
||||||
|
let transcript = Scalar::random(&mut OsRng);
|
||||||
|
let proof = statement.clone().prove(&mut OsRng, transcript, &witness).unwrap();
|
||||||
|
|
||||||
|
let mut verifier = BatchVerifier::new(1);
|
||||||
|
statement.verify(&mut OsRng, &mut verifier, (), transcript, proof);
|
||||||
|
assert!(verifier.verify_vartime());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_weighted_inner_product() {
|
||||||
|
// P = sum(g_bold * a, h_bold * b, g * (a * y * b), h * alpha)
|
||||||
|
let mut verifier = BatchVerifier::new(6);
|
||||||
|
let generators = Generators::new();
|
||||||
|
for i in [1, 2, 4, 8, 16, 32] {
|
||||||
|
let generators = generators.reduce(i);
|
||||||
|
let g = Generators::g();
|
||||||
|
let h = Generators::h();
|
||||||
|
assert_eq!(generators.len(), i);
|
||||||
|
let mut g_bold = vec![];
|
||||||
|
let mut h_bold = vec![];
|
||||||
|
for i in 0 .. i {
|
||||||
|
g_bold.push(generators.generator(GeneratorsList::GBold1, i));
|
||||||
|
h_bold.push(generators.generator(GeneratorsList::HBold1, i));
|
||||||
|
}
|
||||||
|
let g_bold = PointVector(g_bold);
|
||||||
|
let h_bold = PointVector(h_bold);
|
||||||
|
|
||||||
|
let mut a = ScalarVector::new(i);
|
||||||
|
let mut b = ScalarVector::new(i);
|
||||||
|
let alpha = Scalar::random(&mut OsRng);
|
||||||
|
|
||||||
|
let y = Scalar::random(&mut OsRng);
|
||||||
|
let mut y_vec = ScalarVector::new(g_bold.len());
|
||||||
|
y_vec[0] = y;
|
||||||
|
for i in 1 .. y_vec.len() {
|
||||||
|
y_vec[i] = y_vec[i - 1] * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0 .. i {
|
||||||
|
a[i] = Scalar::random(&mut OsRng);
|
||||||
|
b[i] = Scalar::random(&mut OsRng);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let P = g_bold.multiexp(&a) +
|
||||||
|
h_bold.multiexp(&b) +
|
||||||
|
(g * weighted_inner_product(&a, &b, &y_vec)) +
|
||||||
|
(h * alpha);
|
||||||
|
|
||||||
|
let statement = WipStatement::new(generators, P, y);
|
||||||
|
let witness = WipWitness::new(a, b, alpha).unwrap();
|
||||||
|
|
||||||
|
let transcript = Scalar::random(&mut OsRng);
|
||||||
|
let proof = statement.clone().prove(&mut OsRng, transcript, &witness).unwrap();
|
||||||
|
statement.verify(&mut OsRng, &mut verifier, (), transcript, proof);
|
||||||
|
}
|
||||||
|
assert!(verifier.verify_vartime());
|
||||||
|
}
|
||||||
@@ -24,7 +24,10 @@ use crate::{
|
|||||||
use crate::ringct::clsag::{ClsagDetails, ClsagMultisig};
|
use crate::ringct::clsag::{ClsagDetails, ClsagMultisig};
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
use frost::tests::{key_gen, algorithm_machines, sign};
|
use frost::{
|
||||||
|
Participant,
|
||||||
|
tests::{key_gen, algorithm_machines, sign},
|
||||||
|
};
|
||||||
|
|
||||||
const RING_LEN: u64 = 11;
|
const RING_LEN: u64 = 11;
|
||||||
const AMOUNT: u64 = 1337;
|
const AMOUNT: u64 = 1337;
|
||||||
@@ -37,7 +40,7 @@ fn clsag() {
|
|||||||
for real in 0 .. RING_LEN {
|
for real in 0 .. RING_LEN {
|
||||||
let msg = [1; 32];
|
let msg = [1; 32];
|
||||||
|
|
||||||
let mut secrets = (Zeroizing::new(Scalar::zero()), Scalar::zero());
|
let mut secrets = (Zeroizing::new(Scalar::ZERO), Scalar::ZERO);
|
||||||
let mut ring = vec![];
|
let mut ring = vec![];
|
||||||
for i in 0 .. RING_LEN {
|
for i in 0 .. RING_LEN {
|
||||||
let dest = Zeroizing::new(random_scalar(&mut OsRng));
|
let dest = Zeroizing::new(random_scalar(&mut OsRng));
|
||||||
@@ -50,7 +53,7 @@ fn clsag() {
|
|||||||
amount = OsRng.next_u64();
|
amount = OsRng.next_u64();
|
||||||
}
|
}
|
||||||
ring
|
ring
|
||||||
.push([dest.deref() * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]);
|
.push([dest.deref() * ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let image = generate_key_image(&secrets.0);
|
let image = generate_key_image(&secrets.0);
|
||||||
@@ -63,7 +66,7 @@ fn clsag() {
|
|||||||
Commitment::new(secrets.1, AMOUNT),
|
Commitment::new(secrets.1, AMOUNT),
|
||||||
Decoys {
|
Decoys {
|
||||||
i: u8::try_from(real).unwrap(),
|
i: u8::try_from(real).unwrap(),
|
||||||
offsets: (1 ..= RING_LEN).into_iter().collect(),
|
offsets: (1 ..= RING_LEN).collect(),
|
||||||
ring: ring.clone(),
|
ring: ring.clone(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -89,11 +92,11 @@ fn clsag_multisig() {
|
|||||||
let mask;
|
let mask;
|
||||||
let amount;
|
let amount;
|
||||||
if i != u64::from(RING_INDEX) {
|
if i != u64::from(RING_INDEX) {
|
||||||
dest = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
|
dest = &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
||||||
mask = random_scalar(&mut OsRng);
|
mask = random_scalar(&mut OsRng);
|
||||||
amount = OsRng.next_u64();
|
amount = OsRng.next_u64();
|
||||||
} else {
|
} else {
|
||||||
dest = keys[&1].group_key().0;
|
dest = keys[&Participant::new(1).unwrap()].group_key().0;
|
||||||
mask = randomness;
|
mask = randomness;
|
||||||
amount = AMOUNT;
|
amount = AMOUNT;
|
||||||
}
|
}
|
||||||
@@ -103,15 +106,11 @@ fn clsag_multisig() {
|
|||||||
let mask_sum = random_scalar(&mut OsRng);
|
let mask_sum = random_scalar(&mut OsRng);
|
||||||
let algorithm = ClsagMultisig::new(
|
let algorithm = ClsagMultisig::new(
|
||||||
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
||||||
keys[&1].group_key().0,
|
keys[&Participant::new(1).unwrap()].group_key().0,
|
||||||
Arc::new(RwLock::new(Some(ClsagDetails::new(
|
Arc::new(RwLock::new(Some(ClsagDetails::new(
|
||||||
ClsagInput::new(
|
ClsagInput::new(
|
||||||
Commitment::new(randomness, AMOUNT),
|
Commitment::new(randomness, AMOUNT),
|
||||||
Decoys {
|
Decoys { i: RING_INDEX, offsets: (1 ..= RING_LEN).collect(), ring: ring.clone() },
|
||||||
i: RING_INDEX,
|
|
||||||
offsets: (1 ..= RING_LEN).into_iter().collect(),
|
|
||||||
ring: ring.clone(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
mask_sum,
|
mask_sum,
|
||||||
@@ -120,9 +119,9 @@ fn clsag_multisig() {
|
|||||||
|
|
||||||
sign(
|
sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
algorithm.clone(),
|
&algorithm,
|
||||||
keys.clone(),
|
keys.clone(),
|
||||||
algorithm_machines(&mut OsRng, algorithm, &keys),
|
algorithm_machines(&mut OsRng, &algorithm, &keys),
|
||||||
&[1; 32],
|
&[1; 32],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
mod unreduced_scalar;
|
||||||
mod clsag;
|
mod clsag;
|
||||||
mod bulletproofs;
|
mod bulletproofs;
|
||||||
mod address;
|
mod address;
|
||||||
|
mod seed;
|
||||||
|
|||||||
407
coins/monero/src/tests/seed.rs
Normal file
407
coins/monero/src/tests/seed.rs
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
hash,
|
||||||
|
wallet::seed::{
|
||||||
|
Seed, SeedType,
|
||||||
|
classic::{self, trim_by_lang},
|
||||||
|
polyseed,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_classic_seed() {
|
||||||
|
struct Vector {
|
||||||
|
language: classic::Language,
|
||||||
|
seed: String,
|
||||||
|
spend: String,
|
||||||
|
view: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let vectors = [
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Chinese,
|
||||||
|
seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(),
|
||||||
|
spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(),
|
||||||
|
view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::English,
|
||||||
|
seed: "washing thirsty occur lectures tuesday fainted toxic adapt \
|
||||||
|
abnormal memoir nylon mostly building shrugged online ember northern \
|
||||||
|
ruby woes dauntless boil family illness inroads northern"
|
||||||
|
.into(),
|
||||||
|
spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(),
|
||||||
|
view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Dutch,
|
||||||
|
seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \
|
||||||
|
ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \
|
||||||
|
wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst"
|
||||||
|
.into(),
|
||||||
|
spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(),
|
||||||
|
view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::French,
|
||||||
|
seed: "poids vaseux tarte bazar poivre effet entier nuance \
|
||||||
|
sensuel ennui pacte osselet poudre battre alibi mouton \
|
||||||
|
stade paquet pliage gibier type question position projet pliage"
|
||||||
|
.into(),
|
||||||
|
spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(),
|
||||||
|
view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Spanish,
|
||||||
|
seed: "minero ocupar mirar evadir octubre cal logro miope \
|
||||||
|
opaco disco ancla litio clase cuello nasal clase \
|
||||||
|
fiar avance deseo mente grumo negro cordón croqueta clase"
|
||||||
|
.into(),
|
||||||
|
spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(),
|
||||||
|
view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::German,
|
||||||
|
seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \
|
||||||
|
Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \
|
||||||
|
Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide"
|
||||||
|
.into(),
|
||||||
|
spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(),
|
||||||
|
view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Italian,
|
||||||
|
seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \
|
||||||
|
forzare meritare litigare lezione segreto evasione votare buio \
|
||||||
|
licenza cliente dorso natale crescere vento tutelare vetta evasione"
|
||||||
|
.into(),
|
||||||
|
spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(),
|
||||||
|
view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Portuguese,
|
||||||
|
seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \
|
||||||
|
iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \
|
||||||
|
cibernetico hoquei gleba driver buffer azoto megera nogueira agito"
|
||||||
|
.into(),
|
||||||
|
spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(),
|
||||||
|
view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Japanese,
|
||||||
|
seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \
|
||||||
|
かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \
|
||||||
|
おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや"
|
||||||
|
.into(),
|
||||||
|
spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(),
|
||||||
|
view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Russian,
|
||||||
|
seed: "шатер икра нация ехать получать инерция доза реальный \
|
||||||
|
рыжий таможня лопата душа веселый клетка атлас лекция \
|
||||||
|
обгонять паек наивный лыжный дурак стать ежик задача паек"
|
||||||
|
.into(),
|
||||||
|
spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(),
|
||||||
|
view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Esperanto,
|
||||||
|
seed: "ukazo klini peco etikedo fabriko imitado onklino urino \
|
||||||
|
pudro incidento kumuluso ikono smirgi hirundo uretro krii \
|
||||||
|
sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko"
|
||||||
|
.into(),
|
||||||
|
spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(),
|
||||||
|
view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::Lojban,
|
||||||
|
seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \
|
||||||
|
mlatu xedja muvgau palpi xindo sfubu ciste cinri \
|
||||||
|
blabi darno dembi janli blabi fenki bukpu burcu blabi"
|
||||||
|
.into(),
|
||||||
|
spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(),
|
||||||
|
view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: classic::Language::EnglishOld,
|
||||||
|
seed: "glorious especially puff son moment add youth nowhere \
|
||||||
|
throw glide grip wrong rhythm consume very swear \
|
||||||
|
bitter heavy eventually begin reason flirt type unable"
|
||||||
|
.into(),
|
||||||
|
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
|
||||||
|
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for vector in vectors {
|
||||||
|
let trim_seed = |seed: &str| {
|
||||||
|
seed
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|word| trim_by_lang(word, vector.language))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test against Monero
|
||||||
|
{
|
||||||
|
let seed = Seed::from_string(Zeroizing::new(vector.seed.clone())).unwrap();
|
||||||
|
let trim = trim_seed(&vector.seed);
|
||||||
|
println!(
|
||||||
|
"{}. seed: {}, entropy: {:?}, trim: {trim}",
|
||||||
|
line!(),
|
||||||
|
*seed.to_string(),
|
||||||
|
*seed.entropy()
|
||||||
|
);
|
||||||
|
assert_eq!(seed, Seed::from_string(Zeroizing::new(trim)).unwrap());
|
||||||
|
|
||||||
|
let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
|
||||||
|
// For classical seeds, Monero directly uses the entropy as a spend key
|
||||||
|
assert_eq!(
|
||||||
|
Option::<Scalar>::from(Scalar::from_canonical_bytes(*seed.entropy())),
|
||||||
|
Option::<Scalar>::from(Scalar::from_canonical_bytes(spend)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap();
|
||||||
|
// Monero then derives the view key as H(spend)
|
||||||
|
assert_eq!(
|
||||||
|
Scalar::from_bytes_mod_order(hash(&spend)),
|
||||||
|
Scalar::from_canonical_bytes(view).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Seed::from_entropy(SeedType::Classic(vector.language), Zeroizing::new(spend), None)
|
||||||
|
.unwrap(),
|
||||||
|
seed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test against ourselves
|
||||||
|
{
|
||||||
|
let seed = Seed::new(&mut OsRng, SeedType::Classic(vector.language));
|
||||||
|
let trim = trim_seed(&seed.to_string());
|
||||||
|
println!(
|
||||||
|
"{}. seed: {}, entropy: {:?}, trim: {trim}",
|
||||||
|
line!(),
|
||||||
|
*seed.to_string(),
|
||||||
|
*seed.entropy()
|
||||||
|
);
|
||||||
|
assert_eq!(seed, Seed::from_string(Zeroizing::new(trim)).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
seed,
|
||||||
|
Seed::from_entropy(SeedType::Classic(vector.language), seed.entropy(), None).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(seed, Seed::from_string(seed.to_string()).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polyseed() {
|
||||||
|
struct Vector {
|
||||||
|
language: polyseed::Language,
|
||||||
|
seed: String,
|
||||||
|
entropy: String,
|
||||||
|
birthday: u64,
|
||||||
|
has_prefix: bool,
|
||||||
|
has_accent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let vectors = [
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::English,
|
||||||
|
seed: "raven tail swear infant grief assist regular lamp \
|
||||||
|
duck valid someone little harsh puppy airport language"
|
||||||
|
.into(),
|
||||||
|
entropy: "dd76e7359a0ded37cd0ff0f3c829a5ae01673300000000000000000000000000".into(),
|
||||||
|
birthday: 1638446400,
|
||||||
|
has_prefix: true,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::Spanish,
|
||||||
|
seed: "eje fin parte célebre tabú pestaña lienzo puma \
|
||||||
|
prisión hora regalo lengua existir lápiz lote sonoro"
|
||||||
|
.into(),
|
||||||
|
entropy: "5a2b02df7db21fcbe6ec6df137d54c7b20fd2b00000000000000000000000000".into(),
|
||||||
|
birthday: 3118651200,
|
||||||
|
has_prefix: true,
|
||||||
|
has_accent: true,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::French,
|
||||||
|
seed: "valable arracher décaler jeudi amusant dresser mener épaissir risible \
|
||||||
|
prouesse réserve ampleur ajuster muter caméra enchère"
|
||||||
|
.into(),
|
||||||
|
entropy: "11cfd870324b26657342c37360c424a14a050b00000000000000000000000000".into(),
|
||||||
|
birthday: 1679314966,
|
||||||
|
has_prefix: true,
|
||||||
|
has_accent: true,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::Italian,
|
||||||
|
seed: "caduco midollo copione meninge isotopo illogico riflesso tartaruga fermento \
|
||||||
|
olandese normale tristezza episodio voragine forbito achille"
|
||||||
|
.into(),
|
||||||
|
entropy: "7ecc57c9b4652d4e31428f62bec91cfd55500600000000000000000000000000".into(),
|
||||||
|
birthday: 1679316358,
|
||||||
|
has_prefix: true,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::Portuguese,
|
||||||
|
seed: "caverna custear azedo adeus senador apertada sedoso omitir \
|
||||||
|
sujeito aurora videira molho cartaz gesso dentista tapar"
|
||||||
|
.into(),
|
||||||
|
entropy: "45473063711376cae38f1b3eba18c874124e1d00000000000000000000000000".into(),
|
||||||
|
birthday: 1679316657,
|
||||||
|
has_prefix: true,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::Czech,
|
||||||
|
seed: "usmrtit nora dotaz komunita zavalit funkce mzda sotva akce \
|
||||||
|
vesta kabel herna stodola uvolnit ustrnout email"
|
||||||
|
.into(),
|
||||||
|
entropy: "7ac8a4efd62d9c3c4c02e350d32326df37821c00000000000000000000000000".into(),
|
||||||
|
birthday: 1679316898,
|
||||||
|
has_prefix: true,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::Korean,
|
||||||
|
seed: "전망 선풍기 국제 무궁화 설사 기름 이론적 해안 절망 예선 \
|
||||||
|
지우개 보관 절망 말기 시각 귀신"
|
||||||
|
.into(),
|
||||||
|
entropy: "684663fda420298f42ed94b2c512ed38ddf12b00000000000000000000000000".into(),
|
||||||
|
birthday: 1679317073,
|
||||||
|
has_prefix: false,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::Japanese,
|
||||||
|
seed: "うちあわせ ちつじょ つごう しはい けんこう とおる てみやげ はんとし たんとう \
|
||||||
|
といれ おさない おさえる むかう ぬぐう なふだ せまる"
|
||||||
|
.into(),
|
||||||
|
entropy: "94e6665518a6286c6e3ba508a2279eb62b771f00000000000000000000000000".into(),
|
||||||
|
birthday: 1679318722,
|
||||||
|
has_prefix: false,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::ChineseTraditional,
|
||||||
|
seed: "亂 挖 斤 柄 代 圈 枝 轄 魯 論 函 開 勘 番 榮 壁".into(),
|
||||||
|
entropy: "b1594f585987ab0fd5a31da1f0d377dae5283f00000000000000000000000000".into(),
|
||||||
|
birthday: 1679426433,
|
||||||
|
has_prefix: false,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: polyseed::Language::ChineseSimplified,
|
||||||
|
seed: "啊 百 族 府 票 划 伪 仓 叶 虾 借 溜 晨 左 等 鬼".into(),
|
||||||
|
entropy: "21cdd366f337b89b8d1bc1df9fe73047c22b0300000000000000000000000000".into(),
|
||||||
|
birthday: 1679426817,
|
||||||
|
has_prefix: false,
|
||||||
|
has_accent: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for vector in vectors {
|
||||||
|
let add_whitespace = |mut seed: String| {
|
||||||
|
seed.push(' ');
|
||||||
|
seed
|
||||||
|
};
|
||||||
|
|
||||||
|
let seed_without_accents = |seed: &str| {
|
||||||
|
seed
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|w| w.chars().filter(char::is_ascii).collect::<String>())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
};
|
||||||
|
|
||||||
|
let trim_seed = |seed: &str| {
|
||||||
|
let seed_to_trim =
|
||||||
|
if vector.has_accent { seed_without_accents(seed) } else { seed.to_string() };
|
||||||
|
seed_to_trim
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|w| {
|
||||||
|
let mut ascii = 0;
|
||||||
|
let mut to_take = w.len();
|
||||||
|
for (i, char) in w.chars().enumerate() {
|
||||||
|
if char.is_ascii() {
|
||||||
|
ascii += 1;
|
||||||
|
}
|
||||||
|
if ascii == polyseed::PREFIX_LEN {
|
||||||
|
// +1 to include this character, which put us at the prefix length
|
||||||
|
to_take = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.chars().take(to_take).collect::<String>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
};
|
||||||
|
|
||||||
|
// String -> Seed
|
||||||
|
let seed = Seed::from_string(Zeroizing::new(vector.seed.clone())).unwrap();
|
||||||
|
let trim = trim_seed(&vector.seed);
|
||||||
|
let add_whitespace = add_whitespace(vector.seed.clone());
|
||||||
|
let seed_without_accents = seed_without_accents(&vector.seed);
|
||||||
|
println!(
|
||||||
|
"{}. seed: {}, entropy: {:?}, trim: {}, add_whitespace: {}, seed_without_accents: {}",
|
||||||
|
line!(),
|
||||||
|
*seed.to_string(),
|
||||||
|
*seed.entropy(),
|
||||||
|
trim,
|
||||||
|
add_whitespace,
|
||||||
|
seed_without_accents,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure a version with added whitespace still works
|
||||||
|
let whitespaced_seed = Seed::from_string(Zeroizing::new(add_whitespace)).unwrap();
|
||||||
|
assert_eq!(seed, whitespaced_seed);
|
||||||
|
// Check trimmed versions works
|
||||||
|
if vector.has_prefix {
|
||||||
|
let trimmed_seed = Seed::from_string(Zeroizing::new(trim)).unwrap();
|
||||||
|
assert_eq!(seed, trimmed_seed);
|
||||||
|
}
|
||||||
|
// Check versions without accents work
|
||||||
|
if vector.has_accent {
|
||||||
|
let seed_without_accents = Seed::from_string(Zeroizing::new(seed_without_accents)).unwrap();
|
||||||
|
assert_eq!(seed, seed_without_accents);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entropy = Zeroizing::new(hex::decode(vector.entropy).unwrap().try_into().unwrap());
|
||||||
|
assert_eq!(seed.entropy(), entropy);
|
||||||
|
assert!(seed.birthday().abs_diff(vector.birthday) < polyseed::TIME_STEP);
|
||||||
|
|
||||||
|
// Entropy -> Seed
|
||||||
|
let from_entropy =
|
||||||
|
Seed::from_entropy(SeedType::Polyseed(vector.language), entropy, Some(seed.birthday()))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(seed.to_string(), from_entropy.to_string());
|
||||||
|
|
||||||
|
// Check against ourselves
|
||||||
|
{
|
||||||
|
let seed = Seed::new(&mut OsRng, SeedType::Polyseed(vector.language));
|
||||||
|
println!("{}. seed: {}, key: {:?}", line!(), *seed.to_string(), *seed.key());
|
||||||
|
assert_eq!(seed, Seed::from_string(seed.to_string()).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
seed,
|
||||||
|
Seed::from_entropy(
|
||||||
|
SeedType::Polyseed(vector.language),
|
||||||
|
seed.entropy(),
|
||||||
|
Some(seed.birthday())
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
coins/monero/src/tests/unreduced_scalar.rs
Normal file
32
coins/monero/src/tests/unreduced_scalar.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
|
||||||
|
use crate::unreduced_scalar::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recover_scalars() {
|
||||||
|
let test_recover = |stored: &str, recovered: &str| {
|
||||||
|
let stored = UnreducedScalar(hex::decode(stored).unwrap().try_into().unwrap());
|
||||||
|
let recovered =
|
||||||
|
Scalar::from_canonical_bytes(hex::decode(recovered).unwrap().try_into().unwrap()).unwrap();
|
||||||
|
assert_eq!(stored.recover_monero_slide_scalar(), recovered);
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://www.moneroinflation.com/static/data_py/report_scalars_df.pdf
|
||||||
|
// Table 4.
|
||||||
|
test_recover(
|
||||||
|
"cb2be144948166d0a9edb831ea586da0c376efa217871505ad77f6ff80f203f8",
|
||||||
|
"b8ffd6a1aee47828808ab0d4c8524cb5c376efa217871505ad77f6ff80f20308",
|
||||||
|
);
|
||||||
|
test_recover(
|
||||||
|
"343d3df8a1051c15a400649c423dc4ed58bef49c50caef6ca4a618b80dee22f4",
|
||||||
|
"21113355bc682e6d7a9d5b3f2137a30259bef49c50caef6ca4a618b80dee2204",
|
||||||
|
);
|
||||||
|
test_recover(
|
||||||
|
"c14f75d612800ca2c1dcfa387a42c9cc086c005bc94b18d204dd61342418eba7",
|
||||||
|
"4f473804b1d27ab2c789c80ab21d034a096c005bc94b18d204dd61342418eb07",
|
||||||
|
);
|
||||||
|
test_recover(
|
||||||
|
"000102030405060708090a0b0c0d0e0f826c4f6e2329a31bc5bc320af0b2bcbb",
|
||||||
|
"a124cfd387f461bf3719e03965ee6877826c4f6e2329a31bc5bc320af0b2bc0b",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
use core::cmp::Ordering;
|
use core::cmp::Ordering;
|
||||||
use std::io::{self, Read, Write};
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use curve25519_dalek::{
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||||
scalar::Scalar,
|
|
||||||
edwards::{EdwardsPoint, CompressedEdwardsY},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Protocol, hash,
|
Protocol, hash,
|
||||||
serialize::*,
|
serialize::*,
|
||||||
ringct::{RctBase, RctPrunable, RctSignatures},
|
ring_signatures::RingSignature,
|
||||||
|
ringct::{bulletproofs::Bulletproofs, RctType, RctBase, RctPrunable, RctSignatures},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum Input {
|
pub enum Input {
|
||||||
Gen(u64),
|
Gen(u64),
|
||||||
ToKey { amount: u64, key_offsets: Vec<u64>, key_image: EdwardsPoint },
|
ToKey { amount: Option<u64>, key_offsets: Vec<u64>, key_image: EdwardsPoint },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
// Worst-case predictive len
|
pub(crate) fn fee_weight(offsets_weight: usize) -> usize {
|
||||||
pub(crate) fn fee_weight(ring_len: usize) -> usize {
|
// Uses 1 byte for the input type
|
||||||
// Uses 1 byte for the VarInt amount due to amount being 0
|
// Uses 1 byte for the VarInt amount due to amount being 0
|
||||||
// Uses 1 byte for the VarInt encoding of the length of the ring as well
|
1 + 1 + offsets_weight + 32
|
||||||
1 + 1 + 1 + (8 * ring_len) + 32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
@@ -37,24 +37,37 @@ impl Input {
|
|||||||
|
|
||||||
Input::ToKey { amount, key_offsets, key_image } => {
|
Input::ToKey { amount, key_offsets, key_image } => {
|
||||||
w.write_all(&[2])?;
|
w.write_all(&[2])?;
|
||||||
write_varint(amount, w)?;
|
write_varint(&amount.unwrap_or(0), w)?;
|
||||||
write_vec(write_varint, key_offsets, w)?;
|
write_vec(write_varint, key_offsets, w)?;
|
||||||
write_point(key_image, w)
|
write_point(key_image, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = vec![];
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
|
pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
|
||||||
Ok(match read_byte(r)? {
|
Ok(match read_byte(r)? {
|
||||||
255 => Input::Gen(read_varint(r)?),
|
255 => Input::Gen(read_varint(r)?),
|
||||||
2 => Input::ToKey {
|
2 => {
|
||||||
amount: read_varint(r)?,
|
let amount = read_varint(r)?;
|
||||||
key_offsets: read_vec(read_varint, r)?,
|
// https://github.com/monero-project/monero/
|
||||||
key_image: read_torsion_free_point(r)?,
|
// blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
|
||||||
},
|
// src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863
|
||||||
_ => {
|
// A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "Tried to deserialize unknown/unused input type"))?
|
// That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT
|
||||||
|
let amount = if amount == 0 { None } else { Some(amount) };
|
||||||
|
Input::ToKey {
|
||||||
|
amount,
|
||||||
|
key_offsets: read_vec(read_varint, r)?,
|
||||||
|
key_image: read_torsion_free_point(r)?,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,18 +75,20 @@ impl Input {
|
|||||||
// Doesn't bother moving to an enum for the unused Script classes
|
// Doesn't bother moving to an enum for the unused Script classes
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Output {
|
pub struct Output {
|
||||||
pub amount: u64,
|
pub amount: Option<u64>,
|
||||||
pub key: CompressedEdwardsY,
|
pub key: CompressedEdwardsY,
|
||||||
pub view_tag: Option<u8>,
|
pub view_tag: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
pub(crate) fn fee_weight() -> usize {
|
pub(crate) fn fee_weight(view_tags: bool) -> usize {
|
||||||
1 + 1 + 32 + 1
|
// Uses 1 byte for the output type
|
||||||
|
// Uses 1 byte for the VarInt amount due to amount being 0
|
||||||
|
1 + 1 + 32 + if view_tags { 1 } else { 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
write_varint(&self.amount, w)?;
|
write_varint(&self.amount.unwrap_or(0), w)?;
|
||||||
w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
|
w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
|
||||||
w.write_all(&self.key.to_bytes())?;
|
w.write_all(&self.key.to_bytes())?;
|
||||||
if let Some(view_tag) = self.view_tag {
|
if let Some(view_tag) = self.view_tag {
|
||||||
@@ -82,15 +97,27 @@ impl Output {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Output> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::with_capacity(8 + 1 + 32);
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> {
|
||||||
let amount = read_varint(r)?;
|
let amount = read_varint(r)?;
|
||||||
|
let amount = if rct {
|
||||||
|
if amount != 0 {
|
||||||
|
Err(io::Error::other("RCT TX output wasn't 0"))?;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(amount)
|
||||||
|
};
|
||||||
|
|
||||||
let view_tag = match read_byte(r)? {
|
let view_tag = match read_byte(r)? {
|
||||||
2 => false,
|
2 => false,
|
||||||
3 => true,
|
3 => true,
|
||||||
_ => Err(io::Error::new(
|
_ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?,
|
||||||
io::ErrorKind::Other,
|
|
||||||
"Tried to deserialize unknown/unused output type",
|
|
||||||
))?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Output {
|
Ok(Output {
|
||||||
@@ -152,13 +179,19 @@ pub struct TransactionPrefix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionPrefix {
|
impl TransactionPrefix {
|
||||||
pub(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize, extra: usize) -> usize {
|
pub(crate) fn fee_weight(
|
||||||
|
decoy_weights: &[usize],
|
||||||
|
outputs: usize,
|
||||||
|
view_tags: bool,
|
||||||
|
extra: usize,
|
||||||
|
) -> usize {
|
||||||
// Assumes Timelock::None since this library won't let you create a TX with a timelock
|
// Assumes Timelock::None since this library won't let you create a TX with a timelock
|
||||||
|
// 1 input for every decoy weight
|
||||||
1 + 1 +
|
1 + 1 +
|
||||||
varint_len(inputs) +
|
varint_len(decoy_weights.len()) +
|
||||||
(inputs * Input::fee_weight(ring_len)) +
|
decoy_weights.iter().map(|&offsets_weight| Input::fee_weight(offsets_weight)).sum::<usize>() +
|
||||||
1 +
|
varint_len(outputs) +
|
||||||
(outputs * Output::fee_weight()) +
|
(outputs * Output::fee_weight(view_tags)) +
|
||||||
varint_len(extra) +
|
varint_len(extra) +
|
||||||
extra
|
extra
|
||||||
}
|
}
|
||||||
@@ -168,48 +201,72 @@ impl TransactionPrefix {
|
|||||||
self.timelock.write(w)?;
|
self.timelock.write(w)?;
|
||||||
write_vec(Input::write, &self.inputs, w)?;
|
write_vec(Input::write, &self.inputs, w)?;
|
||||||
write_vec(Output::write, &self.outputs, w)?;
|
write_vec(Output::write, &self.outputs, w)?;
|
||||||
write_varint(&self.extra.len().try_into().unwrap(), w)?;
|
write_varint(&self.extra.len(), w)?;
|
||||||
w.write_all(&self.extra)
|
w.write_all(&self.extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = vec![];
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<TransactionPrefix> {
|
pub fn read<R: Read>(r: &mut R) -> io::Result<TransactionPrefix> {
|
||||||
|
let version = read_varint(r)?;
|
||||||
|
// TODO: Create an enum out of version
|
||||||
|
if (version == 0) || (version > 2) {
|
||||||
|
Err(io::Error::other("unrecognized transaction version"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timelock = Timelock::from_raw(read_varint(r)?);
|
||||||
|
|
||||||
|
let inputs = read_vec(|r| Input::read(r), r)?;
|
||||||
|
if inputs.is_empty() {
|
||||||
|
Err(io::Error::other("transaction had no inputs"))?;
|
||||||
|
}
|
||||||
|
let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
|
||||||
|
|
||||||
let mut prefix = TransactionPrefix {
|
let mut prefix = TransactionPrefix {
|
||||||
version: read_varint(r)?,
|
version,
|
||||||
timelock: Timelock::from_raw(read_varint(r)?),
|
timelock,
|
||||||
inputs: read_vec(Input::read, r)?,
|
inputs,
|
||||||
outputs: read_vec(Output::read, r)?,
|
outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?,
|
||||||
extra: vec![],
|
extra: vec![],
|
||||||
};
|
};
|
||||||
prefix.extra = read_vec(read_byte, r)?;
|
prefix.extra = read_vec(read_byte, r)?;
|
||||||
Ok(prefix)
|
Ok(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hash(&self) -> [u8; 32] {
|
||||||
|
hash(&self.serialize())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Monero transaction. For version 1, rct_signatures still contains an accurate fee value.
|
/// Monero transaction. For version 1, rct_signatures still contains an accurate fee value.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Transaction {
|
pub struct Transaction {
|
||||||
pub prefix: TransactionPrefix,
|
pub prefix: TransactionPrefix,
|
||||||
pub signatures: Vec<(Scalar, Scalar)>,
|
pub signatures: Vec<RingSignature>,
|
||||||
pub rct_signatures: RctSignatures,
|
pub rct_signatures: RctSignatures,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Transaction {
|
impl Transaction {
|
||||||
pub(crate) fn fee_weight(
|
pub(crate) fn fee_weight(
|
||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
inputs: usize,
|
decoy_weights: &[usize],
|
||||||
outputs: usize,
|
outputs: usize,
|
||||||
extra: usize,
|
extra: usize,
|
||||||
|
fee: u64,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
TransactionPrefix::fee_weight(protocol.ring_len(), inputs, outputs, extra) +
|
TransactionPrefix::fee_weight(decoy_weights, outputs, protocol.view_tags(), extra) +
|
||||||
RctSignatures::fee_weight(protocol, inputs, outputs)
|
RctSignatures::fee_weight(protocol, decoy_weights.len(), outputs, fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
self.prefix.write(w)?;
|
self.prefix.write(w)?;
|
||||||
if self.prefix.version == 1 {
|
if self.prefix.version == 1 {
|
||||||
for sig in &self.signatures {
|
for ring_sig in &self.signatures {
|
||||||
write_scalar(&sig.0, w)?;
|
ring_sig.write(w)?;
|
||||||
write_scalar(&sig.1, w)?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if self.prefix.version == 2 {
|
} else if self.prefix.version == 2 {
|
||||||
@@ -219,42 +276,74 @@ impl Transaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::with_capacity(2048);
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Transaction> {
|
pub fn read<R: Read>(r: &mut R) -> io::Result<Transaction> {
|
||||||
let prefix = TransactionPrefix::read(r)?;
|
let prefix = TransactionPrefix::read(r)?;
|
||||||
let mut signatures = vec![];
|
let mut signatures = vec![];
|
||||||
let mut rct_signatures = RctSignatures {
|
let mut rct_signatures = RctSignatures {
|
||||||
base: RctBase { fee: 0, ecdh_info: vec![], commitments: vec![] },
|
base: RctBase { fee: 0, encrypted_amounts: vec![], pseudo_outs: vec![], commitments: vec![] },
|
||||||
prunable: RctPrunable::Null,
|
prunable: RctPrunable::Null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if prefix.version == 1 {
|
if prefix.version == 1 {
|
||||||
for _ in 0 .. prefix.inputs.len() {
|
signatures = prefix
|
||||||
signatures.push((read_scalar(r)?, read_scalar(r)?));
|
|
||||||
}
|
|
||||||
rct_signatures.base.fee = prefix
|
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|input| match input {
|
.filter_map(|input| match input {
|
||||||
Input::Gen(..) => 0,
|
Input::ToKey { key_offsets, .. } => Some(RingSignature::read(key_offsets.len(), r)),
|
||||||
Input::ToKey { amount, .. } => *amount,
|
_ => None,
|
||||||
})
|
})
|
||||||
.sum::<u64>()
|
.collect::<Result<_, _>>()?;
|
||||||
.saturating_sub(prefix.outputs.iter().map(|output| output.amount).sum());
|
|
||||||
|
if !matches!(prefix.inputs[0], Input::Gen(..)) {
|
||||||
|
let in_amount = prefix
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.map(|input| match input {
|
||||||
|
Input::Gen(..) => Err(io::Error::other("Input::Gen present in non-coinbase v1 TX"))?,
|
||||||
|
// v1 TXs can burn v2 outputs
|
||||||
|
// dcff3fe4f914d6b6bd4a5b800cc4cca8f2fdd1bd73352f0700d463d36812f328 is one such TX
|
||||||
|
// It includes a pre-RCT signature for a RCT output, yet if you interpret the RCT
|
||||||
|
// output as being worth 0, it passes a sum check (guaranteed since no outputs are RCT)
|
||||||
|
Input::ToKey { amount, .. } => Ok(amount.unwrap_or(0)),
|
||||||
|
})
|
||||||
|
.collect::<io::Result<Vec<_>>>()?
|
||||||
|
.into_iter()
|
||||||
|
.sum::<u64>();
|
||||||
|
|
||||||
|
let mut out = 0;
|
||||||
|
for output in &prefix.outputs {
|
||||||
|
if output.amount.is_none() {
|
||||||
|
Err(io::Error::other("v1 transaction had a 0-amount output"))?;
|
||||||
|
}
|
||||||
|
out += output.amount.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_amount < out {
|
||||||
|
Err(io::Error::other("transaction spent more than it had as inputs"))?;
|
||||||
|
}
|
||||||
|
rct_signatures.base.fee = in_amount - out;
|
||||||
|
}
|
||||||
} else if prefix.version == 2 {
|
} else if prefix.version == 2 {
|
||||||
rct_signatures = RctSignatures::read(
|
rct_signatures = RctSignatures::read(
|
||||||
prefix
|
&prefix
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|input| match input {
|
.map(|input| match input {
|
||||||
Input::Gen(_) => 0,
|
Input::Gen(_) => 0,
|
||||||
Input::ToKey { key_offsets, .. } => key_offsets.len(),
|
Input::ToKey { key_offsets, .. } => key_offsets.len(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect::<Vec<_>>(),
|
||||||
prefix.outputs.len(),
|
prefix.outputs.len(),
|
||||||
r,
|
r,
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "Tried to deserialize unknown version"))?;
|
Err(io::Error::other("Tried to deserialize unknown version"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Transaction { prefix, signatures, rct_signatures })
|
Ok(Transaction { prefix, signatures, rct_signatures })
|
||||||
@@ -268,22 +357,19 @@ impl Transaction {
|
|||||||
} else {
|
} else {
|
||||||
let mut hashes = Vec::with_capacity(96);
|
let mut hashes = Vec::with_capacity(96);
|
||||||
|
|
||||||
self.prefix.write(&mut buf).unwrap();
|
hashes.extend(self.prefix.hash());
|
||||||
|
|
||||||
|
self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap();
|
||||||
hashes.extend(hash(&buf));
|
hashes.extend(hash(&buf));
|
||||||
buf.clear();
|
buf.clear();
|
||||||
|
|
||||||
self.rct_signatures.base.write(&mut buf, self.rct_signatures.prunable.rct_type()).unwrap();
|
hashes.extend(&match self.rct_signatures.prunable {
|
||||||
hashes.extend(hash(&buf));
|
RctPrunable::Null => [0; 32],
|
||||||
buf.clear();
|
|
||||||
|
|
||||||
match self.rct_signatures.prunable {
|
|
||||||
RctPrunable::Null => buf.resize(32, 0),
|
|
||||||
_ => {
|
_ => {
|
||||||
self.rct_signatures.prunable.write(&mut buf).unwrap();
|
self.rct_signatures.prunable.write(&mut buf, self.rct_signatures.rct_type()).unwrap();
|
||||||
buf = hash(&buf).to_vec();
|
hash(&buf)
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
hashes.extend(&buf);
|
|
||||||
|
|
||||||
hash(&hashes)
|
hash(&hashes)
|
||||||
}
|
}
|
||||||
@@ -291,14 +377,16 @@ impl Transaction {
|
|||||||
|
|
||||||
/// Calculate the hash of this transaction as needed for signing it.
|
/// Calculate the hash of this transaction as needed for signing it.
|
||||||
pub fn signature_hash(&self) -> [u8; 32] {
|
pub fn signature_hash(&self) -> [u8; 32] {
|
||||||
|
if self.prefix.version == 1 {
|
||||||
|
return self.prefix.hash();
|
||||||
|
}
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(2048);
|
let mut buf = Vec::with_capacity(2048);
|
||||||
let mut sig_hash = Vec::with_capacity(96);
|
let mut sig_hash = Vec::with_capacity(96);
|
||||||
|
|
||||||
self.prefix.write(&mut buf).unwrap();
|
sig_hash.extend(self.prefix.hash());
|
||||||
sig_hash.extend(hash(&buf));
|
|
||||||
buf.clear();
|
|
||||||
|
|
||||||
self.rct_signatures.base.write(&mut buf, self.rct_signatures.prunable.rct_type()).unwrap();
|
self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap();
|
||||||
sig_hash.extend(hash(&buf));
|
sig_hash.extend(hash(&buf));
|
||||||
buf.clear();
|
buf.clear();
|
||||||
|
|
||||||
@@ -307,4 +395,39 @@ impl Transaction {
|
|||||||
|
|
||||||
hash(&sig_hash)
|
hash(&sig_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_rct_bulletproof(&self) -> bool {
|
||||||
|
match &self.rct_signatures.rct_type() {
|
||||||
|
RctType::Bulletproofs | RctType::BulletproofsCompactAmount | RctType::Clsag => true,
|
||||||
|
RctType::Null |
|
||||||
|
RctType::MlsagAggregate |
|
||||||
|
RctType::MlsagIndividual |
|
||||||
|
RctType::BulletproofsPlus => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_rct_bulletproof_plus(&self) -> bool {
|
||||||
|
match &self.rct_signatures.rct_type() {
|
||||||
|
RctType::BulletproofsPlus => true,
|
||||||
|
RctType::Null |
|
||||||
|
RctType::MlsagAggregate |
|
||||||
|
RctType::MlsagIndividual |
|
||||||
|
RctType::Bulletproofs |
|
||||||
|
RctType::BulletproofsCompactAmount |
|
||||||
|
RctType::Clsag => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the transaction's weight.
|
||||||
|
pub fn weight(&self) -> usize {
|
||||||
|
let blob_size = self.serialize().len();
|
||||||
|
|
||||||
|
let bp = self.is_rct_bulletproof();
|
||||||
|
let bp_plus = self.is_rct_bulletproof_plus();
|
||||||
|
if !(bp || bp_plus) {
|
||||||
|
blob_size
|
||||||
|
} else {
|
||||||
|
blob_size + Bulletproofs::calculate_bp_clawback(bp_plus, self.prefix.outputs.len()).0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
coins/monero/src/unreduced_scalar.rs
Normal file
137
coins/monero/src/unreduced_scalar.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use core::cmp::Ordering;
|
||||||
|
|
||||||
|
use std_shims::{
|
||||||
|
sync::OnceLock,
|
||||||
|
io::{self, *},
|
||||||
|
};
|
||||||
|
|
||||||
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
|
||||||
|
use crate::serialize::*;
|
||||||
|
|
||||||
|
static PRECOMPUTED_SCALARS_CELL: OnceLock<[Scalar; 8]> = OnceLock::new();
|
||||||
|
/// Precomputed scalars used to recover an incorrectly reduced scalar.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn PRECOMPUTED_SCALARS() -> [Scalar; 8] {
|
||||||
|
*PRECOMPUTED_SCALARS_CELL.get_or_init(|| {
|
||||||
|
let mut precomputed_scalars = [Scalar::ONE; 8];
|
||||||
|
for (i, scalar) in precomputed_scalars.iter_mut().enumerate().skip(1) {
|
||||||
|
*scalar = Scalar::from(u8::try_from((i * 2) + 1).unwrap());
|
||||||
|
}
|
||||||
|
precomputed_scalars
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct UnreducedScalar(pub [u8; 32]);
|
||||||
|
|
||||||
|
impl UnreducedScalar {
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
w.write_all(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<UnreducedScalar> {
|
||||||
|
Ok(UnreducedScalar(read_bytes(r)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bits(&self) -> [u8; 256] {
|
||||||
|
let mut bits = [0; 256];
|
||||||
|
for (i, bit) in bits.iter_mut().enumerate() {
|
||||||
|
*bit = core::hint::black_box(1 & (self.0[i / 8] >> (i % 8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
bits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the non-adjacent form of this scalar with width 5.
|
||||||
|
///
|
||||||
|
/// This matches Monero's `slide` function and intentionally gives incorrect outputs under
|
||||||
|
/// certain conditions in order to match Monero.
|
||||||
|
///
|
||||||
|
/// This function does not execute in constant time.
|
||||||
|
fn non_adjacent_form(&self) -> [i8; 256] {
|
||||||
|
let bits = self.as_bits();
|
||||||
|
let mut naf = [0i8; 256];
|
||||||
|
for (b, bit) in bits.into_iter().enumerate() {
|
||||||
|
naf[b] = i8::try_from(bit).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0 .. 256 {
|
||||||
|
if naf[i] != 0 {
|
||||||
|
// if the bit is a one, work our way up through the window
|
||||||
|
// combining the bits with this bit.
|
||||||
|
for b in 1 .. 6 {
|
||||||
|
if (i + b) >= 256 {
|
||||||
|
// if we are at the length of the array then break out
|
||||||
|
// the loop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// potential_carry - the value of the bit at i+b compared to the bit at i
|
||||||
|
let potential_carry = naf[i + b] << b;
|
||||||
|
|
||||||
|
if potential_carry != 0 {
|
||||||
|
if (naf[i] + potential_carry) <= 15 {
|
||||||
|
// if our current "bit" plus the potential carry is less than 16
|
||||||
|
// add it to our current "bit" and set the potential carry bit to 0.
|
||||||
|
naf[i] += potential_carry;
|
||||||
|
naf[i + b] = 0;
|
||||||
|
} else if (naf[i] - potential_carry) >= -15 {
|
||||||
|
// else if our current "bit" minus the potential carry is more than -16
|
||||||
|
// take it away from our current "bit".
|
||||||
|
// we then work our way up through the bits setting ones to zero, when
|
||||||
|
// we hit the first zero we change it to one then stop, this is to factor
|
||||||
|
// in the minus.
|
||||||
|
naf[i] -= potential_carry;
|
||||||
|
#[allow(clippy::needless_range_loop)]
|
||||||
|
for k in (i + b) .. 256 {
|
||||||
|
if naf[k] == 0 {
|
||||||
|
naf[k] = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
naf[k] = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
naf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recover the scalar that an array of bytes was incorrectly interpreted as by Monero's `slide`
|
||||||
|
/// function.
|
||||||
|
///
|
||||||
|
/// In Borromean range proofs Monero was not checking that the scalars used were
|
||||||
|
/// reduced. This lead to the scalar stored being interpreted as a different scalar,
|
||||||
|
/// this function recovers that scalar.
|
||||||
|
///
|
||||||
|
/// See: https://github.com/monero-project/monero/issues/8438
|
||||||
|
pub fn recover_monero_slide_scalar(&self) -> Scalar {
|
||||||
|
if self.0[31] & 128 == 0 {
|
||||||
|
// Computing the w-NAF of a number can only give an output with 1 more bit than
|
||||||
|
// the number, so even if the number isn't reduced, the `slide` function will be
|
||||||
|
// correct when the last bit isn't set.
|
||||||
|
return Scalar::from_bytes_mod_order(self.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let precomputed_scalars = PRECOMPUTED_SCALARS();
|
||||||
|
|
||||||
|
let mut recovered = Scalar::ZERO;
|
||||||
|
for &numb in self.non_adjacent_form().iter().rev() {
|
||||||
|
recovered += recovered;
|
||||||
|
match numb.cmp(&0) {
|
||||||
|
Ordering::Greater => recovered += precomputed_scalars[usize::try_from(numb).unwrap() / 2],
|
||||||
|
Ordering::Less => recovered -= precomputed_scalars[usize::try_from(-numb).unwrap() / 2],
|
||||||
|
Ordering::Equal => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recovered
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
use core::{marker::PhantomData, fmt::Debug};
|
use core::{marker::PhantomData, fmt::Debug};
|
||||||
use std::string::ToString;
|
use std_shims::string::{String, ToString};
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
@@ -60,7 +58,7 @@ pub enum AddressSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AddressType {
|
impl AddressType {
|
||||||
pub fn subaddress(&self) -> bool {
|
pub fn is_subaddress(&self) -> bool {
|
||||||
matches!(self, AddressType::Subaddress) ||
|
matches!(self, AddressType::Subaddress) ||
|
||||||
matches!(self, AddressType::Featured { subaddress: true, .. })
|
matches!(self, AddressType::Featured { subaddress: true, .. })
|
||||||
}
|
}
|
||||||
@@ -75,7 +73,7 @@ impl AddressType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guaranteed(&self) -> bool {
|
pub fn is_guaranteed(&self) -> bool {
|
||||||
matches!(self, AddressType::Featured { guaranteed: true, .. })
|
matches!(self, AddressType::Featured { guaranteed: true, .. })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,19 +112,20 @@ impl<B: AddressBytes> Zeroize for AddressMeta<B> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Error when decoding an address.
|
/// Error when decoding an address.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||||
pub enum AddressError {
|
pub enum AddressError {
|
||||||
#[error("invalid address byte")]
|
#[cfg_attr(feature = "std", error("invalid address byte"))]
|
||||||
InvalidByte,
|
InvalidByte,
|
||||||
#[error("invalid address encoding")]
|
#[cfg_attr(feature = "std", error("invalid address encoding"))]
|
||||||
InvalidEncoding,
|
InvalidEncoding,
|
||||||
#[error("invalid length")]
|
#[cfg_attr(feature = "std", error("invalid length"))]
|
||||||
InvalidLength,
|
InvalidLength,
|
||||||
#[error("invalid key")]
|
#[cfg_attr(feature = "std", error("invalid key"))]
|
||||||
InvalidKey,
|
InvalidKey,
|
||||||
#[error("unknown features")]
|
#[cfg_attr(feature = "std", error("unknown features"))]
|
||||||
UnknownFeatures,
|
UnknownFeatures,
|
||||||
#[error("different network than expected")]
|
#[cfg_attr(feature = "std", error("different network than expected"))]
|
||||||
DifferentNetwork,
|
DifferentNetwork,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,27 +168,40 @@ impl<B: AddressBytes> AddressMeta<B> {
|
|||||||
meta.ok_or(AddressError::InvalidByte)
|
meta.ok_or(AddressError::InvalidByte)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subaddress(&self) -> bool {
|
pub fn is_subaddress(&self) -> bool {
|
||||||
self.kind.subaddress()
|
self.kind.is_subaddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
||||||
self.kind.payment_id()
|
self.kind.payment_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guaranteed(&self) -> bool {
|
pub fn is_guaranteed(&self) -> bool {
|
||||||
self.kind.guaranteed()
|
self.kind.is_guaranteed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Monero address, composed of metadata and a spend/view key.
|
/// A Monero address, composed of metadata and a spend/view key.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Address<B: AddressBytes> {
|
pub struct Address<B: AddressBytes> {
|
||||||
pub meta: AddressMeta<B>,
|
pub meta: AddressMeta<B>,
|
||||||
pub spend: EdwardsPoint,
|
pub spend: EdwardsPoint,
|
||||||
pub view: EdwardsPoint,
|
pub view: EdwardsPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<B: AddressBytes> core::fmt::Debug for Address<B> {
|
||||||
|
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||||
|
fmt
|
||||||
|
.debug_struct("Address")
|
||||||
|
.field("meta", &self.meta)
|
||||||
|
.field("spend", &hex::encode(self.spend.compress().0))
|
||||||
|
.field("view", &hex::encode(self.view.compress().0))
|
||||||
|
// This is not a real field yet is the most valuable thing to know when debugging
|
||||||
|
.field("(address)", &self.to_string())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<B: AddressBytes> Zeroize for Address<B> {
|
impl<B: AddressBytes> Zeroize for Address<B> {
|
||||||
fn zeroize(&mut self) {
|
fn zeroize(&mut self) {
|
||||||
self.meta.zeroize();
|
self.meta.zeroize();
|
||||||
@@ -285,16 +297,16 @@ impl<B: AddressBytes> Address<B> {
|
|||||||
self.meta.network
|
self.meta.network
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subaddress(&self) -> bool {
|
pub fn is_subaddress(&self) -> bool {
|
||||||
self.meta.subaddress()
|
self.meta.is_subaddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
||||||
self.meta.payment_id()
|
self.meta.payment_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guaranteed(&self) -> bool {
|
pub fn is_guaranteed(&self) -> bool {
|
||||||
self.meta.guaranteed()
|
self.meta.is_guaranteed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
use std::{sync::Mutex, collections::HashSet};
|
use std_shims::{vec::Vec, collections::HashSet};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
#[cfg(feature = "cache-distribution")]
|
||||||
|
use std_shims::sync::OnceLock;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "cache-distribution", not(feature = "std")))]
|
||||||
|
use std_shims::sync::Mutex;
|
||||||
|
#[cfg(all(feature = "cache-distribution", feature = "std"))]
|
||||||
|
use async_lock::Mutex;
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
use rand_distr::{Distribution, Gamma};
|
use rand_distr::{Distribution, Gamma};
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
use rand_distr::num_traits::Float;
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
use curve25519_dalek::edwards::EdwardsPoint;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
serialize::varint_len,
|
||||||
wallet::SpendableOutput,
|
wallet::SpendableOutput,
|
||||||
rpc::{RpcError, Rpc},
|
rpc::{RpcError, RpcConnection, Rpc},
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCK_WINDOW: usize = 10;
|
const LOCK_WINDOW: usize = 10;
|
||||||
@@ -19,17 +28,24 @@ const MATURITY: u64 = 60;
|
|||||||
const RECENT_WINDOW: usize = 15;
|
const RECENT_WINDOW: usize = 15;
|
||||||
const BLOCK_TIME: usize = 120;
|
const BLOCK_TIME: usize = 120;
|
||||||
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
|
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
|
const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
|
||||||
|
|
||||||
lazy_static! {
|
// TODO: Resolve safety of this in case a reorg occurs/the network changes
|
||||||
static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
|
// TODO: Update this when scanning a block, as possible
|
||||||
static ref DISTRIBUTION: Mutex<Vec<u64>> = Mutex::new(Vec::with_capacity(3000000));
|
#[cfg(feature = "cache-distribution")]
|
||||||
|
static DISTRIBUTION_CELL: OnceLock<Mutex<Vec<u64>>> = OnceLock::new();
|
||||||
|
#[cfg(feature = "cache-distribution")]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn DISTRIBUTION() -> &'static Mutex<Vec<u64>> {
|
||||||
|
DISTRIBUTION_CELL.get_or_init(|| Mutex::new(Vec::with_capacity(3000000)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn select_n<R: RngCore + CryptoRng>(
|
async fn select_n<'a, R: RngCore + CryptoRng, RPC: RpcConnection>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
rpc: &Rpc,
|
rpc: &Rpc<RPC>,
|
||||||
|
distribution: &[u64],
|
||||||
height: usize,
|
height: usize,
|
||||||
high: u64,
|
high: u64,
|
||||||
per_second: f64,
|
per_second: f64,
|
||||||
@@ -37,6 +53,12 @@ async fn select_n<R: RngCore + CryptoRng>(
|
|||||||
used: &mut HashSet<u64>,
|
used: &mut HashSet<u64>,
|
||||||
count: usize,
|
count: usize,
|
||||||
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
|
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
|
||||||
|
if height >= rpc.get_height().await? {
|
||||||
|
// TODO: Don't use InternalError for the caller's failure
|
||||||
|
Err(RpcError::InternalError("decoys being requested from too young blocks"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
let mut iters = 0;
|
let mut iters = 0;
|
||||||
let mut confirmed = Vec::with_capacity(count);
|
let mut confirmed = Vec::with_capacity(count);
|
||||||
// Retries on failure. Retries are obvious as decoys, yet should be minimal
|
// Retries on failure. Retries are obvious as decoys, yet should be minimal
|
||||||
@@ -44,14 +66,18 @@ async fn select_n<R: RngCore + CryptoRng>(
|
|||||||
let remaining = count - confirmed.len();
|
let remaining = count - confirmed.len();
|
||||||
let mut candidates = Vec::with_capacity(remaining);
|
let mut candidates = Vec::with_capacity(remaining);
|
||||||
while candidates.len() != remaining {
|
while candidates.len() != remaining {
|
||||||
iters += 1;
|
#[cfg(test)]
|
||||||
// This is cheap and on fresh chains, thousands of rounds may be needed
|
{
|
||||||
if iters == 10000 {
|
iters += 1;
|
||||||
Err(RpcError::InternalError("not enough decoy candidates"))?;
|
// This is cheap and on fresh chains, a lot of rounds may be needed
|
||||||
|
if iters == 100 {
|
||||||
|
Err(RpcError::InternalError("hit decoy selection round limit"))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a gamma distribution
|
// Use a gamma distribution
|
||||||
let mut age = GAMMA.sample(rng).exp();
|
let mut age = Gamma::<f64>::new(19.28, 1.0 / 1.61).unwrap().sample(rng).exp();
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
if age > TIP_APPLICATION {
|
if age > TIP_APPLICATION {
|
||||||
age -= TIP_APPLICATION;
|
age -= TIP_APPLICATION;
|
||||||
} else {
|
} else {
|
||||||
@@ -59,9 +85,9 @@ async fn select_n<R: RngCore + CryptoRng>(
|
|||||||
age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
|
age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||||
let o = (age * per_second) as u64;
|
let o = (age * per_second) as u64;
|
||||||
if o < high {
|
if o < high {
|
||||||
let distribution = DISTRIBUTION.lock().unwrap();
|
|
||||||
let i = distribution.partition_point(|s| *s < (high - 1 - o));
|
let i = distribution.partition_point(|s| *s < (high - 1 - o));
|
||||||
let prev = i.saturating_sub(1);
|
let prev = i.saturating_sub(1);
|
||||||
let n = distribution[i] - distribution[prev];
|
let n = distribution[i] - distribution[prev];
|
||||||
@@ -118,24 +144,39 @@ fn offset(ring: &[u64]) -> Vec<u64> {
|
|||||||
/// Decoy data, containing the actual member as well (at index `i`).
|
/// Decoy data, containing the actual member as well (at index `i`).
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
pub struct Decoys {
|
pub struct Decoys {
|
||||||
pub i: u8,
|
pub(crate) i: u8,
|
||||||
pub offsets: Vec<u64>,
|
pub(crate) offsets: Vec<u64>,
|
||||||
pub ring: Vec<[EdwardsPoint; 2]>,
|
pub(crate) ring: Vec<[EdwardsPoint; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::len_without_is_empty)]
|
||||||
impl Decoys {
|
impl Decoys {
|
||||||
|
pub fn fee_weight(offsets: &[u64]) -> usize {
|
||||||
|
varint_len(offsets.len()) + offsets.iter().map(|offset| varint_len(*offset)).sum::<usize>()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.offsets.len()
|
self.offsets.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select decoys using the same distribution as Monero.
|
/// Select decoys using the same distribution as Monero.
|
||||||
pub async fn select<R: RngCore + CryptoRng>(
|
pub async fn select<R: RngCore + CryptoRng, RPC: RpcConnection>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
rpc: &Rpc,
|
rpc: &Rpc<RPC>,
|
||||||
ring_len: usize,
|
ring_len: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
inputs: &[SpendableOutput],
|
inputs: &[SpendableOutput],
|
||||||
) -> Result<Vec<Decoys>, RpcError> {
|
) -> Result<Vec<Decoys>, RpcError> {
|
||||||
|
#[cfg(feature = "cache-distribution")]
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
let mut distribution = DISTRIBUTION().lock();
|
||||||
|
#[cfg(feature = "cache-distribution")]
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
let mut distribution = DISTRIBUTION().lock().await;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "cache-distribution"))]
|
||||||
|
let mut distribution = vec![];
|
||||||
|
|
||||||
let decoy_count = ring_len - 1;
|
let decoy_count = ring_len - 1;
|
||||||
|
|
||||||
// Convert the inputs in question to the raw output data
|
// Convert the inputs in question to the raw output data
|
||||||
@@ -146,29 +187,20 @@ impl Decoys {
|
|||||||
outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()]));
|
outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let distribution_len = {
|
if distribution.len() <= height {
|
||||||
let distribution = DISTRIBUTION.lock().unwrap();
|
let extension = rpc.get_output_distribution(distribution.len(), height).await?;
|
||||||
distribution.len()
|
distribution.extend(extension);
|
||||||
};
|
|
||||||
if distribution_len <= height {
|
|
||||||
let extension = rpc.get_output_distribution(distribution_len, height).await?;
|
|
||||||
DISTRIBUTION.lock().unwrap().extend(extension);
|
|
||||||
}
|
}
|
||||||
|
// If asked to use an older height than previously asked, truncate to ensure accuracy
|
||||||
|
// Should never happen, yet risks desyncing if it did
|
||||||
|
distribution.truncate(height + 1); // height is inclusive, and 0 is a valid height
|
||||||
|
|
||||||
let high;
|
let high = distribution[distribution.len() - 1];
|
||||||
let per_second;
|
#[allow(clippy::cast_precision_loss)]
|
||||||
{
|
let per_second = {
|
||||||
let mut distribution = DISTRIBUTION.lock().unwrap();
|
let blocks = distribution.len().min(BLOCKS_PER_YEAR);
|
||||||
// If asked to use an older height than previously asked, truncate to ensure accuracy
|
let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
|
||||||
// Should never happen, yet risks desyncing if it did
|
(outputs as f64) / ((blocks * BLOCK_TIME) as f64)
|
||||||
distribution.truncate(height + 1); // height is inclusive, and 0 is a valid height
|
|
||||||
|
|
||||||
high = distribution[distribution.len() - 1];
|
|
||||||
per_second = {
|
|
||||||
let blocks = distribution.len().min(BLOCKS_PER_YEAR);
|
|
||||||
let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
|
|
||||||
(outputs as f64) / ((blocks * BLOCK_TIME) as f64)
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut used = HashSet::<u64>::new();
|
let mut used = HashSet::<u64>::new();
|
||||||
@@ -176,7 +208,7 @@ impl Decoys {
|
|||||||
used.insert(o.0);
|
used.insert(o.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Simply create a TX with less than the target amount
|
// TODO: Create a TX with less than the target amount, as allowed by the protocol
|
||||||
if (high - MATURITY) < u64::try_from(inputs.len() * ring_len).unwrap() {
|
if (high - MATURITY) < u64::try_from(inputs.len() * ring_len).unwrap() {
|
||||||
Err(RpcError::InternalError("not enough decoy candidates"))?;
|
Err(RpcError::InternalError("not enough decoy candidates"))?;
|
||||||
}
|
}
|
||||||
@@ -184,9 +216,18 @@ impl Decoys {
|
|||||||
// Select all decoys for this transaction, assuming we generate a sane transaction
|
// Select all decoys for this transaction, assuming we generate a sane transaction
|
||||||
// We should almost never naturally generate an insane transaction, hence why this doesn't
|
// We should almost never naturally generate an insane transaction, hence why this doesn't
|
||||||
// bother with an overage
|
// bother with an overage
|
||||||
let mut decoys =
|
let mut decoys = select_n(
|
||||||
select_n(rng, rpc, height, high, per_second, &real, &mut used, inputs.len() * decoy_count)
|
rng,
|
||||||
.await?;
|
rpc,
|
||||||
|
&distribution,
|
||||||
|
height,
|
||||||
|
high,
|
||||||
|
per_second,
|
||||||
|
&real,
|
||||||
|
&mut used,
|
||||||
|
inputs.len() * decoy_count,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
real.zeroize();
|
real.zeroize();
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(inputs.len());
|
let mut res = Vec::with_capacity(inputs.len());
|
||||||
@@ -224,8 +265,18 @@ impl Decoys {
|
|||||||
|
|
||||||
// Select new outputs until we have a full sized ring again
|
// Select new outputs until we have a full sized ring again
|
||||||
ring.extend(
|
ring.extend(
|
||||||
select_n(rng, rpc, height, high, per_second, &[], &mut used, ring_len - ring.len())
|
select_n(
|
||||||
.await?,
|
rng,
|
||||||
|
rpc,
|
||||||
|
&distribution,
|
||||||
|
height,
|
||||||
|
high,
|
||||||
|
per_second,
|
||||||
|
&[],
|
||||||
|
&mut used,
|
||||||
|
ring_len - ring.len(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
);
|
);
|
||||||
ring.sort_by(|a, b| a.0.cmp(&b.0));
|
ring.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use core::ops::BitXor;
|
use core::ops::BitXor;
|
||||||
use std::io::{self, Read, Write};
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
@@ -12,8 +15,16 @@ use crate::serialize::{
|
|||||||
|
|
||||||
pub const MAX_TX_EXTRA_NONCE_SIZE: usize = 255;
|
pub const MAX_TX_EXTRA_NONCE_SIZE: usize = 255;
|
||||||
|
|
||||||
|
pub const PAYMENT_ID_MARKER: u8 = 0;
|
||||||
|
pub const ENCRYPTED_PAYMENT_ID_MARKER: u8 = 1;
|
||||||
|
// Used as it's the highest value not interpretable as a continued VarInt
|
||||||
|
pub const ARBITRARY_DATA_MARKER: u8 = 127;
|
||||||
|
|
||||||
|
// 1 byte is used for the marker
|
||||||
|
pub const MAX_ARBITRARY_DATA_SIZE: usize = MAX_TX_EXTRA_NONCE_SIZE - 1;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
pub(crate) enum PaymentId {
|
pub enum PaymentId {
|
||||||
Unencrypted([u8; 32]),
|
Unencrypted([u8; 32]),
|
||||||
Encrypted([u8; 8]),
|
Encrypted([u8; 8]),
|
||||||
}
|
}
|
||||||
@@ -23,6 +34,7 @@ impl BitXor<[u8; 8]> for PaymentId {
|
|||||||
|
|
||||||
fn bitxor(self, bytes: [u8; 8]) -> PaymentId {
|
fn bitxor(self, bytes: [u8; 8]) -> PaymentId {
|
||||||
match self {
|
match self {
|
||||||
|
// Don't perform the xor since this isn't intended to be encrypted with xor
|
||||||
PaymentId::Unencrypted(_) => self,
|
PaymentId::Unencrypted(_) => self,
|
||||||
PaymentId::Encrypted(id) => {
|
PaymentId::Encrypted(id) => {
|
||||||
PaymentId::Encrypted((u64::from_le_bytes(id) ^ u64::from_le_bytes(bytes)).to_le_bytes())
|
PaymentId::Encrypted((u64::from_le_bytes(id) ^ u64::from_le_bytes(bytes)).to_le_bytes())
|
||||||
@@ -32,32 +44,32 @@ impl BitXor<[u8; 8]> for PaymentId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PaymentId {
|
impl PaymentId {
|
||||||
pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
PaymentId::Unencrypted(id) => {
|
PaymentId::Unencrypted(id) => {
|
||||||
w.write_all(&[0])?;
|
w.write_all(&[PAYMENT_ID_MARKER])?;
|
||||||
w.write_all(id)?;
|
w.write_all(id)?;
|
||||||
}
|
}
|
||||||
PaymentId::Encrypted(id) => {
|
PaymentId::Encrypted(id) => {
|
||||||
w.write_all(&[1])?;
|
w.write_all(&[ENCRYPTED_PAYMENT_ID_MARKER])?;
|
||||||
w.write_all(id)?;
|
w.write_all(id)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read<R: Read>(r: &mut R) -> io::Result<PaymentId> {
|
pub fn read<R: Read>(r: &mut R) -> io::Result<PaymentId> {
|
||||||
Ok(match read_byte(r)? {
|
Ok(match read_byte(r)? {
|
||||||
0 => PaymentId::Unencrypted(read_bytes(r)?),
|
0 => PaymentId::Unencrypted(read_bytes(r)?),
|
||||||
1 => PaymentId::Encrypted(read_bytes(r)?),
|
1 => PaymentId::Encrypted(read_bytes(r)?),
|
||||||
_ => Err(io::Error::new(io::ErrorKind::Other, "unknown payment ID type"))?,
|
_ => Err(io::Error::other("unknown payment ID type"))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doesn't bother with padding nor MinerGate
|
// Doesn't bother with padding nor MinerGate
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
pub(crate) enum ExtraField {
|
pub enum ExtraField {
|
||||||
PublicKey(EdwardsPoint),
|
PublicKey(EdwardsPoint),
|
||||||
Nonce(Vec<u8>),
|
Nonce(Vec<u8>),
|
||||||
MergeMining(usize, [u8; 32]),
|
MergeMining(usize, [u8; 32]),
|
||||||
@@ -65,7 +77,7 @@ pub(crate) enum ExtraField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ExtraField {
|
impl ExtraField {
|
||||||
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
ExtraField::PublicKey(key) => {
|
ExtraField::PublicKey(key) => {
|
||||||
w.write_all(&[1])?;
|
w.write_all(&[1])?;
|
||||||
@@ -88,43 +100,43 @@ impl ExtraField {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read<R: Read>(r: &mut R) -> io::Result<ExtraField> {
|
pub fn read<R: Read>(r: &mut R) -> io::Result<ExtraField> {
|
||||||
Ok(match read_byte(r)? {
|
Ok(match read_byte(r)? {
|
||||||
1 => ExtraField::PublicKey(read_point(r)?),
|
1 => ExtraField::PublicKey(read_point(r)?),
|
||||||
2 => ExtraField::Nonce({
|
2 => ExtraField::Nonce({
|
||||||
let nonce = read_vec(read_byte, r)?;
|
let nonce = read_vec(read_byte, r)?;
|
||||||
if nonce.len() > MAX_TX_EXTRA_NONCE_SIZE {
|
if nonce.len() > MAX_TX_EXTRA_NONCE_SIZE {
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "too long nonce"))?;
|
Err(io::Error::other("too long nonce"))?;
|
||||||
}
|
}
|
||||||
nonce
|
nonce
|
||||||
}),
|
}),
|
||||||
3 => ExtraField::MergeMining(
|
3 => ExtraField::MergeMining(read_varint(r)?, read_bytes(r)?),
|
||||||
usize::try_from(read_varint(r)?)
|
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "varint for height exceeds usize"))?,
|
|
||||||
read_bytes(r)?,
|
|
||||||
),
|
|
||||||
4 => ExtraField::PublicKeys(read_vec(read_point, r)?),
|
4 => ExtraField::PublicKeys(read_vec(read_point, r)?),
|
||||||
_ => Err(io::Error::new(io::ErrorKind::Other, "unknown extra field"))?,
|
_ => Err(io::Error::other("unknown extra field"))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
pub(crate) struct Extra(Vec<ExtraField>);
|
pub struct Extra(Vec<ExtraField>);
|
||||||
impl Extra {
|
impl Extra {
|
||||||
pub(crate) fn keys(&self) -> Vec<EdwardsPoint> {
|
pub fn keys(&self) -> Option<(EdwardsPoint, Option<Vec<EdwardsPoint>>)> {
|
||||||
let mut keys = Vec::with_capacity(2);
|
let mut key = None;
|
||||||
|
let mut additional = None;
|
||||||
for field in &self.0 {
|
for field in &self.0 {
|
||||||
match field.clone() {
|
match field.clone() {
|
||||||
ExtraField::PublicKey(key) => keys.push(key),
|
ExtraField::PublicKey(this_key) => key = key.or(Some(this_key)),
|
||||||
ExtraField::PublicKeys(additional) => keys.extend(additional),
|
ExtraField::PublicKeys(these_additional) => {
|
||||||
|
additional = additional.or(Some(these_additional))
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keys
|
// Don't return any keys if this was non-standard and didn't include the primary key
|
||||||
|
key.map(|key| (key, additional))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn payment_id(&self) -> Option<PaymentId> {
|
pub fn payment_id(&self) -> Option<PaymentId> {
|
||||||
for field in &self.0 {
|
for field in &self.0 {
|
||||||
if let ExtraField::Nonce(data) = field {
|
if let ExtraField::Nonce(data) = field {
|
||||||
return PaymentId::read::<&[u8]>(&mut data.as_ref()).ok();
|
return PaymentId::read::<&[u8]>(&mut data.as_ref()).ok();
|
||||||
@@ -133,29 +145,23 @@ impl Extra {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn data(&self) -> Vec<Vec<u8>> {
|
pub fn data(&self) -> Vec<Vec<u8>> {
|
||||||
let mut first = true;
|
|
||||||
let mut res = vec![];
|
let mut res = vec![];
|
||||||
for field in &self.0 {
|
for field in &self.0 {
|
||||||
if let ExtraField::Nonce(data) = field {
|
if let ExtraField::Nonce(data) = field {
|
||||||
// Skip the first Nonce, which should be the payment ID
|
if data[0] == ARBITRARY_DATA_MARKER {
|
||||||
if first {
|
res.push(data[1 ..].to_vec());
|
||||||
first = false;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
res.push(data.clone());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(mut keys: Vec<EdwardsPoint>) -> Extra {
|
pub(crate) fn new(key: EdwardsPoint, additional: Vec<EdwardsPoint>) -> Extra {
|
||||||
let mut res = Extra(Vec::with_capacity(3));
|
let mut res = Extra(Vec::with_capacity(3));
|
||||||
if !keys.is_empty() {
|
res.push(ExtraField::PublicKey(key));
|
||||||
res.push(ExtraField::PublicKey(keys[0]));
|
if !additional.is_empty() {
|
||||||
}
|
res.push(ExtraField::PublicKeys(additional));
|
||||||
if keys.len() > 1 {
|
|
||||||
res.push(ExtraField::PublicKeys(keys.drain(1 ..).collect()));
|
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
@@ -165,25 +171,36 @@ impl Extra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub(crate) fn fee_weight(outputs: usize, data: &[Vec<u8>]) -> usize {
|
pub(crate) fn fee_weight(
|
||||||
|
outputs: usize,
|
||||||
|
additional: bool,
|
||||||
|
payment_id: bool,
|
||||||
|
data: &[Vec<u8>]
|
||||||
|
) -> usize {
|
||||||
// PublicKey, key
|
// PublicKey, key
|
||||||
(1 + 32) +
|
(1 + 32) +
|
||||||
// PublicKeys, length, additional keys
|
// PublicKeys, length, additional keys
|
||||||
(1 + 1 + (outputs.saturating_sub(1) * 32)) +
|
(if additional { 1 + 1 + (outputs * 32) } else { 0 }) +
|
||||||
// PaymentId (Nonce), length, encrypted, ID
|
// PaymentId (Nonce), length, encrypted, ID
|
||||||
(1 + 1 + 1 + 8) +
|
(if payment_id { 1 + 1 + 1 + 8 } else { 0 }) +
|
||||||
// Nonce, length, data (if existent)
|
// Nonce, length, ARBITRARY_DATA_MARKER, data
|
||||||
data.iter().map(|v| 1 + varint_len(v.len()) + v.len()).sum::<usize>()
|
data.iter().map(|v| 1 + varint_len(1 + v.len()) + 1 + v.len()).sum::<usize>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
for field in &self.0 {
|
for field in &self.0 {
|
||||||
field.write(w)?;
|
field.write(w)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read<R: Read>(r: &mut R) -> io::Result<Extra> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
self.write(&mut buf).unwrap();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<Extra> {
|
||||||
let mut res = Extra(vec![]);
|
let mut res = Extra(vec![]);
|
||||||
let mut field;
|
let mut field;
|
||||||
while {
|
while {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user